devsofttech_hms_ci4/app/Controllers/Diagnosis.php

4004 lines
154 KiB
PHP

<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use CodeIgniter\I18n\Time;
use Exception;
use Mpdf\HTMLParserMode;
use Mpdf\Mpdf;
class Diagnosis extends BaseController
{
protected $db;
public function __construct()
{
$this->db = \Config\Database::connect();
}
public function index()
{
// Check if it's an AJAX request
$isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest';
if (!$isAjax) {
return view('welcome_message');
}
$user = service('auth')->user();
return view('diagnosis/index', [
'user' => $user
]);
}
public function pathology()
{
return $this->labPath(5);
}
public function biopsy()
{
return $this->labPath(30);
}
public function mri()
{
return $this->labPath(2);
}
public function xray()
{
return $this->labPath(3);
}
public function ultrasound()
{
return $this->labPath(1);
}
public function ctscan()
{
return $this->labPath(4);
}
public function labMaster()
{
return $this->index();
}
public function labPathLegacy($labType)
{
return $this->labPath((int) $labType);
}
private function labPath(int $labType)
{
// Check if it's an AJAX request
$isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest';
if (!$isAjax) {
return view('welcome_message');
}
$user = service('auth')->user();
$labTypeName = $this->getLabTypeName($labType);
return view('diagnosis/request_list', [
'user' => $user,
'lab_type' => $labType,
'lab_type_name' => $labTypeName
]);
}
public function searchLab($labType = null)
{
$labType = $labType !== null ? (int) $labType : (int) $this->request->getPost('lab_type');
$searchText = $this->request->getPost('txtsearch');
// Sanitize input
$searchText = preg_replace('/[^A-Za-z0-9_ \-]/', '', $searchText);
if (trim($searchText) == '') {
// Get recent 200 records
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
p.dob, p.age, p.age_in_month, p.estimate_dob,
GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id,
check_item_request(m.id, i.id)) SEPARATOR '#') as data_array,
r.daily_sr_no, r.lab_test_no
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
LEFT JOIN lab_invoice_request r ON m.id = r.invoice_id
WHERE m.payment_status = 1 AND y.itype_id = ?
GROUP BY m.id
ORDER BY m.id DESC
LIMIT 200";
$query = $this->db->query($sql, [$labType]);
} else {
// Search with criteria
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
p.dob, p.age, p.age_in_month, p.estimate_dob,
GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id,
check_item_request(m.id, i.id)) SEPARATOR '#') as data_array,
r.daily_sr_no, r.lab_test_no
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
LEFT JOIN lab_invoice_request r ON m.id = r.invoice_id
WHERE m.payment_status = 1 AND y.itype_id = ?
AND (m.invoice_code LIKE ? OR p.p_fname LIKE ? OR p.p_code LIKE ?)
GROUP BY m.id
ORDER BY m.id DESC
LIMIT 50";
$searchPattern = '%' . $searchText . '%';
$query = $this->db->query($sql, [$labType, $searchPattern, $searchPattern, $searchPattern]);
}
$results = $query->getResult();
// Calculate age using PHP helper
foreach ($results as $row) {
$row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? '');
}
$data = [
'labreport_preprocess' => $results,
'lab_type' => $labType
];
// Return different view based on lab type
if ($labType == 5 || $labType == 30) { // Pathology or Biopsy
return view('diagnosis/lab_report_tab_path', $data);
} else {
return view('diagnosis/lab_report_tab', $data);
}
}
public function searchLabBySrno($labType = null)
{
$labType = $labType !== null ? (int) $labType : (int) $this->request->getPost('lab_type');
$srNo = $this->request->getPost('txtsearch_srno');
// Sanitize input
$srNo = preg_replace('/[^A-Za-z0-9_ \-]/', '', $srNo);
if (trim($srNo) == '') {
// Get recent 3 days records with serial number
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
p.dob, p.age, p.age_in_month, p.estimate_dob,
GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id,
check_item_request(m.id, i.id)) SEPARATOR '#') as data_array,
r.daily_sr_no, r.lab_test_no
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.daily_sr_no > 0
WHERE m.payment_status = 1 AND y.itype_id = ?
AND m.inv_date >= DATE_ADD(CURDATE(), INTERVAL -3 DAY)
GROUP BY m.id
ORDER BY m.id DESC
LIMIT 50";
$query = $this->db->query($sql, [$labType]);
} else {
// Search by specific serial number
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
p.dob, p.age, p.age_in_month, p.estimate_dob,
GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id,
check_item_request(m.id, i.id)) SEPARATOR '#') as data_array,
r.daily_sr_no, r.lab_test_no
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.daily_sr_no > 0
WHERE m.payment_status = 1 AND y.itype_id = ? AND r.daily_sr_no = ?
GROUP BY m.id
ORDER BY m.id DESC
LIMIT 50";
$query = $this->db->query($sql, [$labType, $srNo]);
}
$results = $query->getResult();
// Calculate age using PHP helper
foreach ($results as $row) {
$row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? '');
}
$data = [
'labreport_preprocess' => $results,
'lab_type' => $labType
];
return view('diagnosis/lab_report_tab', $data);
}
public function searchLabByLabno($labType = null)
{
$labType = $labType !== null ? (int) $labType : (int) $this->request->getPost('lab_type');
$labNo = $this->request->getPost('txtsearch_labno');
// Sanitize input
$labNo = preg_replace('/[^A-Za-z0-9_ \-]/', '', $labNo);
if (trim($labNo) == '') {
// Get recent 3 days records with lab number
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
p.dob, p.age, p.age_in_month, p.estimate_dob,
GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id,
check_item_request(m.id, i.id)) SEPARATOR '#') as data_array,
r.daily_sr_no, r.lab_test_no
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.lab_test_no > 0
WHERE m.payment_status = 1 AND y.itype_id = ?
AND m.inv_date >= DATE_ADD(CURDATE(), INTERVAL -3 DAY)
GROUP BY m.id
ORDER BY m.id DESC
LIMIT 50";
$query = $this->db->query($sql, [$labType]);
} else {
// Search by specific lab number
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
p.dob, p.age, p.age_in_month, p.estimate_dob,
GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id,
check_item_request(m.id, i.id)) SEPARATOR '#') as data_array,
r.daily_sr_no, r.lab_test_no
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.lab_test_no > 0
WHERE m.payment_status = 1 AND y.itype_id = ? AND r.lab_test_no = ?
GROUP BY m.id
ORDER BY m.id DESC
LIMIT 50";
$query = $this->db->query($sql, [$labType, $labNo]);
}
$results = $query->getResult();
// Calculate age using PHP helper
foreach ($results as $row) {
$row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? '');
}
$data = [
'labreport_preprocess' => $results,
'lab_type' => $labType
];
return view('diagnosis/lab_report_tab', $data);
}
public function selectLabInvoicePath($invoiceId, $labType)
{
// Allow both AJAX and regular GET requests
$isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest';
// Get invoice and patient information
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, m.attach_id,
p.id, p.p_fname, p.p_relative, p.p_rname, p.gender, p.dob, p.age, p.age_in_month,
p.estimate_dob, p.p_code, p.udai, p.relation_patient_cardholder, p.p_relative as relative_name,
p.mphone1 as phone_number, p.email1 as email, p.add1 as address_line1, p.city, p.state
FROM invoice_master m
INNER JOIN patient_master p ON p.id = m.attach_id
WHERE m.id = ? AND m.payment_status = 1";
$query = $this->db->query($sql, [$invoiceId]);
$invoice = $query->getRow();
if (!$invoice) {
return view('welcome_message');
}
// Calculate age
$invoice->age = get_age_1($invoice->dob ?? null, $invoice->age ?? '', $invoice->age_in_month ?? '', $invoice->estimate_dob ?? '');
// Get invoice items (tests) for this invoice
$testSql = "SELECT i.id, i.item_name, i.item_id, m.inv_date
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
WHERE i.inv_master_id = ? AND i.item_type = ?
ORDER BY i.item_name";
$testQuery = $this->db->query($testSql, [$invoiceId, $labType]);
$tests = $testQuery->getResult();
// Get invoice items to correlate with tests
$itemSql = "SELECT i.id, i.item_name, i.item_id
FROM invoice_item i
WHERE i.inv_master_id = ? AND i.item_type = ?";
$itemQuery = $this->db->query($itemSql, [$invoiceId, $labType]);
$items = $itemQuery->getResult();
// Get lab invoice request for timing information
$labInvoiceSql = "SELECT * FROM lab_invoice_request WHERE invoice_id = ? AND lab_type = ?";
$labInvoiceQuery = $this->db->query($labInvoiceSql, [$invoiceId, $labType]);
$labInvoice = $labInvoiceQuery->getRow();
$data = [
'invoice' => $invoice,
'tests' => $tests ?? [],
'items' => $items ?? [],
'lab_invoice' => $labInvoice,
'lab_type' => $labType,
'lab_type_name' => $this->getLabTypeName($labType),
'lab_type_route' => $this->getLabTypeRoute($labType),
'print_templates' => $this->getDiagnosisPrintTemplates($labType),
];
return view('diagnosis/pathology_detail', $data);
}
public function labDateShow($invoiceId, $labType)
{
// AJAX endpoint to return lab date/time information
try {
log_message('debug', "labDateShow called: invoiceId={$invoiceId}, labType={$labType}");
$isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest';
$sql = "SELECT * FROM lab_invoice_request WHERE invoice_id = ? AND lab_type = ?";
$query = $this->db->query($sql, [$invoiceId, $labType]);
$labInvoice = $query->getRow();
log_message('debug', "Lab invoice request: " . ($labInvoice ? "found" : "not found"));
$data = [
'lab_invoice_request' => $labInvoice ? [$labInvoice] : [],
'invoiceId' => $invoiceId,
'labType' => $labType
];
$view = view('diagnosis/lab_date_show', $data);
log_message('debug', "labDateShow view rendered, length: " . strlen($view));
return $view;
} catch (\Throwable $e) {
log_message('error', "labDateShow exception: " . $e->getMessage());
return '<div class="alert alert-danger">Error loading lab timing: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
public function testList($invoiceId, $labType)
{
// AJAX endpoint to return test list
try {
log_message('debug', "testList called: invoiceId={$invoiceId}, labType={$labType}");
$isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest';
// Get test list with all details - matching old code structure
$sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date,
CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name,
i.item_name, i.id as test_id, i.item_id,
r.lab_repo_id, y.group_desc as item_type_desc, r.status,
r.id as req_id, r.charge_item_id, r.print_combine
FROM invoice_item i
INNER JOIN invoice_master m ON m.id = i.inv_master_id
INNER JOIN hc_item_type y ON i.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
LEFT JOIN lab_request r ON i.id = r.charge_item_id
LEFT JOIN lab_repo l ON i.item_id = l.charge_id
WHERE m.payment_status = 1 AND y.itype_id = ? AND m.id = ?
ORDER BY i.item_name";
$query = $this->db->query($sql, [$labType, $invoiceId]);
$testList = $query->getResult();
log_message('debug', "testList query returned " . count($testList) . " records");
// Get lab invoice request for timing and lab test number
$labInvoiceSql = "SELECT * FROM lab_invoice_request WHERE invoice_id = ? AND lab_type = ?";
$labInvoiceQuery = $this->db->query($labInvoiceSql, [$invoiceId, $labType]);
$labInvoice = $labInvoiceQuery->getRow();
log_message('debug', "Lab invoice request: " . ($labInvoice ? "found" : "not found"));
// Check sample collection status for each test (has lab_request record)
foreach ($testList as $test) {
$test->check_sample = (!empty($test->req_id)) ? 1 : 0;
}
$data = [
'testlist' => $testList ?? [],
'lab_invoice_request' => $labInvoice ? [$labInvoice] : [],
'invoiceId' => $invoiceId,
'labType' => $labType,
'lab_type' => $labType,
'print_templates' => $this->getDiagnosisPrintTemplates($labType),
];
$view = view('diagnosis/lab_invoice_main_test_list', $data);
log_message('debug', "testList view rendered, length: " . strlen($view));
return $view;
} catch (\Throwable $e) {
log_message('error', "testList exception: " . $e->getMessage() . " - " . $e->getTraceAsString());
return '<div class="alert alert-danger">Error loading test list: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
private function processSampleCollection(int $testId, int $labType, ?int $invoiceId = null): array
{
if ($testId <= 0 || $labType <= 0) {
return ['status' => 'error', 'message' => 'Invalid parameters'];
}
try {
if (($invoiceId ?? 0) <= 0) {
$invoiceRow = $this->db->table('invoice_item')
->select('inv_master_id')
->where('id', $testId)
->where('item_type', $labType)
->get()
->getRow();
if ($invoiceRow) {
$invoiceId = (int) ($invoiceRow->inv_master_id ?? 0);
}
}
if (($invoiceId ?? 0) <= 0) {
return ['status' => 'error', 'message' => 'Invalid invoice ID'];
}
$sql = "SELECT m.invoice_code, p.p_fname as inv_name, t.item_name, t.id as test_id,
if(l.mstRepoKey is null, 0, l.mstRepoKey) as mstRepoKey,
if(l.mstRepoKey is null, t.item_name, l.Title) as Title, m.inv_date,
r.lab_repo_id, y.group_desc as item_type_desc, y.itype_id,
m.insurance_case_id, m.ipd_id, m.id as inv_id, m.attach_id, m.net_amount
FROM invoice_item t
INNER JOIN invoice_master m ON m.id = t.inv_master_id
INNER JOIN hc_item_type y ON t.item_type = y.itype_id
INNER JOIN patient_master p ON p.id = m.attach_id
LEFT JOIN lab_request r ON t.id = r.charge_item_id
LEFT JOIN lab_repo l ON t.item_id = l.charge_id
WHERE m.payment_status = 1 AND y.itype_id = ? AND r.id IS NULL AND t.id = ?";
$query = $this->db->query($sql, [$labType, $testId]);
$testData = $query->getRow();
if (! $testData) {
return ['status' => 'error', 'message' => 'Test not found'];
}
$user = service('auth')->user();
$userName = ($user) ? ($user->first_name . ' ' . $user->last_name) : 'System';
$labRequestData = [
'lab_repo_id' => $testData->mstRepoKey ?? 0,
'charge_id' => $testData->inv_id,
'lab_type' => $testData->itype_id,
'charge_item_id' => $testData->test_id,
'invoice_code' => $testData->invoice_code,
'patient_name' => $testData->inv_name,
'patient_id' => $testData->attach_id,
'ipd_id' => $testData->ipd_id ?? 0,
'org_id' => $testData->insurance_case_id ?? 0,
'Request_Date' => $testData->inv_date,
'report_name' => $testData->Title,
'insert_by' => $userName,
'collected_time' => date('Y-m-d H:i:s')
];
$this->db->table('lab_request')->insert($labRequestData);
$insertId = (int) $this->db->insertID();
$labInvoiceCheckSql = "SELECT id FROM lab_invoice_request WHERE lab_type = ? AND invoice_id = ?";
$labInvoiceCheckQuery = $this->db->query($labInvoiceCheckSql, [$labType, $testData->inv_id]);
$labInvoiceRequest = $labInvoiceCheckQuery->getRow();
if (! $labInvoiceRequest) {
$countSql = "SELECT COUNT(*) as no_rec FROM lab_invoice_request WHERE lab_type = ? AND DATE(report_insert) = CURDATE()";
$countQuery = $this->db->query($countSql, [$labType]);
$countData = $countQuery->getRow();
$countRec = ((int) ($countData->no_rec ?? 0)) + 1;
$labInvoiceData = [
'invoice_id' => $testData->inv_id,
'invoice_code' => $testData->invoice_code,
'lab_type' => $testData->itype_id,
'collected_time' => date('Y-m-d H:i:s'),
'daily_sr_no' => $countRec
];
$this->db->table('lab_invoice_request')->insert($labInvoiceData);
} else {
$this->db->table('lab_invoice_request')
->where('id', $labInvoiceRequest->id)
->update(['collected_time' => date('Y-m-d H:i:s')]);
}
return [
'status' => 'success',
'message' => 'Sample collection recorded successfully',
'request_id' => $insertId,
'invoice_id' => (int) $testData->inv_id,
'lab_type' => $labType,
];
} catch (\Throwable $e) {
return [
'status' => 'error',
'message' => 'Error: ' . $e->getMessage(),
];
}
}
private function getLabTypeName(int $labType): string
{
$types = [
1 => 'Ultrasound',
2 => 'MRI',
3 => 'X-Ray',
4 => 'CT-Scan',
5 => 'Pathology',
6 => 'Echo',
30 => 'Biopsy'
];
return $types[$labType] ?? 'Diagnosis';
}
private function getLabTypeRoute(int $labType): string
{
$routes = [
1 => 'ultrasound',
2 => 'mri',
3 => 'xray',
4 => 'ctscan',
5 => 'pathology',
6 => 'echo',
30 => 'biopsy',
];
return $routes[$labType] ?? 'diagnosis';
}
private function isRadiologyType(int $labType): bool
{
return in_array($labType, [1, 2, 3, 4, 6], true);
}
public function testData()
{
// Diagnostic endpoint to check for test data
// Find an invoice with pathology tests (lab_type=5)
$sql = "SELECT m.id, m.invoice_code, COUNT(i.id) as test_count
FROM invoice_master m
INNER JOIN invoice_item i ON m.id = i.inv_master_id
WHERE m.payment_status = 1 AND i.item_type = 5
GROUP BY m.id
LIMIT 10";
$query = $this->db->query($sql);
$results = $query->getResult();
return $this->response->setJSON([
'message' => 'Sample pathology invoices with test data',
'invoices' => $results,
'instruction' => 'Use an invoice ID from the results above for testing'
]);
}
public function sampleCollection($testId, $labType)
{
$invoiceId = (int) ($this->request->getPost('invoice_id') ?? 0);
$result = $this->processSampleCollection((int) $testId, (int) $labType, $invoiceId > 0 ? $invoiceId : null);
if (($result['status'] ?? 'error') === 'success') {
return $this->response->setJSON($result);
}
return $this->response->setJSON([
'status' => 'error',
'message' => $result['message'] ?? 'Unknown error',
]);
}
public function labTab1Process($testId, $labType)
{
$result = $this->processSampleCollection((int) $testId, (int) $labType, null);
if (($result['status'] ?? 'error') === 'success') {
return (string) ($result['request_id'] ?? 0);
}
return '0';
}
public function updateLabTiming()
{
// AJAX endpoint to update lab collection and report times
$invoiceId = $this->request->getPost('invoice_id');
$labType = $this->request->getPost('lab_type');
$collectedTime = $this->request->getPost('collected_time');
$reportedTime = $this->request->getPost('reported_time');
if (!$invoiceId || !$labType) {
return $this->response->setJSON(['status' => 'error', 'message' => 'Invalid parameters']);
}
try {
$updateData = [];
if (!empty($collectedTime)) {
$updateData['collected_time'] = $collectedTime;
}
if (!empty($reportedTime)) {
$updateData['reported_time'] = $reportedTime;
}
if (empty($updateData)) {
return $this->response->setJSON(['status' => 'error', 'message' => 'No data to update']);
}
$builder = $this->db->table('lab_invoice_request');
$builder->where('invoice_id', $invoiceId);
$builder->where('lab_type', $labType);
$result = $builder->update($updateData);
if ($result) {
return $this->response->setJSON([
'status' => 'success',
'message' => 'Lab timing updated successfully'
]);
} else {
return $this->response->setJSON([
'status' => 'error',
'message' => 'No records updated'
]);
}
} catch (Exception $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
}
}
public function updateLabNo($labReqId)
{
// AJAX endpoint to update lab test number
$labTestNo = $this->request->getPost('lab_test_no');
if (!$labReqId || $labTestNo === '') {
return $this->response->setJSON(['status' => 'error', 'message' => 'Invalid parameters']);
}
try {
$builder = $this->db->table('lab_invoice_request');
$builder->where('id', $labReqId);
$result = $builder->update(['lab_test_no' => $labTestNo]);
if ($result || $this->db->affectedRows() > 0) {
return $this->response->setJSON([
'status' => 'success',
'message' => 'Lab test number updated successfully'
]);
} else {
return $this->response->setJSON([
'status' => 'error',
'message' => 'No records updated'
]);
}
} catch (Exception $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
}
}
public function editTestData($labReqId)
{
// AJAX endpoint to load old HMS-style detailed test-entry table
try {
$labReqId = (int) $labReqId;
log_message('debug', "editTestData called: labReqId={$labReqId}");
$masterQuery = $this->db->query('SELECT * FROM lab_request WHERE id = ?', [$labReqId]);
$labRequestMaster = $masterQuery->getRow();
if (! $labRequestMaster) {
return '<div class="alert alert-danger">Test request not found</div>';
}
$repoTestsSql = "SELECT r.mstRepoKey, t.mstTestKey
FROM lab_repo r
JOIN lab_repotests j ON r.mstRepoKey = j.mstRepoKey
JOIN lab_tests t ON j.mstTestKey = t.mstTestKey
WHERE r.mstRepoKey = ?
ORDER BY j.EOrder";
$repoTests = $this->db->query($repoTestsSql, [$labRequestMaster->lab_repo_id])->getResult();
foreach ($repoTests as $repoTest) {
$existsSql = "SELECT id FROM lab_request_item
WHERE lab_request_id = ? AND lab_repo_id = ? AND lab_test_id = ?
LIMIT 1";
$exists = $this->db->query($existsSql, [
$labReqId,
$repoTest->mstRepoKey,
$repoTest->mstTestKey,
])->getRow();
if (! $exists) {
$this->db->table('lab_request_item')->insert([
'lab_request_id' => $labReqId,
'lab_repo_id' => $repoTest->mstRepoKey,
'lab_test_id' => $repoTest->mstTestKey,
]);
}
}
$itemsSql = "SELECT
d.mstTestKey,
d.Test,
d.TestID,
d.Result,
d.Formula,
d.VRule,
d.VMsg,
d.Unit,
d.FixedNormals,
i.lab_test_value,
i.lab_test_remark,
i.id,
s.EOrder,
GROUP_CONCAT(CONCAT(o.id, ':', o.option_text, ':', o.option_value) ORDER BY o.sort_id) AS option_value
FROM lab_request_item i
JOIN lab_tests d ON i.lab_test_id = d.mstTestKey
JOIN lab_repotests s ON d.mstTestKey = s.mstTestKey AND i.lab_repo_id = s.mstRepoKey
LEFT JOIN lab_tests_option o ON d.mstTestKey = o.mstTestKey
WHERE i.lab_request_id = ?
GROUP BY d.mstTestKey
ORDER BY s.EOrder";
$labRequestItems = $this->db->query($itemsSql, [$labReqId])->getResult();
return view('diagnosis/test_data_entry_table', [
'lab_request_master' => [$labRequestMaster],
'lab_request_item_entry' => $labRequestItems,
]);
} catch (\Throwable $e) {
log_message('error', 'editTestData exception: ' . $e->getMessage());
return '<div class="alert alert-danger">Error: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
public function updateTestValue()
{
// AJAX endpoint to update individual lab test value in modal table
$itemId = (int) $this->request->getPost('test_id');
$testValue = (string) $this->request->getPost('test_value');
if ($itemId <= 0) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Invalid test id',
]);
}
try {
$this->db->table('lab_request_item')
->where('id', $itemId)
->update(['lab_test_value' => $testValue]);
return $this->response->setJSON([
'status' => 'success',
'value' => $testValue,
]);
} catch (\Throwable $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
}
public function createReport($labReqId)
{
// Old HMS flow equivalent: generate Report_Data, update status, then show final report editor
$labReqId = (int) $labReqId;
if ($labReqId <= 0) {
return '<div class="alert alert-danger">Invalid request id</div>';
}
try {
$labRequest = $this->db->table('lab_request')->where('id', $labReqId)->get()->getRow();
if (! $labRequest) {
return '<div class="alert alert-danger">Request not found</div>';
}
if ($this->isRadiologyType((int) ($labRequest->lab_type ?? 0))) {
return $this->createReportXray($labReqId);
}
$gender = 1;
if (! empty($labRequest->patient_id)) {
$patient = $this->db->table('patient_master')
->where('id', (int) $labRequest->patient_id)
->get()
->getRow();
if ($patient && isset($patient->gender)) {
$gender = (int) $patient->gender;
}
}
$itemSql = "SELECT
d.Test,
d.TestID,
d.Formula,
d.FixedNormals,
d.isGenderSpecific,
d.FixedNormalsWomen,
i.lab_test_value
FROM lab_request_item i
JOIN lab_tests d ON i.lab_test_id = d.mstTestKey
JOIN lab_repotests s ON d.mstTestKey = s.mstTestKey AND i.lab_repo_id = s.mstRepoKey
WHERE i.lab_request_id = ?
ORDER BY s.EOrder";
$dataLabTestList = $this->db->query($itemSql, [$labReqId])->getResult();
$repoFormat = $this->db->table('lab_repo')
->select('mstRepoKey, Title, HTMLData')
->where('mstRepoKey', (int) $labRequest->lab_repo_id)
->get()
->getRow();
$reportTitle = $repoFormat && ! empty($repoFormat->Title)
? $repoFormat->Title
: ($labRequest->report_name ?? 'Lab Report');
$reportString = $repoFormat->HTMLData ?? '';
$reportHeader = '<table border="0" cellpadding="1" cellspacing="1" style="width:100%"><tbody><tr><td><h3>' .
htmlspecialchars((string) $reportTitle, ENT_QUOTES, 'UTF-8') .
'</h3></td></tr></tbody></table>';
$reportFooter = $labRequest->Remark ?? '';
$labTestArray = [];
foreach ($dataLabTestList as $row) {
$labTestArray[$row->TestID] = $row->lab_test_value;
}
foreach ($dataLabTestList as $row) {
$fixedNormals = (string) ($row->FixedNormals ?? '');
$fixedNormalsWomen = (string) ($row->FixedNormalsWomen ?? '');
$formula = trim((string) ($row->Formula ?? ''));
if ($formula !== '') {
foreach ($labTestArray as $key => $value) {
$replaceValue = is_numeric($value) ? $value : '0';
$formula = str_replace('{' . $key . '}', (string) $replaceValue, $formula);
}
$labTestValue = function_exists('cal_exp') ? round((float) cal_exp($formula), 2) : ($row->lab_test_value ?? '');
} else {
$labTestValue = $row->lab_test_value ?? '';
}
$normalRange = ((int) ($row->isGenderSpecific ?? 0) === 1 && $gender !== 1)
? $fixedNormalsWomen
: $fixedNormals;
if ($normalRange !== '' && is_numeric($labTestValue)) {
$parts = explode('-', $normalRange);
if (count($parts) >= 2 && is_numeric($parts[0]) && is_numeric($parts[1])) {
$min = (float) $parts[0];
$max = (float) $parts[1];
$numValue = (float) $labTestValue;
if (! ($numValue >= $min && $numValue <= $max)) {
$labTestValue = '<b>' . $labTestValue . '</b>';
}
}
}
$reportString = str_replace('{' . $row->TestID . '}', (string) $labTestValue, $reportString);
}
$completeReport = $reportHeader . $reportString . $reportFooter;
$now = date('Y-m-d H:i:s');
$this->db->table('lab_request')
->where('id', $labReqId)
->update([
'Report_Data' => $completeReport,
'status' => 1,
'reported_time' => $now,
]);
$this->db->table('lab_invoice_request')
->where('invoice_id', (int) $labRequest->charge_id)
->where('lab_type', (int) $labRequest->lab_type)
->update([
'reported_time' => $now,
]);
return $this->showReportFinal($labReqId);
} catch (\Throwable $e) {
return '<div class="alert alert-danger">Error: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
public function showReportFinal($labReqId)
{
$labReqId = (int) $labReqId;
$row = $this->db->table('lab_request')->where('id', $labReqId)->get()->getRow();
if (! $row) {
return '<div class="alert alert-danger">Report not found</div>';
}
if ($this->isRadiologyType((int) ($row->lab_type ?? 0))) {
return $this->showReportFinalXray($labReqId);
}
return view('diagnosis/lab_final_report_show', [
'report_format' => [$row],
]);
}
public function createReportXray($labReqId)
{
$labReqId = (int) $labReqId;
if ($labReqId <= 0) {
return '<div class="alert alert-danger">Invalid request id</div>';
}
try {
$labRequest = $this->db->table('lab_request')->where('id', $labReqId)->get()->getRow();
if (! $labRequest) {
return '<div class="alert alert-danger">Request not found</div>';
}
$repoFormat = $this->db->table('lab_repo')
->select('Title, HTMLData')
->where('mstRepoKey', (int) ($labRequest->lab_repo_id ?? 0))
->get()
->getRow();
$reportTitle = $repoFormat && ! empty($repoFormat->Title)
? (string) $repoFormat->Title
: (string) ($labRequest->report_name ?? 'Radiology Report');
$reportString = (string) ($repoFormat->HTMLData ?? '');
$reportHeader = '<table border="0" cellpadding="1" cellspacing="1" style="width:100%"><tbody><tr><td><h3>' .
htmlspecialchars($reportTitle, ENT_QUOTES, 'UTF-8') .
'</h3></td></tr></tbody></table>';
$reportFooter = (string) ($labRequest->Remark ?? '');
$completeReport = $reportHeader . $reportString . $reportFooter;
$now = date('Y-m-d H:i:s');
$this->db->table('lab_request')
->where('id', $labReqId)
->update([
'Report_Data' => $completeReport,
'status' => 1,
'reported_time' => $now,
]);
$this->db->table('lab_invoice_request')
->where('invoice_id', (int) ($labRequest->charge_id ?? 0))
->where('lab_type', (int) ($labRequest->lab_type ?? 0))
->update([
'reported_time' => $now,
]);
return $this->showReportFinalXray($labReqId);
} catch (\Throwable $e) {
return '<div class="alert alert-danger">Error: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
public function showReportFinalXray($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>';
}
$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_show_xray', [
'report_format' => [$reportRow],
'radiology_ultrasound_template' => $templates,
]);
}
public function openReportEditorPage($labReqId)
{
$labReqId = (int) $labReqId;
$editReason = trim((string) ($this->request->getGet('edit_reason') ?? ''));
$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,
'edit_reason' => $editReason,
]);
}
public function getTemplateXray($templateId)
{
$templateId = (int) $templateId;
if ($templateId <= 0) {
return $this->response->setJSON([
'Findings' => '',
'Impression' => '',
]);
}
$row = $this->db->table('radiology_ultrasound_template')
->select('Findings, Impression')
->where('id', $templateId)
->get()
->getRow();
return $this->response->setJSON([
'Findings' => (string) ($row->Findings ?? ''),
'Impression' => (string) ($row->Impression ?? ''),
]);
}
public function finalUpdateXray($labReqId)
{
$labReqId = (int) $labReqId;
$htmlData = (string) ($this->request->getPost('HTMLData') ?? $this->request->getPost('report_data') ?? '');
$impression = (string) ($this->request->getPost('report_data_Impression') ?? $this->request->getPost('report_data_impression') ?? '');
$editReason = trim((string) ($this->request->getPost('edit_reason') ?? ''));
if ($labReqId <= 0) {
return $this->response->setJSON(['status' => 'error', 'message' => 'Invalid request id']);
}
try {
$before = $this->db->table('lab_request')
->select('id, status, report_edit_req_no, Report_Data, report_data_Impression')
->where('id', $labReqId)
->get(1)
->getRowArray() ?? [];
if ((int) ($before['status'] ?? 0) === 2 && $editReason === '') {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Edit reason is required for verified report changes (NABH audit).',
]);
}
$this->db->table('lab_request')
->where('id', $labReqId)
->update([
'Report_Data' => $htmlData,
'report_data_Impression' => $impression,
]);
$fields = $this->db->getFieldNames('lab_request');
if (in_array('report_edit_req_no', $fields, true) && (int) ($before['status'] ?? 0) === 2) {
$nextEditNo = (int) ($before['report_edit_req_no'] ?? 0) + 1;
$this->db->table('lab_request')
->where('id', $labReqId)
->update(['report_edit_req_no' => $nextEditNo]);
}
if ($this->db->tableExists('lab_log')) {
$user = service('auth')->user();
$userId = (int) ($user->id ?? 0);
$userName = $user ? trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) : 'System';
$beforeHash = md5((string) ($before['Report_Data'] ?? '') . '|' . (string) ($before['report_data_Impression'] ?? ''));
$afterHash = md5($htmlData . '|' . $impression);
$reasonLog = $editReason !== '' ? $editReason : 'NA';
$this->db->table('lab_log')->insert([
'lab_repo_id' => $labReqId,
'log_by_id' => $userId,
'log_by' => $userName,
'log_type_id' => 0,
'log_type' => 'Report Edit',
'log_Faults_id' => 0,
'log_Faults' => 'Radiology',
'comments' => 'Editor save [reason:' . substr($reasonLog, 0, 350) . '] [before:' . substr($beforeHash, 0, 8) . ' after:' . substr($afterHash, 0, 8) . ']',
]);
}
return $this->response->setJSON(['status' => 'success', 'message' => 'Saved']);
} catch (\Throwable $e) {
return $this->response->setJSON(['status' => 'error', 'message' => $e->getMessage()]);
}
}
public function confirmReportXray($labReqId)
{
return $this->confirmReport($labReqId);
}
public function finalUpdate($labReqId)
{
$labReqId = (int) $labReqId;
// Accept both legacy and current POST keys for backward compatibility
$htmlData = (string) ($this->request->getPost('HTMLData') ?? $this->request->getPost('report_data') ?? '');
$impression = (string) ($this->request->getPost('report_data_Impression') ?? $this->request->getPost('report_data_impression') ?? '');
$editReason = trim((string) ($this->request->getPost('edit_reason') ?? ''));
if ($labReqId <= 0) {
return $this->response->setJSON(['status' => 'error', 'message' => 'Invalid request id']);
}
try {
$before = $this->db->table('lab_request')
->select('id, status, report_edit_req_no, Report_Data, report_data_Impression')
->where('id', $labReqId)
->get(1)
->getRowArray() ?? [];
// NABH audit requirement: edit reason required for verified reports (status = 2)
if ((int) ($before['status'] ?? 0) === 2 && $editReason === '') {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Edit reason is required for verified report changes (NABH audit).',
]);
}
$this->db->table('lab_request')
->where('id', $labReqId)
->update([
'Report_Data' => $htmlData,
'report_data_Impression' => $impression,
]);
// Track edit version for verified reports
$fields = $this->db->getFieldNames('lab_request');
if (in_array('report_edit_req_no', $fields, true) && (int) ($before['status'] ?? 0) === 2) {
$nextEditNo = (int) ($before['report_edit_req_no'] ?? 0) + 1;
$this->db->table('lab_request')
->where('id', $labReqId)
->update(['report_edit_req_no' => $nextEditNo]);
}
// NABH compliance: log all changes to verified reports
if ($this->db->tableExists('lab_log')) {
$user = service('auth')->user();
$userId = (int) ($user->id ?? 0);
$userName = $user ? trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) : 'System';
$beforeHash = md5((string) ($before['Report_Data'] ?? '') . '|' . (string) ($before['report_data_Impression'] ?? ''));
$afterHash = md5($htmlData . '|' . $impression);
$reasonLog = $editReason !== '' ? $editReason : 'NA';
$this->db->table('lab_log')->insert([
'lab_repo_id' => $labReqId,
'log_by_id' => $userId,
'log_by' => $userName,
'log_type_id' => 0,
'log_type' => 'Report Edit',
'log_Faults_id' => 0,
'log_Faults' => 'Pathology',
'comments' => 'Editor save [reason:' . substr($reasonLog, 0, 350) . '] [before:' . substr($beforeHash, 0, 8) . ' after:' . substr($afterHash, 0, 8) . ']',
]);
}
return $this->response->setJSON(['status' => 'success', 'message' => 'Saved']);
} catch (\Throwable $e) {
return $this->response->setJSON(['status' => 'error', 'message' => $e->getMessage()]);
}
}
public function confirmReport($labReqId)
{
$labReqId = (int) $labReqId;
if ($labReqId <= 0) {
return $this->response->setJSON(['status' => 'error', 'message' => 'Invalid request id']);
}
try {
$user = service('auth')->user();
$userName = $user ? trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) : 'System';
$this->db->table('lab_request')
->where('id', $labReqId)
->update([
'confirm_by' => $userName,
'status' => 2,
]);
return $this->response->setJSON(['status' => 'success', 'message' => 'Verified and Ready for Print']);
} catch (\Throwable $e) {
return $this->response->setJSON(['status' => 'error', 'message' => $e->getMessage()]);
}
}
public function printSingleReport($labReqId)
{
$labReqId = (int) $labReqId;
$templateId = (int) ($this->request->getGet('template_id') ?? 0);
set_time_limit(300);
if ($labReqId <= 0) {
return '<h3>Invalid request id</h3>';
}
$sql = "SELECT l.*, m.invoice_code, m.attach_id,
p.p_fname, p.p_rname, p.p_relative, p.p_code, p.gender, p.dob, p.age, p.age_in, p.age_in_month, p.estimate_dob,
r.Title as repo_title, g.RepoGrp
FROM lab_request l
LEFT JOIN invoice_master m ON m.id = l.charge_id
LEFT JOIN patient_master p ON p.id = m.attach_id
LEFT JOIN lab_repo r ON r.mstRepoKey = l.lab_repo_id
LEFT JOIN lab_rgroups g ON g.mstRGrpKey = r.GrpKey
WHERE l.id = ?";
$row = $this->db->query($sql, [$labReqId])->getRow();
if (! $row) {
return '<h3>Report not found</h3>';
}
$reportData = trim((string) ($row->Report_Data ?? ''));
$impression = trim((string) ($row->report_data_Impression ?? ''));
$reportHtml = '';
if ($reportData !== '') {
$reportHtml .= $reportData;
}
if ($impression !== '') {
$reportHtml .= '<div style="margin-top:10px;"><b>Impression :</b><br/>' . nl2br($impression) . '</div>';
}
if ($reportHtml === '') {
$reportHtml = $this->resolveFallbackReportHtml((int) ($row->lab_repo_id ?? 0), (int) ($row->charge_item_id ?? 0), (int) ($row->lab_type ?? 0));
}
if ($reportHtml === '') {
$reportHtml = '<p>No report data available for this request. Please use Edit and save report content.</p>';
}
$genderValue = (int) ($row->gender ?? 0);
$genderText = $genderValue === 1 ? 'Male' : ($genderValue === 2 ? 'Female' : '-');
$ageText = '-';
if (function_exists('get_age_1')) {
$ageText = (string) get_age_1(
$row->dob ?? null,
$row->age ?? '',
$row->age_in_month ?? '',
$row->estimate_dob ?? ''
);
} elseif (! empty($row->age)) {
$ageText = (string) $row->age;
}
$printSetting = $this->getDiagnosisTemplateSetting((int) ($row->lab_type ?? 0), $templateId);
$isPlainPaper = false;
// Compute display values here so they are available for token substitution.
$patientName = trim((string) ($row->p_fname ?? ''));
$relativeName = trim((string) ($row->p_relative ?? ''));
$relativeText = trim((string) ($row->p_rname ?? ''));
$repoTitle = trim((string) ($row->report_name ?? $row->repo_title ?? $row->RepoGrp ?? 'Report'));
$headRow = $this->db->table('diagnosis_head_name')
->where('d_type', (int) ($row->lab_type ?? 0))
->get(1)
->getRow();
$tokens = $this->buildPdfTokens([
'invoice_code' => $row->invoice_code ?? '',
'patient_name' => $patientName,
'relative_type' => $relativeName,
'relative_name' => $relativeText,
'age' => $ageText,
'gender' => $genderText,
'uhid' => $row->p_code ?? '',
'collected_time' => $row->collected_time ?? '',
'reported_time' => $row->reported_time ?? '',
'report_title' => $repoTitle,
'doctor_name' => $headRow->doc_name ?? '',
'doctor_education' => $headRow->doc_edu ?? '',
'technician_name' => $headRow->tech_name ?? '',
'signature_image_url' => (string) ($printSetting['signature_image'] ?? ''),
]);
// Render the optional patient info template (tokens resolved).
$rawPatientInfoHtml = trim((string) ($printSetting['patient_info_html'] ?? ''));
if ($rawPatientInfoHtml !== '') {
$patientInfoHtml = $this->applyPdfTokens($rawPatientInfoHtml, $tokens);
} elseif (($printSetting['id'] ?? 0) <= 0) {
$patientInfoHtml = $this->buildDefaultPatientInfoHtml([
'invoice_code' => $row->invoice_code ?? '',
'patient_name' => $patientName,
'relative_name' => $relativeName,
'relative_text' => $relativeText,
'age_text' => $ageText,
'gender_text' => $genderText,
'uhid' => $row->p_code ?? '',
'collected_time' => $row->collected_time ?? '',
'reported_time' => $row->reported_time ?? '',
]);
} else {
$patientInfoHtml = '';
}
$reportHtml = trim((string) ($printSetting['content_prefix_html'] ?? '')) . $reportHtml . trim((string) ($printSetting['content_suffix_html'] ?? ''));
$pdfHtml = view('diagnosis/pdf_single_report', [
'report' => $row,
'reportHtml' => $reportHtml,
'isPlainPaper' => $isPlainPaper,
'ageText' => $ageText,
'genderText' => $genderText,
'printSetting' => $printSetting,
'patientName' => $patientName,
'relativeName' => $relativeName,
'relativeText' => $relativeText,
'repoTitle' => $repoTitle,
'patientInfoHtml' => $patientInfoHtml,
]);
$rawHeaderHtml = trim((string) ($printSetting['header_html'] ?? ''));
$rawFirstPageHeaderHtml = trim((string) ($printSetting['first_page_header_html'] ?? ''));
$rawFooterHtml = trim((string) ($printSetting['footer_html'] ?? ''));
$rawLastPageFooterHtml = trim((string) ($printSetting['last_page_footer_html'] ?? ''));
$autoHeaderHtml = $this->buildAutoMpdfHeaderBlock(
$rawHeaderHtml,
$rawFirstPageHeaderHtml,
'diag_auto_header_single'
);
$autoFooterHtml = $this->buildAutoMpdfFooterBlock(
$rawFooterHtml,
$rawLastPageFooterHtml,
'diag_auto_footer_single'
);
$prefixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_prefix_html'] ?? ''))
. $autoHeaderHtml
. $autoFooterHtml, $tokens);
$suffixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_suffix_html'] ?? '')), $tokens);
$marginTop = $this->cmToMm($printSetting['page_margin_top_cm'] ?? 1.2, 1.2);
$marginBottom = $this->cmToMm($printSetting['page_margin_bottom_cm'] ?? 1.2, 1.2);
$marginLeft = $this->cmToMm($printSetting['page_margin_left_cm'] ?? 1.0, 1.0);
$marginRight = $this->cmToMm($printSetting['page_margin_right_cm'] ?? 1.0, 1.0);
$marginHeader = $this->cmToMm($printSetting['margin_header_cm'] ?? 0.5, 0.5);
$marginFooter = $this->cmToMm($printSetting['margin_footer_cm'] ?? 1.5, 1.5);
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'format' => (string) ($printSetting['page_size'] ?? 'A4'),
'margin_top' => $marginTop,
'margin_bottom' => $marginBottom,
'margin_left' => $marginLeft,
'margin_right' => $marginRight,
'margin_header' => $marginHeader,
'margin_footer' => $marginFooter,
'tempDir' => $mpdfTempDir,
'autoScriptToLang' => false,
'autoLanguageDetection' => false,
'autoArabic' => false,
'autoVietnamese' => false,
]);
// For plain HTML (without explicit mPDF tags), set header/footer via API.
$headerHasTags = preg_match('/<\s*(htmlpageheader|sethtmlpageheader)\b/i', $rawHeaderHtml . $rawFirstPageHeaderHtml) === 1;
$footerHasTags = preg_match('/<\s*(htmlpagefooter|sethtmlpagefooter)\b/i', $rawFooterHtml . $rawLastPageFooterHtml) === 1;
if (! $headerHasTags) {
$plainHeader = $this->applyPdfTokens($rawHeaderHtml . $rawFirstPageHeaderHtml, $tokens);
if ($plainHeader !== '') {
$mpdf->SetHTMLHeader($plainHeader, 'O');
$mpdf->SetHTMLHeader($plainHeader, 'E');
}
}
if (! $footerHasTags) {
$plainFooter = $this->applyPdfTokens($rawFooterHtml, $tokens);
if ($plainFooter === '') {
$plainFooter = $this->applyPdfTokens($rawLastPageFooterHtml, $tokens);
}
if ($plainFooter !== '') {
$mpdf->SetHTMLFooter($plainFooter, 'O');
$mpdf->SetHTMLFooter($plainFooter, 'E');
}
}
$backgroundImage = trim((string) ($printSetting['page_background_image'] ?? ''));
if ($backgroundImage !== '') {
$absolutePath = FCPATH . ltrim(str_replace('\\', '/', $backgroundImage), '/');
if (is_file($absolutePath)) {
$mpdf->SetWatermarkImage($absolutePath, 1, [210, 297], 'F');
$mpdf->showWatermarkImage = true;
}
}
$watermarkType = (string) ($printSetting['watermark_type'] ?? 'none');
$watermarkAlpha = (float) ($printSetting['watermark_alpha'] ?? 0.12);
if ($watermarkType === 'text') {
$text = trim((string) ($printSetting['watermark_text'] ?? ''));
if ($text !== '') {
$mpdf->SetWatermarkText($text, $watermarkAlpha);
$mpdf->showWatermarkText = true;
}
} elseif ($watermarkType === 'image') {
$wmImage = trim((string) ($printSetting['watermark_image'] ?? ''));
if ($wmImage !== '') {
$absolutePath = FCPATH . ltrim(str_replace('\\', '/', $wmImage), '/');
if (is_file($absolutePath)) {
$mpdf->SetWatermarkImage($absolutePath, $watermarkAlpha);
$mpdf->showWatermarkImage = true;
}
}
}
if ($prefixHtml !== '') {
$mpdf->WriteHTML($prefixHtml, HTMLParserMode::HTML_HEADER_BUFFER);
}
$mpdf->WriteHTML($pdfHtml);
if ($suffixHtml !== '') {
$mpdf->WriteHTML($suffixHtml, HTMLParserMode::HTML_HEADER_BUFFER, false, false);
}
$invoiceCode = preg_replace('/[^A-Za-z0-9\-_]/', '_', (string) ($row->invoice_code ?? 'invoice'));
$fileName = 'Report_' . $invoiceCode . '_' . $labReqId . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->setHeader('Pragma', 'no-cache')
->setHeader('Expires', '0')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
private function getDiagnosisPrintTemplates(int $labType): array
{
if ($labType <= 0 || ! $this->db->tableExists('diagnosis_print_templates')) {
return [];
}
return $this->db->table('diagnosis_print_templates')
->select('id, template_name, is_default')
->where('modality', $labType)
->where('status', 1)
->orderBy('is_default', 'DESC')
->orderBy('template_name', 'ASC')
->get()
->getResultArray();
}
private function getDiagnosisTemplateSetting(int $labType, int $templateId = 0): array
{
$legacy = $this->getDiagnosisPrintSetting($labType);
$defaults = array_merge($legacy, [
'page_size' => 'A4',
'page_margin_top_cm' => 6.1,
'id' => 0,
'page_margin_bottom_cm' => 2.5,
'page_margin_left_cm' => 0.7,
'page_margin_right_cm' => 0.7,
'margin_header_cm' => 0.5,
'margin_footer_cm' => 1.5,
'page_background_image' => '',
'watermark_type' => 'none',
'watermark_text' => '',
'watermark_image' => '',
'signature_image' => '',
'watermark_alpha' => 0.12,
'header_html' => '',
'first_page_header_html' => '',
'content_prefix_html' => '',
'content_suffix_html' => '',
'footer_html' => '',
'last_page_footer_html' => '',
]);
if ($labType <= 0 || ! $this->db->tableExists('diagnosis_print_templates')) {
return $defaults;
}
$builder = $this->db->table('diagnosis_print_templates')
->where('modality', $labType)
->where('status', 1);
if ($templateId > 0) {
$builder->where('id', $templateId);
} else {
$builder->orderBy('is_default', 'DESC')->orderBy('id', 'ASC');
}
$row = $builder->get(1)->getRowArray();
if (! is_array($row)) {
return $defaults;
}
foreach ($defaults as $key => $value) {
if (array_key_exists($key, $row) && $row[$key] !== null && $row[$key] !== '') {
if (is_float($value)) {
$defaults[$key] = (float) $row[$key];
} else {
$defaults[$key] = (string) $row[$key];
}
}
}
$pageSize = strtoupper(trim((string) ($defaults['page_size'] ?? 'A4')));
$defaults['id'] = (int) ($row['id'] ?? 0);
if (! in_array($pageSize, ['A4', 'A4-L', 'LETTER', 'LEGAL'], true)) {
$pageSize = 'A4';
}
$defaults['page_size'] = $pageSize;
return $defaults;
}
private function getDiagnosisPrintSetting(int $labType): array
{
$defaults = [
'letter_margin_top' => 12.0,
'letter_margin_left' => 10.0,
'letter_margin_right' => 10.0,
'letter_margin_bottom' => 12.0,
'plain_header_html' => '',
'plain_background_image' => '',
'mpdf_prefix_html' => '',
'mpdf_suffix_html' => '',
'patient_info_html' => '',
];
if ($labType <= 0 || ! $this->db->tableExists('diagnosis_head_name')) {
return $defaults;
}
$fields = $this->db->getFieldNames('diagnosis_head_name');
$select = ['d_type'];
foreach (array_keys($defaults) as $field) {
if (in_array($field, $fields, true)) {
$select[] = $field;
}
}
$row = $this->db->table('diagnosis_head_name')
->select(implode(',', $select))
->where('d_type', $labType)
->get(1)
->getRowArray();
if (! is_array($row)) {
return $defaults;
}
foreach ($defaults as $key => $defaultValue) {
if (array_key_exists($key, $row) && $row[$key] !== null && $row[$key] !== '') {
$defaults[$key] = is_float($defaultValue) ? (float) $row[$key] : (string) $row[$key];
}
}
return $defaults;
}
/**
* Build an associative array of {{token}} => value pairs for PDF template substitution.
* All string values are HTML-escaped so they are safe to embed inside HTML attributes and text.
*/
private function buildPdfTokens(array $data): array
{
$e = static function (string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
};
$age = (string) ($data['age'] ?? '-');
$gender = (string) ($data['gender'] ?? '-');
$hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital';
$hospitalAddress1 = defined('H_address_1') ? (string) constant('H_address_1') : '';
$hospitalPhone = defined('H_phone_No') ? (string) constant('H_phone_No') : '';
$hospitalLogoName = defined('H_logo') ? trim((string) constant('H_logo')) : '';
$hospitalLogoUrl = $this->resolvePdfAssetPath([
$hospitalLogoName !== '' ? 'assets/images/' . ltrim($hospitalLogoName, '/\\') : '',
'assets/img/logo.png',
'assets/images/no_image.svg',
]);
$signatureImageUrl = $this->resolvePdfAssetPath([
(string) ($data['signature_image_url'] ?? ''),
'assets/images/drPreetiSingh.jpg',
]);
return [
'{{invoice_code}}' => $e((string) ($data['invoice_code'] ?? '')),
'{{patient_name}}' => $e((string) ($data['patient_name'] ?? '')),
'{{relative_type}}' => $e((string) ($data['relative_type'] ?? '')),
'{{relative_name}}' => $e((string) ($data['relative_name'] ?? '')),
'{{relative}}' => $e(trim((string) ($data['relative_type'] ?? '') . ' ' . (string) ($data['relative_name'] ?? ''))),
'{{age}}' => $e($age),
'{{gender}}' => $e($gender),
'{{age_sex}}' => $e($age . ' / ' . $gender),
'{{uhid}}' => $e((string) ($data['uhid'] ?? '')),
'{{collected_time}}' => $e((string) ($data['collected_time'] ?? '')),
'{{reported_time}}' => $e((string) ($data['reported_time'] ?? '')),
'{{printed_time}}' => date('d-m-Y h:i A'),
'{{report_title}}' => $e((string) ($data['report_title'] ?? '')),
'{{doctor_name}}' => $e((string) ($data['doctor_name'] ?? '')),
'{{doctor_education}}' => $e((string) ($data['doctor_education'] ?? '')),
'{{technician_name}}' => $e((string) ($data['technician_name'] ?? '')),
'{{hospital_name}}' => $e($hospitalName),
'{{hospital_address_1}}' => $e($hospitalAddress1),
'{{hospital_phone}}' => $e($hospitalPhone),
'{{hospital_logo_name}}' => $e($hospitalLogoName),
'{{hospital_logo_url}}' => $e($hospitalLogoUrl),
'{{hospital_logo_path}}' => $e($hospitalLogoUrl),
'{{signature_image_url}}' => $e($signatureImageUrl),
'{{signature_image_path}}' => $e($signatureImageUrl),
];
}
private function resolvePdfAssetPath(array $candidates): string
{
foreach ($candidates as $candidate) {
$candidate = trim((string) $candidate);
if ($candidate === '') {
continue;
}
if (preg_match('/^https?:\/\//i', $candidate) === 1 || str_starts_with($candidate, 'data:')) {
continue;
}
if (preg_match('/^[A-Za-z]:[\\\\\/]/', $candidate) === 1) {
$absolutePath = $candidate;
} else {
$relativePath = ltrim(str_replace('\\', '/', $candidate), '/');
$absolutePath = str_starts_with($relativePath, 'public/')
? ROOTPATH . $relativePath
: FCPATH . $relativePath;
}
$absolutePath = str_replace('\\', '/', $absolutePath);
if (is_file($absolutePath)) {
return $absolutePath;
}
}
return '';
}
/**
* Replace all {{token}} placeholders in $html using the token map returned by buildPdfTokens().
*/
private function applyPdfTokens(string $html, array $tokens): string
{
return str_replace(array_keys($tokens), array_values($tokens), $html);
}
private function buildDefaultPatientInfoHtml(array $data): string
{
$e = static function (string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
};
$ageSex = $e((string) ($data['age_text'] ?? '-')) . ' / ' . $e((string) ($data['gender_text'] ?? '-'));
$relative = $e(trim((string) ($data['relative_name'] ?? '') . ' ' . (string) ($data['relative_text'] ?? '')));
return '<table width="100%" style="border-collapse:collapse;margin-bottom:8px;font-size:11px;">'
. '<tr>'
. '<td width="50%" style="vertical-align:top;">'
. '<b>Invoice ID:</b> ' . $e((string) ($data['invoice_code'] ?? '')) . '<br>'
. '<b>Patient:</b> ' . $e((string) ($data['patient_name'] ?? '')) . '<br>'
. '<b>Relative:</b> ' . $relative . '<br>'
. '<b>Age/Sex:</b> ' . $ageSex
. '</td>'
. '<td width="50%" style="vertical-align:top;text-align:right;">'
. '<b>UHID:</b> ' . $e((string) ($data['uhid'] ?? '')) . '<br>'
. '<b>Collected:</b> ' . $e((string) ($data['collected_time'] ?? '')) . '<br>'
. '<b>Reported:</b> ' . $e((string) ($data['reported_time'] ?? '')) . '<br>'
. '<b>Printed:</b> ' . $e(date('d-m-Y h:i A'))
. '</td>'
. '</tr></table>';
}
private function buildAutoMpdfHeaderBlock(string $headerHtml, string $firstPageHeaderHtml, string $name): string
{
$headerHtml = trim($headerHtml);
$firstPageHeaderHtml = trim($firstPageHeaderHtml);
$combined = $headerHtml . $firstPageHeaderHtml;
if ($combined === '') {
return '';
}
if (preg_match('/<\s*(htmlpageheader|sethtmlpageheader)\b/i', $combined) === 1) {
return $combined;
}
return '';
}
private function buildAutoMpdfFooterBlock(string $footerHtml, string $lastPageFooterHtml, string $name): string
{
$footerHtml = trim($footerHtml);
$lastPageFooterHtml = trim($lastPageFooterHtml);
$combined = $footerHtml . $lastPageFooterHtml;
if ($combined === '') {
return '';
}
if (preg_match('/<\s*(htmlpagefooter|sethtmlpagefooter)\b/i', $combined) === 1) {
return $combined;
}
return '';
}
private function normalizeMarginMm($rawValue, float $default): float
{
if ($rawValue === null || $rawValue === '') {
return $default;
}
$value = (float) $rawValue;
if (! is_finite($value)) {
$value = $default;
}
$value = max(0.0, min(60.0, $value));
return round($value, 2);
}
private function cmToMm($rawCmValue, float $defaultCm): float
{
if ($rawCmValue === null || $rawCmValue === '') {
return round($defaultCm * 10.0, 2);
}
$cm = (float) $rawCmValue;
if (! is_finite($cm)) {
$cm = $defaultCm;
}
$cm = max(0.0, min(25.0, $cm));
return round($cm * 10.0, 2);
}
private function resolveFallbackReportHtml(int $labRepoId, int $chargeItemId, int $labType): string
{
$html = '';
if ($labRepoId > 0) {
$repoRow = $this->db->table('lab_repo')
->select('HTMLData')
->where('mstRepoKey', $labRepoId)
->get(1)
->getRowArray();
$html = trim((string) ($repoRow['HTMLData'] ?? ''));
}
if ($html !== '') {
return $html;
}
if (! $this->db->tableExists('radiology_ultrasound_template') || $chargeItemId <= 0) {
return '';
}
$invoiceItem = $this->db->table('invoice_item')
->select('item_id')
->where('id', $chargeItemId)
->get(1)
->getRowArray();
$chargeId = (int) ($invoiceItem['item_id'] ?? 0);
$builder = $this->db->table('radiology_ultrasound_template')
->select('Findings, Impression')
->where('Modality', $labType);
if ($chargeId > 0) {
$builder->groupStart()
->where('charge_id', $chargeId)
->orWhere('charge_id', 0)
->groupEnd();
}
$template = $builder->orderBy('id', 'ASC')->get(1)->getRowArray();
if (! is_array($template)) {
return '';
}
$findings = trim((string) ($template['Findings'] ?? ''));
$impression = trim((string) ($template['Impression'] ?? ''));
if ($findings === '' && $impression === '') {
return '';
}
$html = $findings;
if ($impression !== '') {
$html .= '<div style="margin-top:10px;"><b>Impression :</b><br/>' . nl2br($impression) . '</div>';
}
return $html;
}
public function removeTest($labReqId)
{
// Old HMS equivalent of Lab_Admin/report_remove
$labReqId = (int) $labReqId;
if ($labReqId <= 0) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Invalid request id',
]);
}
try {
$requestRow = $this->db->table('lab_request')
->where('id', $labReqId)
->get()
->getRowArray();
if (! $requestRow) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Test request not found',
]);
}
$user = service('auth')->user();
$userName = $user ? trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) : 'System';
$requestRow['remove_update_by'] = trim($userName . ' [' . date('d-m-Y H:i') . ']');
$this->db->transStart();
if ($this->db->tableExists('lab_request_delete')) {
$this->db->table('lab_request_delete')->insert($requestRow);
}
$this->db->table('lab_request_item')->where('lab_request_id', $labReqId)->delete();
$this->db->table('lab_request')->where('id', $labReqId)->delete();
$this->db->transComplete();
if (! $this->db->transStatus()) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Failed to remove test request',
]);
}
return $this->response->setJSON([
'status' => 'success',
'message' => 'Item Removed',
]);
} catch (\Throwable $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
}
public function updateCombineReport()
{
$itemId = (int) $this->request->getPost('item_id');
$checked = (int) $this->request->getPost('checked');
if ($itemId <= 0) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Invalid item id',
]);
}
try {
$this->db->table('lab_request')
->where('id', $itemId)
->update(['print_combine' => $checked > 0 ? 1 : 0]);
return $this->response->setJSON([
'status' => 'success',
'message' => 'Value Update',
]);
} catch (\Throwable $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
}
public function reportCompile($invoiceId, $labType)
{
$invoiceId = (int) $invoiceId;
$labType = (int) $labType;
if ($invoiceId <= 0 || $labType <= 0) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Invalid parameters',
]);
}
try {
$sql = "SELECT l.*, g.RepoGrp
FROM (lab_request l JOIN lab_repo r ON l.lab_repo_id = r.mstRepoKey)
LEFT JOIN lab_rgroups g ON r.GrpKey = g.mstRGrpKey
WHERE l.print_combine = 1 AND l.status = 2 AND l.charge_id = ? AND l.lab_type = ?
ORDER BY g.sort_order";
$compiledRows = $this->db->query($sql, [$invoiceId, $labType])->getResult();
if (empty($compiledRows)) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'No completed report selected for compile',
]);
}
$invoice = $this->db->table('invoice_master')->where('id', $invoiceId)->get()->getRow();
$patient = null;
if ($invoice && ! empty($invoice->attach_id)) {
$patient = $this->db->table('patient_master')->where('id', (int) $invoice->attach_id)->get()->getRow();
}
$header = '<table border="0" cellpadding="2" cellspacing="1" style="width:100%"><tr>';
$header .= '<td width="50%" style="vertical-align:top">';
$header .= '<b>Invoice ID :</b> ' . htmlspecialchars((string) ($invoice->invoice_code ?? ''), ENT_QUOTES, 'UTF-8') . '<br/>';
$header .= '<b>Patient Name :</b> ' . htmlspecialchars((string) ($patient->p_fname ?? ''), ENT_QUOTES, 'UTF-8') . '<br/>';
$header .= '<b>UHID :</b> ' . htmlspecialchars((string) ($patient->p_code ?? ''), ENT_QUOTES, 'UTF-8') . '<br/>';
$header .= '</td>';
$header .= '<td width="50%" style="vertical-align:top">';
$header .= '<b>Print Date :</b> ' . date('d-m-Y h:i:s A') . '<br/>';
$header .= '</td>';
$header .= '</tr></table><hr/>';
$rawData = '';
$groupName = '';
foreach ($compiledRows as $row) {
if (($row->RepoGrp ?? '') !== $groupName) {
$rawData .= '<h1 style="text-align:center; vertical-align:middle">' .
htmlspecialchars((string) ($row->RepoGrp ?? ''), ENT_QUOTES, 'UTF-8') .
'</h1>';
}
$rawData .= (string) ($row->Report_Data ?? '');
$groupName = (string) ($row->RepoGrp ?? '');
}
$invoiceReq = $this->db->table('lab_invoice_request')
->where('invoice_id', $invoiceId)
->where('lab_type', $labType)
->get()
->getRow();
if (! $invoiceReq) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Invoice report record not found',
]);
}
$user = service('auth')->user();
$userId = $user->id ?? '';
$userName = $user ? trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? '')) : 'System';
$compiledBy = $userName . '[' . $userId . '][' . date('d-m-Y h:i:s') . ']';
$this->db->table('lab_invoice_request')
->where('id', (int) $invoiceReq->id)
->update([
'report_data' => $rawData,
'report_header' => $header,
'report_compile' => $compiledBy,
]);
return $this->response->setJSON([
'status' => 'success',
'message' => 'Data Compile',
]);
} catch (\Throwable $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
}
public function printPdfCreate($invoiceId, $labType, $print = 0, $printOnType = 0)
{
// CI4 equivalent of old Lab_Admin/print_pdf_create/{inv}/{lab}/{print}/{print_on_type}
$invoiceId = (int) $invoiceId;
$labType = (int) $labType;
$print = (int) $print;
$printOnType = (int) $printOnType;
$templateId = (int) ($this->request->getGet('template_id') ?? 0);
set_time_limit(300);
if ($invoiceId <= 0 || $labType <= 0) {
return '<h3>Invalid parameters</h3>';
}
$invoiceRequest = $this->db->table('lab_invoice_request')
->where('invoice_id', $invoiceId)
->where('lab_type', $labType)
->get()
->getRow();
if (! $invoiceRequest) {
return '<h3>Error : Record not Exist in lab_invoice_request</h3>';
}
$invoice = $this->db->table('invoice_master')->where('id', $invoiceId)->get()->getRow();
$patient = null;
if ($invoice && ! empty($invoice->attach_id)) {
$patient = $this->db->table('patient_master')->where('id', (int) $invoice->attach_id)->get()->getRow();
}
$head = $this->db->table('diagnosis_head_name')
->where('d_type', $labType)
->get()
->getRow();
$itemType = $this->db->table('hc_item_type')->where('itype_id', $labType)->get()->getRow();
$reportHead = ($itemType->group_desc ?? 'Lab Report')
. ' / Invoice ID :' . ($invoice->invoice_code ?? '')
. ' / Person Name :' . ($patient->p_fname ?? '');
$isPlainPaper = false;
$genderValue = (int) ($patient->gender ?? 0);
$genderText = $genderValue === 1 ? 'Male' : ($genderValue === 2 ? 'Female' : '-');
$ageText = '-';
if (function_exists('get_age_1')) {
$ageText = (string) get_age_1(
$patient->dob ?? null,
$patient->age ?? '',
$patient->age_in_month ?? '',
$patient->estimate_dob ?? ''
);
} elseif (! empty($patient->age)) {
$ageText = (string) $patient->age;
}
$reportHtml = trim((string) ($invoiceRequest->report_data ?? ''));
if ($reportHtml === '') {
$reportHtml = '<p>No compiled report data available for this request.</p>';
}
$printSetting = $this->getDiagnosisTemplateSetting($labType, $templateId);
$patientName = trim((string) ($patient->p_fname ?? ''));
$relativeName = trim((string) ($patient->p_relative ?? ''));
$relativeText = trim((string) ($patient->p_rname ?? ''));
$repoTitle = trim((string) ($itemType->group_desc ?? 'Lab Report'));
$tokens = $this->buildPdfTokens([
'invoice_code' => $invoice->invoice_code ?? '',
'patient_name' => $patientName,
'relative_type' => $relativeName,
'relative_name' => $relativeText,
'age' => $ageText,
'gender' => $genderText,
'uhid' => $patient->p_code ?? '',
'collected_time' => $invoiceRequest->collected_time ?? '',
'reported_time' => $invoiceRequest->reported_time ?? '',
'report_title' => $repoTitle,
'doctor_name' => $head->doc_name ?? '',
'doctor_education' => $head->doc_edu ?? '',
'technician_name' => $head->tech_name ?? '',
'signature_image_url' => (string) ($printSetting['signature_image'] ?? ''),
]);
$rawPatientInfoHtml = trim((string) ($printSetting['patient_info_html'] ?? ''));
if ($rawPatientInfoHtml !== '') {
$patientInfoHtml = $this->applyPdfTokens($rawPatientInfoHtml, $tokens);
} elseif (($printSetting['id'] ?? 0) <= 0) {
$patientInfoHtml = $this->buildDefaultPatientInfoHtml([
'invoice_code' => $invoice->invoice_code ?? '',
'patient_name' => $patientName,
'relative_name' => $relativeName,
'relative_text' => $relativeText,
'age_text' => $ageText,
'gender_text' => $genderText,
'uhid' => $patient->p_code ?? '',
'collected_time' => $invoiceRequest->collected_time ?? '',
'reported_time' => $invoiceRequest->reported_time ?? '',
]);
} else {
$patientInfoHtml = '';
}
$reportHtml = trim((string) ($printSetting['content_prefix_html'] ?? '')) . $reportHtml . trim((string) ($printSetting['content_suffix_html'] ?? ''));
$marginTop = $this->cmToMm($printSetting['page_margin_top_cm'] ?? 1.2, 1.2);
$marginBottom = $this->cmToMm($printSetting['page_margin_bottom_cm'] ?? 1.2, 1.2);
$marginLeft = $this->cmToMm($printSetting['page_margin_left_cm'] ?? 1.0, 1.0);
$marginRight = $this->cmToMm($printSetting['page_margin_right_cm'] ?? 1.0, 1.0);
$marginHeader = $this->cmToMm($printSetting['margin_header_cm'] ?? 0.5, 0.5);
$marginFooter = $this->cmToMm($printSetting['margin_footer_cm'] ?? 1.5, 1.5);
$pdfHtml = view('diagnosis/pdf_compiled_report', [
'invoiceRequest' => $invoiceRequest,
'invoice' => $invoice,
'patient' => $patient,
'reportHead' => $reportHead,
'reportHtml' => $reportHtml,
'isPlainPaper' => $isPlainPaper,
'ageText' => $ageText,
'genderText' => $genderText,
'printSetting' => $printSetting,
'labType' => $labType,
'invoiceId' => $invoiceId,
'itemType' => $itemType,
'patientName' => $patientName,
'relativeName' => $relativeName,
'relativeText' => $relativeText,
'repoTitle' => $repoTitle,
'patientInfoHtml' => $patientInfoHtml,
]);
$rawHeaderHtml = trim((string) ($printSetting['header_html'] ?? ''));
$rawFirstPageHeaderHtml = trim((string) ($printSetting['first_page_header_html'] ?? ''));
$rawFooterHtml = trim((string) ($printSetting['footer_html'] ?? ''));
$rawLastPageFooterHtml = trim((string) ($printSetting['last_page_footer_html'] ?? ''));
$autoHeaderHtml = $this->buildAutoMpdfHeaderBlock(
$rawHeaderHtml,
$rawFirstPageHeaderHtml,
'diag_auto_header_compiled'
);
$autoFooterHtml = $this->buildAutoMpdfFooterBlock(
$rawFooterHtml,
$rawLastPageFooterHtml,
'diag_auto_footer_compiled'
);
$prefixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_prefix_html'] ?? ''))
. $autoHeaderHtml
. $autoFooterHtml, $tokens);
$suffixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_suffix_html'] ?? '')), $tokens);
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$t_start = microtime(true);
$mpdf = new Mpdf([
'format' => (string) ($printSetting['page_size'] ?? 'A4'),
'margin_top' => $marginTop,
'margin_bottom' => $marginBottom,
'margin_left' => $marginLeft,
'margin_right' => $marginRight,
'margin_header' => $marginHeader,
'margin_footer' => $marginFooter,
'tempDir' => $mpdfTempDir,
'autoScriptToLang' => false,
'autoLanguageDetection' => false,
'autoArabic' => false,
'autoVietnamese' => false,
]);
$t_mpdf_init = microtime(true);
// For plain HTML (without explicit mPDF tags), set header/footer via API.
$headerHasTags = preg_match('/<\s*(htmlpageheader|sethtmlpageheader)\b/i', $rawHeaderHtml . $rawFirstPageHeaderHtml) === 1;
$footerHasTags = preg_match('/<\s*(htmlpagefooter|sethtmlpagefooter)\b/i', $rawFooterHtml . $rawLastPageFooterHtml) === 1;
if (! $headerHasTags) {
$plainHeader = $this->applyPdfTokens($rawHeaderHtml . $rawFirstPageHeaderHtml, $tokens);
if ($plainHeader !== '') {
$mpdf->SetHTMLHeader($plainHeader, 'O');
$mpdf->SetHTMLHeader($plainHeader, 'E');
}
}
if (! $footerHasTags) {
$plainFooter = $this->applyPdfTokens($rawFooterHtml, $tokens);
if ($plainFooter === '') {
$plainFooter = $this->applyPdfTokens($rawLastPageFooterHtml, $tokens);
}
if ($plainFooter !== '') {
$mpdf->SetHTMLFooter($plainFooter, 'O');
$mpdf->SetHTMLFooter($plainFooter, 'E');
}
}
$backgroundImage = trim((string) ($printSetting['page_background_image'] ?? ''));
if ($backgroundImage !== '') {
$absolutePath = FCPATH . ltrim(str_replace('\\', '/', $backgroundImage), '/');
if (is_file($absolutePath)) {
$mpdf->SetWatermarkImage($absolutePath, 1, [210, 297], 'F');
$mpdf->showWatermarkImage = true;
}
}
$watermarkType = (string) ($printSetting['watermark_type'] ?? 'none');
$watermarkAlpha = (float) ($printSetting['watermark_alpha'] ?? 0.12);
if ($watermarkType === 'text') {
$text = trim((string) ($printSetting['watermark_text'] ?? ''));
if ($text !== '') {
$mpdf->SetWatermarkText($text, $watermarkAlpha);
$mpdf->showWatermarkText = true;
}
} elseif ($watermarkType === 'image') {
$wmImage = trim((string) ($printSetting['watermark_image'] ?? ''));
if ($wmImage !== '') {
$absolutePath = FCPATH . ltrim(str_replace('\\', '/', $wmImage), '/');
if (is_file($absolutePath)) {
$mpdf->SetWatermarkImage($absolutePath, $watermarkAlpha);
$mpdf->showWatermarkImage = true;
}
}
}
if ($prefixHtml !== '') {
$mpdf->WriteHTML($prefixHtml, HTMLParserMode::HTML_HEADER_BUFFER);
}
$t_prefix = microtime(true);
$mpdf->WriteHTML($pdfHtml);
$t_body = microtime(true);
if ($suffixHtml !== '') {
$mpdf->WriteHTML($suffixHtml, HTMLParserMode::HTML_HEADER_BUFFER, false, false);
}
$t_suffix = microtime(true);
$invoiceCode = preg_replace('/[^A-Za-z0-9\-_]/', '_', (string) ($invoice->invoice_code ?? 'invoice'));
$fileName = 'Compiled_Report_' . $invoiceCode . '_' . $labType . '.pdf';
$pdfOutput = $mpdf->Output($fileName, 'S');
$t_output = microtime(true);
log_message('debug', sprintf(
'[PDF_TIMING printPdfCreate] invoice=%s lab=%s | new_Mpdf()=%.2fs | WriteHTML(prefix)=%.2fs | WriteHTML(body)=%.2fs | WriteHTML(suffix)=%.2fs | Output()=%.2fs | TOTAL=%.2fs | body_bytes=%d',
$invoiceId, $labType,
$t_mpdf_init - $t_start,
$t_prefix - $t_mpdf_init,
$t_body - $t_prefix,
$t_suffix - $t_body,
$t_output - $t_suffix,
$t_output - $t_start,
strlen($pdfHtml)
));
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->setHeader('Pragma', 'no-cache')
->setHeader('Expires', '0')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($pdfOutput);
}
public function reportFileList($invoiceId, $labType, $delete = 0)
{
$invoiceId = (int) $invoiceId;
$labType = (int) $labType;
$delete = (int) $delete;
if ($invoiceId <= 0 || $labType <= 0) {
return view('diagnosis/lab_report_file_list', ['lab_file_list' => []]);
}
$files = [];
if ($this->db->tableExists('file_upload_data')) {
$files = $this->db->table('file_upload_data')
->where('isdelete', $delete)
->where('charge_id', $invoiceId)
->where('charge_type', $labType)
->orderBy('id', 'DESC')
->get()
->getResult();
}
return view('diagnosis/lab_report_file_list', [
'lab_file_list' => $files,
'invoice_id' => $invoiceId,
'lab_type' => $labType,
]);
}
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';
$isDicom = $this->isDicomMimeOrExtension($mime, $ext);
$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,
'is_dicom' => $isDicom,
'dicom_preview_url' => $isDicom ? base_url('diagnosis/imaging-dicom-preview/' . (int) ($row['id'] ?? 0)) : '',
'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 imagingDicomPreview($fileId)
{
$fileId = (int) $fileId;
if ($fileId <= 0 || ! $this->db->tableExists('file_upload_data')) {
return $this->response->setStatusCode(404)->setBody('DICOM file not found');
}
$fileRow = $this->db->table('file_upload_data')
->where('id', $fileId)
->where('isdelete', 0)
->get(1)
->getRowArray();
if (empty($fileRow)) {
return $this->response->setStatusCode(404)->setBody('DICOM file not found');
}
$absolutePath = $this->resolveUploadAbsolutePath((string) ($fileRow['full_path'] ?? ''));
if ($absolutePath === '' || ! is_file($absolutePath)) {
return $this->response->setStatusCode(404)->setBody('DICOM file missing on server');
}
$mime = $this->detectMimeType($absolutePath);
$ext = strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION));
if (! $this->isDicomMimeOrExtension($mime, $ext)) {
return $this->response->setStatusCode(415)->setBody('File is not a DICOM');
}
$preview = $this->callAiServerDicomPreview($absolutePath, $mime);
if (($preview['error'] ?? '') !== '') {
return $this->response->setStatusCode(422)->setBody((string) $preview['error']);
}
$bytes = (string) ($preview['bytes'] ?? '');
if ($bytes === '') {
return $this->response->setStatusCode(422)->setBody('Unable to render DICOM preview');
}
return $this->response
->setHeader('Content-Type', (string) ($preview['mime'] ?? 'image/jpeg'))
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
->setBody($bytes);
}
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', 'dcm', 'dicom'];
if (! in_array($ext, $allowed, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported file type. Allowed: image, PDF, DICOM (.dcm)']);
}
$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()) {
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']);
}
if (! $this->db->tableExists('lab_ai_extraction_batches') || ! $this->db->tableExists('lab_ai_extraction_values')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'AI extraction tables not found. Run migrations first.']);
}
$fileId = (int) $this->request->getPost('file_upload_id');
$invoiceId = (int) $this->request->getPost('invoice_id');
$labType = (int) $this->request->getPost('lab_type');
$panelName = trim((string) $this->request->getPost('panel_name'));
if ($fileId <= 0 || $invoiceId <= 0 || $labType <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Required fields missing']);
}
$fileRow = $this->db->table('file_upload_data')
->where('id', $fileId)
->where('charge_id', $invoiceId)
->where('charge_type', $labType)
->get(1)
->getRowArray();
if (empty($fileRow)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Report file not found']);
}
$fullPath = trim((string) ($fileRow['full_path'] ?? ''));
$absolutePath = $this->resolveUploadAbsolutePath($fullPath);
if ($absolutePath === '' || ! is_file($absolutePath)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'File not accessible on server']);
}
$ocrText = $this->extractTextFromLabFile($absolutePath);
if ($ocrText === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'OCR failed. Configure DIAGNOSIS_AI_OCR_ENDPOINT on your AI server.']);
}
$parsed = $this->parseLabValuesWithAi($ocrText, $panelName);
if (empty($parsed['values']) || ! is_array($parsed['values'])) {
$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'] ?? ''));
$rawResponse = (string) json_encode($parsed['raw'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$batchId = $this->insertLabAiBatch(
$invoiceId,
$labType,
$fileId,
$panelName,
$modelName,
$ocrText,
$rawResponse,
(string) ($parsed['provider'] ?? 'ai-server')
);
if ($batchId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to store AI extraction batch']);
}
$storedCount = 0;
foreach ($parsed['values'] as $item) {
if (! is_array($item)) {
continue;
}
$testName = trim((string) ($item['test_name'] ?? ''));
if ($testName === '') {
continue;
}
$ok = $this->db->table('lab_ai_extraction_values')->insert([
'batch_id' => $batchId,
'test_name' => $testName,
'test_value' => trim((string) ($item['value'] ?? '')),
'unit' => trim((string) ($item['unit'] ?? '')),
'reference_range' => trim((string) ($item['reference_range'] ?? '')),
'abnormal_flag' => trim((string) ($item['abnormal_flag'] ?? '')),
'raw_line' => trim((string) ($item['raw_line'] ?? '')),
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
if ($ok) {
$storedCount++;
}
}
$this->auditClinicalUpdate('lab_ai_extraction_batches', 'created', $batchId, null, [
'invoice_id' => $invoiceId,
'lab_type' => $labType,
'file_upload_id' => $fileId,
'stored_values' => $storedCount,
]);
return $this->response->setJSON([
'update' => 1,
'error_text' => 'AI extracted ' . $storedCount . ' values. Doctor verification pending.',
'ai_provider' => (string) ($parsed['provider'] ?? 'ai-server'),
'ai_model' => $modelName,
'batch_id' => $batchId,
'stored_values' => $storedCount,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function aiExtractedValues($invoiceId, $labType)
{
$invoiceId = (int) $invoiceId;
$labType = (int) $labType;
if ($invoiceId <= 0 || $labType <= 0 || ! $this->db->tableExists('lab_ai_extraction_batches')) {
return view('diagnosis/lab_ai_extracted_values', [
'batch' => null,
'values' => [],
]);
}
$batch = $this->db->table('lab_ai_extraction_batches')
->where('invoice_id', $invoiceId)
->where('lab_type', $labType)
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$values = [];
if (! empty($batch['id']) && $this->db->tableExists('lab_ai_extraction_values')) {
$values = $this->db->table('lab_ai_extraction_values')
->where('batch_id', (int) $batch['id'])
->orderBy('id', 'ASC')
->get()
->getResultArray();
}
return view('diagnosis/lab_ai_extracted_values', [
'batch' => $batch,
'values' => $values,
]);
}
public function aiVerifyExtractedValues($batchId)
{
if (! $this->request->isAJAX()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$batchId = (int) $batchId;
if ($batchId <= 0 || ! $this->db->tableExists('lab_ai_extraction_batches')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid batch id']);
}
$user = function_exists('auth') ? auth()->user() : null;
$verifiedBy = (string) ($user->id ?? 'system');
$updated = $this->db->table('lab_ai_extraction_batches')
->where('id', $batchId)
->update([
'doctor_verified' => 1,
'verified_by' => $verifiedBy,
'verified_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
if (! $updated) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to verify batch']);
}
$this->auditClinicalUpdate('lab_ai_extraction_batches', 'doctor_verified', $batchId, 0, 1, $verifiedBy);
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Marked as doctor-verified',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
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;
}
$ext = strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION));
if ((str_starts_with($mime, 'image/') || $this->isDicomMimeOrExtension($mime, $ext)) && count($visionImages) < 4) {
$visionImages[] = [
'abs_path' => $absolutePath,
'mime' => $mime,
];
}
}
$patientName = trim((string) ($labRequest['patient_name'] ?? 'Unknown'));
$patientAge = 0;
$patientGender = 'Unknown';
$patientId = (int) ($labRequest['patient_id'] ?? 0);
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
$patient = $this->db->table('patient_master')
->select('gender, age, age_in_month, dob')
->where('id', $patientId)
->get(1)
->getRowArray();
$genderRaw = (int) ($patient['gender'] ?? 0);
$patientGender = $genderRaw === 1 ? 'Male' : ($genderRaw === 2 ? 'Female' : 'Unknown');
$patientAge = (int) ($patient['age'] ?? 0);
if ($patientAge <= 0) {
$months = (int) ($patient['age_in_month'] ?? 0);
if ($months > 0) {
$patientAge = max(1, (int) floor($months / 12));
}
}
if ($patientAge <= 0) {
$dob = trim((string) ($patient['dob'] ?? ''));
if ($dob !== '' && $dob !== '0000-00-00') {
try {
$patientAge = (int) ((new \DateTime($dob))->diff(new \DateTime('today'))->y ?? 0);
} catch (\Throwable $e) {
$patientAge = 0;
}
}
}
}
$analysis = $this->generateImagingDiagnosisDraft($studyName, $ocrChunks, $visionImages, $patientName, $patientAge, $patientGender);
if (($analysis['error'] ?? '') !== '') {
$this->updateImagingFileAiState($fileIds, 'failed', (string) $analysis['error'], 1);
return $this->response->setJSON(['update' => 0, 'error_text' => (string) $analysis['error']]);
}
$promptUsed = $this->buildImagingDiagnosisPrompt($studyName);
$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'] ?? 'ai-server'),
'model' => (string) ($analysis['model'] ?? ''),
'prompt_used' => $promptUsed,
'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),
(string) ($analysis['provider'] ?? 'ai-server')
);
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'] ?? 'ai-server'),
'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,
int $fileId,
string $panelName,
string $modelName,
string $ocrText,
string $rawResponse,
string $provider = 'ai-server'
): int {
$ok = $this->db->table('lab_ai_extraction_batches')->insert([
'invoice_id' => $invoiceId,
'lab_type' => $labType,
'file_upload_id' => $fileId,
'panel_name' => $panelName,
'ai_provider' => $provider,
'model_name' => $modelName,
'ocr_text' => $ocrText,
'raw_response_json' => $rawResponse,
'status' => 'completed',
'doctor_verified' => 0,
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
if (! $ok) {
return 0;
}
return (int) $this->db->insertID();
}
private function resolveUploadAbsolutePath(string $fullPath): string
{
$fullPath = trim($fullPath);
if ($fullPath === '') {
return '';
}
if (is_file($fullPath)) {
return $fullPath;
}
$normalized = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $fullPath);
if (is_file($normalized)) {
return $normalized;
}
$fileName = basename($normalized);
$candidates = [
ROOTPATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . $fileName,
WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . $fileName,
];
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
return $candidate;
}
}
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, string $patientName = 'Unknown', int $patientAge = 0, string $patientGender = 'Unknown'): array
{
if ($visionImages === []) {
return [
'error' => 'No image available for AI diagnosis.',
];
}
$firstImagePath = (string) ($visionImages[0]['abs_path'] ?? '');
$firstImageMime = (string) ($visionImages[0]['mime'] ?? 'image/jpeg');
if ($firstImagePath === '' || ! is_file($firstImagePath)) {
return [
'error' => 'Image file not found on server for AI diagnosis.',
];
}
$response = $this->callAiServerDiagnosis($studyName, $firstImagePath, $firstImageMime, $patientName, $patientAge, $patientGender);
if (($response['error'] ?? '') !== '') {
return $response;
}
$report = $response['report'] ?? [];
$technique = $this->normalizeAiTextValue($report['Technique'] ?? $studyName, $studyName);
$impression = $this->normalizeAiTextValue($report['Impression'] ?? 'AI diagnosis draft prepared.', 'AI diagnosis draft prepared.');
$findings = $report['Findings'] ?? [];
$narrative = '';
$labels = [];
$ocrText = [];
if (is_array($findings)) {
$narrative = $this->normalizeAiTextValue($findings['Narrative'] ?? '', '');
$labels = $this->normalizeAiListValue($findings['Labels'] ?? []);
$ocrText = $this->normalizeAiListValue($findings['OCR Text'] ?? []);
} else {
$narrative = $this->normalizeAiTextValue($findings, '');
}
$labelHtml = '';
if ($labels !== []) {
$safeLabels = array_slice(array_map(static fn($x) => trim((string) $x), $labels), 0, 8);
$safeLabels = array_values(array_filter($safeLabels, static fn($x) => $x !== ''));
if ($safeLabels !== []) {
$labelHtml = '<p><strong>Detected labels:</strong> ' . esc(implode(', ', $safeLabels)) . '</p>';
}
}
$ocrHtml = '';
if ($ocrText !== []) {
$ocrFirst = trim((string) ($ocrText[0] ?? ''));
if ($ocrFirst !== '') {
$ocrHtml = '<p><strong>OCR notes:</strong> ' . nl2br(esc(mb_substr($ocrFirst, 0, 1500))) . '</p>';
}
}
$findingsHtml = '<p><strong>Technique:</strong> ' . esc($technique) . '</p>';
if ($narrative !== '') {
$paragraphs = preg_split('/\R{2,}|\n/u', $narrative) ?: [];
$paragraphs = array_values(array_filter(array_map(static fn($item) => trim((string) $item), $paragraphs), static fn($item) => $item !== ''));
if ($paragraphs === []) {
$findingsHtml .= '<p>' . nl2br(esc($narrative)) . '</p>';
} else {
foreach ($paragraphs as $paragraph) {
$findingsHtml .= '<p>' . esc($paragraph) . '</p>';
}
}
} else {
$findingsHtml .= $labelHtml . $ocrHtml;
}
return [
'provider' => (string) ($response['provider'] ?? 'ai-server'),
'model' => (string) ($response['model'] ?? 'ai-server-diagnosis'),
'title' => trim((string) ($studyName !== '' ? $studyName : ($technique !== '' ? $technique : 'Imaging Study'))),
'findings_html' => $findingsHtml,
'impression_html' => '<p>' . esc($impression) . '</p>',
'summary_text' => mb_substr($impression !== '' ? $impression : 'AI diagnosis draft prepared.', 0, 220),
'raw' => $response['raw'] ?? [],
'error' => '',
];
}
/**
* @param mixed $value
*/
private function normalizeAiTextValue($value, string $fallback = ''): string
{
if (is_string($value)) {
$text = trim($value);
return $text !== '' ? $text : trim($fallback);
}
if (is_scalar($value)) {
$text = trim((string) $value);
return $text !== '' ? $text : trim($fallback);
}
if (is_array($value)) {
$chunks = [];
array_walk_recursive($value, static function ($item) use (&$chunks): void {
if (is_scalar($item)) {
$text = trim((string) $item);
if ($text !== '') {
$chunks[] = $text;
}
}
});
if ($chunks !== []) {
return trim(implode(' ', array_slice($chunks, 0, 8)));
}
}
return trim($fallback);
}
/**
* @param mixed $value
* @return array<int, string>
*/
private function normalizeAiListValue($value): array
{
if (is_string($value)) {
$text = trim($value);
return $text !== '' ? [$text] : [];
}
if (is_scalar($value)) {
$text = trim((string) $value);
return $text !== '' ? [$text] : [];
}
if (! is_array($value)) {
return [];
}
$items = [];
array_walk_recursive($value, static function ($item) use (&$items): void {
if (is_scalar($item)) {
$text = trim((string) $item);
if ($text !== '') {
$items[] = $text;
}
}
});
return array_slice($items, 0, 20);
}
private function extractTextFromLabFile(string $absolutePath): string
{
return $this->extractTextWithAiServerOcr($absolutePath);
}
private function extractTextWithAiServerOcr(string $absolutePath): string
{
$endpoint = rtrim($this->readSetting('DIAGNOSIS_AI_OCR_ENDPOINT'), '/');
if ($endpoint === '') {
return '';
}
$mime = $this->detectMimeType($absolutePath);
if (! str_starts_with($mime, 'image/')) {
return '';
}
try {
$client = service('curlrequest', $this->httpOptions());
$headers = [
'Accept' => 'application/json',
];
$token = trim($this->readSetting('DIAGNOSIS_AI_PARSE_TOKEN'));
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$curlFile = new \CURLFile($absolutePath, $mime, basename($absolutePath));
$response = $client->post($endpoint, [
'headers' => $headers,
'multipart' => [
'file' => $curlFile,
],
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
return '';
}
$decoded = json_decode((string) $response->getBody(), true);
if (! is_array($decoded)) {
return '';
}
foreach (['ocr_text', 'text', 'extracted_text'] as $key) {
$value = trim((string) ($decoded[$key] ?? ''));
if ($value !== '') {
return $value;
}
}
} catch (\Throwable $e) {
return '';
}
return '';
}
private function extractTextWithDocumentIntelligence(string $absolutePath, string $endpoint, string $key): string
{
try {
$client = service('curlrequest', $this->httpOptions());
$bytes = @file_get_contents($absolutePath);
if ($bytes === false) {
return '';
}
$analyzeUrl = $endpoint . '/documentintelligence/documentModels/prebuilt-read:analyze?api-version=2024-02-29-preview';
$submit = $client->post($analyzeUrl, [
'headers' => [
'Content-Type' => 'application/octet-stream',
'Ocp-Apim-Subscription-Key' => $key,
],
'body' => $bytes,
]);
if ($submit->getStatusCode() < 200 || $submit->getStatusCode() >= 300) {
return '';
}
$operationLocation = trim((string) $submit->getHeaderLine('Operation-Location'));
if ($operationLocation === '') {
return '';
}
for ($i = 0; $i < 12; $i++) {
usleep(800000);
$poll = $client->get($operationLocation, [
'headers' => [
'Ocp-Apim-Subscription-Key' => $key,
],
]);
if ($poll->getStatusCode() < 200 || $poll->getStatusCode() >= 300) {
continue;
}
$decoded = json_decode((string) $poll->getBody(), true);
if (! is_array($decoded)) {
continue;
}
$status = strtolower((string) ($decoded['status'] ?? ''));
if ($status === 'succeeded') {
$lines = [];
$pages = $decoded['analyzeResult']['pages'] ?? [];
foreach ($pages as $page) {
$pageLines = $page['lines'] ?? [];
foreach ($pageLines as $line) {
$content = trim((string) ($line['content'] ?? ''));
if ($content !== '') {
$lines[] = $content;
}
}
}
return trim(implode("\n", $lines));
}
if ($status === 'failed') {
return '';
}
}
} catch (\Throwable $e) {
return '';
}
return '';
}
private function extractTextWithAzureVisionLlm(string $absolutePath): string
{
$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 '';
}
if ($apiVersion === '') {
$apiVersion = '2024-10-21';
}
$mime = $this->detectMimeType($absolutePath);
if (! str_starts_with($mime, 'image/')) {
return '';
}
$bytes = @file_get_contents($absolutePath);
if ($bytes === false) {
return '';
}
$dataUrl = 'data:' . $mime . ';base64,' . base64_encode($bytes);
$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' => [
['type' => 'text', 'text' => 'Read and return all text from this medical lab report image exactly as plain text.'],
['type' => 'image_url', 'image_url' => ['url' => $dataUrl]],
],
]],
'temperature' => 0,
'max_tokens' => 3000,
],
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
return '';
}
$decoded = json_decode((string) $response->getBody(), true);
return trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
} catch (\Throwable $e) {
return '';
}
}
/**
* @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;
}
$externalError = trim((string) ($external['error'] ?? ''));
return [
'provider' => 'none',
'model' => '',
'values' => [],
'raw' => [],
'error' => $externalError !== '' ? $externalError : 'AI parse failed. Configure DIAGNOSIS_AI_PARSE_ENDPOINT on your AI server.',
];
}
/**
* @return array<string, mixed>
*/
private function callAiServerDiagnosis(string $studyName, string $filePath, string $mime = 'image/jpeg', string $patientName = 'Unknown', int $patientAge = 0, string $patientGender = 'Unknown'): array
{
$base = $this->resolveAiServerBaseUrl();
if ($base === '') {
return [
'error' => 'AI server URL is missing. Configure DIAGNOSIS_AI_SERVER_URL or set parse/OCR endpoint in AI Settings.',
];
}
if (! is_file($filePath)) {
return [
'error' => 'Image file not found: ' . basename($filePath),
];
}
$isDicom = $this->isDicomMimeOrExtension($mime, strtolower(pathinfo($filePath, PATHINFO_EXTENSION)));
if ($isDicom) {
$ext = 'dcm';
} else {
$ext = str_contains($mime, 'png') ? 'png' : (str_contains($mime, 'webp') ? 'webp' : 'jpg');
}
$token = trim($this->readSetting('DIAGNOSIS_AI_PARSE_TOKEN'));
$headers = ['Accept' => 'application/json'];
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$url = $base . '/diagnosis';
$prompt = $this->buildImagingDiagnosisPrompt($studyName);
$attempts = $this->aiMaxRetries();
$lastError = 'AI server diagnosis request failed.';
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
$client = service('curlrequest', $this->httpOptions());
$uploadMime = $isDicom ? 'application/dicom' : $mime;
$curlFile = new \CURLFile($filePath, $uploadMime, 'study.' . $ext);
$response = $client->post($url, [
'headers' => $headers,
'multipart' => [
'file' => $curlFile,
'patient_name' => $patientName !== '' ? $patientName : 'Unknown',
'patient_age' => (string) max(0, $patientAge),
'patient_gender' => $patientGender !== '' ? $patientGender : 'Unknown',
'clinical_indication' => $studyName !== '' ? $studyName : 'Imaging study',
'report_prompt' => $prompt,
],
]);
$status = $response->getStatusCode();
$body = (string) $response->getBody();
if ($status < 200 || $status >= 300) {
$lastError = 'AI server HTTP ' . $status . ': ' . mb_substr($body, 0, 180);
continue;
}
$decoded = json_decode($body, true);
if (! is_array($decoded)) {
$lastError = 'AI server returned non-JSON response.';
continue;
}
$report = $decoded['diagnosis_report'] ?? null;
if (! is_array($report)) {
$lastError = trim((string) ($decoded['detail'] ?? 'AI server response missing diagnosis_report.'));
continue;
}
return [
'provider' => 'ai-server',
'model' => 'python-fastapi',
'report' => $report,
'raw' => $decoded,
'error' => '',
];
} catch (\Throwable $e) {
$lastError = $e->getMessage() !== '' ? $e->getMessage() : 'AI server request timeout/error.';
}
}
return [
'error' => $lastError,
];
}
private function buildImagingDiagnosisPrompt(string $studyName): string
{
$template = trim($this->readSetting('DIAGNOSIS_AI_IMAGING_PROMPT'));
if ($template === '') {
$template = "Generate a radiology report in concise clinical style.\n"
. "Use this structure exactly:\n"
. "1) Findings Draft\n"
. "2) Technique\n"
. "3) Impression Draft\n\n"
. "Include checks for lungs/pleura, mediastinum/heart size, diaphragm/costophrenic angles, bones/soft tissue, and lines/tubes/devices if present.\n"
. "If a structure is normal, explicitly state it as normal.\n"
. "Do not mention AI. Keep output clinically readable for doctors.\n"
. "Study Name: {study_name}";
}
$name = trim($studyName) !== '' ? trim($studyName) : 'Imaging study';
$prompt = str_replace(['{study_name}', '{{study_name}}'], $name, $template);
return trim($prompt);
}
private function resolveAiServerBaseUrl(): string
{
$base = rtrim($this->readSetting('DIAGNOSIS_AI_SERVER_URL'), '/');
if ($base !== '') {
return $base;
}
$candidateUrls = [
trim((string) $this->readSetting('DIAGNOSIS_AI_PARSE_ENDPOINT')),
trim((string) $this->readSetting('DIAGNOSIS_AI_OCR_ENDPOINT')),
];
foreach ($candidateUrls as $url) {
if ($url === '') {
continue;
}
$parts = @parse_url($url);
if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
continue;
}
$base = (string) $parts['scheme'] . '://' . (string) $parts['host'];
if (! empty($parts['port'])) {
$base .= ':' . (int) $parts['port'];
}
return rtrim($base, '/');
}
return '';
}
/**
* @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>
*/
private function parseLabValuesWithAzureOpenAi(string $ocrText, string $panelName): 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 [
'provider' => 'azure-openai',
'model' => '',
'values' => [],
'raw' => [],
'error' => 'Azure OpenAI settings are missing.',
];
}
if ($apiVersion === '') {
$apiVersion = '2024-10-21';
}
$schemaHint = '{"panel_name":"string","values":[{"test_name":"string","value":"string","unit":"string","reference_range":"string","abnormal_flag":"H|L|N|","raw_line":"string"}] }';
$prompt = "You are an expert lab report parser for Indian hospitals.\n" .
"Extract only measurable pathology values from OCR text.\n" .
"Return strict JSON only, no markdown, no comments, with schema: " . $schemaHint . "\n" .
"If uncertain, keep empty string. panel_name input: " . $panelName . "\n\n" .
"OCR_TEXT:\n" . mb_substr($ocrText, 0, 14000);
$url = $endpoint . '/openai/deployments/' . rawurlencode($deployment) . '/chat/completions?api-version=' . rawurlencode($apiVersion);
$attempts = $this->aiMaxRetries();
$lastError = 'Azure AI parsing 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' => $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.';
}
}
return [
'provider' => 'azure-openai',
'model' => '',
'values' => [],
'raw' => [],
'error' => $lastError,
];
}
/**
* @return array<string, mixed>
*/
private function httpOptions(): array
{
$timeout = $this->aiTimeoutSeconds();
$options = [
'timeout' => $timeout,
'http_errors' => false,
];
if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) {
$options['verify'] = false;
}
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)) {
$value = trim((string) constant($name));
if ($value !== '') {
return $value;
}
}
if (! $this->db->tableExists('hospital_setting')) {
return '';
}
$row = $this->db->table('hospital_setting')
->select('s_value')
->where('s_name', $name)
->get(1)
->getRowArray();
return trim((string) ($row['s_value'] ?? ''));
}
private function detectMimeType(string $path): string
{
$mime = '';
if (function_exists('mime_content_type')) {
$mime = (string) @mime_content_type($path);
}
if ($mime === '') {
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$map = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'bmp' => 'image/bmp',
'gif' => 'image/gif',
'pdf' => 'application/pdf',
'dcm' => 'application/dicom',
'dicom' => 'application/dicom',
];
$mime = $map[$ext] ?? 'application/octet-stream';
}
if ($mime === 'application/octet-stream') {
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (in_array($ext, ['dcm', 'dicom'], true)) {
return 'application/dicom';
}
}
return $mime;
}
private function isDicomMimeOrExtension(string $mime, string $ext): bool
{
$mime = strtolower(trim($mime));
$ext = strtolower(trim($ext));
if (in_array($ext, ['dcm', 'dicom'], true)) {
return true;
}
return str_contains($mime, 'dicom');
}
/**
* @return array<string, mixed>
*/
private function callAiServerDicomPreview(string $filePath, string $mime = 'application/dicom'): array
{
$base = $this->resolveAiServerBaseUrl();
if ($base === '') {
return [
'error' => 'AI server URL is missing for DICOM preview.',
];
}
if (! is_file($filePath)) {
return [
'error' => 'DICOM file not found on server.',
];
}
$token = trim($this->readSetting('DIAGNOSIS_AI_PARSE_TOKEN'));
$headers = ['Accept' => 'image/*'];
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$url = $base . '/dicom-preview';
$attempts = $this->aiMaxRetries();
$lastError = 'DICOM preview request failed.';
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
$client = service('curlrequest', $this->httpOptions());
$curlFile = new \CURLFile($filePath, 'application/dicom', basename($filePath));
$response = $client->post($url, [
'headers' => $headers,
'multipart' => [
'file' => $curlFile,
],
]);
$status = $response->getStatusCode();
$body = (string) $response->getBody();
if ($status < 200 || $status >= 300) {
$lastError = 'AI server HTTP ' . $status . ': ' . mb_substr($body, 0, 180);
continue;
}
$contentType = strtolower(trim((string) $response->getHeaderLine('Content-Type')));
if (! str_starts_with($contentType, 'image/')) {
$lastError = 'AI server did not return image data for DICOM preview.';
continue;
}
return [
'bytes' => $body,
'mime' => $contentType,
'error' => '',
];
} catch (\Throwable $e) {
$lastError = $e->getMessage() !== '' ? $e->getMessage() : 'DICOM preview service timeout/error.';
}
}
return [
'error' => $lastError,
];
}
public function selectLabRadiology($invoiceId, $labType)
{
return $this->selectLabInvoicePath((int) $invoiceId, (int) $labType);
}
}