5309 lines
216 KiB
PHP
5309 lines
216 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\OpdModel;
|
|
use App\Models\PaymentModel;
|
|
use CodeIgniter\I18n\Time;
|
|
use Mpdf\HTMLParserMode;
|
|
use Mpdf\Mpdf;
|
|
|
|
class Opd extends BaseController
|
|
{
|
|
private const OPD_PAPER_SETTING_KEY = 'OPD_PAPER_PRINT_SETTINGS';
|
|
private const OPD_PAPER_SETTING_KEY_PREFIX = 'OPD_PAPER_PS__';
|
|
private const OPD_PAPER_SETTING_KEY_PREFIX_LEGACY = 'OPD_PAPER_PRINT_SETTINGS__';
|
|
private const OPD_TEMPLATE_KEY_PREFIX = 'OPD_PRINT_TEMPLATE__';
|
|
|
|
/**
|
|
* @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('template.opd_print') || $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 = $this->normalizeOpdTemplateId((string) $this->request->getGet('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',
|
|
'short_description', 'doctor_short_description',
|
|
'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,
|
|
'paper_settings' => $this->getOpdPaperPrintSetting($name),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function collectOpdTemplateNames(): array
|
|
{
|
|
return $this->collectStoredOpdTemplateNames();
|
|
}
|
|
|
|
private function readOpdTemplateContentByName(string $name): string
|
|
{
|
|
$name = $this->normalizeOpdTemplateId($name);
|
|
if ($name === '') {
|
|
return '';
|
|
}
|
|
|
|
return $this->readStoredOpdTemplateContent($name);
|
|
}
|
|
|
|
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 = $this->normalizeOpdTemplateId((string) $this->request->getPost('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']);
|
|
}
|
|
|
|
if (! $this->saveStoredOpdTemplateContent($name, $content)) {
|
|
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(),
|
|
]);
|
|
}
|
|
|
|
public function print_template_rename()
|
|
{
|
|
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']);
|
|
}
|
|
|
|
$oldName = $this->normalizeOpdTemplateId((string) $this->request->getPost('old_name'));
|
|
$newName = $this->normalizeOpdTemplateId((string) $this->request->getPost('new_name'));
|
|
|
|
if ($oldName === '' || $newName === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Both old and new template names are required']);
|
|
}
|
|
if ($oldName === $newName) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'New template name must be different']);
|
|
}
|
|
|
|
if (! $this->renameStoredOpdTemplateContent($oldName, $newName)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template not found or rename failed']);
|
|
}
|
|
|
|
$this->renameOpdPaperSettingStorageKey($oldName, $newName);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Template renamed',
|
|
'old_name' => $oldName,
|
|
'new_name' => $newName,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
private function renameOpdPaperSettingStorageKey(string $oldName, string $newName): void
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return;
|
|
}
|
|
|
|
$newKey = $this->getOpdPaperSettingStorageKey($newName);
|
|
$oldKeys = $this->getOpdPaperSettingCandidateKeys($oldName);
|
|
if (empty($oldKeys)) {
|
|
return;
|
|
}
|
|
|
|
$existsNew = $this->db->table('hospital_setting')
|
|
->select('id')
|
|
->where('s_name', $newKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
// If target key exists, just clean old aliases.
|
|
if (! empty($existsNew['id'])) {
|
|
foreach ($oldKeys as $oldKey) {
|
|
if ($oldKey === $newKey) {
|
|
continue;
|
|
}
|
|
$this->db->table('hospital_setting')->where('s_name', $oldKey)->delete();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
foreach ($oldKeys as $oldKey) {
|
|
if ($oldKey === $newKey) {
|
|
continue;
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('id,s_value')
|
|
->where('s_name', $oldKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($row)) {
|
|
continue;
|
|
}
|
|
|
|
$this->db->table('hospital_setting')->insert([
|
|
's_name' => $newKey,
|
|
's_value' => (string) ($row['s_value'] ?? ''),
|
|
]);
|
|
|
|
$this->db->table('hospital_setting')->where('id', (int) ($row['id'] ?? 0))->delete();
|
|
break;
|
|
}
|
|
}
|
|
|
|
public function print_template_delete()
|
|
{
|
|
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']);
|
|
}
|
|
|
|
$templateName = $this->normalizeOpdTemplateId((string) $this->request->getPost('template_name'));
|
|
|
|
if ($templateName === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name is required']);
|
|
}
|
|
|
|
if (! $this->deleteStoredOpdTemplateContent($templateName)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template not found or delete failed']);
|
|
}
|
|
|
|
$this->deleteOpdPaperSettingStorageKey($templateName);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Template deleted',
|
|
'template_name' => $templateName,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
private function deleteOpdPaperSettingStorageKey(string $templateName): void
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return;
|
|
}
|
|
|
|
$keys = $this->getOpdPaperSettingCandidateKeys($templateName);
|
|
foreach ($keys as $key) {
|
|
$this->db->table('hospital_setting')->where('s_name', $key)->delete();
|
|
}
|
|
}
|
|
|
|
private function normalizeOpdTemplateId(string $templateId): string
|
|
{
|
|
$templateId = strtolower(trim($templateId));
|
|
$templateId = str_replace([' ', '.'], '_', $templateId);
|
|
$templateId = preg_replace('/[^a-z0-9_\-]+/', '_', $templateId) ?? '';
|
|
$templateId = preg_replace('/_{2,}/', '_', $templateId) ?? $templateId;
|
|
|
|
return trim($templateId, '_-');
|
|
}
|
|
|
|
private function getOpdTemplateStorageKey(string $templateId): string
|
|
{
|
|
$templateId = $this->normalizeOpdTemplateId($templateId);
|
|
if ($templateId === '') {
|
|
return '';
|
|
}
|
|
|
|
return self::OPD_TEMPLATE_KEY_PREFIX . $templateId;
|
|
}
|
|
|
|
private function readStoredOpdTemplateContent(string $templateId): string
|
|
{
|
|
$storageKey = $this->getOpdTemplateStorageKey($templateId);
|
|
if ($storageKey === '' || ! $this->db->tableExists('hospital_setting')) {
|
|
return '';
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->where('s_name', $storageKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
return trim((string) ($row['s_value'] ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function collectStoredOpdTemplateNames(): array
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return [];
|
|
}
|
|
|
|
$rows = $this->db->table('hospital_setting')
|
|
->select('s_name')
|
|
->like('s_name', self::OPD_TEMPLATE_KEY_PREFIX, 'after')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$names = [];
|
|
foreach ($rows as $row) {
|
|
$storageKey = (string) ($row['s_name'] ?? '');
|
|
if (! str_starts_with($storageKey, self::OPD_TEMPLATE_KEY_PREFIX)) {
|
|
continue;
|
|
}
|
|
|
|
$templateId = substr($storageKey, strlen(self::OPD_TEMPLATE_KEY_PREFIX));
|
|
$templateId = $this->normalizeOpdTemplateId((string) $templateId);
|
|
if ($templateId !== '') {
|
|
$names[] = $templateId;
|
|
}
|
|
}
|
|
|
|
$names = array_values(array_unique($names));
|
|
sort($names);
|
|
|
|
return $names;
|
|
}
|
|
|
|
private function saveStoredOpdTemplateContent(string $templateId, string $content): bool
|
|
{
|
|
$storageKey = $this->getOpdTemplateStorageKey($templateId);
|
|
if ($storageKey === '' || ! $this->db->tableExists('hospital_setting')) {
|
|
return false;
|
|
}
|
|
|
|
$this->ensureHospitalSettingValueCanStoreLargePayload();
|
|
|
|
$existing = $this->db->table('hospital_setting')
|
|
->select('id')
|
|
->where('s_name', $storageKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (! empty($existing['id'])) {
|
|
return (bool) $this->db->table('hospital_setting')
|
|
->where('id', (int) $existing['id'])
|
|
->update(['s_value' => $content]);
|
|
}
|
|
|
|
return (bool) $this->db->table('hospital_setting')->insert([
|
|
's_name' => $storageKey,
|
|
's_value' => $content,
|
|
]);
|
|
}
|
|
|
|
private function renameStoredOpdTemplateContent(string $oldTemplateId, string $newTemplateId): bool
|
|
{
|
|
$oldKey = $this->getOpdTemplateStorageKey($oldTemplateId);
|
|
$newKey = $this->getOpdTemplateStorageKey($newTemplateId);
|
|
if ($oldKey === '' || $newKey === '' || ! $this->db->tableExists('hospital_setting')) {
|
|
return false;
|
|
}
|
|
|
|
$existingOld = $this->db->table('hospital_setting')
|
|
->select('id')
|
|
->where('s_name', $oldKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
if (empty($existingOld['id'])) {
|
|
return false;
|
|
}
|
|
|
|
$existingNew = $this->db->table('hospital_setting')
|
|
->select('id')
|
|
->where('s_name', $newKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
if (! empty($existingNew['id'])) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) $this->db->table('hospital_setting')
|
|
->where('id', (int) $existingOld['id'])
|
|
->update(['s_name' => $newKey]);
|
|
}
|
|
|
|
private function deleteStoredOpdTemplateContent(string $templateId): bool
|
|
{
|
|
$storageKey = $this->getOpdTemplateStorageKey($templateId);
|
|
if ($storageKey === '' || ! $this->db->tableExists('hospital_setting')) {
|
|
return false;
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('id')
|
|
->where('s_name', $storageKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
if (empty($row['id'])) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) $this->db->table('hospital_setting')
|
|
->where('id', (int) $row['id'])
|
|
->delete();
|
|
}
|
|
|
|
public function paper_print_settings()
|
|
{
|
|
if (! $this->canManageOpdPrintTemplates()) {
|
|
return $this->response->setStatusCode(403)->setBody('Access denied: OPD paper print settings are restricted.');
|
|
}
|
|
|
|
$templateName = $this->normalizeOpdTemplateId((string) ($this->request->getPost('template_name') ?? $this->request->getGet('template_name') ?? ''));
|
|
|
|
$notice = '';
|
|
$noticeType = 'success';
|
|
$settings = $this->getOpdPaperPrintSetting($templateName);
|
|
|
|
if (strtolower($this->request->getMethod()) === 'post') {
|
|
$isReset = (int) ($this->request->getPost('reset') ?? 0) === 1;
|
|
if ($isReset) {
|
|
$cleared = $this->clearOpdPaperPrintSetting($templateName);
|
|
$settings = $this->getOpdPaperPrintSetting($templateName);
|
|
$notice = $cleared ? 'OPD paper print settings reset.' : 'Unable to reset OPD paper print settings.';
|
|
$noticeType = $cleared ? 'success' : 'danger';
|
|
|
|
if ($this->request->isAJAX()) {
|
|
return $this->response->setJSON([
|
|
'status' => $cleared ? 'success' : 'error',
|
|
'notice' => $notice,
|
|
'notice_type' => $noticeType,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
'settings' => $settings,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$pageSize = strtoupper(trim((string) ($this->request->getPost('page_size') ?? 'A4')));
|
|
if (! in_array($pageSize, ['A4', 'A4-L', 'A5', 'A6', 'LETTER', 'LEGAL', 'CUSTOM'], true)) {
|
|
$pageSize = 'A4';
|
|
}
|
|
|
|
$customWidthMm = (int) $this->normalizeOpdPrintMarginCm($this->request->getPost('custom_width_mm'), 210.0);
|
|
$customHeightMm = (int) $this->normalizeOpdPrintMarginCm($this->request->getPost('custom_height_mm'), 297.0);
|
|
if ($customWidthMm < 20) {
|
|
$customWidthMm = 20;
|
|
}
|
|
if ($customWidthMm > 600) {
|
|
$customWidthMm = 600;
|
|
}
|
|
if ($customHeightMm < 20) {
|
|
$customHeightMm = 20;
|
|
}
|
|
if ($customHeightMm > 1000) {
|
|
$customHeightMm = 1000;
|
|
}
|
|
|
|
$settings = [
|
|
'page_size' => $pageSize,
|
|
'custom_width_mm' => $customWidthMm,
|
|
'custom_height_mm' => $customHeightMm,
|
|
'page_margin_top_cm' => $this->normalizeOpdPrintMarginCm($this->request->getPost('page_margin_top_cm'), 6.1),
|
|
'page_margin_bottom_cm' => $this->normalizeOpdPrintMarginCm($this->request->getPost('page_margin_bottom_cm'), 2.5),
|
|
'page_margin_left_cm' => $this->normalizeOpdPrintMarginCm($this->request->getPost('page_margin_left_cm'), 0.7),
|
|
'page_margin_right_cm' => $this->normalizeOpdPrintMarginCm($this->request->getPost('page_margin_right_cm'), 0.7),
|
|
'margin_header_cm' => $this->normalizeOpdPrintMarginCm($this->request->getPost('margin_header_cm'), 0.5),
|
|
'margin_footer_cm' => $this->normalizeOpdPrintMarginCm($this->request->getPost('margin_footer_cm'), 1.5),
|
|
'paper_html_content' => (string) ($this->request->getPost('paper_html_content') ?? ''),
|
|
'header_html' => (string) ($this->request->getPost('header_html') ?? ''),
|
|
'footer_html' => (string) ($this->request->getPost('footer_html') ?? ''),
|
|
];
|
|
|
|
$saved = $this->saveOpdPaperPrintSetting($settings, $templateName);
|
|
if ($saved) {
|
|
$notice = 'OPD paper print settings saved.';
|
|
$noticeType = 'success';
|
|
} else {
|
|
$notice = 'Unable to save OPD paper print settings.';
|
|
$noticeType = 'danger';
|
|
}
|
|
|
|
if ($this->request->isAJAX()) {
|
|
return $this->response->setJSON([
|
|
'status' => $saved ? 'success' : 'error',
|
|
'notice' => $notice,
|
|
'notice_type' => $noticeType,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
return view('billing/opd_paper_print_settings', [
|
|
'settings' => $settings,
|
|
'notice' => $notice,
|
|
'notice_type' => $noticeType,
|
|
]);
|
|
}
|
|
|
|
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(
|
|
['DIAGNOSIS_AI_ALLOW_INSECURE_SSL', 'AI_DIAGNOSIS_AI_ALLOW_INSECURE_SSL', 'APP_DIAGNOSIS_AI_ALLOW_INSECURE_SSL', 'H_DIAGNOSIS_AI_ALLOW_INSECURE_SSL'],
|
|
['DIAGNOSIS_AI_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,
|
|
coalesce(ds.Spec, '') as Spec,
|
|
oa.No_opd,
|
|
oa.count_booking,
|
|
oa.count_wait,
|
|
oa.count_visit,
|
|
oa.count_cancel,
|
|
MOD(d.id,5) as color_code
|
|
from (
|
|
select
|
|
o.doc_id,
|
|
count(*) as No_opd,
|
|
sum(case when o.opd_status = 1 and pr.id is null then 1 else 0 end) as count_booking,
|
|
sum(case when o.opd_status = 1 and pr.id is not null then 1 else 0 end) as count_wait,
|
|
sum(case when o.opd_status = 2 then 1 else 0 end) as count_visit,
|
|
sum(case when o.opd_status = 3 then 1 else 0 end) as count_cancel
|
|
from opd_master o
|
|
left join opd_prescription pr on pr.opd_id = o.opd_id
|
|
where o.apointment_date = " . $opdDateSql . "
|
|
group by o.doc_id
|
|
) oa
|
|
join doctor_master d on d.id = oa.doc_id and d.active = 1
|
|
left join (
|
|
select s.doc_id, group_concat(DISTINCT m.SpecName) as Spec
|
|
from doc_spec s
|
|
join med_spec m on m.id = s.med_spec_id
|
|
group by s.doc_id
|
|
) ds on ds.doc_id = d.id
|
|
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
|
|
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 . " and d.active=1
|
|
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.');
|
|
}
|
|
|
|
$sql = "select o.opd_id,o.opd_code,o.opd_no,o.opd_status,
|
|
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(pr.id is null, 0, 1) as has_prescription,
|
|
if(
|
|
coalesce(trim(pr.pulse),'')<>''
|
|
or coalesce(trim(pr.spo2),'')<>''
|
|
or coalesce(trim(pr.bp),'')<>''
|
|
or coalesce(trim(pr.diastolic),'')<>''
|
|
or coalesce(trim(pr.temp),'')<>''
|
|
or coalesce(trim(pr.rr_min),'')<>''
|
|
or coalesce(trim(pr.height),'')<>''
|
|
or coalesce(trim(pr.weight),'')<>''
|
|
or coalesce(trim(pr.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 o.apointment_date=" . $opdDateSql . " and o.doc_id=" . $docId;
|
|
$query = $this->db->query($sql);
|
|
$allRows = $query->getResult();
|
|
|
|
$opdList0 = [];
|
|
$opdList1 = [];
|
|
$opdList2 = [];
|
|
$opdList3 = [];
|
|
|
|
foreach ($allRows as $row) {
|
|
$status = (int) ($row->opd_status ?? 0);
|
|
$hasPrescription = (int) ($row->has_prescription ?? 0) === 1;
|
|
|
|
if ($status === 1 && ! $hasPrescription) {
|
|
$opdList0[] = $row;
|
|
continue;
|
|
}
|
|
|
|
if ($status === 1 && $hasPrescription) {
|
|
$opdList1[] = $row;
|
|
continue;
|
|
}
|
|
|
|
if ($status === 2) {
|
|
$opdList2[] = $row;
|
|
continue;
|
|
}
|
|
|
|
if ($status === 3) {
|
|
$opdList3[] = $row;
|
|
}
|
|
}
|
|
|
|
usort($opdList0, static function ($left, $right): int {
|
|
return strcmp((string) ($right->opd_code ?? ''), (string) ($left->opd_code ?? ''));
|
|
});
|
|
|
|
usort($opdList1, static function ($left, $right): int {
|
|
$leftVitals = (int) ($left->has_vitals ?? 0);
|
|
$rightVitals = (int) ($right->has_vitals ?? 0);
|
|
if ($leftVitals !== $rightVitals) {
|
|
return $leftVitals <=> $rightVitals;
|
|
}
|
|
|
|
$leftQueue = (int) ($left->queue_no ?? 0);
|
|
$rightQueue = (int) ($right->queue_no ?? 0);
|
|
$leftQueueSort = $leftQueue > 0 ? $leftQueue : 999999;
|
|
$rightQueueSort = $rightQueue > 0 ? $rightQueue : 999999;
|
|
if ($leftQueueSort !== $rightQueueSort) {
|
|
return $leftQueueSort <=> $rightQueueSort;
|
|
}
|
|
|
|
return ((int) ($left->opd_id ?? 0)) <=> ((int) ($right->opd_id ?? 0));
|
|
});
|
|
|
|
usort($opdList2, static function ($left, $right): int {
|
|
return ((int) ($left->opd_id ?? 0)) <=> ((int) ($right->opd_id ?? 0));
|
|
});
|
|
|
|
usort($opdList3, static function ($left, $right): int {
|
|
return ((int) ($left->opd_id ?? 0)) <=> ((int) ($right->opd_id ?? 0));
|
|
});
|
|
|
|
$docMaster[0]->No_opd = count($allRows);
|
|
$docMaster[0]->count_booking = count($opdList0);
|
|
$docMaster[0]->count_wait = count($opdList1);
|
|
$docMaster[0]->count_visit = count($opdList2);
|
|
$docMaster[0]->count_cancel = count($opdList3);
|
|
|
|
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();
|
|
|
|
$sql = "select id,payment_date,insert_time,payment_mode,credit_debit,amount,remark,card_tran_id,update_by,
|
|
(case payment_mode when 0 then 'Zero Amount' when 1 then 'Cash' when 2 then 'Bank/Online' when 3 then 'Return Cash' when 4 then 'Bank Return' else 'Other' end) as payment_mode_str
|
|
from payment_history where payof_type=1 and payof_id=" . (int) $opdId . " order by id desc";
|
|
$query = $this->db->query($sql);
|
|
$data['payment_history_rows'] = $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';
|
|
}
|
|
|
|
$templateKey = $this->normalizeOpdTemplateId((string) $this->request->getGet('template'));
|
|
|
|
$allowedModes = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only'];
|
|
if (! in_array($layoutMode, $allowedModes, true)) {
|
|
$layoutMode = 'full';
|
|
}
|
|
|
|
// If template is explicitly requested, always render full layout so header/footer are applied.
|
|
if ($templateKey !== '') {
|
|
$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;
|
|
|
|
$isComposedTemplate = $templateKey !== '' && str_starts_with($templateKey, 'compose_');
|
|
if ($isComposedTemplate) {
|
|
$html = $this->renderComposedOpdPrint($data, $templateKey);
|
|
} else {
|
|
$customHtml = '';
|
|
if ($templateKey !== '') {
|
|
$customHtml = $this->readStoredOpdTemplateContent($templateKey);
|
|
}
|
|
|
|
if ($customHtml !== '') {
|
|
$legacyVars = $this->buildLegacyDashboardTemplateVars($data);
|
|
$html = $this->renderCurlyTemplate($customHtml, $legacyVars);
|
|
} else {
|
|
$html = view('billing/opd_letter_head_pdf', $data);
|
|
}
|
|
}
|
|
|
|
// SHORT-CIRCUIT: When DB template content is found, render it directly with token
|
|
// substitution and skip all per-template paper settings entirely.
|
|
if ($customHtml !== '') {
|
|
$tokenVars = $this->buildOpdPdfTokenVars($data);
|
|
$html = $this->applyOpdPdfTemplateTokens($customHtml, $tokenVars);
|
|
$html = $this->sanitizeOpdNamedHeaderFooterReferences($html);
|
|
|
|
$mpdf = new Mpdf([
|
|
'mode' => 'utf-8',
|
|
'format' => 'A4',
|
|
'orientation' => 'P',
|
|
'margin_left' => 5,
|
|
'margin_right' => 5,
|
|
'margin_top' => 5,
|
|
'margin_bottom' => 5,
|
|
'margin_header' => 5,
|
|
'margin_footer' => 5,
|
|
'default_font' => 'dejavusans',
|
|
'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'));
|
|
}
|
|
|
|
$docPrintSetting = $this->getOpdPaperPrintSetting($templateKey);
|
|
$tokenVars = $this->buildOpdPdfTokenVars($data);
|
|
|
|
$paper = strtolower(trim((string) $this->request->getGet('paper')));
|
|
$format = strtoupper(trim((string) ($docPrintSetting['page_size'] ?? 'A4')));
|
|
if ($format === '') {
|
|
$format = 'A4';
|
|
}
|
|
if (in_array($paper, ['a5', 'a6', 'letter'], true)) {
|
|
$format = strtoupper($paper);
|
|
}
|
|
|
|
$customWidthMm = (int) $this->normalizeOpdPrintMarginCm($docPrintSetting['custom_width_mm'] ?? null, 210.0);
|
|
$customHeightMm = (int) $this->normalizeOpdPrintMarginCm($docPrintSetting['custom_height_mm'] ?? null, 297.0);
|
|
if ($customWidthMm < 20) {
|
|
$customWidthMm = 20;
|
|
}
|
|
if ($customWidthMm > 600) {
|
|
$customWidthMm = 600;
|
|
}
|
|
if ($customHeightMm < 20) {
|
|
$customHeightMm = 20;
|
|
}
|
|
if ($customHeightMm > 1000) {
|
|
$customHeightMm = 1000;
|
|
}
|
|
$mpdfFormat = $format === 'CUSTOM' ? [$customWidthMm, $customHeightMm] : $format;
|
|
|
|
$hasDocSetting = ! empty($docPrintSetting);
|
|
if ($hasDocSetting) {
|
|
$marginLeftCm = $this->normalizeOpdPrintMarginCm($docPrintSetting['page_margin_left_cm'] ?? null, 0.7);
|
|
$marginRightCm = $this->normalizeOpdPrintMarginCm($docPrintSetting['page_margin_right_cm'] ?? null, 0.7);
|
|
$marginTopCm = $this->normalizeOpdPrintMarginCm($docPrintSetting['page_margin_top_cm'] ?? null, 6.1);
|
|
$marginBottomCm = $this->normalizeOpdPrintMarginCm($docPrintSetting['page_margin_bottom_cm'] ?? null, 2.5);
|
|
$marginHeaderCm = $this->normalizeOpdPrintMarginCm($docPrintSetting['margin_header_cm'] ?? null, 0.5);
|
|
$marginFooterCm = $this->normalizeOpdPrintMarginCm($docPrintSetting['margin_footer_cm'] ?? null, 1.5);
|
|
$marginLeft = $marginLeftCm * 10;
|
|
$marginRight = $marginRightCm * 10;
|
|
$marginTop = $marginTopCm * 10;
|
|
$marginBottom = $marginBottomCm * 10;
|
|
$marginHeader = $marginHeaderCm * 10;
|
|
$marginFooter = $marginFooterCm * 10;
|
|
} else {
|
|
$marginLeftCm = 0.7;
|
|
$marginRightCm = 0.7;
|
|
$marginTopCm = 6.1;
|
|
$marginBottomCm = 2.5;
|
|
$marginHeaderCm = 0.5;
|
|
$marginFooterCm = 1.5;
|
|
$marginLeft = 8;
|
|
$marginRight = 8;
|
|
$marginTop = 8;
|
|
$marginBottom = 8;
|
|
$marginHeader = 5;
|
|
$marginFooter = 10;
|
|
}
|
|
|
|
if ($format === 'A6') {
|
|
$marginLeft = 6;
|
|
$marginRight = 6;
|
|
$marginTop = 6;
|
|
$marginBottom = 6;
|
|
$marginLeftCm = 0.6;
|
|
$marginRightCm = 0.6;
|
|
$marginTopCm = 0.6;
|
|
$marginBottomCm = 0.6;
|
|
}
|
|
|
|
// Add margin variables to token replacement
|
|
$tokenVars['MarginTop'] = (string) $marginTopCm;
|
|
$tokenVars['MarginBottom'] = (string) $marginBottomCm;
|
|
$tokenVars['MarginLeft'] = (string) $marginLeftCm;
|
|
$tokenVars['MarginRight'] = (string) $marginRightCm;
|
|
$tokenVars['MarginHeader'] = (string) $marginHeaderCm;
|
|
$tokenVars['MarginFooter'] = (string) $marginFooterCm;
|
|
// Aliases for alternate naming
|
|
$tokenVars['page_margin_top_cm'] = (string) $marginTopCm;
|
|
$tokenVars['page_margin_bottom_cm'] = (string) $marginBottomCm;
|
|
$tokenVars['page_margin_left_cm'] = (string) $marginLeftCm;
|
|
$tokenVars['page_margin_right_cm'] = (string) $marginRightCm;
|
|
$tokenVars['margin_header_cm'] = (string) $marginHeaderCm;
|
|
$tokenVars['margin_footer_cm'] = (string) $marginFooterCm;
|
|
|
|
$mpdf = new Mpdf([
|
|
'mode' => 'utf-8',
|
|
'format' => $mpdfFormat,
|
|
'orientation' => 'P',
|
|
'margin_left' => $marginLeft,
|
|
'margin_right' => $marginRight,
|
|
'margin_top' => $marginTop,
|
|
'margin_bottom' => $marginBottom,
|
|
'margin_header' => $marginHeader,
|
|
'margin_footer' => $marginFooter,
|
|
'default_font' => 'dejavusans',
|
|
'autoScriptToLang' => true,
|
|
'autoLangToFont' => true,
|
|
'tempDir' => WRITEPATH . 'cache',
|
|
]);
|
|
|
|
$rawHeaderHtml = trim((string) ($docPrintSetting['header_html'] ?? ''));
|
|
$rawFooterHtml = trim((string) ($docPrintSetting['footer_html'] ?? ''));
|
|
$rawPaperHtmlContent = trim((string) ($docPrintSetting['paper_html_content'] ?? ''));
|
|
$prefixHtml = '';
|
|
|
|
// Build base @page CSS block from margin settings.
|
|
$buildPageStyleBlock = static function (bool $useHeader, bool $useFooter) use ($marginTopCm, $marginBottomCm, $marginLeftCm, $marginRightCm, $marginHeaderCm, $marginFooterCm): string {
|
|
$css = '<style>@page {' . "\n"
|
|
. 'margin-top: ' . $marginTopCm . 'cm;' . "\n"
|
|
. 'margin-bottom: ' . $marginBottomCm . 'cm;' . "\n"
|
|
. 'margin-left: ' . $marginLeftCm . 'cm;' . "\n"
|
|
. 'margin-right: ' . $marginRightCm . 'cm;' . "\n"
|
|
. 'margin-header: ' . $marginHeaderCm . 'cm;' . "\n"
|
|
. 'margin-footer: ' . $marginFooterCm . 'cm;' . "\n";
|
|
|
|
if ($useHeader) {
|
|
$css .= 'header: html_myHeader;' . "\n";
|
|
}
|
|
if ($useFooter) {
|
|
$css .= 'footer: html_myFooter;' . "\n";
|
|
}
|
|
|
|
$css .= '}' . "\n" . '</style>' . "\n";
|
|
|
|
return $css;
|
|
};
|
|
|
|
// Resolve body content first.
|
|
if ($rawPaperHtmlContent !== '') {
|
|
$hasContentPlaceholder = preg_match('/<\?=\s*\$content\s*\?>|<\?=\$content\?>|\{\{content\}\}|\{content\}/i', $rawPaperHtmlContent) === 1;
|
|
$paperHtml = $this->applyOpdPdfTemplateTokens($rawPaperHtmlContent, $tokenVars);
|
|
$paperHtml = str_replace(['<?= $content ?>', '<?=$content?>', '{{content}}', '{content}'], $html, $paperHtml);
|
|
if ($hasContentPlaceholder && strpos($paperHtml, $html) === false) {
|
|
$paperHtml .= $html;
|
|
}
|
|
$html = $this->sanitizeOpdNamedHeaderFooterReferences($paperHtml);
|
|
}
|
|
|
|
// If separate header/footer fields are provided, always apply them around the resolved body.
|
|
if ($rawHeaderHtml !== '' || $rawFooterHtml !== '') {
|
|
$combinedHtml = $buildPageStyleBlock($rawHeaderHtml !== '', $rawFooterHtml !== '');
|
|
|
|
if ($rawHeaderHtml !== '') {
|
|
$renderedHeaderHtml = $this->applyOpdPdfTemplateTokens($rawHeaderHtml, $tokenVars);
|
|
if (!preg_match('/<\s*htmlpageheader\b/i', $renderedHeaderHtml)) {
|
|
$combinedHtml .= '<htmlpageheader name="myHeader">' . "\n" . $renderedHeaderHtml . "\n" . '</htmlpageheader>' . "\n";
|
|
} else {
|
|
$combinedHtml .= $renderedHeaderHtml . "\n";
|
|
}
|
|
}
|
|
|
|
if ($rawFooterHtml !== '') {
|
|
$renderedFooterHtml = $this->applyOpdPdfTemplateTokens($rawFooterHtml, $tokenVars);
|
|
if (!preg_match('/<\s*htmlpagefooter\b/i', $renderedFooterHtml)) {
|
|
$combinedHtml .= '<htmlpagefooter name="myFooter">' . "\n" . $renderedFooterHtml . "\n" . '</htmlpagefooter>' . "\n";
|
|
} else {
|
|
$combinedHtml .= $renderedFooterHtml . "\n";
|
|
}
|
|
}
|
|
|
|
$combinedHtml .= $html;
|
|
$html = $this->sanitizeOpdNamedHeaderFooterReferences($combinedHtml);
|
|
$rawHeaderHtml = '';
|
|
$rawFooterHtml = '';
|
|
}
|
|
|
|
if ($rawHeaderHtml !== '') {
|
|
$renderedHeaderHtml = $this->applyOpdPdfTemplateTokens($rawHeaderHtml, $tokenVars);
|
|
if (preg_match('/<\s*(htmlpageheader|sethtmlpageheader)\b/i', $renderedHeaderHtml) === 1) {
|
|
$prefixHtml .= $renderedHeaderHtml;
|
|
} else {
|
|
$mpdf->SetHTMLHeader($renderedHeaderHtml, 'O');
|
|
$mpdf->SetHTMLHeader($renderedHeaderHtml, 'E');
|
|
}
|
|
}
|
|
|
|
if ($rawFooterHtml !== '') {
|
|
$renderedFooterHtml = $this->applyOpdPdfTemplateTokens($rawFooterHtml, $tokenVars);
|
|
if (preg_match('/<\s*(htmlpagefooter|sethtmlpagefooter)\b/i', $renderedFooterHtml) === 1) {
|
|
$prefixHtml .= $renderedFooterHtml;
|
|
} else {
|
|
$mpdf->SetHTMLFooter($renderedFooterHtml, 'O');
|
|
$mpdf->SetHTMLFooter($renderedFooterHtml, 'E');
|
|
}
|
|
}
|
|
|
|
if ($prefixHtml !== '') {
|
|
$mpdf->WriteHTML($prefixHtml, HTMLParserMode::HTML_HEADER_BUFFER);
|
|
}
|
|
|
|
$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_blank_print', 'meta_only');
|
|
$url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId);
|
|
if ($printConfig['template'] !== '') {
|
|
$url .= '?template=' . urlencode($printConfig['template']);
|
|
} else {
|
|
$url .= '?layout=' . urlencode($printConfig['layout']);
|
|
}
|
|
return redirect()->to($url);
|
|
}
|
|
|
|
public function opd_blank_print(int $opdId)
|
|
{
|
|
$printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_print_format', 'header_meta');
|
|
$url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId);
|
|
if ($printConfig['template'] !== '') {
|
|
$url .= '?template=' . urlencode($printConfig['template']);
|
|
} else {
|
|
$url .= '?layout=' . urlencode($printConfig['layout']);
|
|
}
|
|
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);
|
|
if ($printConfig['template'] !== '') {
|
|
$url .= '?template=' . urlencode($printConfig['template']);
|
|
} else {
|
|
$url .= '?layout=' . urlencode($printConfig['layout']);
|
|
}
|
|
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 = $this->normalizeOpdTemplateId($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 ?? ''));
|
|
$doctorShortDescription = trim((string) ($doctor->doc_sign ?? $opd->doc_sign ?? ''));
|
|
$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'] : [];
|
|
|
|
// ── Build Hindi lookup maps from opd_dose_* master tables ──────────────
|
|
// opd_dose_frequency : dose_sign → dose_sign_hindi (day-type freq: Daily, Weekly…)
|
|
// opd_dose_shed : dose_show_sign → dose_show_desc (schedule abbrev: QD, BID, AC…)
|
|
// opd_dose_when : dose_sign → dose_sign_hindi (timing: EMPTY STOMACH, ONCE A DAY…)
|
|
$doseFreqHindiMap = [];
|
|
$doseShedHindiMap = [];
|
|
$doseWhenHindiMap = [];
|
|
|
|
if ($this->db->tableExists('opd_dose_frequency')) {
|
|
$rows = $this->db->table('opd_dose_frequency')
|
|
->select('dose_sign, dose_sign_hindi')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$k = strtolower(trim((string) ($r['dose_sign'] ?? '')));
|
|
$v = trim((string) ($r['dose_sign_hindi'] ?? ''));
|
|
if ($k !== '' && $v !== '') {
|
|
$doseFreqHindiMap[$k] = $v;
|
|
}
|
|
}
|
|
}
|
|
if (empty($doseFreqHindiMap)) {
|
|
$doseFreqHindiMap = [
|
|
'daily' => 'रोज', 'alternate day' => 'वैकल्पिक दिन', 'weekly' => 'हर हफ़्ते',
|
|
'weekly twice' => 'हफ़्ते दो बार में', 'weekly thrice' => 'हफ़्ते में तीन बार',
|
|
'fort night' => '2 हफ़्ते में', 'monthly' => 'महीने के',
|
|
'mon' => 'सोमवार', 'tue' => 'मंगलवार', 'wed' => 'बुधवार',
|
|
'thu' => 'गुरुवार', 'fri' => 'शुक्रवार', 'sat' => 'शनिवार', 'sun' => 'रविवार',
|
|
];
|
|
}
|
|
|
|
if ($this->db->tableExists('opd_dose_shed')) {
|
|
$rows = $this->db->table('opd_dose_shed')
|
|
->select('dose_show_sign, dose_show_desc')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$k = strtolower(trim((string) ($r['dose_show_sign'] ?? '')));
|
|
$v = trim((string) ($r['dose_show_desc'] ?? ''));
|
|
if ($k !== '' && $v !== '' && $v !== '-') {
|
|
$doseShedHindiMap[$k] = $v;
|
|
}
|
|
}
|
|
}
|
|
if (empty($doseShedHindiMap)) {
|
|
// Fallback mirrors opd_dose_shed table (dose_show_sign → dose_show_desc)
|
|
$doseShedHindiMap = [
|
|
'q (every)' => 'प्रत्येक',
|
|
'qd (everday)' => 'प्रतिदिन',
|
|
'qod (alternate day)' => 'हर एक दिन छोड़कर',
|
|
'qh (every hour)' => 'हर घंटे',
|
|
'sos (as when required)' => 'जरूरत पड़ने पर',
|
|
'ac (before meal)' => 'भोजन से पहले',
|
|
'pc (after meal)' => 'भोजन के बाद',
|
|
'bid (two time a day)' => 'दिन में दो बार',
|
|
'tid (three time a day)' => 'दिन में तीन बार',
|
|
'qid (four time a day)' => 'दिन में चार बार',
|
|
'od (one time a day)' => 'हर सुबह नाश्ते के बाद',
|
|
'bt (at sleep)' => 'सोते समय',
|
|
'bbf (before breakfast)' => 'नाश्ते से पहले',
|
|
'hs (after dinner)' => 'रात को भोजन के बाद',
|
|
'bl (before lunch)' => 'रात को भोजन से पहले',
|
|
'al (after lunch)' => 'रात को भोजन से पहले',
|
|
'tw (two time a week)' => 'हफ्ते में दो बार',
|
|
'qam (every morning)' => 'हर सुबह',
|
|
'qpm (every night)' => 'हर रात',
|
|
'q4h (every four hour)' => 'हर चार घंटों में',
|
|
'prn (as per need)' => 'जरूरत के मुताबिक',
|
|
];
|
|
}
|
|
|
|
if ($this->db->tableExists('opd_dose_when')) {
|
|
$rows = $this->db->table('opd_dose_when')
|
|
->select('dose_sign, dose_sign_hindi')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$k = strtolower(trim((string) ($r['dose_sign'] ?? '')));
|
|
$v = trim((string) ($r['dose_sign_hindi'] ?? ''));
|
|
if ($k !== '' && $v !== '') {
|
|
$doseWhenHindiMap[$k] = $v;
|
|
}
|
|
}
|
|
}
|
|
if (empty($doseWhenHindiMap)) {
|
|
$doseWhenHindiMap = [
|
|
'empty stomach' => 'सुबह खाली पेट', 'once a day' => 'दिन में एक बार',
|
|
'once a week' => 'सप्ताह मेँ एक बार', 'once a fiften day' => 'पन्द्रह दिन में एक बार',
|
|
'only now' => 'केवल अभी',
|
|
'morning 8 unit--- evening 5 unit' => 'सुबह 8 यूनिट------- शाम 5 यूनिट',
|
|
];
|
|
}
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
|
|
$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['med_name'] ?? $med['drug_name'] ?? $med['medicine_name'] ?? $med['item_name'] ?? ''));
|
|
$generic = trim((string) ($med['genericname'] ?? $med['generic_name'] ?? $med['salt_name'] ?? ''));
|
|
$dose = trim((string) ($med['dosage_label'] ?? $med['dosage'] ?? $med['drug_dose'] ?? $med['dose'] ?? ''));
|
|
$freq = trim((string) ($med['dosage_freq_label'] ?? $med['dosage_freq'] ?? $med['drug_freq'] ?? $med['frequency'] ?? ''));
|
|
$days = trim((string) ($med['no_of_days'] ?? $med['drug_day'] ?? $med['days'] ?? ''));
|
|
$when = trim((string) ($med['dosage_when_label'] ?? $med['dosage_when'] ?? ''));
|
|
$remark = trim((string) ($med['remark'] ?? ''));
|
|
$inst = trim(($when !== '' ? $when : '') . ($remark !== '' ? ($when !== '' ? ' | ' : '') . $remark : ''));
|
|
$doseFreqText = trim($dose . ' ' . $freq);
|
|
|
|
// Hindi for freq/schedule:
|
|
// 1) exact match against doseShedHindiMap (DB: opd_dose_shed)
|
|
// 2) extract "ABBR (SIGN)" part up to closing ')' — handles "OD (ONE TIME A DAY) Daily"
|
|
// 3) exact match in doseFreqHindiMap (DB: opd_dose_frequency)
|
|
// 4) translated fallback
|
|
$freqKey = strtolower(trim($freq));
|
|
$doseFreqLocal = $doseShedHindiMap[$freqKey] ?? $doseFreqHindiMap[$freqKey] ?? '';
|
|
if ($doseFreqLocal === '' && preg_match('/^(.+?\))/u', $freq, $_m) === 1) {
|
|
$signKey = strtolower(trim($_m[1]));
|
|
$doseFreqLocal = $doseShedHindiMap[$signKey] ?? '';
|
|
}
|
|
if ($doseFreqLocal === '' && $doseFreqText !== '') {
|
|
$fallback = $this->translateToLocalPatientText($doseFreqText);
|
|
if (strtolower($fallback) !== strtolower($doseFreqText)) {
|
|
$doseFreqLocal = $fallback;
|
|
}
|
|
}
|
|
|
|
// Hindi for when/timing (opd_dose_when → opd_dose_shed → fallback)
|
|
$whenKey = strtolower(trim($when));
|
|
$instLocal = $doseWhenHindiMap[$whenKey] ?? $doseShedHindiMap[$whenKey] ?? '';
|
|
if ($instLocal === '' && preg_match('/^(.+?\))/u', $when, $_m) === 1) {
|
|
$signKey = strtolower(trim($_m[1]));
|
|
$instLocal = $doseWhenHindiMap[$signKey] ?? $doseShedHindiMap[$signKey] ?? '';
|
|
}
|
|
if ($instLocal === '' && $when !== '') {
|
|
$fallback = $this->translateToLocalPatientText($when);
|
|
if (strtolower($fallback) !== strtolower($when)) {
|
|
$instLocal = $fallback;
|
|
}
|
|
}
|
|
|
|
$nameHtml = esc($name);
|
|
if ($generic !== '') {
|
|
$nameHtml .= '<div style="font-size:11px;color:#444;">Salt/Generic: ' . esc($generic) . '</div>';
|
|
}
|
|
$medicalHtml .= '<tr>'
|
|
. '<td>' . $i . '</td>'
|
|
. '<td>' . $nameHtml . '</td>'
|
|
. '<td>' . esc($doseFreqText)
|
|
. ($doseFreqLocal !== '' ? ('<div style="font-size:11px;color:#444;line-height:1.4;" lang="hi">' . esc($doseFreqLocal) . '</div>') : '')
|
|
. '</td>'
|
|
. '<td>' . esc($days) . '</td>'
|
|
. '<td>' . esc($inst)
|
|
. ($instLocal !== '' ? ('<div style="font-size:11px;color:#444;line-height:1.4;" lang="hi">' . esc($instLocal) . '</div>') : '')
|
|
. '</td>'
|
|
. '</tr>';
|
|
}
|
|
$medicalHtml .= '</table>';
|
|
}
|
|
|
|
$rxRead = static function (array $row, array $keys): string {
|
|
foreach ($keys as $key) {
|
|
if (array_key_exists($key, $row)) {
|
|
$value = trim((string) ($row[$key] ?? ''));
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
};
|
|
|
|
$rxInvestigation = $rxRead($rx, ['investigation', 'Investigations', 'investigations', '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 = $rxRead($rx, ['advice', 'Advice', 'prescription_advice', 'advice_notes', 'advice_note']);
|
|
$rxAdviceLocal = '';
|
|
if ($rxAdvice === '' && !empty($data['rx_advices']) && is_array($data['rx_advices'])) {
|
|
$parts = [];
|
|
$partsLocal = [];
|
|
foreach ($data['rx_advices'] as $adv) {
|
|
$txt = trim((string) ($adv['advice_txt'] ?? $adv['advice'] ?? ''));
|
|
if ($txt !== '') {
|
|
$parts[] = $txt;
|
|
}
|
|
|
|
$localTxt = trim((string) ($adv['advice_hindi'] ?? ($adv['advice_txt_hindi'] ?? '')));
|
|
if ($localTxt !== '') {
|
|
$partsLocal[] = $localTxt;
|
|
}
|
|
}
|
|
$rxAdvice = implode(', ', $parts);
|
|
if (!empty($partsLocal)) {
|
|
$rxAdviceLocal = implode(' | ', $partsLocal);
|
|
}
|
|
}
|
|
|
|
$complaintText = $rxRead($rx, ['complaints', 'Complaint', 'complaint', 'chief_complaint', 'chief_complaints']);
|
|
if ($complaintText === '') {
|
|
$complaintText = trim((string) ($opd->complaints ?? ($opd->complaint ?? '')));
|
|
}
|
|
|
|
$provisionalDiagnosisText = $rxRead($rx, ['Provisional_diagnosis', 'provisional_diagnosis', 'provisional_diagnosis_text']);
|
|
$diagnosisText = $provisionalDiagnosisText !== ''
|
|
? $provisionalDiagnosisText
|
|
: $rxRead($rx, ['diagnosis', 'Diagnosis', 'final_diagnosis', 'dx']);
|
|
|
|
if ($diagnosisText === '') {
|
|
$diagnosisText = implode(', ', is_array($data['selected_morbidities'] ?? null) ? ($data['selected_morbidities'] ?? []) : []);
|
|
}
|
|
|
|
$findingExaminationsText = $rxRead($rx, ['Finding_Examinations', 'finding_examinations', 'finding', 'findings', 'examination']);
|
|
$prescriberRemarksText = $rxRead($rx, ['Prescriber_Remarks', 'prescriber_remarks', 'remarks', 'remark', 'doctor_note']);
|
|
$nextVisitText = $rxRead($rx, ['next_visit', 'next_visit_date', 'follow_up', 'followup', 'review_after']);
|
|
$complaintLocal = $this->translateToLocalPatientText($complaintText);
|
|
$diagnosisLocal = $this->translateToLocalPatientText($diagnosisText);
|
|
$investigationLocal = $this->translateToLocalPatientText($rxInvestigation);
|
|
if ($rxAdviceLocal === '') {
|
|
$rxAdviceLocal = $this->translateToLocalPatientText($rxAdvice);
|
|
}
|
|
|
|
if (strtolower(trim($complaintLocal)) === strtolower(trim($complaintText))) {
|
|
$complaintLocal = '';
|
|
}
|
|
if (strtolower(trim($diagnosisLocal)) === strtolower(trim($diagnosisText))) {
|
|
$diagnosisLocal = '';
|
|
}
|
|
if (strtolower(trim($investigationLocal)) === strtolower(trim($rxInvestigation))) {
|
|
$investigationLocal = '';
|
|
}
|
|
if (strtolower(trim($rxAdviceLocal)) === strtolower(trim($rxAdvice))) {
|
|
$rxAdviceLocal = '';
|
|
}
|
|
|
|
$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,
|
|
'short_description' => $doctorShortDescription,
|
|
'doctor_short_description' => $doctorShortDescription,
|
|
'opd_fee_desc' => $opdFeeDesc,
|
|
'total_no_visit' => $totalNoVisit,
|
|
'last_opdvisit_date' => $lastVisitText,
|
|
'str_opd_book_date' => $bookTime,
|
|
'Complaint' => $complaintText,
|
|
'complaint' => $complaintText,
|
|
'complaints' => $complaintText,
|
|
'Complaint_local' => $complaintLocal,
|
|
'diagnosis' => $diagnosisText,
|
|
'Diagnosis' => $diagnosisText,
|
|
'diagnosis_local' => $diagnosisLocal,
|
|
'Provisional_diagnosis' => $provisionalDiagnosisText,
|
|
'provisional_diagnosis' => $provisionalDiagnosisText,
|
|
'investigation' => $rxInvestigation,
|
|
'investigation_local' => $investigationLocal,
|
|
'medical' => $medicalHtml,
|
|
'doctor' => '<p style="text-align:right;">Dr. ' . esc((string) ($opd->doc_name ?? '')) . '</p>',
|
|
'top_content' => '',
|
|
'vital_content' => implode(' | ', $vitals),
|
|
'Finding_Examinations' => $findingExaminationsText,
|
|
'finding_examinations' => $findingExaminationsText,
|
|
'Prescriber_Remarks' => $prescriberRemarksText,
|
|
'advice' => $rxAdvice,
|
|
'advice_local' => $rxAdviceLocal,
|
|
'next_visit' => $nextVisitText,
|
|
'refer_to' => (string) ($rx['refer_to'] ?? ''),
|
|
'painscale' => '',
|
|
'painscale_img' => '',
|
|
'morbidities' => implode(', ', is_array($data['selected_morbidities'] ?? null) ? ($data['selected_morbidities'] ?? []) : []),
|
|
'Addiction' => '',
|
|
'Complication' => '',
|
|
];
|
|
}
|
|
|
|
private function translateToLocalPatientText(string $input): string
|
|
{
|
|
$text = trim($input);
|
|
if ($text === '') {
|
|
return '';
|
|
}
|
|
|
|
$map = [
|
|
'before food' => 'भोजन से पहले',
|
|
'after food' => 'भोजन के बाद',
|
|
'after breakfast' => 'नाश्ते के बाद',
|
|
'before breakfast' => 'नाश्ते से पहले',
|
|
'after lunch' => 'दोपहर के भोजन के बाद',
|
|
'before lunch' => 'दोपहर के भोजन से पहले',
|
|
'after dinner' => 'रात के भोजन के बाद',
|
|
'before dinner' => 'रात के भोजन से पहले',
|
|
'morning' => 'सुबह',
|
|
'afternoon' => 'दोपहर',
|
|
'evening' => 'शाम',
|
|
'night' => 'रात',
|
|
// Dose schedules (longer phrases first for uksort)
|
|
'one time a day' => 'दिन में एक बार',
|
|
'two times a day' => 'दिन में दो बार',
|
|
'three times a day' => 'दिन में तीन बार',
|
|
'four times a day' => 'दिन में चार बार',
|
|
'twice a day' => 'दिन में दो बार',
|
|
'thrice a day' => 'दिन में तीन बार',
|
|
'once daily' => 'दिन में एक बार',
|
|
'twice daily' => 'दिन में दो बार',
|
|
'thrice daily' => 'दिन में तीन बार',
|
|
'at bed time' => 'सोते समय',
|
|
'at sleep' => 'सोते समय',
|
|
'at bedtime' => 'सोते समय',
|
|
'with food' => 'भोजन के साथ',
|
|
'with water' => 'पानी के साथ',
|
|
'with milk' => 'दूध के साथ',
|
|
'empty stomach' => 'खाली पेट',
|
|
'before meal' => 'भोजन से पहले',
|
|
'after meal' => 'भोजन के बाद',
|
|
'as required' => 'जरूरत पड़ने पर',
|
|
'as needed' => 'जरूरत पड़ने पर',
|
|
'when required' => 'जरूरत पड़ने पर',
|
|
'every 4 hours' => 'हर 4 घंटे',
|
|
'every 6 hours' => 'हर 6 घंटे',
|
|
'every 8 hours' => 'हर 8 घंटे',
|
|
'every 12 hours' => 'हर 12 घंटे',
|
|
'daily' => 'रोज',
|
|
'weekly' => 'साप्ताहिक',
|
|
'monthly' => 'मासिक',
|
|
'alternate day' => 'एक दिन छोड़कर',
|
|
'for' => 'के लिए',
|
|
'days' => 'दिन',
|
|
'day' => 'दिन',
|
|
'tab' => 'टैबलेट',
|
|
'cap' => 'कैप्सूल',
|
|
'syp' => 'सिरप',
|
|
'inj' => 'इंजेक्शन',
|
|
'apply' => 'लगाएं',
|
|
'continue' => 'जारी रखें',
|
|
'rest' => 'आराम करें',
|
|
'hydrate' => 'पर्याप्त पानी लें',
|
|
'avoid oily food' => 'तैलीय भोजन से बचें',
|
|
'avoid spicy food' => 'मसालेदार भोजन से बचें',
|
|
'follow up' => 'फॉलो-अप करें',
|
|
'review' => 'पुनः जांच',
|
|
'advice' => 'सलाह',
|
|
'investigation' => 'जांच',
|
|
'diagnosis' => 'निदान',
|
|
'fever' => 'बुखार',
|
|
'cough' => 'खांसी',
|
|
'cold' => 'जुकाम',
|
|
'headache' => 'सिर दर्द',
|
|
'abdominal pain' => 'पेट दर्द',
|
|
];
|
|
|
|
uksort($map, static function ($a, $b) {
|
|
return strlen((string) $b) <=> strlen((string) $a);
|
|
});
|
|
|
|
$translated = $text;
|
|
foreach ($map as $en => $local) {
|
|
$translated = preg_replace('/\b' . preg_quote($en, '/') . '\b/i', $local, $translated) ?? $translated;
|
|
}
|
|
|
|
$translated = preg_replace('/\s+/', ' ', trim($translated)) ?? trim($translated);
|
|
return $translated;
|
|
}
|
|
|
|
/**
|
|
* @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 getOpdPaperSettingStorageKey(string $templateName = ''): string
|
|
{
|
|
$templateName = $this->normalizeOpdTemplateId($templateName);
|
|
if ($templateName === '') {
|
|
return self::OPD_PAPER_SETTING_KEY;
|
|
}
|
|
|
|
// Keep key short to avoid s_name length failures in legacy schemas.
|
|
return self::OPD_PAPER_SETTING_KEY_PREFIX . substr(md5($templateName), 0, 20);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getOpdPaperSettingCandidateKeys(string $templateName = ''): array
|
|
{
|
|
$templateName = $this->normalizeOpdTemplateId($templateName);
|
|
if ($templateName === '') {
|
|
return [self::OPD_PAPER_SETTING_KEY];
|
|
}
|
|
|
|
$newKey = $this->getOpdPaperSettingStorageKey($templateName);
|
|
$legacyKey = self::OPD_PAPER_SETTING_KEY_PREFIX_LEGACY . $templateName;
|
|
|
|
$keys = [$newKey, $legacyKey];
|
|
return array_values(array_unique($keys));
|
|
}
|
|
|
|
private function readOpdPaperSettingPayloadByKey(string $storageKey): string
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return '';
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->where('s_name', $storageKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
return trim((string) ($row['s_value'] ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function getOpdPaperPrintSetting(string $templateName = ''): array
|
|
{
|
|
$defaults = [
|
|
'page_size' => 'A4',
|
|
'custom_width_mm' => 210,
|
|
'custom_height_mm' => 297,
|
|
'page_margin_top_cm' => 6.1,
|
|
'page_margin_bottom_cm' => 2.5,
|
|
'page_margin_left_cm' => 0.7,
|
|
'page_margin_right_cm' => 0.7,
|
|
'margin_header_cm' => 0.5,
|
|
'margin_footer_cm' => 1.5,
|
|
'paper_html_content' => '',
|
|
'header_html' => '',
|
|
'footer_html' => '',
|
|
];
|
|
|
|
$storageKey = $this->getOpdPaperSettingStorageKey($templateName);
|
|
$raw = '';
|
|
$candidateKeys = $this->getOpdPaperSettingCandidateKeys($templateName);
|
|
error_log('[OPD_PAPER_READ_DEBUG] Template: ' . $templateName . ' | CandidateKeys: ' . implode(', ', $candidateKeys));
|
|
|
|
foreach ($candidateKeys as $candidateKey) {
|
|
$raw = $this->readOpdPaperSettingPayloadByKey($candidateKey);
|
|
if ($raw !== '') {
|
|
error_log('[OPD_PAPER_READ_DEBUG] Found data in key: ' . $candidateKey . ' | PayloadLen: ' . strlen($raw));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($raw === '' && $storageKey !== self::OPD_PAPER_SETTING_KEY) {
|
|
// Backward compatibility with legacy global settings.
|
|
$raw = $this->readOpdPaperSettingPayloadByKey(self::OPD_PAPER_SETTING_KEY);
|
|
if ($raw !== '') {
|
|
error_log('[OPD_PAPER_READ_DEBUG] Fell back to global key | PayloadLen: ' . strlen($raw));
|
|
}
|
|
}
|
|
if ($raw === '') {
|
|
error_log('[OPD_PAPER_READ_DEBUG] No data found, using defaults');
|
|
return $defaults;
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
if (! is_array($decoded)) {
|
|
error_log('[OPD_PAPER_READ_DEBUG] JSON decode failed');
|
|
return $defaults;
|
|
}
|
|
|
|
return array_merge($defaults, $decoded);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $settings
|
|
*/
|
|
private function saveOpdPaperPrintSetting(array $settings, string $templateName = ''): bool
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return false;
|
|
}
|
|
|
|
$payload = json_encode($settings, JSON_UNESCAPED_UNICODE);
|
|
if ($payload === false) {
|
|
return false;
|
|
}
|
|
|
|
$storageKey = $this->getOpdPaperSettingStorageKey($templateName);
|
|
|
|
// Debug logging - to be removed after testing
|
|
error_log('[OPD_PAPER_SAVE_DEBUG] Template: ' . $templateName . ' | StorageKey: ' . $storageKey . ' | PayloadLen: ' . strlen($payload));
|
|
|
|
$this->ensureHospitalSettingValueCanStoreLargePayload();
|
|
|
|
$existing = $this->db->table('hospital_setting')
|
|
->select('id')
|
|
->where('s_name', $storageKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (! empty($existing['id'])) {
|
|
$ok = $this->db->table('hospital_setting')
|
|
->where('id', (int) $existing['id'])
|
|
->update(['s_value' => $payload]);
|
|
|
|
if (! $ok) {
|
|
error_log('[OPD_PAPER_SAVE_DEBUG] UPDATE failed for key: ' . $storageKey);
|
|
return false;
|
|
}
|
|
|
|
$persisted = $this->isOpdPaperSettingPayloadPersisted($payload, $storageKey);
|
|
error_log('[OPD_PAPER_SAVE_DEBUG] UPDATE done, persisted check: ' . ($persisted ? 'true' : 'false'));
|
|
return $persisted;
|
|
}
|
|
|
|
$ok = $this->db->table('hospital_setting')->insert([
|
|
's_name' => $storageKey,
|
|
's_value' => $payload,
|
|
]);
|
|
|
|
if (! $ok) {
|
|
error_log('[OPD_PAPER_SAVE_DEBUG] INSERT failed for key: ' . $storageKey);
|
|
return false;
|
|
}
|
|
|
|
$persisted = $this->isOpdPaperSettingPayloadPersisted($payload, $storageKey);
|
|
error_log('[OPD_PAPER_SAVE_DEBUG] INSERT done, persisted check: ' . ($persisted ? 'true' : 'false'));
|
|
return $persisted;
|
|
}
|
|
|
|
private function ensureHospitalSettingValueCanStoreLargePayload(): void
|
|
{
|
|
try {
|
|
$row = $this->db->query("SHOW COLUMNS FROM hospital_setting LIKE 's_value'")->getRowArray();
|
|
$type = strtolower(trim((string) ($row['Type'] ?? '')));
|
|
if ($type === '') {
|
|
return;
|
|
}
|
|
|
|
// Legacy schema uses varchar(200), which truncates template HTML payloads.
|
|
if (str_starts_with($type, 'varchar(')) {
|
|
$this->db->query('ALTER TABLE hospital_setting MODIFY s_value LONGTEXT CHARACTER SET utf32 COLLATE utf32_unicode_ci DEFAULT NULL');
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Keep save flow non-fatal; caller validates persistence right after save.
|
|
}
|
|
}
|
|
|
|
private function isOpdPaperSettingPayloadPersisted(string $payload, string $storageKey): bool
|
|
{
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->where('s_name', $storageKey)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
return ((string) ($row['s_value'] ?? '')) === $payload;
|
|
}
|
|
|
|
private function clearOpdPaperPrintSetting(string $templateName = ''): bool
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return false;
|
|
}
|
|
|
|
$deleted = false;
|
|
foreach ($this->getOpdPaperSettingCandidateKeys($templateName) as $storageKey) {
|
|
$ok = (bool) $this->db->table('hospital_setting')
|
|
->where('s_name', $storageKey)
|
|
->delete();
|
|
$deleted = $deleted || $ok;
|
|
}
|
|
|
|
return $deleted;
|
|
}
|
|
|
|
private function normalizeOpdPrintMarginCm($value, float $default): float
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return $default;
|
|
}
|
|
|
|
$number = (float) $value;
|
|
if (! is_finite($number)) {
|
|
$number = $default;
|
|
}
|
|
|
|
$number = max(0.0, min(25.0, $number));
|
|
|
|
return round($number, 2);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, string>
|
|
*/
|
|
private function buildOpdPdfTokenVars(array $data): array
|
|
{
|
|
$legacy = $this->buildLegacyDashboardTemplateVars($data);
|
|
$tokens = [];
|
|
foreach ($legacy as $key => $value) {
|
|
if (is_scalar($value)) {
|
|
$tokens[(string) $key] = (string) $value;
|
|
}
|
|
}
|
|
|
|
// Add case-insensitive token aliases for legacy template compatibility.
|
|
foreach ($tokens as $key => $value) {
|
|
$lower = strtolower((string) $key);
|
|
if (! array_key_exists($lower, $tokens)) {
|
|
$tokens[$lower] = $value;
|
|
}
|
|
|
|
$ucfirst = ucfirst($lower);
|
|
if (! array_key_exists($ucfirst, $tokens)) {
|
|
$tokens[$ucfirst] = $value;
|
|
}
|
|
}
|
|
|
|
$formatBlock = static function (string $label, string $value, bool $lineBreakAfterLabel = false): string {
|
|
$value = trim($value);
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
|
|
$headingHtml = '<span style="font-weight:700;font-size:13px;line-height:1.35;">' . $label . ' :</span>';
|
|
|
|
if ($lineBreakAfterLabel) {
|
|
return '<div style="margin-bottom:4px;">' . $headingHtml . '<br/>' . $value . '</div>';
|
|
}
|
|
|
|
return '<div style="margin-bottom:4px;">' . $headingHtml . ' ' . $value . '</div>';
|
|
};
|
|
|
|
$tokens['vital_content_raw'] = (string) ($tokens['vital_content'] ?? '');
|
|
$tokens['Complaint_raw'] = (string) ($tokens['Complaint'] ?? '');
|
|
$tokens['diagnosis_raw'] = (string) ($tokens['diagnosis'] ?? '');
|
|
$tokens['Provisional_diagnosis_raw'] = (string) ($tokens['Provisional_diagnosis'] ?? '');
|
|
$tokens['Finding_Examinations_raw'] = (string) ($tokens['Finding_Examinations'] ?? '');
|
|
$tokens['investigation_raw'] = (string) ($tokens['investigation'] ?? '');
|
|
$tokens['Prescriber_Remarks_raw'] = (string) ($tokens['Prescriber_Remarks'] ?? '');
|
|
$tokens['advice_raw'] = (string) ($tokens['advice'] ?? '');
|
|
$tokens['next_visit_raw'] = (string) ($tokens['next_visit'] ?? '');
|
|
|
|
$tokens['vital_content'] = $formatBlock('Vitals', (string) ($tokens['vital_content'] ?? ''), false);
|
|
$tokens['Complaint'] = $formatBlock('Complaint', (string) ($tokens['Complaint_raw'] ?? ''));
|
|
$tokens['complaint'] = $tokens['Complaint'];
|
|
$tokens['diagnosis'] = $formatBlock('Diagnosis', (string) ($tokens['diagnosis_raw'] ?? ''));
|
|
$tokens['Diagnosis'] = $tokens['diagnosis'];
|
|
$tokens['Provisional_diagnosis'] = $formatBlock('Provisional Diagnosis', (string) ($tokens['Provisional_diagnosis_raw'] ?? ''));
|
|
$tokens['provisional_diagnosis'] = $tokens['Provisional_diagnosis'];
|
|
$tokens['Finding_Examinations'] = $formatBlock('Finding / Examination', (string) ($tokens['Finding_Examinations_raw'] ?? ''));
|
|
$tokens['finding_examinations'] = $tokens['Finding_Examinations'];
|
|
$tokens['investigation'] = $formatBlock('Investigation Advised', (string) ($tokens['investigation_raw'] ?? ''));
|
|
$tokens['Prescriber_Remarks'] = $formatBlock('Remarks', (string) ($tokens['Prescriber_Remarks_raw'] ?? ''));
|
|
$adviceRaw = trim((string) ($tokens['advice_raw'] ?? ''));
|
|
$adviceLocalText = trim((string) ($tokens['advice_local'] ?? ''));
|
|
if ($adviceRaw !== '') {
|
|
$advLabelHtml = '<span style="font-weight:700;font-size:13px;line-height:1.35;">सलाह (Advice) :</span>';
|
|
$adviceHtml = '<div style="margin-bottom:4px;">';
|
|
if ($adviceLocalText !== '') {
|
|
$adviceHtml .= $advLabelHtml . ' <span lang="hi">' . $adviceLocalText . '</span>';
|
|
$adviceHtml .= '<div style="font-size:10px;color:#666;margin-top:1px;">(Advice: ' . $adviceRaw . ')</div>';
|
|
} else {
|
|
$adviceHtml .= $advLabelHtml . ' ' . $adviceRaw;
|
|
}
|
|
$adviceHtml .= '</div>';
|
|
$tokens['advice'] = $adviceHtml;
|
|
} else {
|
|
$tokens['advice'] = '';
|
|
}
|
|
$tokens['next_visit'] = $formatBlock('Next Visit', (string) ($tokens['next_visit_raw'] ?? ''));
|
|
|
|
$medicalHtml = trim((string) ($tokens['medical'] ?? ''));
|
|
$tokens['Rx'] = $medicalHtml !== ''
|
|
? '<div style="margin:8px 0 6px 0;"><div style="font-weight:700;font-size:24px;line-height:1.2;margin-bottom:8px;">Rx :</div>' . $medicalHtml . '</div>'
|
|
: '';
|
|
$tokens['rx'] = $tokens['Rx'];
|
|
|
|
$tokens['VitalsBlock'] = (string) ($tokens['vital_content'] ?? '');
|
|
$tokens['ComplaintBlock'] = (string) ($tokens['Complaint'] ?? '');
|
|
$tokens['DiagnosisBlock'] = (string) ($tokens['diagnosis'] ?? '');
|
|
$tokens['ProvisionalDiagnosisBlock'] = (string) ($tokens['Provisional_diagnosis'] ?? '');
|
|
$tokens['FindingBlock'] = (string) ($tokens['Finding_Examinations'] ?? '');
|
|
$tokens['InvestigationBlock'] = (string) ($tokens['investigation'] ?? '');
|
|
$tokens['RemarksBlock'] = (string) ($tokens['Prescriber_Remarks'] ?? '');
|
|
$tokens['AdviceBlock'] = (string) ($tokens['advice'] ?? '');
|
|
$tokens['NextVisitBlock'] = (string) ($tokens['next_visit'] ?? '');
|
|
|
|
$tokens['RxTable'] = $medicalHtml;
|
|
$tokens['rx_table'] = $medicalHtml;
|
|
|
|
$fullBlocks = array_values(array_filter([
|
|
(string) ($tokens['VitalsBlock'] ?? ''),
|
|
(string) ($tokens['ComplaintBlock'] ?? ''),
|
|
(string) ($tokens['DiagnosisBlock'] ?? ''),
|
|
(string) ($tokens['ProvisionalDiagnosisBlock'] ?? ''),
|
|
(string) ($tokens['FindingBlock'] ?? ''),
|
|
(string) ($tokens['Rx'] ?? ''),
|
|
(string) ($tokens['InvestigationBlock'] ?? ''),
|
|
(string) ($tokens['RemarksBlock'] ?? ''),
|
|
(string) ($tokens['AdviceBlock'] ?? ''),
|
|
(string) ($tokens['NextVisitBlock'] ?? ''),
|
|
], static fn ($value): bool => trim((string) $value) !== ''));
|
|
|
|
$tokens['RxFullBlock'] = implode("\n", $fullBlocks);
|
|
$tokens['rx_full_block'] = $tokens['RxFullBlock'];
|
|
|
|
// Load hospital settings from database
|
|
$hName = $this->readSettingValueFromDb('H_Name');
|
|
$hAddress1 = $this->readSettingValueFromDb('H_address_1');
|
|
$hAddress2 = $this->readSettingValueFromDb('H_address_2');
|
|
$hPhone = $this->readSettingValueFromDb('H_phone_No');
|
|
$hEmail = $this->readSettingValueFromDb('H_Email');
|
|
$hLogo = $this->readSettingValueFromDb('H_logo');
|
|
|
|
// Try constants as fallback if database values are empty
|
|
if ($hName === '') {
|
|
$hName = defined('H_Name') ? (string) constant('H_Name') : '';
|
|
}
|
|
if ($hAddress1 === '') {
|
|
$hAddress1 = defined('H_address_1') ? (string) constant('H_address_1') : '';
|
|
}
|
|
if ($hAddress2 === '') {
|
|
$hAddress2 = defined('H_address_2') ? (string) constant('H_address_2') : '';
|
|
}
|
|
if ($hPhone === '') {
|
|
$hPhone = defined('H_phone_No') ? (string) constant('H_phone_No') : '';
|
|
}
|
|
if ($hEmail === '') {
|
|
$hEmail = defined('H_Email') ? (string) constant('H_Email') : '';
|
|
}
|
|
if ($hLogo === '') {
|
|
$hLogo = defined('H_logo') ? (string) constant('H_logo') : '';
|
|
}
|
|
|
|
$tokens['H_Name'] = $hName;
|
|
$tokens['H_address_1'] = $hAddress1;
|
|
$tokens['H_address_2'] = $hAddress2;
|
|
$tokens['H_phone_No'] = $hPhone;
|
|
$tokens['H_Email'] = $hEmail;
|
|
$tokens['H_logo'] = $hLogo;
|
|
$tokens['H_logo_abs'] = $hLogo !== '' ? FCPATH . 'assets/images/' . $hLogo : '';
|
|
|
|
$tokens['hospital_name'] = $hName;
|
|
$tokens['hospital_address'] = trim($hAddress1 . ', ' . $hAddress2, ', ');
|
|
$tokens['hospital_phone'] = $hPhone;
|
|
$tokens['hospital_email'] = $hEmail;
|
|
$tokens['print_time'] = date('d-m-Y H:i:s');
|
|
$tokens['current_date'] = date('d-m-Y');
|
|
$tokens['doctor_name'] = trim((string) ($tokens['doctor_name'] ?? (($data['opd_master'][0]->doc_name ?? '') ?? '')));
|
|
|
|
$shortDescriptionRaw = trim((string) ($tokens['short_description'] ?? ''));
|
|
$shortDescriptionHtml = $shortDescriptionRaw !== ''
|
|
? nl2br(htmlspecialchars($shortDescriptionRaw, ENT_QUOTES, 'UTF-8'))
|
|
: '';
|
|
$tokens['short_description_raw'] = $shortDescriptionRaw;
|
|
$tokens['doctor_short_description_raw'] = $shortDescriptionRaw;
|
|
$tokens['short_description'] = $shortDescriptionHtml;
|
|
$tokens['doctor_short_description'] = $shortDescriptionHtml;
|
|
|
|
$tokens['doctor_sign_html'] = nl2br((string) ($data['opd_master'][0]->doc_sign ?? ''));
|
|
$tokens['content'] = (string) ($tokens['content'] ?? '');
|
|
|
|
$adviceLocal = trim((string) ($tokens['advice_local'] ?? ''));
|
|
$tokens['advice_local_line'] = $adviceLocal !== ''
|
|
? ('<br><strong>सलाह:</strong> ' . $adviceLocal)
|
|
: '';
|
|
|
|
return $tokens;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $vars
|
|
*/
|
|
private function applyOpdPdfTemplateTokens(string $templateHtml, array $vars): string
|
|
{
|
|
if ($templateHtml === '') {
|
|
return '';
|
|
}
|
|
|
|
$templateHtml = $this->normalizeLegacyOpdPaperTemplate($templateHtml);
|
|
|
|
$replace = [];
|
|
foreach ($vars as $key => $value) {
|
|
$replace['{{' . $key . '}}'] = $value;
|
|
$replace['{' . $key . '}'] = $value;
|
|
}
|
|
|
|
return strtr($templateHtml, $replace);
|
|
}
|
|
|
|
private function normalizeLegacyOpdPaperTemplate(string $html): string
|
|
{
|
|
$out = str_replace(["\r\n", "\r"], "\n", $html);
|
|
|
|
$mapPatterns = [
|
|
'/<\?=\s*H_Name\s*\?>/i' => '{{H_Name}}',
|
|
'/<\?=\s*H_address_1\s*\?>/i' => '{{H_address_1}}',
|
|
'/<\?=\s*H_address_2\s*\?>/i' => '{{H_address_2}}',
|
|
'/<\?=\s*H_phone_No\s*\?>/i' => '{{H_phone_No}}',
|
|
'/<\?=\s*H_Email\s*\?>/i' => '{{H_Email}}',
|
|
'/<\?=\s*H_logo\s*\?>/i' => '{{H_logo}}',
|
|
'/<\?=\s*\$opd_master\[0\]->doc_name\s*\?>/i' => '{{doctor_name}}',
|
|
'/<\?=\s*nl2br\(\s*\$opd_master\[0\]->doc_sign\s*\)\s*\?>/i' => '{{doctor_sign_html}}',
|
|
'/<\?=\s*date\(\s*[\"\']d-m-Y\s+H:i:s[\"\']\s*\)\s*\?>/i' => '{{print_time}}',
|
|
'/<\?=\s*\$content\s*\?>/i' => '{{content}}',
|
|
'/<\?=\s*\$content\s*;?\s*\?>/i' => '{{content}}',
|
|
'/<\?=\s*\$\w+\s*\?>/i' => '',
|
|
];
|
|
|
|
foreach ($mapPatterns as $pattern => $replacement) {
|
|
$out = (string) preg_replace($pattern, $replacement, $out);
|
|
}
|
|
|
|
// Legacy conditional phone block fallback.
|
|
$out = (string) preg_replace(
|
|
'/<\?php\s*if\s*\(\s*H_phone_No\s*!\s*=\s*[\"\']{0,1}\s*[\"\']{0,1}\s*\)\s*\{\s*echo\s*[\"\']Phone:\s*[\"\']\s*\.\s*H_phone_No\s*;\s*\}\s*\?>/is',
|
|
'Phone: {{H_phone_No}}',
|
|
$out
|
|
);
|
|
|
|
// Convert generic echo statements that match a known scalar token key.
|
|
$out = (string) preg_replace('/<\?php\s+echo\s+\$content\s*;?\s*\?>/i', '{{content}}', $out);
|
|
|
|
// Remove remaining PHP blocks that cannot be interpreted safely.
|
|
$out = (string) preg_replace('/<\?(?:php|=)[\s\S]*?\?>/i', '', $out);
|
|
|
|
return trim($out);
|
|
}
|
|
|
|
private function sanitizeOpdNamedHeaderFooterReferences(string $html): string
|
|
{
|
|
if ($html === '') {
|
|
return '';
|
|
}
|
|
|
|
$hasMyHeaderBlock = preg_match('/<\s*htmlpageheader\b[^>]*\bname\s*=\s*["\']myHeader["\']/i', $html) === 1;
|
|
$hasMyFooterBlock = preg_match('/<\s*htmlpagefooter\b[^>]*\bname\s*=\s*["\']myFooter["\']/i', $html) === 1;
|
|
|
|
if (! $hasMyHeaderBlock) {
|
|
$html = (string) preg_replace('/^\s*header\s*:\s*html_myHeader\s*;\s*$/im', '', $html);
|
|
}
|
|
if (! $hasMyFooterBlock) {
|
|
$html = (string) preg_replace('/^\s*footer\s*:\s*html_myFooter\s*;\s*$/im', '', $html);
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Read a setting value from the hospital_setting table
|
|
*/
|
|
private function readSettingValueFromDb(string $name): string
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return '';
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->where('s_name', $name)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
return trim((string) ($row['s_value'] ?? ''));
|
|
}
|
|
|
|
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();
|
|
|
|
if (!empty($data['rx_medicines'])) {
|
|
$mapDose = [];
|
|
$mapWhen = [];
|
|
$mapFreq = [];
|
|
$mapWhere = [];
|
|
$mapDoseLocal = [];
|
|
$mapWhenLocal = [];
|
|
$mapFreqLocal = [];
|
|
$mapWhereLocal = [];
|
|
$medGenericMap = [];
|
|
$missingGenericMedIds = [];
|
|
|
|
if ($this->db->tableExists('opd_dose_shed')) {
|
|
$f = $this->db->getFieldNames('opd_dose_shed');
|
|
$idF = $this->resolveFirstField($f, ['dose_shed_id', 'id']);
|
|
$labelF = $this->resolveFirstField($f, ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']);
|
|
$localLabelF = $this->resolveFirstField($f, ['dose_show_desc', 'dose_sign_hindi', 'dose_sign_desc']);
|
|
if ($idF !== null && $labelF !== null) {
|
|
$select = [$idF . ' as id', $labelF . ' as label'];
|
|
if ($localLabelF !== null && $localLabelF !== $labelF) {
|
|
$select[] = $localLabelF . ' as local_label';
|
|
}
|
|
$rows = $this->db->table('opd_dose_shed')->select(implode(',', $select))->where($labelF . ' !=', '')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$id = trim((string) ($r['id'] ?? ''));
|
|
$label = trim((string) ($r['label'] ?? ''));
|
|
if ($id !== '' && $label !== '') {
|
|
$mapDose[$id] = $label;
|
|
}
|
|
$localLabel = trim((string) ($r['local_label'] ?? ''));
|
|
if ($id !== '' && $localLabel !== '') {
|
|
$mapDoseLocal[$id] = $localLabel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->db->tableExists('opd_dose_when')) {
|
|
$f = $this->db->getFieldNames('opd_dose_when');
|
|
$idF = $this->resolveFirstField($f, ['dose_when_id', 'id']);
|
|
$labelF = $this->resolveFirstField($f, ['dose_sign', 'dose_sign_desc', 'name']);
|
|
$localLabelF = $this->resolveFirstField($f, ['dose_sign_hindi', 'dose_sign_desc']);
|
|
if ($idF !== null && $labelF !== null) {
|
|
$select = [$idF . ' as id', $labelF . ' as label'];
|
|
if ($localLabelF !== null && $localLabelF !== $labelF) {
|
|
$select[] = $localLabelF . ' as local_label';
|
|
}
|
|
$rows = $this->db->table('opd_dose_when')->select(implode(',', $select))->where($labelF . ' !=', '')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$id = trim((string) ($r['id'] ?? ''));
|
|
$label = trim((string) ($r['label'] ?? ''));
|
|
if ($id !== '' && $label !== '') {
|
|
$mapWhen[$id] = $label;
|
|
}
|
|
$localLabel = trim((string) ($r['local_label'] ?? ''));
|
|
if ($id !== '' && $localLabel !== '') {
|
|
$mapWhenLocal[$id] = $localLabel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->db->tableExists('opd_dose_frequency')) {
|
|
$f = $this->db->getFieldNames('opd_dose_frequency');
|
|
$idF = $this->resolveFirstField($f, ['dose_freq_id', 'id']);
|
|
$labelF = $this->resolveFirstField($f, ['dose_sign', 'dose_sign_desc', 'name']);
|
|
$localLabelF = $this->resolveFirstField($f, ['dose_sign_hindi', 'dose_sign_desc']);
|
|
if ($idF !== null && $labelF !== null) {
|
|
$select = [$idF . ' as id', $labelF . ' as label'];
|
|
if ($localLabelF !== null && $localLabelF !== $labelF) {
|
|
$select[] = $localLabelF . ' as local_label';
|
|
}
|
|
$rows = $this->db->table('opd_dose_frequency')->select(implode(',', $select))->where($labelF . ' !=', '')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$id = trim((string) ($r['id'] ?? ''));
|
|
$label = trim((string) ($r['label'] ?? ''));
|
|
if ($id !== '' && $label !== '') {
|
|
$mapFreq[$id] = $label;
|
|
}
|
|
$localLabel = trim((string) ($r['local_label'] ?? ''));
|
|
if ($id !== '' && $localLabel !== '') {
|
|
$mapFreqLocal[$id] = $localLabel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->db->tableExists('opd_dose_where')) {
|
|
$f = $this->db->getFieldNames('opd_dose_where');
|
|
$idF = $this->resolveFirstField($f, ['dose_where_id', 'id']);
|
|
$labelF = $this->resolveFirstField($f, ['dose_sign', 'dose_sign_desc', 'name']);
|
|
$localLabelF = $this->resolveFirstField($f, ['dose_sign_hindi', 'dose_sign_desc']);
|
|
if ($idF !== null && $labelF !== null) {
|
|
$select = [$idF . ' as id', $labelF . ' as label'];
|
|
if ($localLabelF !== null && $localLabelF !== $labelF) {
|
|
$select[] = $localLabelF . ' as local_label';
|
|
}
|
|
$rows = $this->db->table('opd_dose_where')->select(implode(',', $select))->where($labelF . ' !=', '')->get()->getResultArray();
|
|
foreach ($rows as $r) {
|
|
$id = trim((string) ($r['id'] ?? ''));
|
|
$label = trim((string) ($r['label'] ?? ''));
|
|
if ($id !== '' && $label !== '') {
|
|
$mapWhere[$id] = $label;
|
|
}
|
|
$localLabel = trim((string) ($r['local_label'] ?? ''));
|
|
if ($id !== '' && $localLabel !== '') {
|
|
$mapWhereLocal[$id] = $localLabel;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($data['rx_medicines'] as &$medRow) {
|
|
$doseRaw = trim((string) ($medRow['dosage'] ?? ''));
|
|
$whenRaw = trim((string) ($medRow['dosage_when'] ?? ''));
|
|
$freqRaw = trim((string) ($medRow['dosage_freq'] ?? ''));
|
|
$whereRaw = trim((string) ($medRow['dosage_where'] ?? ''));
|
|
$medRow['dosage_label'] = ($doseRaw !== '' && $doseRaw !== '0' && isset($mapDose[$doseRaw])) ? $mapDose[$doseRaw] : ($doseRaw === '0' ? '' : $doseRaw);
|
|
$medRow['dosage_when_label'] = ($whenRaw !== '' && $whenRaw !== '0' && isset($mapWhen[$whenRaw])) ? $mapWhen[$whenRaw] : ($whenRaw === '0' ? '' : $whenRaw);
|
|
$medRow['dosage_freq_label'] = ($freqRaw !== '' && $freqRaw !== '0' && isset($mapFreq[$freqRaw])) ? $mapFreq[$freqRaw] : ($freqRaw === '0' ? '' : $freqRaw);
|
|
$medRow['dosage_where_label'] = ($whereRaw !== '' && $whereRaw !== '0' && isset($mapWhere[$whereRaw])) ? $mapWhere[$whereRaw] : ($whereRaw === '0' ? '' : $whereRaw);
|
|
$medRow['dosage_local_label'] = ($doseRaw !== '' && $doseRaw !== '0' && isset($mapDoseLocal[$doseRaw])) ? $mapDoseLocal[$doseRaw] : '';
|
|
$medRow['dosage_when_local_label'] = ($whenRaw !== '' && $whenRaw !== '0' && isset($mapWhenLocal[$whenRaw])) ? $mapWhenLocal[$whenRaw] : '';
|
|
$medRow['dosage_freq_local_label'] = ($freqRaw !== '' && $freqRaw !== '0' && isset($mapFreqLocal[$freqRaw])) ? $mapFreqLocal[$freqRaw] : '';
|
|
$medRow['dosage_where_local_label'] = ($whereRaw !== '' && $whereRaw !== '0' && isset($mapWhereLocal[$whereRaw])) ? $mapWhereLocal[$whereRaw] : '';
|
|
|
|
$genericInline = trim((string) ($medRow['genericname'] ?? ($medRow['salt_name'] ?? '')));
|
|
if ($genericInline === '') {
|
|
$medId = (int) ($medRow['med_id'] ?? 0);
|
|
if ($medId > 0) {
|
|
$missingGenericMedIds[$medId] = true;
|
|
}
|
|
}
|
|
}
|
|
unset($medRow);
|
|
|
|
if (!empty($missingGenericMedIds) && $this->db->tableExists('opd_med_master')) {
|
|
$masterFields = $this->db->getFieldNames('opd_med_master');
|
|
$masterIdField = $this->resolveFirstField($masterFields, ['id']);
|
|
$masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']);
|
|
$masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']);
|
|
$labelField = $masterSaltField ?? $masterGenericField;
|
|
|
|
if ($masterIdField !== null && $labelField !== null) {
|
|
$rows = $this->db->table('opd_med_master')
|
|
->select($masterIdField . ' as med_id,' . $labelField . ' as generic_fallback')
|
|
->whereIn($masterIdField, array_map('intval', array_keys($missingGenericMedIds)))
|
|
->get()
|
|
->getResultArray();
|
|
|
|
foreach ($rows as $r) {
|
|
$id = (int) ($r['med_id'] ?? 0);
|
|
$label = trim((string) ($r['generic_fallback'] ?? ''));
|
|
if ($id > 0 && $label !== '') {
|
|
$medGenericMap[$id] = $label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($data['rx_medicines'] as &$medRow) {
|
|
$genericInline = trim((string) ($medRow['genericname'] ?? ($medRow['salt_name'] ?? '')));
|
|
if ($genericInline === '') {
|
|
$fallback = $medGenericMap[(int) ($medRow['med_id'] ?? 0)] ?? '';
|
|
if ($fallback !== '') {
|
|
$medRow['genericname'] = $fallback;
|
|
}
|
|
}
|
|
}
|
|
unset($medRow);
|
|
}
|
|
} 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'] ?? ($advRow['advice_txt_hindi'] ?? '')));
|
|
if ($currentHindi !== '') {
|
|
$data['rx_advices'][$idx]['advice_hindi'] = $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(),
|
|
]);
|
|
}
|
|
|
|
public function opd_cancel(int $opdId)
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid request',
|
|
]);
|
|
}
|
|
|
|
$opdId = (int) ($this->request->getPost('oid') ?? $opdId);
|
|
if ($opdId <= 0) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid OPD ID',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRowArray();
|
|
if (empty($opdRow)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'OPD not found',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$userId = (int) ($user->id ?? 0);
|
|
$userName = (string) ($user->username ?? $user->email ?? 'User');
|
|
$userLabel = $userName . '[' . $userId . ']:T-' . date('d-m-Y H:i:s');
|
|
|
|
$remarkInput = trim((string) $this->request->getPost('input_remark'));
|
|
$remarkPrefix = $remarkInput !== '' ? $remarkInput : 'Cancel OPD';
|
|
$statusRemark = $remarkPrefix . ' [Update By:' . $userLabel . ']';
|
|
|
|
$sql = "select coalesce(sum(if(credit_debit>0,amount*-1,amount)),0) as paid_amount
|
|
from payment_history where payof_type=1 and payof_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$paymentHistory = $query->getResult();
|
|
$paidAmount = (float) ($paymentHistory[0]->paid_amount ?? 0);
|
|
|
|
$refundPaymentId = 0;
|
|
$this->db->transStart();
|
|
|
|
if ($paidAmount > 0 && (int) ($opdRow['opd_status'] ?? 0) !== 3) {
|
|
$paymentModel = new PaymentModel();
|
|
$refundPaymentId = $paymentModel->insertPayment([
|
|
'payment_mode' => '1',
|
|
'payof_type' => '1',
|
|
'payof_id' => $opdId,
|
|
'payof_code' => (string) ($opdRow['opd_code'] ?? ''),
|
|
'credit_debit' => '1',
|
|
'amount' => $paidAmount,
|
|
'payment_date' => date('Y-m-d H:i:s'),
|
|
'remark' => 'Refund: ' . $remarkPrefix,
|
|
'update_by' => $userLabel,
|
|
'update_by_id' => $userId,
|
|
]);
|
|
}
|
|
|
|
$this->db->table('opd_master')
|
|
->where('opd_id', $opdId)
|
|
->update([
|
|
'opd_status' => '3',
|
|
'opd_status_remark' => $statusRemark,
|
|
]);
|
|
|
|
$this->db->transComplete();
|
|
if (! $this->db->transStatus()) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Unable to cancel OPD. Please retry.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'showcontent' => 'OPD cancelled' . ($refundPaymentId > 0 ? ' and refund posted' : ''),
|
|
'refund_payment_id' => $refundPaymentId,
|
|
'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 AI server diagnosis nor OCR endpoints are configured.
|
|
if (! $readiness['ai_server'] && ! $readiness['ocr_server']) {
|
|
$msg = 'AI not configured. Set DIAGNOSIS_AI_SERVER_URL and DIAGNOSIS_AI_OCR_ENDPOINT in AI Settings.';
|
|
$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 = 'AI server could not read usable text from this file. ' . $readiness['summary'] . '. Ensure OCR endpoint and token settings are 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{ai_server:bool,ocr_server:bool,summary:string}
|
|
*/
|
|
private function getScanAiReadiness(): array
|
|
{
|
|
$base = $this->resolveAiServerBaseUrlForScan();
|
|
$aiServer = $base !== '';
|
|
|
|
$ocrEndpoint = rtrim($this->readScanAiSettingValue([], ['DIAGNOSIS_AI_OCR_ENDPOINT']), '/');
|
|
$ocrReady = $ocrEndpoint !== '';
|
|
|
|
$parts = [];
|
|
$parts[] = 'AIServer=' . ($aiServer ? 'set' : 'missing base URL');
|
|
$parts[] = 'OCR=' . ($ocrReady ? 'set' : 'missing endpoint');
|
|
|
|
return [
|
|
'ai_server' => $aiServer,
|
|
'ocr_server' => $ocrReady,
|
|
'summary' => implode(', ', $parts),
|
|
];
|
|
}
|
|
|
|
private function callAzureOpenAiVision(string $filePath, string $mimeType, string $prompt, int $maxTokens = 1200, bool $jsonObject = false): string
|
|
{
|
|
unset($filePath, $mimeType, $prompt, $maxTokens, $jsonObject);
|
|
$this->addScanAiDebug('visual interpretation via Azure is disabled; using AI server OCR/diagnosis flow');
|
|
return '';
|
|
}
|
|
|
|
private function callAzureOpenAiText(string $prompt, int $maxTokens = 1200, bool $jsonObject = false): string
|
|
{
|
|
unset($prompt, $maxTokens, $jsonObject);
|
|
$this->addScanAiDebug('text generation via Azure is disabled; using AI server flow');
|
|
return '';
|
|
}
|
|
|
|
private function extractTextWithAzureDocumentIntelligence(string $filePath): string
|
|
{
|
|
$endpoint = rtrim($this->readScanAiSettingValue([], ['DIAGNOSIS_AI_OCR_ENDPOINT']), '/');
|
|
$token = trim($this->readScanAiSettingValue([], ['DIAGNOSIS_AI_PARSE_TOKEN']));
|
|
|
|
if ($endpoint === '' || !is_file($filePath)) {
|
|
$this->addScanAiDebug('ai server OCR skipped: endpoint/file missing');
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
$headers = ['Accept' => 'application/json'];
|
|
if ($token !== '') {
|
|
$headers['Authorization'] = 'Bearer ' . $token;
|
|
}
|
|
|
|
$client = service('curlrequest', $this->buildAzureCurlOptions($endpoint, 40, false));
|
|
$mime = $this->guessMimeTypeFromPath($filePath);
|
|
if (! str_starts_with($mime, 'image/')) {
|
|
return '';
|
|
}
|
|
|
|
$curlFile = new \CURLFile($filePath, $mime, basename($filePath));
|
|
$response = $client->post($endpoint, [
|
|
'headers' => $headers,
|
|
'multipart' => [
|
|
'file' => $curlFile,
|
|
],
|
|
]);
|
|
|
|
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
|
|
return '';
|
|
}
|
|
|
|
$decoded = json_decode((string) $response->getBody(), true);
|
|
if (! is_array($decoded)) {
|
|
return '';
|
|
}
|
|
|
|
foreach (['ocr_text', 'text', 'extracted_text'] as $key) {
|
|
$text = trim((string) ($decoded[$key] ?? ''));
|
|
if ($text !== '') {
|
|
return $text;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
return '';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function resolveAiServerBaseUrlForScan(): string
|
|
{
|
|
$base = rtrim($this->readScanAiSettingValue([], ['DIAGNOSIS_AI_SERVER_URL']), '/');
|
|
if ($base !== '') {
|
|
return $base;
|
|
}
|
|
|
|
$candidates = [
|
|
trim($this->readScanAiSettingValue([], ['DIAGNOSIS_AI_PARSE_ENDPOINT'])),
|
|
trim($this->readScanAiSettingValue([], ['DIAGNOSIS_AI_OCR_ENDPOINT'])),
|
|
];
|
|
|
|
foreach ($candidates as $url) {
|
|
if ($url === '') {
|
|
continue;
|
|
}
|
|
|
|
$parts = @parse_url($url);
|
|
if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
|
|
continue;
|
|
}
|
|
|
|
$base = (string) $parts['scheme'] . '://' . (string) $parts['host'];
|
|
if (! empty($parts['port'])) {
|
|
$base .= ':' . (int) $parts['port'];
|
|
}
|
|
|
|
return rtrim($base, '/');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
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 !== '') {
|
|
$clean = $this->sanitizeExtractedScanText($docIntelText, $scanType);
|
|
return $clean !== '' ? $clean : (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 !== '') {
|
|
$clean = $this->sanitizeExtractedScanText($azureText, $scanType);
|
|
return $clean !== '' ? $clean : (preg_replace('/\s+/', ' ', trim($azureText)) ?: trim($azureText));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Clean OCR output so OPD UI stores readable clinical text only.
|
|
*/
|
|
private function sanitizeExtractedScanText(string $text, string $scanType): string
|
|
{
|
|
$text = trim($text);
|
|
if ($text === '') {
|
|
return '';
|
|
}
|
|
|
|
// Remove control and non-ASCII characters that usually come from OCR noise.
|
|
$text = preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', ' ', $text) ?? $text;
|
|
|
|
// Drop repeated-char junk words like "ffffff".
|
|
$text = preg_replace('/\b([A-Za-z])\1{4,}\b/', ' ', $text) ?? $text;
|
|
|
|
// Collapse very long repeated words into one token.
|
|
$text = preg_replace('/\b([A-Za-z]{2,})\b(?:\s+\1\b){2,}/i', '$1', $text) ?? $text;
|
|
|
|
$isEcg = strtolower(trim($scanType)) === 'ecg' || stripos($text, 'ecg') !== false;
|
|
if ($isEcg) {
|
|
// Common OCR fixes for ECG templates.
|
|
$replacements = [
|
|
'/\bKECG\b/i' => 'ECG',
|
|
'/\bUniconfirmed\b/i' => 'Unconfirmed',
|
|
'/\bavt\b/i' => 'aVL',
|
|
'/\baVR\b/i' => 'aVR',
|
|
'/\baVF\b/i' => 'aVF',
|
|
'/\baVL\b/i' => 'aVL',
|
|
'/\bVT\b/' => 'V1',
|
|
];
|
|
|
|
foreach ($replacements as $pattern => $replacement) {
|
|
$text = preg_replace($pattern, $replacement, $text) ?? $text;
|
|
}
|
|
}
|
|
|
|
// Normalize whitespace for compact storage/response.
|
|
$text = preg_replace('/\s+/', ' ', trim($text)) ?? trim($text);
|
|
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* @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,
|
|
];
|
|
}
|
|
}
|