Diagnosis: add full-page radiology editor with imaging AI workflow
This commit is contained in:
parent
aa845ac5ff
commit
01156a1ee2
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
66
app/Views/diagnosis/imaging_ai_result_modal.php
Normal file
66
app/Views/diagnosis/imaging_ai_result_modal.php
Normal 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>
|
||||
67
app/Views/diagnosis/imaging_upload_gallery.php
Normal file
67
app/Views/diagnosis/imaging_upload_gallery.php
Normal 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; ?>
|
||||
357
app/Views/diagnosis/lab_final_report_editor.php
Normal file
357
app/Views/diagnosis/lab_final_report_editor.php
Normal 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>
|
||||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user