diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index c12805d..06a2a25 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -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');
diff --git a/app/Controllers/Diagnosis.php b/app/Controllers/Diagnosis.php
index f7957f2..ff8be8f 100644
--- a/app/Controllers/Diagnosis.php
+++ b/app/Controllers/Diagnosis.php
@@ -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 '
Report not found
';
+ }
+
+ $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>
+ */
+ 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 $ocrChunks
+ * @param array> $visionImages
+ * @return array
+ */
+ 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
+ */
+ 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
+ */
+ 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
*/
@@ -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)) {
diff --git a/app/Views/diagnosis/imaging_ai_result_modal.php b/app/Views/diagnosis/imaging_ai_result_modal.php
new file mode 100644
index 0000000..b99984f
--- /dev/null
+++ b/app/Views/diagnosis/imaging_ai_result_modal.php
@@ -0,0 +1,66 @@
+
+
+
+
+
= esc($title) ?>
+
+
Study: = esc($studyName) ?>
+
+
Stored batch #= esc((string) $batchId) ?> | = esc($provider) ?>= $model !== '' ? ' / ' . esc($model) : '' ?>
+
+
+
+
+
+
+
+
+ = esc($summaryText) ?>
+
+
+
+
+
+
+
+ = $findingsHtml !== '' ? $findingsHtml : '
No findings generated.
' ?>
+
+
+
+
+
+
+
+ = $impressionHtml !== '' ? $impressionHtml : '
No impression generated.
' ?>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/diagnosis/imaging_upload_gallery.php b/app/Views/diagnosis/imaging_upload_gallery.php
new file mode 100644
index 0000000..5d6013c
--- /dev/null
+++ b/app/Views/diagnosis/imaging_upload_gallery.php
@@ -0,0 +1,67 @@
+
+
+
+
+
+
Uploaded Images
+
+
Study: = esc($studyName) ?>
+
+
+
Latest uploads for this imaging study
+
+
+
+
+ No uploaded images or PDFs found for this study.
+
+
+
+
+
+
+
+ = esc((string) ($row['name'] ?? 'Uploaded file')) ?>
+
+ = esc((string) $row['ai_status']) ?>
+
+
+
+
+ = esc((string) ($row['desc'] ?? '')) ?>
+
+ | = esc((string) $row['scan_type']) ?>
+
+
+
+
+
+
+
+
+
+
+
Preview unavailable
+
+
+
+ Uploaded: = esc((string) ($row['insert_time'] ?? '')) ?>
+
+
+
+
+ = esc((string) $row['ai_alert_text']) ?>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/diagnosis/lab_final_report_editor.php b/app/Views/diagnosis/lab_final_report_editor.php
new file mode 100644
index 0000000..c462c90
--- /dev/null
+++ b/app/Views/diagnosis/lab_final_report_editor.php
@@ -0,0 +1,357 @@
+
+
+
diff --git a/app/Views/diagnosis/lab_final_report_show_xray.php b/app/Views/diagnosis/lab_final_report_show_xray.php
index 975845c..9dcb8ef 100644
--- a/app/Views/diagnosis/lab_final_report_show_xray.php
+++ b/app/Views/diagnosis/lab_final_report_show_xray.php
@@ -3,14 +3,38 @@ $report = $report_format[0] ?? null;
$templates = $radiology_ultrasound_template ?? [];
?>
+
+
Report not found.
+
= csrf_field() ?>
-
+
@@ -22,13 +46,19 @@ $templates = $radiology_ultrasound_template ?? [];
-
+
+
+
-
+
diff --git a/app/Views/diagnosis/lab_invoice_main_test_list.php b/app/Views/diagnosis/lab_invoice_main_test_list.php
index 171eee9..a0021e9 100644
--- a/app/Views/diagnosis/lab_invoice_main_test_list.php
+++ b/app/Views/diagnosis/lab_invoice_main_test_list.php
@@ -14,8 +14,29 @@ if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0) {
}
?>
+
+
+
+
-
+
0): ?>
@@ -38,7 +59,7 @@ if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0) {
0): ?>
status ?? 0); ?>
-
+
item_name ?? ''); ?>
@@ -103,36 +124,52 @@ if (!empty($lab_invoice_request) && count($lab_invoice_request) > 0) {
onclick="removeTest(req_id ?? '0'); ?>)">
Remove
+
+
+
+
+
-
+
+
-
-
-
-
-
-
-
+
diff --git a/app/Views/diagnosis/pathology_detail.php b/app/Views/diagnosis/pathology_detail.php
index 5b9b863..97e67c1 100644
--- a/app/Views/diagnosis/pathology_detail.php
+++ b/app/Views/diagnosis/pathology_detail.php
@@ -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;
?>
+
+
+
+
= esc($currentLabTypeName) ?> - = esc($flowTypeLabel) ?> Invoice Details
-
+
-
+
@@ -100,7 +143,7 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
-
+
@@ -139,7 +182,7 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col
-
+
@@ -231,7 +274,7 @@ $collectedTimeLabel = $isPathologyFlow ? 'Sample Collection Time' : 'Request Col