Diagnosis: add full-page radiology editor with imaging AI workflow

This commit is contained in:
DevSoftTech Server 2026-03-11 03:24:45 +05:30
parent aa845ac5ff
commit 01156a1ee2
8 changed files with 1729 additions and 113 deletions

View File

@ -221,6 +221,35 @@ $routes->post('Payment_Medical/change_to_cash', 'Payment_Medical::change_to_cash
$routes->post('Payment_Medical/update_amount', 'Payment_Medical::update_amount');
$routes->post('Payment_Medical/change_user', 'Payment_Medical::change_user');
// Finance & Accounting SOP - Phase 1
$routes->get('Finance', 'Finance::index');
$routes->get('Finance/index', 'Finance::index');
$routes->get('finance', 'Finance::index');
$routes->get('finance/index', 'Finance::index');
$routes->get('Finance/vendors_table', 'Finance::vendorsTable');
$routes->get('Finance/po_table', 'Finance::poTable');
$routes->get('Finance/grn_table', 'Finance::grnTable');
$routes->get('Finance/invoice_table', 'Finance::invoiceTable');
$routes->get('Finance/cashbook', 'Finance::cashbook');
$routes->get('Finance/cash_transactions_table', 'Finance::cashTransactionsTable');
$routes->get('Finance/scroll_table', 'Finance::scrollTable');
$routes->get('Finance/doctor_payout', 'Finance::doctorPayout');
$routes->get('Finance/doctor_agreements_table', 'Finance::doctorAgreementsTable');
$routes->get('Finance/doctor_payouts_table', 'Finance::doctorPayoutsTable');
$routes->get('Finance/bank_deposits', 'Finance::bankDeposits');
$routes->get('Finance/bank_deposits_table', 'Finance::bankDepositsTable');
$routes->get('Finance/compliance_report', 'Finance::complianceReport');
$routes->post('Finance/vendor_create', 'Finance::vendorCreate');
$routes->post('Finance/po_create', 'Finance::poCreate');
$routes->post('Finance/grn_create', 'Finance::grnCreate');
$routes->post('Finance/invoice_create', 'Finance::invoiceCreate');
$routes->post('Finance/cash_transaction_create', 'Finance::cashTransactionCreate');
$routes->post('Finance/scroll_create', 'Finance::scrollCreate');
$routes->post('Finance/doctor_agreement_create', 'Finance::doctorAgreementCreate');
$routes->post('Finance/doctor_payout_create', 'Finance::doctorPayoutCreate');
$routes->post('Finance/doctor_payout_approve', 'Finance::doctorPayoutApprove');
$routes->post('Finance/bank_deposit_create', 'Finance::bankDepositCreate');
service('auth')->routes($routes);
// Legacy compatibility: some IPD charge screens still refresh via old IpdNew URL.
@ -364,8 +393,8 @@ $routes->post('nursing-bedside-items/insurance/remove', 'Setting\\NursingBedside
$routes->get('Opd/addopd/(:num)', 'Opd::addopd/$1');
$routes->get('Opd/addopd/(:num)/(:num)', 'Opd::addopd/$1/$2');
$routes->get('opd/appointment', 'Opd::appointment');
$routes->get('opd/appointment/(:segment)', 'Opd::appointment/$1');
$routes->get('opd/appointment', 'Opd::appointment', ['filter' => 'permission:doctor_work.appointment.view,doctor_work.access']);
$routes->get('opd/appointment/(:segment)', 'Opd::appointment/$1', ['filter' => 'permission:doctor_work.appointment.view,doctor_work.access']);
$routes->get('Opd/get_appointment_data', 'Opd::get_appointment_data');
$routes->get('Opd/get_appointment_data/(:segment)', 'Opd::get_appointment_data/$1');
$routes->get('Opd/get_appointment_list/(:num)', 'Opd::get_appointment_list/$1');
@ -412,12 +441,12 @@ $routes->post('Opd_prescription/section_template_save', 'Opd_prescription::secti
$routes->post('Opd_prescription/section_template_track_usage', 'Opd_prescription::section_template_track_usage');
$routes->get('Opd_prescription/section_template_list', 'Opd_prescription::section_template_list');
$routes->get('Opd_prescription/section_past_data', 'Opd_prescription::section_past_data');
$routes->get('Opd_prescription/template_workspace', 'Opd_prescription::template_workspace');
$routes->get('Opd_prescription/template_workspace', 'Opd_prescription::template_workspace', ['filter' => 'permission:doctor_work.template_workspace.access,doctor_work.access']);
$routes->get('Opd_prescription/clinical_template_monitor', 'Opd_prescription::clinical_template_monitor');
$routes->get('Opd_prescription/clinical_template_usage_analytics', 'Opd_prescription::clinical_template_usage_analytics');
$routes->get('Opd_prescription/clinical_template_doctors', 'Opd_prescription::clinical_template_doctors');
$routes->post('Opd_prescription/clinical_template_update', 'Opd_prescription::clinical_template_update');
$routes->get('Opd_prescription/rx_group_panel', 'Opd_prescription::rx_group_panel');
$routes->get('Opd_prescription/rx_group_panel', 'Opd_prescription::rx_group_panel', ['filter' => 'permission:doctor_work.rx_group.manage,doctor_work.access']);
$routes->get('Opd_prescription/save_rx_group_list', 'Opd_prescription::save_rx_group_list');
$routes->get('Opd_prescription/save_rx_group_list/(:num)', 'Opd_prescription::save_rx_group_list/$1');
$routes->get('Opd_prescription/save_rx_group_edit/(:num)', 'Opd_prescription::save_rx_group_edit/$1');
@ -434,7 +463,7 @@ $routes->get('Opd_prescription/rx_group_medicine_suggest', 'Opd_prescription::rx
$routes->get('Opd_prescription/rx_group_generic_suggest', 'Opd_prescription::rx_group_generic_suggest');
$routes->get('Opd_prescription/rx_group_dose_masters', 'Opd_prescription::rx_group_dose_masters');
$routes->get('Opd_prescription/opd_medicince', 'Opd_prescription::opd_medicince');
$routes->get('Opd_prescription/opd_medicince', 'Opd_prescription::opd_medicince', ['filter' => 'permission:doctor_work.medicine.manage,doctor_work.access']);
$routes->get('Opd_prescription/opd_medicince_edit/(:num)', 'Opd_prescription::opd_medicince_edit/$1');
$routes->get('Opd_prescription/opd_medicince_data', 'Opd_prescription::opd_medicince_data');
$routes->get('Opd_prescription/opd_medicince_export', 'Opd_prescription::opd_medicince_export');
@ -446,7 +475,7 @@ $routes->post('Opd_prescription/opd_medicince_remove/(:num)', 'Opd_prescription:
$routes->get('Opd_prescription/opd_medicince_duplicate_report', 'Opd_prescription::opd_medicince_duplicate_report');
$routes->post('Opd_prescription/opd_medicince_merge_duplicates', 'Opd_prescription::opd_medicince_merge_duplicates');
$routes->post('Opd_prescription/opd_medicince_autofix_duplicates', 'Opd_prescription::opd_medicince_autofix_duplicates');
$routes->get('Opd_prescription/opd_advice', 'Opd_prescription::opd_advice');
$routes->get('Opd_prescription/opd_advice', 'Opd_prescription::opd_advice', ['filter' => 'permission:doctor_work.advice.manage,doctor_work.access']);
$routes->get('Opd_prescription/opd_advice_edit/(:num)', 'Opd_prescription::opd_advice_edit/$1');
$routes->get('Opd_prescription/opd_advice_data', 'Opd_prescription::opd_advice_data');
$routes->get('Opd_prescription/opd_advice_get/(:num)', 'Opd_prescription::opd_advice_get/$1');
@ -545,7 +574,7 @@ $routes->get('Report/billing_operations_report', 'Report::billing_operations_rep
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)', 'Report::billing_operations_report_data/$1/$2');
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)/(:num)', 'Report::billing_operations_report_data/$1/$2/$3');
$routes->get('Report/diagnosis_report', 'Report::diagnosis_report');
$routes->get('Report/diagnosis_report', 'Report::diagnosis_report', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
$routes->get('Report/diagnosis_report_data/(:segment)', 'Report::diagnosis_report_data/$1');
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2');
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2/$3');
@ -590,13 +619,13 @@ $routes->get('org-packing/print/(:num)', 'OrgPacking::printList/$1');
$routes->post('api/search-org-cases', 'OrgPacking::searchCases');
// Diagnosis Routes
$routes->get('diagnosis', 'Diagnosis::index');
$routes->get('diagnosis/pathology', 'Diagnosis::pathology');
$routes->get('diagnosis/biopsy', 'Diagnosis::biopsy');
$routes->get('diagnosis/ultrasound', 'Diagnosis::ultrasound');
$routes->get('diagnosis/xray', 'Diagnosis::xray');
$routes->get('diagnosis/mri', 'Diagnosis::mri');
$routes->get('diagnosis/ctscan', 'Diagnosis::ctscan');
$routes->get('diagnosis', 'Diagnosis::index', ['filter' => 'permission:diagnosis.access']);
$routes->get('diagnosis/pathology', 'Diagnosis::pathology', ['filter' => 'permission:diagnosis.access']);
$routes->get('diagnosis/biopsy', 'Diagnosis::biopsy', ['filter' => 'permission:diagnosis.access']);
$routes->get('diagnosis/ultrasound', 'Diagnosis::ultrasound', ['filter' => 'permission:diagnosis.access']);
$routes->get('diagnosis/xray', 'Diagnosis::xray', ['filter' => 'permission:diagnosis.access']);
$routes->get('diagnosis/mri', 'Diagnosis::mri', ['filter' => 'permission:diagnosis.access']);
$routes->get('diagnosis/ctscan', 'Diagnosis::ctscan', ['filter' => 'permission:diagnosis.access']);
$routes->post('diagnosis/search-lab', 'Diagnosis::searchLab');
$routes->post('diagnosis/search-lab-srno', 'Diagnosis::searchLabBySrno');
$routes->post('diagnosis/search-lab-labno', 'Diagnosis::searchLabByLabno');
@ -608,6 +637,7 @@ $routes->get('diagnosis/edit-test-data/(:num)', 'Diagnosis::editTestData/$1');
$routes->post('diagnosis/update-test-value', 'Diagnosis::updateTestValue');
$routes->post('diagnosis/create-report/(:num)', 'Diagnosis::createReport/$1');
$routes->post('diagnosis/create-report-xray/(:num)', 'Diagnosis::createReportXray/$1');
$routes->get('diagnosis/open-report-editor/(:num)', 'Diagnosis::openReportEditorPage/$1');
$routes->get('diagnosis/show-report-final/(:num)', 'Diagnosis::showReportFinal/$1');
$routes->get('diagnosis/show-report-final-xray/(:num)', 'Diagnosis::showReportFinalXray/$1');
$routes->get('diagnosis/print-single-report/(:num)', 'Diagnosis::printSingleReport/$1');
@ -619,8 +649,11 @@ $routes->match(['get', 'post'], 'diagnosis/get-template-xray/(:num)', 'Diagnosis
$routes->post('diagnosis/remove-test/(:num)', 'Diagnosis::removeTest/$1');
$routes->post('diagnosis/update-combine-report', 'Diagnosis::updateCombineReport');
$routes->post('diagnosis/report-compile/(:num)/(:num)', 'Diagnosis::reportCompile/$1/$2');
$routes->post('diagnosis/upload-report-file', 'Diagnosis::uploadReportFile');
$routes->get('diagnosis/report-file-list/(:num)/(:num)', 'Diagnosis::reportFileList/$1/$2');
$routes->get('diagnosis/report-file-list/(:num)/(:num)/(:num)', 'Diagnosis::reportFileList/$1/$2/$3');
$routes->get('diagnosis/imaging-upload-gallery/(:num)/(:num)/(:num)', 'Diagnosis::imagingUploadGallery/$1/$2/$3');
$routes->post('diagnosis/imaging-ai-diagnosis/(:num)', 'Diagnosis::imagingAiDiagnosis/$1');
$routes->get('diagnosis/select-lab-radiology/(:num)/(:num)', 'Diagnosis::selectLabRadiology/$1/$2');
$routes->get('diagnosis/print-pdf-create/(:num)/(:num)/(:num)', 'Diagnosis::printPdfCreate/$1/$2/$3');
$routes->get('diagnosis/print-pdf-create/(:num)/(:num)/(:num)/(:num)', 'Diagnosis::printPdfCreate/$1/$2/$3/$4');
@ -645,6 +678,8 @@ $routes->get('Lab_Report/xray_test_list/(:num)/(:num)', 'Diagnosis::testList/$1/
$routes->get('Lab_Report/lab_tab_1_process/(:num)/(:num)', 'Diagnosis::labTab1Process/$1/$2');
$routes->get('Lab_Report/report_file_list/(:num)/(:num)', 'Diagnosis::reportFileList/$1/$2');
$routes->get('Lab_Report/report_file_list/(:num)/(:num)/(:num)', 'Diagnosis::reportFileList/$1/$2/$3');
$routes->get('Lab_Report/imaging_upload_gallery/(:num)/(:num)/(:num)', 'Diagnosis::imagingUploadGallery/$1/$2/$3');
$routes->post('Lab_Report/imaging_ai_diagnosis/(:num)', 'Diagnosis::imagingAiDiagnosis/$1');
$routes->get('Lab_Report/select_lab_radiology/(:num)/(:num)', 'Diagnosis::selectLabRadiology/$1/$2');
$routes->match(['get', 'post'], 'Lab_Report/create_report_xray/(:num)', 'Diagnosis::createReportXray/$1');
$routes->get('Lab_Report/show_report_final/(:num)', 'Diagnosis::showReportFinalXray/$1');

View File

@ -1043,6 +1043,77 @@ class Diagnosis extends BaseController
]);
}
public function openReportEditorPage($labReqId)
{
$labReqId = (int) $labReqId;
$reportRow = $this->db->table('lab_request')->where('id', $labReqId)->get()->getRow();
if (! $reportRow) {
return '<div class="alert alert-danger">Report not found</div>';
}
$patientRow = $this->db->table('patient_master')
->select('gender, age, age_in, age_in_month, dob')
->where('id', (int) ($reportRow->patient_id ?? 0))
->get()
->getRow();
$genderRaw = (int) ($patientRow->gender ?? 0);
$reportRow->gender_text = $genderRaw === 1 ? 'Male' : ($genderRaw === 2 ? 'Female' : '-');
$ageVal = (int) ($patientRow->age ?? 0);
$ageIn = trim((string) ($patientRow->age_in ?? ''));
$ageInMonth = (int) ($patientRow->age_in_month ?? 0);
if ($ageVal > 0) {
$ageUnit = $ageIn !== '' ? $ageIn : 'Years';
$reportRow->age_text = $ageVal . ' ' . $ageUnit;
} elseif ($ageInMonth > 0) {
$reportRow->age_text = $ageInMonth . ' Months';
} else {
$dob = (string) ($patientRow->dob ?? '');
if ($dob !== '' && $dob !== '0000-00-00') {
try {
$dobDate = new \DateTime($dob);
$today = new \DateTime('today');
$diff = $dobDate->diff($today);
$reportRow->age_text = $diff->y > 0 ? ($diff->y . ' Years') : ($diff->m . ' Months');
} catch (\Throwable $e) {
$reportRow->age_text = '-';
}
} else {
$reportRow->age_text = '-';
}
}
$invoiceItem = $this->db->table('invoice_item')
->select('item_id')
->where('id', (int) ($reportRow->charge_item_id ?? 0))
->get()
->getRow();
$templateBuilder = $this->db->table('radiology_ultrasound_template')
->select('id, template_name')
->where('Modality', (int) ($reportRow->lab_type ?? 0));
if (! empty($invoiceItem->item_id)) {
$templateBuilder->groupStart()
->where('charge_id', (int) $invoiceItem->item_id)
->orWhere('charge_id', 0)
->groupEnd();
}
$templates = $templateBuilder
->orderBy('template_name', 'ASC')
->get()
->getResult();
return view('diagnosis/lab_final_report_editor', [
'report_format' => [$reportRow],
'radiology_ultrasound_template' => $templates,
]);
}
public function getTemplateXray($templateId)
{
$templateId = (int) $templateId;
@ -1435,6 +1506,177 @@ class Diagnosis extends BaseController
]);
}
public function imagingUploadGallery($invoiceId, $labType, $labReqId = 0)
{
$invoiceId = (int) $invoiceId;
$labType = (int) $labType;
$labReqId = (int) $labReqId;
if ($invoiceId <= 0 || $labType <= 0 || ! $this->db->tableExists('file_upload_data')) {
return view('diagnosis/imaging_upload_gallery', [
'files' => [],
'study_name' => '',
]);
}
$studyName = '';
if ($labReqId > 0) {
$requestRow = $this->db->table('lab_request')
->select('id, report_name')
->where('id', $labReqId)
->get(1)
->getRowArray();
$studyName = trim((string) ($requestRow['report_name'] ?? ''));
}
$builder = $this->db->table('file_upload_data')
->where('isdelete', 0)
->where('charge_id', $invoiceId)
->where('charge_type', $labType);
$fields = $this->db->getFieldNames('file_upload_data');
if ($labReqId > 0 && in_array('repo_id', $fields, true)) {
$builder->where('repo_id', $labReqId);
}
$rows = $builder->orderBy('id', 'DESC')->get()->getResultArray();
$files = [];
foreach ($rows as $row) {
$fullPath = trim((string) ($row['full_path'] ?? ''));
$url = $this->buildUploadPublicUrl($fullPath);
$mime = trim((string) ($row['file_type'] ?? ''));
$ext = strtolower(pathinfo((string) ($row['file_name'] ?? ''), PATHINFO_EXTENSION));
$isImage = (int) ($row['is_image'] ?? 0) === 1 || str_starts_with($mime, 'image/');
$isPdf = $ext === 'pdf' || $mime === 'application/pdf';
$files[] = [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['orig_name'] ?? $row['file_name'] ?? 'Uploaded file'),
'desc' => (string) ($row['file_desc'] ?? ''),
'url' => $url,
'is_image' => $isImage,
'is_pdf' => $isPdf,
'insert_time' => (string) ($row['insert_time'] ?? $row['insert_date'] ?? ''),
'ai_status' => (string) ($row['ai_status'] ?? ''),
'ai_alert_text' => (string) ($row['ai_alert_text'] ?? ''),
'scan_type' => (string) ($row['scan_type'] ?? ''),
];
}
return view('diagnosis/imaging_upload_gallery', [
'files' => $files,
'study_name' => $studyName,
]);
}
public function uploadReportFile()
{
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->db->tableExists('file_upload_data')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'file_upload_data table not found']);
}
$invoiceId = (int) $this->request->getPost('invoice_id');
$labType = (int) $this->request->getPost('lab_type');
$reqId = (int) $this->request->getPost('req_id');
$fileDesc = trim((string) $this->request->getPost('file_desc'));
$scanType = trim((string) $this->request->getPost('scan_type'));
if ($invoiceId <= 0 || $labType <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Missing invoice/lab context']);
}
$upload = $this->request->getFile('report_file');
if (! $upload || ! $upload->isValid()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'No valid file uploaded']);
}
$ext = strtolower((string) $upload->getExtension());
$allowed = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'pdf'];
if (! in_array($ext, $allowed, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported file type']);
}
$targetDir = ROOTPATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'diagnosis' . DIRECTORY_SEPARATOR . date('Y') . DIRECTORY_SEPARATOR . date('m');
if (! is_dir($targetDir) && ! @mkdir($targetDir, 0775, true) && ! is_dir($targetDir)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create upload directory']);
}
$storedName = 'diag_' . date('Ymd_His') . '_' . substr(md5((string) mt_rand()), 0, 8) . '.' . $ext;
try {
$upload->move($targetDir, $storedName, true);
} catch (\Throwable $e) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Upload failed: ' . $e->getMessage()]);
}
$absolutePath = $targetDir . DIRECTORY_SEPARATOR . $storedName;
$relativePath = 'uploads/diagnosis/' . date('Y') . '/' . date('m') . '/' . $storedName;
$publicUrl = base_url($relativePath);
$user = function_exists('auth') ? auth()->user() : null;
$uploadBy = trim((string) ($user->username ?? $user->email ?? 'system'));
$uploadById = (int) ($user->id ?? 0);
$fields = $this->db->getFieldNames('file_upload_data');
$insert = [];
$fieldMap = [
'name' => $storedName,
'file_name' => $storedName,
'orig_name' => (string) $upload->getClientName(),
'client_name' => (string) $upload->getClientName(),
'file_ext' => '.' . $ext,
'file_type' => (string) $upload->getClientMimeType(),
'full_path' => str_replace('\\', '/', $absolutePath),
'file_path' => str_replace('\\', '/', $targetDir) . '/',
'raw_name' => pathinfo($storedName, PATHINFO_FILENAME),
'store_file_name' => $storedName,
'file_size' => round(((float) $upload->getSize()) / 1024, 2),
'is_image' => in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif'], true) ? 1 : 0,
'image_type' => $ext,
'insert_date' => Time::now('Asia/Kolkata')->toDateTimeString(),
'insert_time' => Time::now('Asia/Kolkata')->toDateTimeString(),
'file_desc' => $fileDesc !== '' ? $fileDesc : 'Diagnosis Report Upload',
'repo_id' => $reqId,
'charge_id' => $invoiceId,
'charge_type' => $labType,
'upload_by' => $uploadBy,
'upload_by_id' => $uploadById,
'scan_type' => $scanType !== '' ? $scanType : 'upload',
'show_type' => 0,
'isdelete' => 0,
'document_type' => 'diagnosis-report',
'content_description' => 'Uploaded from diagnosis editor',
'ai_status' => 'pending',
'ai_alert_flag' => 0,
'ai_alert_text' => null,
];
foreach ($fieldMap as $key => $value) {
if (in_array($key, $fields, true)) {
$insert[$key] = $value;
}
}
$ok = $this->db->table('file_upload_data')->insert($insert);
if (! $ok) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to save upload metadata']);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'File uploaded successfully',
'file_id' => (int) $this->db->insertID(),
'file_name' => $storedName,
'file_url' => $publicUrl,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function aiExtractLabReportValues()
{
if (! $this->request->isAJAX()) {
@ -1480,9 +1722,14 @@ class Diagnosis extends BaseController
return $this->response->setJSON(['update' => 0, 'error_text' => 'OCR failed. Configure AZURE_DOCINTEL_ENDPOINT/KEY or use readable image.']);
}
$parsed = $this->parseLabValuesWithAzureOpenAi($ocrText, $panelName);
$parsed = $this->parseLabValuesWithAi($ocrText, $panelName);
if (empty($parsed['values']) || ! is_array($parsed['values'])) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'AI parsing failed. Check Azure OpenAI settings and deployment.']);
$errorText = trim((string) ($parsed['error'] ?? ''));
if ($errorText === '') {
$errorText = 'AI parsing failed. Check AI endpoint/settings and try retry.';
}
return $this->response->setJSON(['update' => 0, 'error_text' => $errorText]);
}
$modelName = trim((string) ($parsed['model'] ?? ''));
@ -1539,6 +1786,8 @@ class Diagnosis extends BaseController
return $this->response->setJSON([
'update' => 1,
'error_text' => 'AI extracted ' . $storedCount . ' values. Doctor verification pending.',
'ai_provider' => (string) ($parsed['provider'] ?? 'azure-openai'),
'ai_model' => $modelName,
'batch_id' => $batchId,
'stored_values' => $storedCount,
'csrfName' => csrf_token(),
@ -1617,6 +1866,156 @@ class Diagnosis extends BaseController
]);
}
public function imagingAiDiagnosis($labReqId)
{
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$labReqId = (int) $labReqId;
if ($labReqId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request id']);
}
$labRequest = $this->db->table('lab_request')
->where('id', $labReqId)
->get(1)
->getRowArray();
if (empty($labRequest)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Imaging request not found']);
}
$labType = (int) ($labRequest['lab_type'] ?? 0);
if (! $this->isRadiologyType($labType)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'AI diagnosis is available for imaging only']);
}
if (! $this->db->tableExists('file_upload_data')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'file_upload_data table not found']);
}
if (! $this->db->tableExists('lab_ai_extraction_batches')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'AI batch table not found. Run migrations first.']);
}
$invoiceId = (int) ($labRequest['charge_id'] ?? 0);
$studyName = trim((string) ($labRequest['report_name'] ?? 'Imaging Study'));
$files = $this->findImagingFilesForRequest($invoiceId, $labType, $labReqId);
if (empty($files)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'No uploaded image/PDF found for this study']);
}
$fileIds = array_values(array_filter(array_map(static fn($row) => (int) ($row['id'] ?? 0), $files)));
$this->updateImagingFileAiState($fileIds, 'processing', 'AI diagnosis is running');
$ocrChunks = [];
$visionImages = [];
$sourceFiles = [];
foreach ($files as $fileRow) {
$absolutePath = $this->resolveUploadAbsolutePath((string) ($fileRow['full_path'] ?? ''));
if ($absolutePath === '' || ! is_file($absolutePath)) {
continue;
}
$fileName = (string) ($fileRow['orig_name'] ?? $fileRow['file_name'] ?? basename($absolutePath));
$mime = $this->detectMimeType($absolutePath);
$sourceFiles[] = [
'id' => (int) ($fileRow['id'] ?? 0),
'name' => $fileName,
'scan_type' => (string) ($fileRow['scan_type'] ?? ''),
];
$ocrText = trim($this->extractTextFromLabFile($absolutePath));
if ($ocrText !== '') {
$ocrChunks[] = 'FILE: ' . $fileName . "\n" . $ocrText;
}
if (str_starts_with($mime, 'image/') && count($visionImages) < 4) {
$imagePayload = $this->buildVisionImagePayload($absolutePath, $mime);
if ($imagePayload !== '') {
$visionImages[] = [
'type' => 'image_url',
'image_url' => ['url' => $imagePayload],
];
}
}
}
$analysis = $this->generateImagingDiagnosisDraft($studyName, $ocrChunks, $visionImages);
if (($analysis['error'] ?? '') !== '') {
$this->updateImagingFileAiState($fileIds, 'failed', (string) $analysis['error'], 1);
return $this->response->setJSON(['update' => 0, 'error_text' => (string) $analysis['error']]);
}
$rawPayload = [
'type' => 'imaging-ai-diagnosis',
'request_id' => $labReqId,
'invoice_id' => $invoiceId,
'lab_type' => $labType,
'study_name' => $studyName,
'title' => (string) ($analysis['title'] ?? $studyName),
'findings_html' => (string) ($analysis['findings_html'] ?? ''),
'impression_html' => (string) ($analysis['impression_html'] ?? ''),
'summary_text' => (string) ($analysis['summary_text'] ?? ''),
'provider' => (string) ($analysis['provider'] ?? 'azure-openai'),
'model' => (string) ($analysis['model'] ?? ''),
'source_files' => $sourceFiles,
'raw' => $analysis['raw'] ?? [],
];
$ocrText = trim(implode("\n\n", $ocrChunks));
$batchId = $this->insertLabAiBatch(
$invoiceId,
$labType,
(int) ($fileIds[0] ?? 0),
$studyName,
(string) ($analysis['model'] ?? ''),
$ocrText,
(string) json_encode($rawPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
if ($batchId <= 0) {
$this->updateImagingFileAiState($fileIds, 'failed', 'Unable to store AI diagnosis result', 1);
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to store AI diagnosis result']);
}
$this->updateImagingFileAiState($fileIds, 'completed', (string) ($analysis['summary_text'] ?? 'AI draft ready'));
$this->auditClinicalUpdate('lab_ai_extraction_batches', 'created', $batchId, null, [
'invoice_id' => $invoiceId,
'lab_type' => $labType,
'request_id' => $labReqId,
'source_file_ids' => $fileIds,
'mode' => 'imaging-ai-diagnosis',
]);
$html = view('diagnosis/imaging_ai_result_modal', [
'batch_id' => $batchId,
'title' => (string) ($analysis['title'] ?? $studyName),
'study_name' => $studyName,
'findings_html' => (string) ($analysis['findings_html'] ?? ''),
'impression_html' => (string) ($analysis['impression_html'] ?? ''),
'summary_text' => (string) ($analysis['summary_text'] ?? ''),
'provider' => (string) ($analysis['provider'] ?? 'azure-openai'),
'model' => (string) ($analysis['model'] ?? ''),
]);
return $this->response->setJSON([
'update' => 1,
'error_text' => 'AI diagnosis draft is ready and stored.',
'batch_id' => $batchId,
'html' => $html,
'findings_html' => (string) ($analysis['findings_html'] ?? ''),
'impression_html' => (string) ($analysis['impression_html'] ?? ''),
'summary_text' => (string) ($analysis['summary_text'] ?? ''),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
private function insertLabAiBatch(
int $invoiceId,
int $labType,
@ -1679,6 +2078,199 @@ class Diagnosis extends BaseController
return '';
}
/**
* @return array<int, array<string, mixed>>
*/
private function findImagingFilesForRequest(int $invoiceId, int $labType, int $labReqId): array
{
if ($invoiceId <= 0 || $labType <= 0 || ! $this->db->tableExists('file_upload_data')) {
return [];
}
$builder = $this->db->table('file_upload_data')
->where('isdelete', 0)
->where('charge_id', $invoiceId)
->where('charge_type', $labType);
$fields = $this->db->getFieldNames('file_upload_data');
if ($labReqId > 0 && in_array('repo_id', $fields, true)) {
$builder->where('repo_id', $labReqId);
}
return $builder->orderBy('id', 'DESC')->get()->getResultArray();
}
private function updateImagingFileAiState(array $fileIds, string $status, string $message = '', int $alertFlag = 0): void
{
$fileIds = array_values(array_filter(array_map('intval', $fileIds)));
if (empty($fileIds) || ! $this->db->tableExists('file_upload_data')) {
return;
}
$fields = $this->db->getFieldNames('file_upload_data');
$update = [];
if (in_array('ai_status', $fields, true)) {
$update['ai_status'] = $status;
}
if (in_array('ai_alert_text', $fields, true)) {
$update['ai_alert_text'] = $message !== '' ? mb_substr($message, 0, 255) : null;
}
if (in_array('ai_alert_flag', $fields, true)) {
$update['ai_alert_flag'] = $alertFlag > 0 ? 1 : 0;
}
if ($update === []) {
return;
}
$this->db->table('file_upload_data')->whereIn('id', $fileIds)->update($update);
}
private function buildUploadPublicUrl(string $fullPath): string
{
$normalized = str_replace('\\', '/', trim($fullPath));
if ($normalized === '') {
return '';
}
$publicPos = stripos($normalized, '/public/');
if ($publicPos !== false) {
return base_url(substr($normalized, $publicPos + 8));
}
$uploadsPos = stripos($normalized, 'uploads/');
if ($uploadsPos !== false) {
return base_url(substr($normalized, $uploadsPos));
}
return '';
}
private function buildVisionImagePayload(string $absolutePath, string $mime): string
{
$bytes = @file_get_contents($absolutePath);
if ($bytes === false || $bytes === '') {
return '';
}
return 'data:' . $mime . ';base64,' . base64_encode($bytes);
}
/**
* @param array<int, string> $ocrChunks
* @param array<int, array<string, mixed>> $visionImages
* @return array<string, mixed>
*/
private function generateImagingDiagnosisDraft(string $studyName, array $ocrChunks, array $visionImages): array
{
$endpoint = rtrim($this->readSetting('AZURE_OPENAI_ENDPOINT'), '/');
$apiKey = trim($this->readSetting('AZURE_OPENAI_API_KEY'));
$deployment = trim($this->readSetting('AZURE_OPENAI_DEPLOYMENT'));
$apiVersion = trim($this->readSetting('AZURE_OPENAI_API_VERSION'));
if ($endpoint === '' || $apiKey === '' || $deployment === '') {
return [
'error' => 'Azure OpenAI settings are missing for imaging AI diagnosis.',
];
}
if ($apiVersion === '') {
$apiVersion = '2024-10-21';
}
if ($visionImages === [] && $ocrChunks === []) {
return [
'error' => 'No image or OCR context available for AI diagnosis.',
];
}
$content = [[
'type' => 'text',
'text' => "You are assisting a radiologist in drafting an imaging report for an Indian hospital.\n" .
"Study name: {$studyName}\n" .
"Review the uploaded imaging files and any OCR text.\n" .
"Return strict JSON only with keys: title, findings_html, impression_html, summary_text.\n" .
"Rules:\n" .
"1. findings_html must be concise HTML paragraphs or bullet list suitable for the report editor.\n" .
"2. impression_html must be concise HTML suitable for the Impression field.\n" .
"3. summary_text must be plain text under 220 characters.\n" .
"4. Do not include markdown fences.\n" .
"5. If image quality is poor, say that clearly in findings_html/impression_html.\n\n" .
($ocrChunks !== [] ? ("OCR_CONTEXT:\n" . mb_substr(implode("\n\n", $ocrChunks), 0, 14000)) : 'OCR_CONTEXT: none'),
]];
foreach ($visionImages as $imagePart) {
$content[] = $imagePart;
}
$url = $endpoint . '/openai/deployments/' . rawurlencode($deployment) . '/chat/completions?api-version=' . rawurlencode($apiVersion);
$attempts = $this->aiMaxRetries();
$lastError = 'AI diagnosis generation failed.';
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
$client = service('curlrequest', $this->httpOptions());
$response = $client->post($url, [
'headers' => [
'api-key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => [
'messages' => [[
'role' => 'user',
'content' => $content,
]],
'temperature' => 0.2,
'response_format' => ['type' => 'json_object'],
'max_tokens' => 2200,
],
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
$lastError = 'Azure OpenAI HTTP ' . $response->getStatusCode();
continue;
}
$decoded = json_decode((string) $response->getBody(), true);
$contentText = trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
if ($contentText === '') {
$lastError = 'Azure OpenAI returned empty content.';
continue;
}
$json = json_decode($contentText, true);
if (! is_array($json)) {
$lastError = 'Azure OpenAI returned invalid JSON content.';
continue;
}
$findingsHtml = trim((string) ($json['findings_html'] ?? ''));
$impressionHtml = trim((string) ($json['impression_html'] ?? ''));
if ($findingsHtml === '' && $impressionHtml === '') {
$lastError = 'AI returned empty report sections.';
continue;
}
return [
'provider' => 'azure-openai',
'model' => (string) ($decoded['model'] ?? $deployment),
'title' => trim((string) ($json['title'] ?? $studyName)),
'findings_html' => $findingsHtml,
'impression_html' => $impressionHtml,
'summary_text' => trim((string) ($json['summary_text'] ?? 'AI diagnosis draft prepared.')),
'raw' => $decoded,
'error' => '',
];
} catch (\Throwable $e) {
$lastError = $e->getMessage() !== '' ? $e->getMessage() : 'Azure OpenAI request timeout/error.';
}
}
return [
'error' => $lastError,
];
}
private function extractTextFromLabFile(string $absolutePath): string
{
$docEndpoint = rtrim($this->readSetting('AZURE_DOCINTEL_ENDPOINT'), '/');
@ -1824,6 +2416,108 @@ class Diagnosis extends BaseController
}
}
/**
* @return array<string, mixed>
*/
private function parseLabValuesWithAi(string $ocrText, string $panelName): array
{
$external = $this->parseLabValuesWithExternalService($ocrText, $panelName);
if (! empty($external['values']) && is_array($external['values'])) {
return $external;
}
$azure = $this->parseLabValuesWithAzureOpenAi($ocrText, $panelName);
if (! empty($azure['values']) && is_array($azure['values'])) {
return $azure;
}
$externalError = trim((string) ($external['error'] ?? ''));
$azureError = trim((string) ($azure['error'] ?? ''));
return [
'provider' => 'none',
'model' => '',
'values' => [],
'raw' => [],
'error' => $externalError !== '' ? $externalError : $azureError,
];
}
/**
* @return array<string, mixed>
*/
private function parseLabValuesWithExternalService(string $ocrText, string $panelName): array
{
$endpoint = rtrim($this->readSetting('DIAGNOSIS_AI_PARSE_ENDPOINT'), '/');
if ($endpoint === '') {
return [
'provider' => 'external',
'model' => '',
'values' => [],
'raw' => [],
'error' => '',
];
}
$token = trim($this->readSetting('DIAGNOSIS_AI_PARSE_TOKEN'));
$headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$attempts = $this->aiMaxRetries();
$lastError = 'External AI service request failed.';
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
$client = service('curlrequest', $this->httpOptions());
$response = $client->post($endpoint, [
'headers' => $headers,
'json' => [
'ocr_text' => mb_substr($ocrText, 0, 20000),
'panel_name' => $panelName,
],
]);
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
$decoded = json_decode((string) $response->getBody(), true);
if (! is_array($decoded)) {
$lastError = 'External AI response is not valid JSON.';
continue;
}
$values = $decoded['values'] ?? null;
if (! is_array($values)) {
$lastError = trim((string) ($decoded['error'] ?? 'External AI response missing values array.'));
continue;
}
return [
'provider' => (string) ($decoded['provider'] ?? 'external'),
'model' => (string) ($decoded['model'] ?? 'external-ai'),
'values' => $values,
'raw' => $decoded,
'error' => '',
];
}
$lastError = 'External AI HTTP ' . $response->getStatusCode();
} catch (\Throwable $e) {
$lastError = $e->getMessage() !== '' ? $e->getMessage() : 'External AI request timeout/error.';
}
}
return [
'provider' => 'external',
'model' => '',
'values' => [],
'raw' => [],
'error' => $lastError,
];
}
/**
* @return array<string, mixed>
*/
@ -1835,7 +2529,13 @@ class Diagnosis extends BaseController
$apiVersion = trim($this->readSetting('AZURE_OPENAI_API_VERSION'));
if ($endpoint === '' || $apiKey === '' || $deployment === '') {
return [];
return [
'provider' => 'azure-openai',
'model' => '',
'values' => [],
'raw' => [],
'error' => 'Azure OpenAI settings are missing.',
];
}
if ($apiVersion === '') {
$apiVersion = '2024-10-21';
@ -1850,47 +2550,65 @@ class Diagnosis extends BaseController
$url = $endpoint . '/openai/deployments/' . rawurlencode($deployment) . '/chat/completions?api-version=' . rawurlencode($apiVersion);
try {
$client = service('curlrequest', $this->httpOptions());
$response = $client->post($url, [
'headers' => [
'api-key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => [
'messages' => [[
'role' => 'user',
'content' => $prompt,
]],
'temperature' => 0,
'response_format' => ['type' => 'json_object'],
'max_tokens' => 2200,
],
]);
$attempts = $this->aiMaxRetries();
$lastError = 'Azure AI parsing failed.';
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
return [];
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
$client = service('curlrequest', $this->httpOptions());
$response = $client->post($url, [
'headers' => [
'api-key' => $apiKey,
'Content-Type' => 'application/json',
],
'json' => [
'messages' => [[
'role' => 'user',
'content' => $prompt,
]],
'temperature' => 0,
'response_format' => ['type' => 'json_object'],
'max_tokens' => 2200,
],
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
$lastError = 'Azure OpenAI HTTP ' . $response->getStatusCode();
continue;
}
$decoded = json_decode((string) $response->getBody(), true);
$content = trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
if ($content === '') {
$lastError = 'Azure OpenAI returned empty content.';
continue;
}
$json = json_decode($content, true);
if (! is_array($json)) {
$lastError = 'Azure OpenAI returned invalid JSON content.';
continue;
}
return [
'provider' => 'azure-openai',
'model' => (string) ($decoded['model'] ?? $deployment),
'values' => is_array($json['values'] ?? null) ? $json['values'] : [],
'raw' => $decoded,
'error' => '',
];
} catch (\Throwable $e) {
$lastError = $e->getMessage() !== '' ? $e->getMessage() : 'Azure OpenAI request timeout/error.';
}
$decoded = json_decode((string) $response->getBody(), true);
$content = trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
if ($content === '') {
return [];
}
$json = json_decode($content, true);
if (! is_array($json)) {
return [];
}
return [
'model' => (string) ($decoded['model'] ?? $deployment),
'values' => is_array($json['values'] ?? null) ? $json['values'] : [],
'raw' => $decoded,
];
} catch (\Throwable $e) {
return [];
}
return [
'provider' => 'azure-openai',
'model' => '',
'values' => [],
'raw' => [],
'error' => $lastError,
];
}
/**
@ -1898,8 +2616,9 @@ class Diagnosis extends BaseController
*/
private function httpOptions(): array
{
$timeout = $this->aiTimeoutSeconds();
$options = [
'timeout' => 45,
'timeout' => $timeout,
'http_errors' => false,
];
@ -1910,6 +2629,34 @@ class Diagnosis extends BaseController
return $options;
}
private function aiTimeoutSeconds(): int
{
$timeout = (int) $this->readSetting('DIAGNOSIS_AI_TIMEOUT_SECONDS');
if ($timeout <= 0) {
$timeout = 45;
}
if ($timeout > 180) {
$timeout = 180;
}
return $timeout;
}
private function aiMaxRetries(): int
{
$retries = (int) $this->readSetting('DIAGNOSIS_AI_RETRY_ATTEMPTS');
if ($retries <= 0) {
$retries = 2;
}
if ($retries > 5) {
$retries = 5;
}
return $retries;
}
private function readSetting(string $name): string
{
if (defined($name)) {

View File

@ -0,0 +1,66 @@
<?php
$batchId = (int) ($batch_id ?? 0);
$title = trim((string) ($title ?? 'AI Diagnosis'));
$studyName = trim((string) ($study_name ?? ''));
$findingsHtml = (string) ($findings_html ?? '');
$impressionHtml = (string) ($impression_html ?? '');
$summaryText = trim((string) ($summary_text ?? ''));
$provider = trim((string) ($provider ?? 'azure-openai'));
$model = trim((string) ($model ?? ''));
$findingsInputId = 'ai_findings_' . $batchId;
$impressionInputId = 'ai_impression_' . $batchId;
?>
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-3">
<div>
<h5 class="mb-1"><?= esc($title) ?></h5>
<?php if ($studyName !== ''): ?>
<div class="small text-muted">Study: <?= esc($studyName) ?></div>
<?php endif; ?>
<div class="small text-muted">Stored batch #<?= esc((string) $batchId) ?> | <?= esc($provider) ?><?= $model !== '' ? ' / ' . esc($model) : '' ?></div>
</div>
<div class="d-flex gap-2">
<button
type="button"
class="btn btn-primary"
data-findings-target="#<?= esc($findingsInputId) ?>"
data-impression-target="#<?= esc($impressionInputId) ?>"
onclick="pasteAiDiagnosisDraft(this)">
Paste In Report
</button>
<button
type="button"
class="btn btn-success"
data-findings-target="#<?= esc($findingsInputId) ?>"
data-impression-target="#<?= esc($impressionInputId) ?>"
onclick="pasteAiDiagnosisDraftAndSave(this)">
Paste + Save
</button>
</div>
</div>
<?php if ($summaryText !== ''): ?>
<div class="alert alert-info py-2"><?= esc($summaryText) ?></div>
<?php endif; ?>
<div class="row g-3">
<div class="col-lg-7">
<div class="card h-100">
<div class="card-header"><strong>Findings Draft</strong></div>
<div class="card-body">
<?= $findingsHtml !== '' ? $findingsHtml : '<div class="text-muted">No findings generated.</div>' ?>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card h-100">
<div class="card-header"><strong>Impression Draft</strong></div>
<div class="card-body">
<?= $impressionHtml !== '' ? $impressionHtml : '<div class="text-muted">No impression generated.</div>' ?>
</div>
</div>
</div>
</div>
<textarea id="<?= esc($findingsInputId) ?>" style="display:none;"><?= esc($findingsHtml) ?></textarea>
<textarea id="<?= esc($impressionInputId) ?>" style="display:none;"><?= esc($impressionHtml) ?></textarea>

View File

@ -0,0 +1,67 @@
<?php
$fileRows = is_array($files ?? null) ? $files : [];
$studyName = trim((string) ($study_name ?? ''));
?>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div>
<h6 class="mb-1">Uploaded Images</h6>
<?php if ($studyName !== ''): ?>
<div class="small text-muted">Study: <?= esc($studyName) ?></div>
<?php endif; ?>
</div>
<div class="small text-muted">Latest uploads for this imaging study</div>
</div>
</div>
<?php if ($fileRows === []): ?>
<div class="alert alert-warning mb-0">No uploaded images or PDFs found for this study.</div>
<?php else: ?>
<div class="row g-3">
<?php foreach ($fileRows as $row): ?>
<div class="col-md-6 col-xl-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
<strong class="small"><?= esc((string) ($row['name'] ?? 'Uploaded file')) ?></strong>
<?php if (!empty($row['ai_status'])): ?>
<span class="badge bg-light text-dark border"><?= esc((string) $row['ai_status']) ?></span>
<?php endif; ?>
</div>
<div class="small text-muted mb-2">
<?= esc((string) ($row['desc'] ?? '')) ?>
<?php if (!empty($row['scan_type'])): ?>
| <?= esc((string) $row['scan_type']) ?>
<?php endif; ?>
</div>
<?php if (!empty($row['url']) && !empty($row['is_image'])): ?>
<a href="<?= esc((string) $row['url']) ?>" target="_blank" rel="noopener">
<img src="<?= esc((string) $row['url']) ?>" alt="Uploaded image" class="img-fluid rounded border" style="width:100%; max-height:220px; object-fit:cover;">
</a>
<?php elseif (!empty($row['url']) && !empty($row['is_pdf'])): ?>
<div class="border rounded p-3 text-center bg-light">
<div class="display-6 text-danger"><i class="bi bi-file-earmark-pdf"></i></div>
<a href="<?= esc((string) $row['url']) ?>" target="_blank" rel="noopener">Open PDF</a>
</div>
<?php else: ?>
<div class="border rounded p-3 text-muted bg-light">Preview unavailable</div>
<?php endif; ?>
<div class="mt-2 small text-muted">
Uploaded: <?= esc((string) ($row['insert_time'] ?? '')) ?>
</div>
<?php if (!empty($row['ai_alert_text'])): ?>
<div class="mt-2 small <?= !empty($row['ai_status']) && $row['ai_status'] === 'failed' ? 'text-danger' : 'text-muted' ?>">
<?= esc((string) $row['ai_alert_text']) ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>

View File

@ -0,0 +1,357 @@
<?php
$report = $report_format[0] ?? null;
$templates = $radiology_ultrasound_template ?? [];
?>
<form method="post" onsubmit="return false;">
<div class="card admin-card">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h3 class="mb-0">Radiology Report Editor - <?= esc($report->report_name ?? 'Report') ?></h3>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="backToInvoiceEditor()">
<i class="bi bi-arrow-left"></i> Back
</button>
</div>
<div class="card-body">
<?php if (!$report): ?>
<div class="alert alert-danger">Report not found.</div>
<?php else: ?>
<input type="hidden" id="hid_value_req_id" name="req_id" value="<?= esc($report->id ?? '') ?>">
<input type="hidden" id="report_mode" name="report_mode" value="xray">
<input type="hidden" id="hid_value_report_name" value="<?= esc($report->report_name ?? '') ?>">
<input type="hidden" id="invoice_id" value="<?= esc($report->charge_id ?? '') ?>">
<input type="hidden" id="lab_type" value="<?= esc($report->lab_type ?? '') ?>">
<?php
$ipdId = (int) ($report->ipd_id ?? 0);
$orgId = (int) ($report->org_id ?? 0);
$patientType = 'Direct';
if ($ipdId > 0) {
$patientType = 'IPD';
} elseif ($orgId > 0) {
$patientType = 'TPA';
}
?>
<div class="card mb-3 border-light-subtle">
<div class="card-body py-2">
<div class="fw-semibold mb-1">Patient Information</div>
<div class="row g-2 small">
<div class="col-md-3"><strong>Name:</strong> <?= esc($report->patient_name ?? '-') ?></div>
<div class="col-md-3"><strong>Invoice:</strong> <?= esc($report->invoice_code ?? (($report->charge_id ?? '') !== '' ? ('#' . (string) $report->charge_id) : '-')) ?></div>
<div class="col-md-2"><strong>Gender:</strong> <?= esc($report->gender_text ?? '-') ?></div>
<div class="col-md-2"><strong>Age:</strong> <?= esc($report->age_text ?? '-') ?></div>
<div class="col-md-2"><strong>Patient Type:</strong> <?= esc($patientType) ?></div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" onclick="update_report()">
<i class="bi bi-save"></i> Save
</button>
<button type="button" class="btn btn-success" onclick="report_final()">
<i class="bi bi-check-circle"></i> Verified
</button>
<button type="button" class="btn btn-outline-primary" onclick="showImagingUploadsFromEditor()">
<i class="bi bi-images"></i> Show Upload Images
</button>
<button type="button" class="btn btn-outline-danger" onclick="runImagingAiDiagnosisFromEditor()">
<i class="bi bi-magic"></i> AI Diagnosis
</button>
</div>
</div>
</div>
<hr/>
<div class="row g-3">
<div class="col-md-8">
<div class="form-group">
<label class="form-label"><strong>Report Findings</strong></label>
<textarea id="HTMLShow" name="HTMLShow" class="form-control" rows="12"><?= $report->Report_Data ?? '' ?></textarea>
<script>
if (typeof CKEDITOR !== 'undefined') {
CKEDITOR.replace('HTMLShow');
}
</script>
</div>
<hr/>
<div class="form-group">
<label class="form-label"><strong>Impression</strong></label>
<textarea id="report_data_Impression" name="report_data_Impression" class="form-control" rows="8"><?= $report->report_data_Impression ?? '' ?></textarea>
<script>
if (typeof CKEDITOR !== 'undefined') {
CKEDITOR.replace('report_data_Impression', {
toolbar: [
['Bold', 'Italic', 'Underline', '-', 'FontSize', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['NumberedList', 'BulletedList', '-', 'Link', 'Unlink', '-', 'Source']
]
});
}
</script>
</div>
<hr/>
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" onclick="update_report()">
<i class="bi bi-save"></i> Save Report
</button>
<button type="button" class="btn btn-success" onclick="report_final()">
<i class="bi bi-check-lg"></i> Mark as Verified
</button>
</div>
</div>
<div class="col-md-4" style="border-left:1px solid #e4ebf5; padding-left:12px;">
<label class="form-label"><strong>Templates</strong></label>
<input type="text" id="template_search" class="form-control form-control-sm" placeholder="Search templates..." autocomplete="off" />
<div id="templateList" style="max-height:60vh; overflow-y:auto; margin-top:8px;">
<?php foreach ($templates as $tpl): ?>
<div class="template-item mb-1">
<a href="javascript:set_template(<?= (int) ($tpl->id ?? 0) ?>)"><?= esc($tpl->template_name ?? '') ?></a>
</div>
<?php endforeach; ?>
<div id="no_templates_msg" style="display:none; color:#888; padding:6px;">No templates found</div>
</div>
</div>
</div>
<!-- Imaging Support Modal -->
<div id="imagingSupportModal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imagingSupportModalTitle">Imaging Support</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="imagingSupportModalBody" style="max-height: 70vh; overflow-y: auto;">
<!-- Content loaded dynamically -->
</div>
</div>
</div>
</div>
<script>
// Use global baseUrl if available
if (typeof baseUrl === 'undefined') {
window.baseUrl = '<?= base_url() ?>';
}
function backToInvoiceEditor() {
const invoiceId = document.getElementById('invoice_id') ? document.getElementById('invoice_id').value : '';
const labType = document.getElementById('lab_type') ? document.getElementById('lab_type').value : '';
if (typeof load_form === 'function' && invoiceId && labType) {
load_form(baseUrl + 'diagnosis/select-lab-invoice/' + invoiceId + '/' + labType, 'Diagnosis');
return;
}
window.history.back();
}
(function () {
const input = document.getElementById('template_search');
if (!input) {
return;
}
input.addEventListener('input', function () {
const q = (this.value || '').toLowerCase().trim();
const items = document.querySelectorAll('#templateList .template-item');
let count = 0;
items.forEach(function (item) {
const show = item.textContent.toLowerCase().indexOf(q) !== -1;
item.style.display = show ? '' : 'none';
if (show) {
count++;
}
});
const msg = document.getElementById('no_templates_msg');
if (msg) {
msg.style.display = count === 0 ? 'block' : 'none';
}
});
})();
// Modal helper function
function openImagingSupportModal(title, html) {
document.getElementById('imagingSupportModalTitle').textContent = title || 'Imaging Support';
document.getElementById('imagingSupportModalBody').innerHTML = html;
new bootstrap.Modal(document.getElementById('imagingSupportModal')).show();
}
// Show imaging uploads (gallery)
function showImagingUploadsFromEditor() {
const reqId = document.getElementById('hid_value_req_id').value;
const testName = document.getElementById('hid_value_report_name').value;
if (!reqId) { alert('Request ID not found'); return; }
showImagingUploads(reqId, testName);
}
function showImagingUploads(reqId, testName) {
const labType = document.getElementById('lab_type').value;
fetch(baseUrl + 'diagnosis/imaging-upload-gallery/' + reqId + '/' + labType + '/' + reqId, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.text())
.then(html => openImagingSupportModal(testName + ' - Uploaded Images', html))
.catch(e => alert('Error loading images: ' + e.message));
}
// Run AI diagnosis
function runImagingAiDiagnosisFromEditor() {
const reqId = document.getElementById('hid_value_req_id').value;
const testName = document.getElementById('hid_value_report_name').value;
if (!reqId) { alert('Request ID not found'); return; }
runImagingAiDiagnosis(reqId, testName);
}
function runImagingAiDiagnosis(reqId, testName) {
const btn = event.target.closest('button');
if (btn) btn.disabled = true;
const indicator = document.createElement('span');
indicator.textContent = ' Processing...';
if (btn) btn.appendChild(indicator);
fetch(baseUrl + 'diagnosis/imaging-ai-diagnosis/' + reqId, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: '<?= csrf_token() ?>=<?= csrf_hash() ?>'
})
.then(r => r.text())
.then(html => {
openImagingSupportModal(testName + ' - AI Diagnosis', html);
})
.catch(e => alert('Error: ' + e.message))
.finally(() => {
if (btn) {
btn.disabled = false;
if (indicator.parentNode) indicator.parentNode.removeChild(indicator);
}
});
}
// Template functions
function set_template(templateId) {
fetch(baseUrl + 'diagnosis/get-template-xray/' + templateId, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(data => {
const findings = data.Findings || '';
const impression = data.Impression || '';
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLShow) {
CKEDITOR.instances.HTMLShow.setData(findings);
} else {
document.getElementById('HTMLShow').value = findings;
}
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.report_data_Impression) {
CKEDITOR.instances.report_data_Impression.setData(impression);
} else {
document.getElementById('report_data_Impression').value = impression;
}
})
.catch(e => console.error(e));
}
function update_report() {
const reqId = document.getElementById('hid_value_req_id').value;
if (!reqId) { alert('Invalid request'); return; }
// Get data from CKEditor if available, otherwise from textarea
let reportData = document.getElementById('HTMLShow').value;
let impressionData = document.getElementById('report_data_Impression').value;
if (typeof CKEDITOR !== 'undefined') {
if (CKEDITOR.instances.HTMLShow) {
reportData = CKEDITOR.instances.HTMLShow.getData();
}
if (CKEDITOR.instances.report_data_Impression) {
impressionData = CKEDITOR.instances.report_data_Impression.getData();
}
}
const data = new FormData();
data.append('<?= csrf_token() ?>', '<?= csrf_hash() ?>');
data.append('report_data', reportData);
data.append('report_data_impression', impressionData);
fetch(baseUrl + 'diagnosis/update-report/' + reqId, {
method: 'POST',
body: data,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(result => {
alert(result.message || 'Report saved successfully');
})
.catch(e => alert('Error saving: ' + e.message));
}
function report_final() {
const reqId = document.getElementById('hid_value_req_id').value;
if (!reqId) { alert('Invalid request'); return; }
if (!confirm('Mark this report as verified?')) return;
const data = new FormData();
data.append('<?= csrf_token() ?>', '<?= csrf_hash() ?>');
fetch(baseUrl + 'diagnosis/report-verify/' + reqId, {
method: 'POST',
body: data,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(result => {
alert(result.message || 'Report verified successfully');
})
.catch(e => alert('Error: ' + e.message));
}
// Paste AI diagnosis draft to editor
function pasteAiDiagnosisDraftAndSave(button) {
applyAiDiagnosisDraftToEditor(button, true);
}
function applyAiDiagnosisDraftToEditor(button, autoSave) {
const findingsHtml = document.getElementById('ai_findings_text') ? document.getElementById('ai_findings_text').textContent : '';
const impressionHtml = document.getElementById('ai_impression_text') ? document.getElementById('ai_impression_text').textContent : '';
if (typeof CKEDITOR !== 'undefined') {
if (CKEDITOR.instances.HTMLShow) {
CKEDITOR.instances.HTMLShow.setData(findingsHtml);
} else {
document.getElementById('HTMLShow').value = findingsHtml;
}
if (CKEDITOR.instances.report_data_Impression) {
CKEDITOR.instances.report_data_Impression.setData(impressionHtml);
} else {
document.getElementById('report_data_Impression').value = impressionHtml;
}
} else {
document.getElementById('HTMLShow').value = findingsHtml;
document.getElementById('report_data_Impression').value = impressionHtml;
}
if (autoSave) {
setTimeout(() => update_report(), 180);
}
}
</script>
<?php endif; ?>
</div>
</div>
</form>

View File

@ -3,14 +3,38 @@ $report = $report_format[0] ?? null;
$templates = $radiology_ultrasound_template ?? [];
?>
<style>
.xray-editor-shell {
background: #f9fbff;
border: 1px solid #dce5f2;
border-radius: 12px;
padding: 12px;
}
.xray-action-row {
position: sticky;
bottom: 0;
z-index: 2;
background: linear-gradient(180deg, rgba(249,251,255,0.1) 0%, rgba(249,251,255,0.95) 22%, rgba(249,251,255,1) 100%);
padding-top: 8px;
}
.xray-template-pane {
border-left: 1px solid #e4ebf5;
padding-left: 12px;
}
.xray-template-pane a {
text-decoration: none;
}
</style>
<?php if (! $report): ?>
<div class="alert alert-danger mb-0">Report not found.</div>
<?php else: ?>
<input type="hidden" id="hid_value_req_id" value="<?= esc($report->id ?? '') ?>">
<input type="hidden" id="report_mode" value="xray">
<input type="hidden" id="hid_value_report_name" value="<?= esc($report->report_name ?? '') ?>">
<?= csrf_field() ?>
<div class="row g-3">
<div class="row g-3 xray-editor-shell">
<div class="col-lg-8">
<div class="mb-2">
<label class="form-label">Report</label>
@ -22,13 +46,19 @@ $templates = $radiology_ultrasound_template ?? [];
<textarea id="report_data_Impression" name="report_data_Impression" class="form-control" rows="7"><?= $report->report_data_Impression ?? '' ?></textarea>
</div>
<div class="d-flex gap-2">
<div class="d-flex gap-2 flex-wrap xray-action-row">
<button type="button" class="btn btn-primary" onclick="update_report()">Save</button>
<button type="button" class="btn btn-success" onclick="report_final()">Verified</button>
<button type="button" class="btn btn-outline-primary" onclick="showImagingUploadsFromEditor()">
<i class="bi bi-images"></i> Show Upload Images
</button>
<button type="button" class="btn btn-outline-danger" onclick="runImagingAiDiagnosisFromEditor()">
<i class="bi bi-magic"></i> AI Diagnosis
</button>
</div>
</div>
<div class="col-lg-4">
<div class="col-lg-4 xray-template-pane">
<label class="form-label">Templates</label>
<input type="text" id="template_search" class="form-control form-control-sm" placeholder="Search templates..." autocomplete="off" />

View File

@ -14,8 +14,29 @@ if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0) {
}
?>
<?php if ($isRadiologyFlow): ?>
<style>
.diag-worklist-title {
font-weight: 700;
color: #0f2e62;
}
.diag-test-card {
border: 1px solid #d8e3f2;
border-left: 4px solid #188a58;
border-radius: 10px;
box-shadow: 0 4px 14px rgba(16, 24, 40, 0.06);
}
.diag-test-card .card-body {
padding: 12px 14px;
}
.diag-more-box {
background: #f7fafd;
}
</style>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0"><?php echo htmlspecialchars($flowTitle); ?></h6>
<h6 class="mb-0 diag-worklist-title"><?php echo htmlspecialchars($flowTitle); ?></h6>
</div>
<?php if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0): ?>
@ -38,7 +59,7 @@ if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0) {
<?php if (!empty($testlist) && count($testlist) > 0): ?>
<?php foreach ($testlist as $test): ?>
<?php $status = (int) ($test->status ?? 0); ?>
<div class="card mb-3">
<div class="card mb-3 diag-test-card">
<div class="card-body">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<div class="fw-semibold"><?php echo htmlspecialchars($test->item_name ?? ''); ?></div>
@ -103,36 +124,52 @@ if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0) {
onclick="removeTest(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>)">
<i class="bi bi-trash"></i> Remove
</button>
<?php if ($isRadiologyFlow): ?>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="uploadFiles(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-upload"></i> Upload Files
</button>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="scanReportFile(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-camera"></i> Webcam Scan
</button>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="showFiles(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-images"></i> Show Upload Images
</button>
<?php endif; ?>
<?php endif; ?>
</div>
<button type="button" class="btn btn-light btn-sm"
onclick="toggleTestDetails(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>)">
<i class="bi bi-three-dots"></i> More
</button>
<?php if (! $isRadiologyFlow): ?>
<button type="button" class="btn btn-light btn-sm"
onclick="toggleTestDetails(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>)">
<i class="bi bi-three-dots"></i> More
</button>
<div id="details_<?php echo htmlspecialchars($test->req_id ?? '0'); ?>" class="mt-2 p-2 border rounded" style="display:none;">
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="uploadFiles(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-upload"></i> Upload Files
</button>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="scanReport(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-scanner"></i> Scan
</button>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="showFiles(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-files"></i> Show Files
</button>
<?php if ($status == 2): ?>
<button type="button" class="btn btn-outline-success btn-sm"
onclick="openForEdit(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-pencil"></i> Open for Edit
<div id="details_<?php echo htmlspecialchars($test->req_id ?? '0'); ?>" class="mt-2 p-2 border rounded diag-more-box" style="display:none;">
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="uploadFiles(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-upload"></i> Upload Files
</button>
<?php endif; ?>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="scanReportFile(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-camera"></i> Webcam Scan
</button>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="showFiles(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-files"></i> Show Files
</button>
<?php if ($status == 2): ?>
<button type="button" class="btn btn-outline-success btn-sm"
onclick="openForEdit(<?php echo htmlspecialchars($test->req_id ?? '0'); ?>, '<?php echo htmlspecialchars($test->item_name ?? ''); ?>')">
<i class="bi bi-pencil"></i> Open for Edit
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>

View File

@ -7,8 +7,51 @@ $flowTypeLabel = $isPathologyFlow ? 'Lab' : 'Imaging';
$timingTitle = $isPathologyFlow ? 'Lab Timing Information' : 'Imaging Workflow Timing';
$testListTitle = $isPathologyFlow ? 'Test List' : 'Imaging Study List';
$collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Collection Time';
$isImagingFlow = !$isPathologyFlow;
?>
<?php if ($isImagingFlow): ?>
<style>
.diagnosis-ui {
background: linear-gradient(180deg, #f7f9fd 0%, #eef3f9 100%);
border: 1px solid #dbe4f0;
border-radius: 14px;
padding: 14px;
}
.diagnosis-ui .card {
border: 1px solid #dfe7f3;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(18, 38, 63, 0.06);
}
.diagnosis-ui .card-header {
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.diagnosis-ui .card-title {
letter-spacing: 0.2px;
font-weight: 700;
}
.diagnosis-ui .table > tbody > tr > th {
white-space: nowrap;
width: 32%;
}
.diagnosis-ui .btn {
border-radius: 8px;
}
#testDataEntryModal .modal-content {
border-radius: 14px;
border: 1px solid #dfe7f3;
}
#testDataEntryModal .modal-header {
background: linear-gradient(90deg, #0a3c8f 0%, #165cb8 100%);
color: #fff;
}
#testDataEntryModal .btn-close {
filter: invert(1);
}
</style>
<?php endif; ?>
<div class="pagetitle">
<h1><?= esc($currentLabTypeName) ?> - <?= esc($flowTypeLabel) ?> Invoice Details</h1>
<nav>
@ -20,11 +63,11 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
</nav>
</div>
<section class="section">
<section class="section<?= $isImagingFlow ? ' diagnosis-ui' : '' ?>">
<div class="row">
<!-- Person Profile Card -->
<div class="col-lg-4">
<div class="card">
<div class="card profile-card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">Person Profile</h5>
</div>
@ -100,7 +143,7 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
<!-- Lab Timing & Tests Section -->
<div class="col-lg-8">
<!-- Lab Timing Card -->
<div class="card mb-3" id="labTimingCard">
<div class="card mb-3 diag-panel" id="labTimingCard">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0"><?= esc($timingTitle) ?></h5>
</div>
@ -139,7 +182,7 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
</div>
<!-- Test List Card -->
<div class="card" id="testListCard">
<div class="card diag-panel" id="testListCard">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0"><?= esc($testListTitle) ?></h5>
</div>
@ -231,7 +274,7 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
<!-- Test Data Entry Modal -->
<div class="modal fade" id="testDataEntryModal" tabindex="-1" role="dialog" aria-labelledby="testDataEntryLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="testDataEntryLabel">Test Data Entry</h5>
@ -249,6 +292,24 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
</div>
</div>
<div class="modal fade" id="imagingSupportModal" tabindex="-1" aria-labelledby="imagingSupportLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title" id="imagingSupportLabel">Imaging Support</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="imagingSupportBody">
<div class="text-center py-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var baseUrl = '<?php echo rtrim(base_url(), '/'); ?>/';
var invoiceId = '<?php echo htmlspecialchars($invoice->inv_id ?? $invoice->id ?? '0'); ?>';
@ -589,44 +650,35 @@ function report_create() {
}
function createReportXray(reqId, testName) {
// Create report, then load full-page editor
const modalBody = document.getElementById('testDataEntryBody');
const csrfField = modalBody ? modalBody.querySelector('input[name]') : null;
const formData = new FormData();
const data = new FormData();
if (csrfField) {
formData.append(csrfField.name, csrfField.value);
data.append(csrfField.name, csrfField.value);
}
// Create/initialize report first
fetch(baseUrl + 'diagnosis/create-report-xray/' + reqId, {
method: 'POST',
body: formData,
body: data,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
destroyReportEditor();
document.getElementById('testDataEntryBody').innerHTML = html;
const label = document.getElementById('testDataEntryLabel');
if (label) {
label.textContent = testName || 'Final Report';
}
const modal = new bootstrap.Modal(document.getElementById('testDataEntryModal'));
modal.show();
initializeReportEditor();
refreshTestList();
refreshLabDateShow();
// Load full-page editor using load_form
load_form(baseUrl + 'diagnosis/open-report-editor/' + reqId, testName + ' - Report Editor');
})
.catch(error => {
console.error('createReportXray error:', error);
alert('Error loading report editor: ' + error);
alert('Error preparing report editor: ' + error.message);
});
}
function set_template(templateId) {
const modalBody = document.getElementById('testDataEntryBody');
const csrfField = modalBody ? modalBody.querySelector('input[name]') : null;
@ -828,16 +880,241 @@ if (testDataEntryModalEl) {
}
function uploadFiles(reqId, testName) {
alert('Upload files not yet implemented for req ID: ' + reqId);
diagnosisUploadForReq(reqId, testName, 'file');
}
function scanReport(reqId, testName) {
alert('Scan report not yet implemented for req ID: ' + reqId);
function scanReportFile(reqId, testName) {
diagnosisUploadForReq(reqId, testName, 'camera');
}
function diagnosisUploadFromEditor(mode) {
const reqInput = document.getElementById('hid_value_req_id');
const title = (document.getElementById('testDataEntryLabel') || {}).textContent || 'Radiology';
const reqId = reqInput ? parseInt(reqInput.value || '0', 10) : 0;
diagnosisUploadForReq(reqId, title, mode || 'file');
}
function diagnosisUploadForReq(reqId, testName, mode) {
if (!invoiceId || !labType) {
alert('Invoice context missing');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*,application/pdf';
if ((mode || '').toLowerCase() === 'camera') {
input.accept = 'image/*';
input.setAttribute('capture', 'environment');
}
input.onchange = function () {
const file = input.files && input.files[0] ? input.files[0] : null;
if (!file) {
return;
}
const modalBody = document.getElementById('testDataEntryBody');
const csrfField = modalBody ? modalBody.querySelector('input[name]') : null;
const formData = new FormData();
formData.append('report_file', file);
formData.append('invoice_id', String(invoiceId));
formData.append('lab_type', String(labType));
formData.append('req_id', String(reqId || 0));
formData.append('file_desc', (testName || 'Imaging Report') + ((mode === 'camera') ? ' (Camera)' : ' (Upload)'));
formData.append('scan_type', (mode === 'camera') ? 'camera' : 'upload');
if (csrfField) {
formData.append(csrfField.name, csrfField.value);
}
fetch(baseUrl + 'diagnosis/upload-report-file', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if ((result.update || 0) !== 1) {
alert(result.error_text || 'Upload failed');
return;
}
alert((mode === 'camera' ? 'Webcam image saved successfully.' : 'File uploaded successfully.') + ' Use Show Upload Images or AI Diagnosis in the report editor.');
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload request failed');
});
};
input.click();
}
function showFiles(reqId, testName) {
const url = baseUrl + 'Lab_Report/report_file_list/' + invoiceId + '/' + labType;
window.open(url, '_blank');
if (parseInt(labType || '0', 10) === 5 || parseInt(labType || '0', 10) === 30) {
const url = baseUrl + 'Lab_Report/report_file_list/' + invoiceId + '/' + labType;
window.open(url, '_blank');
return;
}
showImagingUploads(reqId, testName);
}
function getEditorImagingContext() {
const reqInput = document.getElementById('hid_value_req_id');
const reportNameInput = document.getElementById('hid_value_report_name');
const modalLabel = document.getElementById('testDataEntryLabel');
return {
reqId: reqInput ? parseInt(reqInput.value || '0', 10) : 0,
testName: (reportNameInput && reportNameInput.value)
? reportNameInput.value
: ((modalLabel && modalLabel.textContent) ? modalLabel.textContent.trim() : 'Imaging Study')
};
}
function openImagingSupportModal(title, html) {
const titleEl = document.getElementById('imagingSupportLabel');
const bodyEl = document.getElementById('imagingSupportBody');
if (titleEl) {
titleEl.textContent = title || 'Imaging Support';
}
if (bodyEl) {
bodyEl.innerHTML = html || '<div class="alert alert-warning mb-0">No content found.</div>';
}
const modal = new bootstrap.Modal(document.getElementById('imagingSupportModal'));
modal.show();
}
function showImagingUploads(reqId, testName) {
const studyReqId = parseInt(reqId || '0', 10);
if (!studyReqId) {
alert('Study context missing');
return;
}
openImagingSupportModal((testName || 'Imaging Study') + ' Uploads', '<div class="text-center py-4"><div class="spinner-border" role="status"></div></div>');
fetch(baseUrl + 'diagnosis/imaging-upload-gallery/' + invoiceId + '/' + labType + '/' + studyReqId, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.text())
.then(html => {
openImagingSupportModal((testName || 'Imaging Study') + ' Uploads', html);
})
.catch(error => {
console.error('showImagingUploads error:', error);
openImagingSupportModal((testName || 'Imaging Study') + ' Uploads', '<div class="alert alert-danger mb-0">Unable to load uploaded images.</div>');
});
}
function showImagingUploadsFromEditor() {
const context = getEditorImagingContext();
showImagingUploads(context.reqId, context.testName);
}
function runImagingAiDiagnosis(reqId, testName) {
const studyReqId = parseInt(reqId || '0', 10);
if (!studyReqId) {
alert('Study context missing');
return;
}
const modalBody = document.getElementById('testDataEntryBody');
const csrfField = modalBody ? modalBody.querySelector('input[name]') : null;
const formData = new FormData();
if (csrfField) {
formData.append(csrfField.name, csrfField.value);
}
openImagingSupportModal((testName || 'Imaging Study') + ' AI Diagnosis', '<div class="text-center py-4"><div class="spinner-border" role="status"></div><div class="mt-2 text-muted">AI is reviewing uploaded images...</div></div>');
fetch(baseUrl + 'diagnosis/imaging-ai-diagnosis/' + studyReqId, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(result => {
if ((result.update || 0) !== 1) {
openImagingSupportModal((testName || 'Imaging Study') + ' AI Diagnosis', '<div class="alert alert-danger mb-0">' + (result.error_text || 'AI diagnosis failed') + '</div>');
return;
}
openImagingSupportModal((testName || 'Imaging Study') + ' AI Diagnosis', result.html || '<div class="alert alert-warning mb-0">AI result not available.</div>');
})
.catch(error => {
console.error('runImagingAiDiagnosis error:', error);
openImagingSupportModal((testName || 'Imaging Study') + ' AI Diagnosis', '<div class="alert alert-danger mb-0">AI diagnosis request failed.</div>');
});
}
function runImagingAiDiagnosisFromEditor() {
const context = getEditorImagingContext();
runImagingAiDiagnosis(context.reqId, context.testName);
}
function applyAiDiagnosisDraftToEditor(button, autoSave) {
if (!button) {
return;
}
const findingsTarget = button.getAttribute('data-findings-target');
const impressionTarget = button.getAttribute('data-impression-target');
const findingsInput = findingsTarget ? document.querySelector(findingsTarget) : null;
const impressionInput = impressionTarget ? document.querySelector(impressionTarget) : null;
const findingsHtml = findingsInput ? findingsInput.value : '';
const impressionHtml = impressionInput ? impressionInput.value : '';
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.HTMLShow) {
CKEDITOR.instances.HTMLShow.setData(findingsHtml || '');
} else {
const htmlField = document.getElementById('HTMLShow');
if (htmlField) {
htmlField.value = findingsHtml || '';
}
}
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances.report_data_Impression) {
CKEDITOR.instances.report_data_Impression.setData(impressionHtml || '');
} else {
const impressionField = document.getElementById('report_data_Impression');
if (impressionField) {
impressionField.value = impressionHtml || '';
}
}
const modalEl = document.getElementById('imagingSupportModal');
const modal = modalEl ? bootstrap.Modal.getInstance(modalEl) : null;
if (modal) {
modal.hide();
}
if (autoSave === true) {
setTimeout(function () {
update_report();
}, 180);
return;
}
alert('AI draft pasted into the report editor. Review before saving.');
}
function pasteAiDiagnosisDraft(button) {
applyAiDiagnosisDraftToEditor(button, false);
}
function pasteAiDiagnosisDraftAndSave(button) {
applyAiDiagnosisDraftToEditor(button, true);
}
function openForEdit(reqId, testName) {