Add report panel updates and related billing/discharge enhancements

This commit is contained in:
DevSoftTech Server 2026-03-10 02:43:35 +05:30
parent 2127063c7f
commit aa845ac5ff
36 changed files with 6764 additions and 242 deletions

View File

@ -102,6 +102,7 @@ class AuthGroups extends ShieldAuthGroups
'template.ct' => 'Can access CT templates',
'template.mri' => 'Can access MRI templates',
'template.echo' => 'Can access ECHO templates',
'template.discharge' => 'Can access IPD discharge templates',
];
/**

View File

@ -233,6 +233,15 @@ $routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)/(:num)', 'Ipd_d
$routes->get('Ipd_discharge/section_past_data', 'Ipd_discharge::section_past_data');
$routes->get('Ipd_discharge/section_template_list', 'Ipd_discharge::section_template_list');
$routes->post('Ipd_discharge/section_template_save', 'Ipd_discharge::section_template_save');
$routes->get('Ipd_discharge/surgery_master_lookup', 'Ipd_discharge::surgery_master_lookup');
$routes->get('Ipd_discharge/surgery_master_list', 'Ipd_discharge::surgery_master_list');
$routes->post('Ipd_discharge/surgery_master_save', 'Ipd_discharge::surgery_master_save');
$routes->post('Ipd_discharge/surgery_master_delete', 'Ipd_discharge::surgery_master_delete');
$routes->get('Ipd_discharge/diagnosis_icd_lookup', 'Ipd_discharge::diagnosis_icd_lookup');
$routes->post('Ipd_discharge/diagnosis_icd_seed_starter', 'Ipd_discharge::diagnosis_icd_seed_starter');
$routes->get('Ipd_discharge/dietary_master_list', 'Ipd_discharge::dietary_master_list');
$routes->post('Ipd_discharge/dietary_master_save', 'Ipd_discharge::dietary_master_save');
$routes->post('Ipd_discharge/dietary_master_delete', 'Ipd_discharge::dietary_master_delete');
$routes->match(['get', 'post'], 'Ipd_discharge/preview_discharge_report/(:num)', 'Ipd_discharge::preview_discharge_report/$1');
$routes->get('Ipd_discharge/show_discharge/(:num)', 'Ipd_discharge::show_discharge/$1');
$routes->get('Ipd_discharge/show_discharge/(:num)/(:num)', 'Ipd_discharge::show_discharge/$1/$2');
@ -521,12 +530,21 @@ $routes->post('Opdcase/showfee', 'Opdcase::showfee');
$routes->post('Opdcase/confirm_opd', 'Opdcase::confirm_opd');
$routes->get('Opdcase/invoice/(:num)', 'Opd::invoice/$1');
$routes->get('Report', 'Report::index');
$routes->get('Report/index', 'Report::index');
$routes->get('report', 'Report::index');
$routes->get('report/index', 'Report::index');
$routes->get('Report/collection_report', 'Report::collection_report');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3/$4');
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4/$5');
$routes->get('Report/billing_operations_report', 'Report::billing_operations_report');
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)', 'Report::billing_operations_report_data/$1/$2');
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)/(:num)', 'Report::billing_operations_report_data/$1/$2/$3');
$routes->get('Report/diagnosis_report', 'Report::diagnosis_report');
$routes->get('Report/diagnosis_report_data/(:segment)', 'Report::diagnosis_report_data/$1');
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2');
@ -549,6 +567,14 @@ $routes->get('Report/insurance_combined_report', 'Report::insurance_combined_rep
$routes->get('Report/insurance_combined_report_data/(:segment)/(:segment)/(:segment)', 'Report::insurance_combined_report_data/$1/$2/$3');
$routes->get('Report/insurance_combined_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::insurance_combined_report_data/$1/$2/$3/$4');
// Legacy Report4 compatibility for Insurance Case reports
$routes->get('Report4/echs_ipd_list_main', 'Report::echs_ipd_list_main');
$routes->get('Report4/echs_opd_list_main', 'Report::echs_opd_list_main');
$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_ipd_list_data/$1/$2/$3');
$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_ipd_list_data/$1/$2/$3/$4');
$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_opd_list_data/$1/$2/$3');
$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_opd_list_data/$1/$2/$3/$4');
// Organization Packing Routes
$routes->get('org-packing', 'OrgPacking::index');
$routes->post('org-packing/search', 'OrgPacking::search');

View File

@ -4,8 +4,9 @@ namespace App\Controllers\Billing;
use App\Controllers\BaseController;
use App\Models\InvoiceModel;
use App\Models\ItemModel;
use App\Models\OrganizationCaseBillDetailModel;
use App\Models\OrganizationCaseModel;
use Mpdf\Mpdf;
class CaseMaster extends BaseController
{
@ -118,8 +119,6 @@ class CaseMaster extends BaseController
'input_insurance_id' => 'required|min_length[2]|max_length[30]',
'input_card_holder_name' => 'required|min_length[2]|max_length[50]',
];
$validation = service('validation');
$validation->setRules($rules);
if (! $validation->withRequest($this->request)->run()) {
$errorText = implode("\n", $validation->getErrors());
@ -157,7 +156,6 @@ class CaseMaster extends BaseController
];
$existing = $this->db->table('organization_case_master')
->where('status', 0)
->where('case_type', $caseType)
->where('p_id', $pId)
->get()
@ -484,7 +482,11 @@ class CaseMaster extends BaseController
$data['payment_history'] = $this->db->query($sql)->getResult();
if ($print > 0) {
return view('Invoice/caseinvoice_print_V', $data);
$html = view('Invoice/caseinvoice_print_V', $data);
$caseCode = (string) ($data['orgcase'][0]->case_id_code ?? $casecode);
$fileName = 'Org_Invoice_' . preg_replace('/[^A-Za-z0-9\-_]/', '_', $caseCode) . '_' . date('YmdHis') . '.pdf';
return $this->renderInlinePdf($html, $fileName, 'Organization Invoice ' . $caseCode);
}
return view('Invoice/caseinvoice_V', $data);
@ -794,8 +796,10 @@ class CaseMaster extends BaseController
->get()
->getResult();
$data['showinvoice'] = $this->db->query('select * from v_case_invoice v where v.id=' . $caseid . ' order by s_Date')->getResult();
$data['invoiceGtotal'] = $this->db->query('select sum(amount) as GTotal from v_case_invoice v where v.id=' . $caseid)->getResult();
$organizationCaseModel = new OrganizationCaseModel();
$invoiceData = $organizationCaseModel->getInvoiceRowsAndTotal($caseid);
$data['showinvoice'] = $invoiceData['rows'];
$data['invoiceGtotal'] = [(object) ['GTotal' => $invoiceData['total']]];
return view('Invoice/caseinvoice_V', $data);
}
@ -922,7 +926,13 @@ class CaseMaster extends BaseController
->getResult();
if ($print === 1) {
return $this->response->setBody((string) ($data['orgcase'][0]->contingent_bill ?? ''));
$content = (string) ($data['orgcase'][0]->contingent_bill ?? '');
$pageHead = '<style>@page {margin-top: 0.5cm;margin-bottom: 0.5cm;margin-left: 0.5cm;margin-right: 0.5cm;}</style>';
$html = $pageHead . $content;
$caseCode = (string) ($data['orgcase'][0]->case_id_code ?? $caseid);
$fileName = 'Contingent_Bill_' . preg_replace('/[^A-Za-z0-9\-_]/', '_', $caseCode) . '_' . date('YmdHis') . '.pdf';
return $this->renderInlinePdf($html, $fileName, 'Contingent Bill ' . $caseCode);
}
return view('Invoice/Org_Contingent_bill', $data);
@ -958,10 +968,10 @@ class CaseMaster extends BaseController
->get()
->getResult();
$sql = 'select * from v_case_invoice v where v.id=' . $caseid . ' order by Charge_type,Adate';
$data['showinvoice'] = $this->db->query($sql)->getResult();
$data['invoiceGtotal'] = $this->db->query('select sum(amount) as GTotal from v_case_invoice v where v.id=' . $caseid)->getResult();
$organizationCaseModel = new OrganizationCaseModel();
$invoiceData = $organizationCaseModel->getInvoiceRowsAndTotal($caseid);
$data['showinvoice'] = $invoiceData['rows'];
$data['invoiceGtotal'] = [(object) ['GTotal' => $invoiceData['total']]];
$claimId = (string) ($data['orgcase'][0]->insurance_no_1 ?? '');
@ -1360,4 +1370,28 @@ class CaseMaster extends BaseController
);
}
}
private function renderInlinePdf(string $html, string $fileName, string $title)
{
$mpdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'orientation' => 'P',
'margin_left' => 8,
'margin_right' => 8,
'margin_top' => 8,
'margin_bottom' => 8,
'default_font' => 'dejavusans',
'tempDir' => WRITEPATH . 'cache',
]);
$mpdf->SetTitle($title);
$mpdf->WriteHTML($html);
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
}

View File

@ -7,6 +7,7 @@ use App\Models\BedAssignmentHistoryModel;
use App\Models\ItemIpdModel;
use App\Models\IpdBillingModel;
use App\Models\IpdModel;
use App\Models\NursingBedsideItemModel;
use App\Models\PaymentModel;
class Ipd extends BaseController
@ -15,6 +16,7 @@ class Ipd extends BaseController
protected BedAssignmentHistoryModel $bedAssignmentModel;
protected ItemIpdModel $itemIpdModel;
protected IpdModel $ipdEditModel;
protected NursingBedsideItemModel $nursingBedsideItemModel;
public function __construct()
{
@ -22,6 +24,7 @@ class Ipd extends BaseController
$this->bedAssignmentModel = new BedAssignmentHistoryModel();
$this->itemIpdModel = new ItemIpdModel();
$this->ipdEditModel = new IpdModel();
$this->nursingBedsideItemModel = new NursingBedsideItemModel();
helper(['common', 'form', 'age']);
}
@ -193,6 +196,7 @@ class Ipd extends BaseController
}
if ($tab === 'ipd-charges') {
$this->syncNursingChargesToInvoice($ipdId);
$panelData['ipd_charges_grouped'] = $this->ipdModel->getIpdChargesGrouped($ipdId);
$panelData['ipd_charges_total'] = $this->ipdModel->getIpdChargesTotal($ipdId);
$panelData['ipd_packages'] = $this->ipdModel->getIpdPackages($ipdId);
@ -203,8 +207,14 @@ class Ipd extends BaseController
$insCompId = 1;
}
$panelData['bedside_items_by_category'] = $this->nursingBedsideItemModel->getBillableGroupedByCategory($insCompId);
$panelData['ipd_insurance_id'] = $insCompId;
$panelData['doc_list'] = $this->ipdModel->getIpdDoctorList();
$doctorIds = array_map(static fn ($doc) => (int) ($doc->id ?? 0), $panelData['doc_list']);
$doctorIds = array_values(array_filter($doctorIds, static fn ($id) => $id > 0));
$panelData['doctor_visit_fee_types'] = $this->ipdModel->getDoctorVisitFeeTypes();
$panelData['doctor_visit_fee_map'] = $this->ipdModel->getDoctorVisitFeeMap($doctorIds);
$panelData['item_types'] = $this->itemIpdModel->getItemTypes();
$itemLists = [];
@ -419,6 +429,238 @@ class Ipd extends BaseController
return $this->response->setJSON(['ok' => true]);
}
public function addBedsideCharge(int $ipdId)
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
'billing.ipd.current-admission',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
}
$bedsideItemId = (int) ($this->request->getPost('bedside_item_id') ?? 0);
$qty = (float) ($this->request->getPost('item_qty') ?? 1);
$itemDate = (string) ($this->request->getPost('item_date') ?? date('Y-m-d'));
$visitTime = (string) ($this->request->getPost('visit_time') ?? '');
$comment = (string) ($this->request->getPost('comment') ?? '');
$docId = (int) ($this->request->getPost('doc_id') ?? 0);
$referName = (string) ($this->request->getPost('refer_name') ?? '');
if ($bedsideItemId <= 0) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Select a bedside item']);
}
$caseMeta = $this->ipdModel->getIpdCaseMeta($ipdId);
$insId = (int) ($caseMeta['insurance_id'] ?? 0);
$item = $this->nursingBedsideItemModel->getBillableItemForInsurance($bedsideItemId, $insId);
if (! is_array($item) || empty($item)) {
return $this->response->setStatusCode(404)->setJSON(['error' => 'Bedside item not found or not billable']);
}
if ($qty <= 0) {
$qty = 1;
}
$rateInput = $this->request->getPost('item_rate');
$insuranceRate = $item['insurance_rate'] ?? null;
$rate = ($rateInput !== null && $rateInput !== '')
? (float) $rateInput
: ($insuranceRate !== null ? (float) $insuranceRate : (float) ($item['default_rate'] ?? 0));
if ($rate < 0) {
$rate = 0;
}
$orgCode = trim((string) ($item['insurance_code'] ?? ''));
$docName = '';
if ($docId > 0) {
$docName = $this->ipdModel->getDoctorNameById($docId);
} else {
$docName = $referName;
}
$typeId = $this->resolveBedsideItemTypeId((string) ($item['category'] ?? 'Bedside'));
$amount = $rate * $qty;
$nursingChargeId = $this->insertNursingIpdChargeRow([
'ipd_id' => $ipdId,
'item_id' => $bedsideItemId,
'item_name' => (string) ($item['item_name'] ?? ''),
'item_type' => (string) ($item['item_type'] ?? 'Bedside'),
'doctor_id' => $docId > 0 ? $docId : null,
'doctor_name' => $docName !== '' ? $docName : null,
'visit_date' => $itemDate,
'visit_time' => $visitTime !== '' ? $visitTime : null,
'consultation_type' => null,
'duration' => null,
'specialization' => null,
'hospital_clinic' => null,
'is_outside_doctor' => 0,
'rate' => $rate,
'qty' => (int) $qty,
'amount' => $amount,
'performed_by' => null,
'performed_date' => $itemDate,
'performed_time' => $visitTime !== '' ? $visitTime : null,
'remarks' => $comment,
'include_in_bill' => 1,
'updated_by' => null,
]);
$invoiceComment = $comment;
if ($nursingChargeId > 0) {
$marker = '[NURSING_CHARGE_ID:' . $nursingChargeId . ']';
$invoiceComment = trim($invoiceComment . ' ' . $marker);
}
$insertData = [
'ipd_id' => $ipdId,
'item_type' => $typeId,
'item_id' => $bedsideItemId,
'item_name' => (string) ($item['item_name'] ?? ''),
'item_rate' => $rate,
'item_added_date' => date('Y-m-d H:i:s'),
'item_qty' => $qty,
'item_amount' => $amount,
'doc_id' => $docId,
'doc_name' => $docName,
'date_item' => $itemDate,
'comment' => $invoiceComment,
'ins_id' => $insId,
'org_code' => $orgCode,
];
$insertId = $this->ipdEditModel->insertIpdItem($insertData);
return $this->response->setJSON([
'ok' => $insertId > 0,
'id' => $insertId,
]);
}
public function addDoctorVisitCharge(int $ipdId)
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
'billing.ipd.current-admission',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
}
$docId = (int) ($this->request->getPost('doc_id') ?? 0);
$feeTypeId = (int) ($this->request->getPost('fee_type_id') ?? 0);
$qty = (float) ($this->request->getPost('item_qty') ?? 1);
$itemDate = (string) ($this->request->getPost('item_date') ?? date('Y-m-d'));
$visitTime = (string) ($this->request->getPost('visit_time') ?? '');
$duration = (int) ($this->request->getPost('duration') ?? 0);
$hospitalClinic = trim((string) ($this->request->getPost('hospital_clinic') ?? ''));
$isOutsideDoctor = (int) ($this->request->getPost('is_outside_doctor') ?? 0) === 1 ? 1 : 0;
$comment = trim((string) ($this->request->getPost('comment') ?? ''));
if ($docId <= 0) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Select doctor']);
}
if ($qty <= 0) {
$qty = 1;
}
$feeDetail = $this->ipdModel->getDoctorVisitFeeDetail($docId, $feeTypeId);
$rateInput = $this->request->getPost('item_rate');
$rate = ($rateInput !== null && $rateInput !== '')
? (float) $rateInput
: (float) ($feeDetail['amount'] ?? 0);
if ($rate < 0) {
$rate = 0;
}
$docName = $this->ipdModel->getDoctorNameById($docId);
if ($docName === '') {
return $this->response->setStatusCode(404)->setJSON(['error' => 'Doctor not found']);
}
$visitLabel = trim((string) ($feeDetail['fee_desc'] ?? 'Doctor Visit'));
if ($visitLabel === '') {
$visitLabel = 'Doctor Visit';
}
$itemName = 'Doctor Visit - ' . $visitLabel;
$typeId = $this->resolveDoctorVisitItemTypeId();
$amount = $rate * $qty;
$caseMeta = $this->ipdModel->getIpdCaseMeta($ipdId);
$insId = (int) ($caseMeta['insurance_id'] ?? 0);
$nursingChargeId = $this->insertNursingIpdChargeRow([
'ipd_id' => $ipdId,
'item_id' => (int) ($feeDetail['id'] ?? $docId),
'item_name' => $itemName,
'item_type' => 'Doctor Visit',
'doctor_id' => $docId,
'doctor_name' => $docName,
'visit_date' => $itemDate,
'visit_time' => $visitTime !== '' ? $visitTime : null,
'consultation_type' => (string) ($feeDetail['fee_type'] ?? ''),
'duration' => $duration > 0 ? $duration : null,
'specialization' => null,
'hospital_clinic' => $hospitalClinic !== '' ? $hospitalClinic : null,
'is_outside_doctor' => $isOutsideDoctor,
'rate' => $rate,
'qty' => (int) $qty,
'amount' => $amount,
'performed_by' => null,
'performed_date' => $itemDate,
'performed_time' => $visitTime !== '' ? $visitTime : null,
'remarks' => $comment,
'include_in_bill' => 1,
'updated_by' => null,
]);
$invoiceComment = $comment;
if ($nursingChargeId > 0) {
$marker = '[NURSING_CHARGE_ID:' . $nursingChargeId . ']';
$invoiceComment = trim($invoiceComment . ' ' . $marker);
}
$insertData = [
'ipd_id' => $ipdId,
'item_type' => $typeId,
'item_id' => (int) ($feeDetail['id'] ?? $docId),
'item_name' => $itemName,
'item_rate' => $rate,
'item_added_date' => date('Y-m-d H:i:s'),
'item_qty' => $qty,
'item_amount' => $amount,
'doc_id' => $docId,
'doc_name' => $docName,
'date_item' => $itemDate,
'comment' => $invoiceComment,
'ins_id' => $insId,
'org_code' => '',
];
$insertId = $this->ipdEditModel->insertIpdItem($insertData);
return $this->response->setJSON([
'ok' => $insertId > 0,
'id' => $insertId,
]);
}
public function deleteIpdCharge(int $itemId)
{
$permission = $this->requireAnyPermission([
@ -434,8 +676,165 @@ class Ipd extends BaseController
}
$result = $this->ipdEditModel->deleteIpdInvoiceItem($itemId);
if ($result !== 1) {
return $this->response->setJSON([
'ok' => false,
'error' => 'Unable to remove charge item. Please refresh and try again.',
]);
}
return $this->response->setJSON(['ok' => $result === 1]);
return $this->response->setJSON([
'ok' => true,
'deleted_id' => $itemId,
]);
}
public function updateNursingCharge(int $ipdId, int $chargeId)
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
'billing.ipd.current-admission',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
}
if ($chargeId <= 0 || $ipdId <= 0 || ! $this->db->tableExists('nursing_ipd_charges')) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid charge reference']);
}
$row = $this->db->table('nursing_ipd_charges')
->where('charge_id', $chargeId)
->where('ipd_id', $ipdId)
->get()
->getRowArray();
if (! is_array($row) || empty($row)) {
return $this->response->setStatusCode(404)->setJSON(['error' => 'Charge history row not found']);
}
$rate = (float) ($this->request->getPost('rate') ?? ($row['rate'] ?? 0));
$qty = (float) ($this->request->getPost('qty') ?? ($row['qty'] ?? 1));
$comment = trim((string) ($this->request->getPost('comment') ?? ($row['remarks'] ?? '')));
$visitDate = (string) ($this->request->getPost('visit_date') ?? ($row['visit_date'] ?? date('Y-m-d')));
$visitTime = (string) ($this->request->getPost('visit_time') ?? ($row['visit_time'] ?? ''));
if ($qty <= 0) {
$qty = 1;
}
if ($rate < 0) {
$rate = 0;
}
$amount = $rate * $qty;
$user = auth()->user();
$updatedBy = ($user && isset($user->username))
? (string) $user->username
: (($user && isset($user->email)) ? (string) $user->email : 'system');
$this->db->table('nursing_ipd_charges')
->where('charge_id', $chargeId)
->where('ipd_id', $ipdId)
->update([
'rate' => $rate,
'qty' => $qty,
'amount' => $amount,
'remarks' => $comment,
'visit_date' => $visitDate,
'visit_time' => $visitTime,
'updated_by' => $updatedBy . ' [' . date('Y-m-d H:i:s') . ']',
]);
$marker = '[NURSING_CHARGE_ID:' . $chargeId . ']';
$invoiceRows = $this->db->table('ipd_invoice_item')
->select('id')
->where('ipd_id', $ipdId)
->like('comment', $marker)
->get()
->getResultArray();
$invoiceComment = trim($comment . ' ' . $marker);
foreach ($invoiceRows as $invoiceRow) {
$invoiceId = (int) ($invoiceRow['id'] ?? 0);
if ($invoiceId <= 0) {
continue;
}
$this->ipdEditModel->updateIpdItem([
'item_rate' => $rate,
'item_qty' => $qty,
'item_amount' => $amount,
'date_item' => $visitDate,
'comment' => $invoiceComment,
], $invoiceId);
}
return $this->response->setJSON(['ok' => true]);
}
public function deleteNursingCharge(int $ipdId, int $chargeId)
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
'billing.ipd.current-admission',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid request']);
}
if ($chargeId <= 0 || $ipdId <= 0 || ! $this->db->tableExists('nursing_ipd_charges')) {
return $this->response->setStatusCode(400)->setJSON(['error' => 'Invalid charge reference']);
}
$row = $this->db->table('nursing_ipd_charges')
->where('charge_id', $chargeId)
->where('ipd_id', $ipdId)
->get()
->getRowArray();
if (! is_array($row) || empty($row)) {
return $this->response->setStatusCode(404)->setJSON(['error' => 'Charge history row not found']);
}
$user = auth()->user();
$updatedBy = ($user && isset($user->username))
? (string) $user->username
: (($user && isset($user->email)) ? (string) $user->email : 'system');
$this->db->table('nursing_ipd_charges')
->where('charge_id', $chargeId)
->where('ipd_id', $ipdId)
->update([
'include_in_bill' => 0,
'updated_by' => $updatedBy . ' [' . date('Y-m-d H:i:s') . ']',
]);
$marker = '[NURSING_CHARGE_ID:' . $chargeId . ']';
$invoiceRows = $this->db->table('ipd_invoice_item')
->select('id')
->where('ipd_id', $ipdId)
->like('comment', $marker)
->get()
->getResultArray();
foreach ($invoiceRows as $invoiceRow) {
$invoiceId = (int) ($invoiceRow['id'] ?? 0);
if ($invoiceId <= 0) {
continue;
}
$this->ipdEditModel->deleteIpdInvoiceItem($invoiceId);
}
return $this->response->setJSON(['ok' => true]);
}
public function updateIpdChargePackage(int $itemId)
@ -939,4 +1338,141 @@ class Ipd extends BaseController
return $this->response->setStatusCode(403)->setBody('Access denied');
}
private function resolveBedsideItemTypeId(string $category): int
{
$groupDesc = 'Bedside - ' . trim($category);
if ($groupDesc === 'Bedside -') {
$groupDesc = 'Bedside - General';
}
$row = $this->db->table('ipd_item_type')
->select('itype_id')
->where('group_desc', $groupDesc)
->get()
->getRow();
if ($row) {
return (int) ($row->itype_id ?? 0);
}
$this->db->table('ipd_item_type')->insert([
'group_desc' => $groupDesc,
]);
return (int) $this->db->insertID();
}
private function resolveDoctorVisitItemTypeId(): int
{
$groupDesc = 'Doctor Visit';
$row = $this->db->table('ipd_item_type')
->select('itype_id')
->where('group_desc', $groupDesc)
->get()
->getRow();
if ($row) {
return (int) ($row->itype_id ?? 0);
}
$this->db->table('ipd_item_type')->insert([
'group_desc' => $groupDesc,
]);
return (int) $this->db->insertID();
}
private function insertNursingIpdChargeRow(array $data): int
{
if (! $this->db->tableExists('nursing_ipd_charges')) {
return 0;
}
$this->db->table('nursing_ipd_charges')->insert($data);
return (int) $this->db->insertID();
}
private function syncNursingChargesToInvoice(int $ipdId): void
{
if (! $this->db->tableExists('nursing_ipd_charges')) {
return;
}
$rows = $this->db->table('nursing_ipd_charges')
->where('ipd_id', $ipdId)
->where('include_in_bill', 1)
->orderBy('charge_id', 'ASC')
->get()
->getResultArray();
if (empty($rows)) {
return;
}
$caseMeta = $this->ipdModel->getIpdCaseMeta($ipdId);
$insId = (int) ($caseMeta['insurance_id'] ?? 0);
foreach ($rows as $row) {
$chargeId = (int) ($row['charge_id'] ?? 0);
if ($chargeId <= 0) {
continue;
}
$marker = '[NURSING_CHARGE_ID:' . $chargeId . ']';
$existsByMarker = $this->db->table('ipd_invoice_item')
->where('ipd_id', $ipdId)
->like('comment', $marker)
->countAllResults();
if ($existsByMarker > 0) {
continue;
}
$itemName = trim((string) ($row['item_name'] ?? ''));
$dateItem = (string) ($row['visit_date'] ?? date('Y-m-d'));
$qty = (float) ($row['qty'] ?? 1);
$rate = (float) ($row['rate'] ?? 0);
$amount = (float) ($row['amount'] ?? ($rate * $qty));
$docId = (int) ($row['doctor_id'] ?? 0);
$existsByValue = $this->db->table('ipd_invoice_item')
->where('ipd_id', $ipdId)
->where('item_name', $itemName)
->where('date_item', $dateItem)
->where('item_qty', $qty)
->where('item_amount', $amount)
->where('doc_id', $docId)
->countAllResults();
if ($existsByValue > 0) {
continue;
}
$rawType = trim((string) ($row['item_type'] ?? ''));
$typeId = stripos($rawType, 'doctor') !== false
? $this->resolveDoctorVisitItemTypeId()
: $this->resolveBedsideItemTypeId($rawType !== '' ? $rawType : 'General');
$remarks = trim((string) ($row['remarks'] ?? ''));
$comment = trim($remarks . ' ' . $marker);
$this->ipdEditModel->insertIpdItem([
'ipd_id' => $ipdId,
'item_type' => $typeId,
'item_id' => (int) ($row['item_id'] ?? $chargeId),
'item_name' => $itemName,
'item_rate' => $rate,
'item_added_date' => date('Y-m-d H:i:s'),
'item_qty' => $qty,
'item_amount' => $amount,
'doc_id' => $docId,
'doc_name' => (string) ($row['doctor_name'] ?? ''),
'date_item' => $dateItem,
'comment' => $comment,
'ins_id' => $insId,
'org_code' => '',
]);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -266,4 +266,5 @@ class ItemIpd extends BaseController
'csrfHash' => csrf_hash(),
]);
}
}

View File

@ -37,11 +37,7 @@ class Patient extends BaseController
public function create()
{
if (!$this->request->isAJAX()) {
return $this->response
->setStatusCode(400)
->setJSON(['insertid' => 0, 'error_text' => 'Invalid request']);
}
$isAjax = $this->request->isAJAX();
$chk_age = $this->request->getPost('chk_age');
$age_month = (string) $this->request->getPost('input_age_month');
@ -67,12 +63,18 @@ class Patient extends BaseController
$validation->setRules($rules);
if (!$validation->withRequest($this->request)->run()) {
$errorText = implode("\n", $validation->getErrors());
if ($isAjax) {
return $this->response->setJSON([
'insertid' => 0,
'error_text' => $errorText,
]);
}
return redirect()->to(base_url('billing/patient'))
->withInput()
->with('error', $errorText);
}
$data = [
'mphone1' => $this->request->getPost('input_mphone1'),
'p_fname' => strtoupper((string) $this->request->getPost('input_name')),
@ -100,12 +102,23 @@ class Patient extends BaseController
$patientModel = new PatientModel();
$insertId = $patientModel->insertPatient($data);
if ($insertId <= 0) {
if ($isAjax) {
return $this->response->setJSON([
'insertid' => 0,
'error_text' => 'Unable to save patient.',
]);
}
return redirect()->to(base_url('billing/patient'))
->withInput()
->with('error', 'Unable to save patient.');
}
$this->saveNamesToNameList([
(string) $this->request->getPost('input_name'),
(string) $this->request->getPost('input_relative_name'),
]);
// Check Multiple UHID
$relativeName = trim(strtoupper((string) $this->request->getPost('input_relative_name')));
$patientName = trim(strtoupper((string) $this->request->getPost('input_name')));
@ -143,9 +156,13 @@ class Patient extends BaseController
$patientModel->insertDuplicateLog($dupData);
}
if ($isAjax) {
return $this->response->setJSON(['insertid' => $insertId]);
}
return redirect()->to(base_url('billing/patient/person_record/' . $insertId));
}
public function search_adv()
{
$inputMphone1 = trim((string) $this->request->getPost('input_mphone1'));
@ -416,6 +433,10 @@ class Patient extends BaseController
$pid = (int) $this->request->getPost('p_id');
$patientModel = new PatientModel();
$patientModel->updatePatient($data, $pid);
$this->saveNamesToNameList([
(string) $this->request->getPost('input_name'),
(string) $this->request->getPost('input_relative_name'),
]);
return $this->response->setJSON([
'update' => 1,
@ -778,19 +799,8 @@ class Patient extends BaseController
return $this->response->setJSON([]);
}
$sql = "select * from name_list where name like '" . $this->db->escapeLikeString($q) . "%' order by name limit 20";
$query = $this->db->query($sql);
$rows = $query->getResultArray();
$rowSet = [];
foreach ($rows as $row) {
$rowSet[] = [
'label' => trim($row['name']),
'value' => trim($row['name']),
];
}
return $this->response->setJSON($rowSet);
$patientModel = new PatientModel();
return $this->response->setJSON($patientModel->getNameSuggestions($q));
}
private function getProfileFilePath(int $fileId): string
@ -922,10 +932,38 @@ class Patient extends BaseController
return null;
}
$date = trim($date);
$dt = \DateTime::createFromFormat('Y-m-d', $date);
if (!$dt) {
$dt = \DateTime::createFromFormat('d/m/Y', $date);
}
return $dt ? $dt->format('Y-m-d') : null;
}
private function saveNamesToNameList(array $names): void
{
if (! $this->db->tableExists('name_list')) {
return;
}
$builder = $this->db->table('name_list');
foreach ($names as $rawName) {
$name = preg_replace('/\s+/', ' ', trim((string) $rawName));
if ($name === '' || strlen($name) < 2) {
continue;
}
$name = ucwords(strtolower($name));
$exists = $builder->select('id')->where('name', $name)->limit(1)->get()->getRowArray();
if ($exists) {
continue;
}
$builder->insert(['name' => $name]);
}
}
}

View File

@ -4,6 +4,11 @@ namespace App\Controllers;
class Report extends BaseController
{
public function index()
{
return view('report/index');
}
public function collection_report()
{
$employees = $this->db->table('users')
@ -142,6 +147,189 @@ class Report extends BaseController
return array_values(array_unique($ids));
}
public function billing_operations_report()
{
$doctors = $this->db->table('opd_master')
->select('doc_name')
->where('doc_name IS NOT NULL', null, false)
->where("doc_name != ''", null, false)
->groupBy('doc_name')
->orderBy('doc_name', 'ASC')
->get()
->getResult();
return view('report/billing_operations_report', [
'doctors' => $doctors,
]);
}
public function billing_operations_report_data(string $dateRange, string $doctor = '0', int $output = 0)
{
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$doctorFilter = trim(urldecode($doctor));
$opdBuilder = $this->db->table('opd_master o')
->select('o.doc_name')
->select('COUNT(o.opd_id) as total_opd', false)
->where('o.apointment_date >=', $minRange)
->where('o.apointment_date <=', $maxRange)
->where('o.doc_name IS NOT NULL', null, false)
->where("o.doc_name != ''", null, false);
if ($doctorFilter !== '' && $doctorFilter !== '0') {
$opdBuilder->where('o.doc_name', $doctorFilter);
}
$opdRows = $opdBuilder
->groupBy('o.doc_name')
->orderBy('total_opd', 'DESC')
->get()
->getResult();
$invoiceRows = $this->db->table('invoice_master m')
->select('m.id, m.inv_name, m.inv_date, m.total_amount, m.discount_amount')
->where('m.inv_date >=', $minRange)
->where('m.inv_date <=', $maxRange)
->where('m.invoice_status', 1)
->orderBy('m.id', 'DESC')
->get()
->getResultArray();
$invoiceIds = array_values(array_filter(array_map(static function (array $row): int {
return (int) ($row['id'] ?? 0);
}, $invoiceRows)));
$testAgg = [];
$paymentAgg = [];
if (! empty($invoiceIds)) {
$testRows = $this->db->table('invoice_item')
->select('inv_master_id')
->select('COUNT(id) as test_count', false)
->select('SUM(item_qty) as test_qty', false)
->select('SUM(item_amount) as test_amount', false)
->whereIn('inv_master_id', $invoiceIds)
->groupBy('inv_master_id')
->get()
->getResultArray();
foreach ($testRows as $row) {
$testAgg[(int) ($row['inv_master_id'] ?? 0)] = $row;
}
$payRows = $this->db->table('payment_history')
->select('payof_id')
->select("SUM(IF(credit_debit=0, amount, 0)) as total_received", false)
->select("SUM(IF(credit_debit=1, amount, 0)) as total_refund", false)
->where('payof_type', 2)
->whereIn('payof_id', $invoiceIds)
->groupBy('payof_id')
->get()
->getResultArray();
foreach ($payRows as $row) {
$paymentAgg[(int) ($row['payof_id'] ?? 0)] = $row;
}
}
$invoiceDetailRows = [];
$invoiceSummary = [
'invoice_count' => 0,
'total_invoice_amount' => 0.0,
'total_discount' => 0.0,
'total_test_count' => 0,
'total_test_qty' => 0.0,
'total_test_amount' => 0.0,
'total_received' => 0.0,
'total_refund' => 0.0,
];
foreach ($invoiceRows as $row) {
$invId = (int) ($row['id'] ?? 0);
$test = $testAgg[$invId] ?? ['test_count' => 0, 'test_qty' => 0, 'test_amount' => 0];
$pay = $paymentAgg[$invId] ?? ['total_received' => 0, 'total_refund' => 0];
$invoiceAmount = (float) ($row['total_amount'] ?? 0);
$discountAmount = (float) ($row['discount_amount'] ?? 0);
$received = (float) ($pay['total_received'] ?? 0);
$refund = (float) ($pay['total_refund'] ?? 0);
$netReceived = $received - $refund;
$invoiceSummary['invoice_count']++;
$invoiceSummary['total_invoice_amount'] += $invoiceAmount;
$invoiceSummary['total_discount'] += $discountAmount;
$invoiceSummary['total_test_count'] += (int) ($test['test_count'] ?? 0);
$invoiceSummary['total_test_qty'] += (float) ($test['test_qty'] ?? 0);
$invoiceSummary['total_test_amount'] += (float) ($test['test_amount'] ?? 0);
$invoiceSummary['total_received'] += $received;
$invoiceSummary['total_refund'] += $refund;
$invoiceDetailRows[] = (object) [
'id' => $invId,
'inv_name' => (string) ($row['inv_name'] ?? ''),
'inv_date' => (string) ($row['inv_date'] ?? ''),
'invoice_amount' => $invoiceAmount,
'discount_amount' => $discountAmount,
'test_count' => (int) ($test['test_count'] ?? 0),
'test_qty' => (float) ($test['test_qty'] ?? 0),
'test_amount' => (float) ($test['test_amount'] ?? 0),
'received_amount' => $received,
'refund_amount' => $refund,
'net_received' => $netReceived,
'pending_amount' => $invoiceAmount - $netReceived,
];
}
$ipdPaymentRows = $this->db->table('payment_history p')
->select('p.payof_id as ipd_id, i.ipd_code, pat.p_fname as patient_name')
->select("SUM(IF(p.credit_debit=0, p.amount, 0)) as total_received", false)
->select("SUM(IF(p.credit_debit=1, p.amount, 0)) as total_refund", false)
->join('ipd_master i', 'i.id = p.payof_id', 'left')
->join('patient_master pat', 'pat.id = i.p_id', 'left')
->where('p.payof_type', 4)
->where('p.payment_date >=', $minRange)
->where('p.payment_date <=', $maxRange)
->groupBy('p.payof_id')
->orderBy('total_received', 'DESC')
->get()
->getResult();
$ipdSummary = [
'total_received' => 0.0,
'total_refund' => 0.0,
'net_received' => 0.0,
'ipd_count' => 0,
];
foreach ($ipdPaymentRows as $row) {
$received = (float) ($row->total_received ?? 0);
$refund = (float) ($row->total_refund ?? 0);
$ipdSummary['total_received'] += $received;
$ipdSummary['total_refund'] += $refund;
$ipdSummary['ipd_count']++;
}
$ipdSummary['net_received'] = $ipdSummary['total_received'] - $ipdSummary['total_refund'];
$data = [
'min_range' => $minRange,
'max_range' => $maxRange,
'doctor_filter' => $doctorFilter,
'opd_rows' => $opdRows,
'invoice_summary' => $invoiceSummary,
'invoice_rows' => $invoiceDetailRows,
'ipd_summary' => $ipdSummary,
'ipd_payment_rows' => $ipdPaymentRows,
];
$content = view('report/billing_operations_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'Billing_Operations_Report');
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
public function diagnosis_report()
{
$itemTypes = $this->db->table('hc_item_type')
@ -228,6 +416,48 @@ class Report extends BaseController
return view('report/insurance_credit_main');
}
public function echs_ipd_list_main()
{
return $this->insurance_ipd_report();
}
public function echs_opd_list_main()
{
return $this->insurance_opd_report();
}
public function echs_ipd_list_data(
string $dateRange,
string $orgStatus = '-1',
string $ipdType = '0',
int $output = 0
) {
$insuranceId = $this->mapLegacyInsuranceFilter($ipdType);
return $this->insurance_ipd_report_data($dateRange, $insuranceId, $orgStatus, $output);
}
public function echs_opd_list_data(
string $dateRange,
string $orgStatus = '-1',
string $ipdType = '0',
int $output = 0
) {
$insuranceId = $this->mapLegacyInsuranceFilter($ipdType);
return $this->insurance_opd_report_data($dateRange, $insuranceId, $orgStatus, $output);
}
private function mapLegacyInsuranceFilter(string $legacyType): string
{
$legacyValue = (int) trim($legacyType);
if ($legacyValue > 0) {
return 'G' . $legacyValue;
}
if ($legacyValue < 0) {
return (string) abs($legacyValue);
}
return '0';
}
public function insurance_opd_report()
{
$insuranceGroups = $this->db->table('h_insurance_group')
@ -494,4 +724,668 @@ class Report extends BaseController
return $this->response->setBody($content);
}
public function nabh_audit_report()
{
return view('report/nabh_audit_report');
}
public function nabh_audit_report_data(
string $dateRange,
string $module = 'all',
string $status = 'all',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRange($dateRange);
$module = strtolower(trim($module));
if (! in_array($module, ['all', 'ipd', 'opd'], true)) {
$module = 'all';
}
$status = strtolower(trim($status));
if (! in_array($status, ['all', 'critical-missing', 'compliant'], true)) {
$status = 'all';
}
$ipdRows = [];
$opdRows = [];
if ($module === 'all' || $module === 'ipd') {
$ipdRows = $this->getIpdNabhAuditRows($minRange, $maxRange);
}
if ($module === 'all' || $module === 'opd') {
$opdRows = $this->getOpdNabhAuditRows($minRange, $maxRange);
}
$rows = array_merge($ipdRows, $opdRows);
if ($status === 'critical-missing') {
$rows = array_values(array_filter($rows, static function (array $row): bool {
return (int) ($row['critical_missing_count'] ?? 0) > 0;
}));
} elseif ($status === 'compliant') {
$rows = array_values(array_filter($rows, static function (array $row): bool {
return (int) ($row['critical_missing_count'] ?? 0) === 0;
}));
}
usort($rows, static function (array $a, array $b): int {
return strcmp((string) ($b['encounter_datetime'] ?? ''), (string) ($a['encounter_datetime'] ?? ''));
});
$summary = $this->buildNabhSummary($rows);
$data = [
'rows' => $rows,
'summary' => $summary,
'min_range' => $minRange,
'max_range' => $maxRange,
'module_filter' => $module,
'status_filter' => $status,
];
$content = view('report/nabh_audit_report_table', $data);
if ($output === 1) {
ExportExcel($content, 'NABH_Audit_Report');
return $this->response->setBody('');
}
return $this->response->setBody($content);
}
private function buildNabhSummary(array $rows): array
{
$summary = [
'all' => ['label' => 'Overall', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
'ipd' => ['label' => 'IPD Discharge', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
'opd' => ['label' => 'OPD Consult', 'total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0.0],
];
$completionAccumulator = [
'all' => 0.0,
'ipd' => 0.0,
'opd' => 0.0,
];
foreach ($rows as $row) {
$module = strtolower((string) ($row['module_key'] ?? 'all'));
if (! isset($summary[$module])) {
$module = 'all';
}
$isCompliant = (int) ($row['critical_missing_count'] ?? 0) === 0;
$completion = (float) ($row['completion_percent'] ?? 0);
$summary['all']['total']++;
$completionAccumulator['all'] += $completion;
if ($isCompliant) {
$summary['all']['compliant']++;
} else {
$summary['all']['critical_missing']++;
}
if ($module !== 'all') {
$summary[$module]['total']++;
$completionAccumulator[$module] += $completion;
if ($isCompliant) {
$summary[$module]['compliant']++;
} else {
$summary[$module]['critical_missing']++;
}
}
}
foreach ($summary as $key => $item) {
$total = (int) ($item['total'] ?? 0);
$summary[$key]['avg_completion'] = $total > 0
? round($completionAccumulator[$key] / $total, 1)
: 0.0;
}
return $summary;
}
private function getIpdNabhAuditRows(string $minRange, string $maxRange): array
{
if (! $this->db->tableExists('ipd_master')) {
return [];
}
$ipdFields = $this->db->getFieldNames('ipd_master') ?? [];
if (! in_array('id', $ipdFields, true)) {
return [];
}
$dateCol = $this->resolveExistingColumn($ipdFields, [
'discharge_date',
'discharged_at',
'discharge_datetime',
'register_date',
'admit_date',
'created_at',
]);
if ($dateCol === null) {
return [];
}
$ipdCodeCol = $this->resolveExistingColumn($ipdFields, ['ipd_code', 'ipd_no', 'ipd_number']);
$patientFkCol = $this->resolveExistingColumn($ipdFields, ['p_id', 'patient_id']);
$statusCol = $this->resolveExistingColumn($ipdFields, ['ipd_status', 'discharge_status', 'discharg_status', 'is_discharged']);
$builder = $this->db->table('ipd_master i')
->select('i.id as encounter_id')
->select('i.' . $dateCol . ' as encounter_datetime', false);
if ($ipdCodeCol !== null) {
$builder->select('i.' . $ipdCodeCol . ' as encounter_code', false);
} else {
$builder->select("'' as encounter_code", false);
}
if ($patientFkCol !== null) {
$builder->select('i.' . $patientFkCol . ' as patient_id', false);
} else {
$builder->select('0 as patient_id', false);
}
$patientNameExpr = "''";
$patientCodeExpr = "''";
if ($patientFkCol !== null && $this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$nameCol = $this->resolveExistingColumn($patientFields, ['p_fname', 'patient_name', 'name']);
$codeCol = $this->resolveExistingColumn($patientFields, ['p_code', 'patient_code', 'uhid_no']);
$pkCol = $this->resolveExistingColumn($patientFields, ['id']);
if ($pkCol !== null) {
$builder->join('patient_master p', 'p.' . $pkCol . ' = i.' . $patientFkCol, 'left');
if ($nameCol !== null) {
$patientNameExpr = 'p.' . $nameCol;
}
if ($codeCol !== null) {
$patientCodeExpr = 'p.' . $codeCol;
}
}
}
$builder->select($patientNameExpr . ' as patient_name', false)
->select($patientCodeExpr . ' as patient_code', false)
->where('i.' . $dateCol . ' >=', $minRange)
->where('i.' . $dateCol . ' <=', $maxRange)
->orderBy('i.' . $dateCol, 'DESC');
if ($statusCol !== null) {
$builder->where('i.' . $statusCol, 1);
}
$resultRows = $builder->get()->getResultArray();
$finalRows = [];
foreach ($resultRows as $row) {
$ipdId = (int) ($row['encounter_id'] ?? 0);
if ($ipdId <= 0) {
continue;
}
$checklist = $this->buildIpdChecklistForAudit($ipdId, (int) ($row['patient_id'] ?? 0));
$total = (int) ($checklist['total_count'] ?? 0);
$okCount = (int) ($checklist['ok_count'] ?? 0);
$finalRows[] = [
'module_key' => 'ipd',
'module_label' => 'IPD Discharge',
'encounter_id' => $ipdId,
'encounter_code' => (string) ($row['encounter_code'] ?? ''),
'patient_code' => (string) ($row['patient_code'] ?? ''),
'patient_name' => (string) ($row['patient_name'] ?? ''),
'encounter_datetime' => (string) ($row['encounter_datetime'] ?? ''),
'ok_count' => $okCount,
'total_count' => $total,
'completion_percent' => $total > 0 ? round(($okCount * 100) / $total, 1) : 0.0,
'critical_missing_count' => (int) ($checklist['critical_missing_count'] ?? 0),
'critical_missing_labels' => implode(', ', $checklist['critical_missing'] ?? []),
];
}
return $finalRows;
}
private function buildIpdChecklistForAudit(int $ipdId, int $patientId): array
{
$hasAdmissionReason = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_complaint',
$ipdId,
['comp_report', 'comp_remark']
) || $this->tableHasAnyNonEmptyValueByIpd('ipd_discharge_complaint_remark', $ipdId, ['comp_remark']);
$hasDiagnosis = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_diagnosis',
$ipdId,
['comp_report', 'comp_remark']
) || $this->tableHasAnyNonEmptyValueByIpd('ipd_discharge_diagnosis_remark', $ipdId, ['comp_remark']);
$hasCourse = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_course',
$ipdId,
['comp_report', 'comp_remark']
) || $this->tableHasAnyNonEmptyValueByIpd('ipd_discharge_course_remark', $ipdId, ['comp_remark']);
$hasProcedure = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_surgery',
$ipdId,
['surgery_name', 'surgery_remark']
) || $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_procedure',
$ipdId,
['procedure_name', 'procedure_remark']
);
$hasCondition = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_1_b_final',
$ipdId,
['discharge_condition', 'condition_at_discharge', 'condition_remark', 'remark']
) || $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_general_exam_col',
$ipdId,
['value']
);
$hasMedication = $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_drug',
$ipdId,
['drug_name', 'drug_dose', 'drug_day']
) || $this->tableHasAnyNonEmptyValueByIpd(
'ipd_discharge_prescrption_prescribed',
$ipdId,
['med_name', 'med_type', 'qty', 'no_of_days', 'remark']
);
$instructionText = $this->firstNonEmptyValueByIpd(
'ipd_discharge_instructions',
$ipdId,
['comp_remark', 'footer_text']
);
$reviewAfter = $this->firstNonEmptyValueByIpd('ipd_discharge_instructions', $ipdId, ['review_after']);
$hasFollowUp = trim($instructionText) !== '' || trim($reviewAfter) !== '';
$allergySnapshot = $this->getLatestOpdAllergySnapshotByPatient($patientId);
$hasAllergyAdr = trim((string) ($allergySnapshot['drug_allergy_status'] ?? '')) !== ''
|| trim((string) ($allergySnapshot['drug_allergy_details'] ?? '')) !== ''
|| trim((string) ($allergySnapshot['adr_history'] ?? '')) !== '';
$instructionLower = strtolower((string) $instructionText);
$hasRedFlags = strpos($instructionLower, 'emergency') !== false
|| strpos($instructionLower, 'warning') !== false
|| strpos($instructionLower, 'red flag') !== false
|| strpos($instructionLower, 'immediately') !== false
|| strpos($instructionLower, 'return if') !== false;
$items = [
['label' => 'Reason for admission / presenting complaints', 'ok' => $hasAdmissionReason, 'critical' => true],
['label' => 'Final diagnosis documented', 'ok' => $hasDiagnosis, 'critical' => true],
['label' => 'Course / treatment in hospital documented', 'ok' => $hasCourse, 'critical' => true],
['label' => 'Surgery/procedure documented (if applicable)', 'ok' => $hasProcedure, 'critical' => false],
['label' => 'Condition at discharge documented', 'ok' => $hasCondition, 'critical' => true],
['label' => 'Discharge medication documented', 'ok' => $hasMedication, 'critical' => true],
['label' => 'Follow-up instructions / review plan documented', 'ok' => $hasFollowUp, 'critical' => true],
['label' => 'Drug allergy / ADR history documented', 'ok' => $hasAllergyAdr, 'critical' => false],
['label' => 'Red-flag / emergency return advice documented', 'ok' => $hasRedFlags, 'critical' => false],
];
$criticalMissing = [];
$okCount = 0;
foreach ($items as $item) {
if (! empty($item['ok'])) {
$okCount++;
continue;
}
if (! empty($item['critical'])) {
$criticalMissing[] = (string) ($item['label'] ?? '');
}
}
return [
'ok_count' => $okCount,
'total_count' => count($items),
'critical_missing' => $criticalMissing,
'critical_missing_count' => count($criticalMissing),
];
}
private function getOpdNabhAuditRows(string $minRange, string $maxRange): array
{
if (! $this->db->tableExists('opd_master')) {
return [];
}
$opdFields = $this->db->getFieldNames('opd_master') ?? [];
if (! in_array('opd_id', $opdFields, true)) {
return [];
}
$dateCol = $this->resolveExistingColumn($opdFields, [
'apointment_date',
'appointment_date',
'created_at',
'entry_date',
]);
if ($dateCol === null) {
return [];
}
$opdCodeCol = $this->resolveExistingColumn($opdFields, ['opd_code', 'opd_no']);
$patientFkCol = $this->resolveExistingColumn($opdFields, ['p_id', 'patient_id']);
$builder = $this->db->table('opd_master o')
->select('o.opd_id as encounter_id')
->select('o.' . $dateCol . ' as encounter_datetime', false);
if ($opdCodeCol !== null) {
$builder->select('o.' . $opdCodeCol . ' as encounter_code', false);
} else {
$builder->select("'' as encounter_code", false);
}
if ($patientFkCol !== null) {
$builder->select('o.' . $patientFkCol . ' as patient_id', false);
} else {
$builder->select('0 as patient_id', false);
}
$patientNameExpr = "''";
$patientCodeExpr = "''";
if ($patientFkCol !== null && $this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$nameCol = $this->resolveExistingColumn($patientFields, ['p_fname', 'patient_name', 'name']);
$codeCol = $this->resolveExistingColumn($patientFields, ['p_code', 'patient_code', 'uhid_no']);
$pkCol = $this->resolveExistingColumn($patientFields, ['id']);
if ($pkCol !== null) {
$builder->join('patient_master p', 'p.' . $pkCol . ' = o.' . $patientFkCol, 'left');
if ($nameCol !== null) {
$patientNameExpr = 'p.' . $nameCol;
}
if ($codeCol !== null) {
$patientCodeExpr = 'p.' . $codeCol;
}
}
}
$prescriptionCols = [];
if ($this->db->tableExists('opd_prescription')) {
$prescriptionFields = $this->db->getFieldNames('opd_prescription') ?? [];
if (in_array('id', $prescriptionFields, true) && in_array('opd_id', $prescriptionFields, true)) {
$builder->join(
'opd_prescription pr',
'pr.id = (SELECT MAX(px.id) FROM opd_prescription px WHERE px.opd_id = o.opd_id)',
'left',
false
);
foreach ([
'complaints',
'diagnosis',
'Provisional_diagnosis',
'Finding_Examinations',
'Prescriber_Remarks',
'investigation',
'advice',
'next_visit',
'refer_to',
'bp',
'diastolic',
'pulse',
'temp',
'spo2',
'rr_min',
'height',
'weight',
'drug_allergy_status',
'drug_allergy_details',
'adr_history',
'current_medications',
] as $column) {
if (in_array($column, $prescriptionFields, true)) {
$builder->select('pr.' . $column, false);
$prescriptionCols[] = $column;
}
}
}
}
$builder->select($patientNameExpr . ' as patient_name', false)
->select($patientCodeExpr . ' as patient_code', false)
->where('o.' . $dateCol . ' >=', $minRange)
->where('o.' . $dateCol . ' <=', $maxRange)
->orderBy('o.' . $dateCol, 'DESC');
$resultRows = $builder->get()->getResultArray();
$finalRows = [];
foreach ($resultRows as $row) {
$checklist = $this->buildOpdChecklistForAudit($row, $prescriptionCols);
$total = (int) ($checklist['total_count'] ?? 0);
$okCount = (int) ($checklist['ok_count'] ?? 0);
$finalRows[] = [
'module_key' => 'opd',
'module_label' => 'OPD Consult',
'encounter_id' => (int) ($row['encounter_id'] ?? 0),
'encounter_code' => (string) ($row['encounter_code'] ?? ''),
'patient_code' => (string) ($row['patient_code'] ?? ''),
'patient_name' => (string) ($row['patient_name'] ?? ''),
'encounter_datetime' => (string) ($row['encounter_datetime'] ?? ''),
'ok_count' => $okCount,
'total_count' => $total,
'completion_percent' => $total > 0 ? round(($okCount * 100) / $total, 1) : 0.0,
'critical_missing_count' => (int) ($checklist['critical_missing_count'] ?? 0),
'critical_missing_labels' => implode(', ', $checklist['critical_missing'] ?? []),
];
}
return $finalRows;
}
private function buildOpdChecklistForAudit(array $row, array $availableCols): array
{
$hasComplaints = $this->rowHasAnyNonEmptyValue($row, ['complaints']);
$hasDiagnosis = $this->rowHasAnyNonEmptyValue($row, ['diagnosis', 'Provisional_diagnosis']);
$hasClinicalCourse = $this->rowHasAnyNonEmptyValue($row, ['Finding_Examinations', 'Prescriber_Remarks']);
$hasProcedure = $this->rowHasAnyNonEmptyValue($row, ['investigation']);
$hasCondition = $this->rowHasAnyNonEmptyValue($row, ['bp', 'diastolic', 'pulse', 'temp', 'spo2', 'rr_min', 'height', 'weight']);
$hasMedicationPlan = $this->rowHasAnyNonEmptyValue($row, ['advice', 'current_medications']);
$hasFollowUp = $this->rowHasAnyNonEmptyValue($row, ['next_visit', 'refer_to']);
$hasAllergyAdr = $this->rowHasAnyNonEmptyValue($row, ['drug_allergy_status', 'drug_allergy_details', 'adr_history']);
$adviceText = strtolower(trim((string) ($row['advice'] ?? '')) . ' ' . trim((string) ($row['Prescriber_Remarks'] ?? '')));
$hasRedFlags = strpos($adviceText, 'emergency') !== false
|| strpos($adviceText, 'warning') !== false
|| strpos($adviceText, 'red flag') !== false
|| strpos($adviceText, 'immediately') !== false
|| strpos($adviceText, 'return if') !== false;
// If OPD prescription table is unavailable for the row, mark item as not applicable by treating as empty.
if ($availableCols === []) {
$hasComplaints = false;
$hasDiagnosis = false;
$hasClinicalCourse = false;
$hasProcedure = false;
$hasCondition = false;
$hasMedicationPlan = false;
$hasFollowUp = false;
$hasAllergyAdr = false;
$hasRedFlags = false;
}
$items = [
['label' => 'Reason for consultation / presenting complaints', 'ok' => $hasComplaints, 'critical' => true],
['label' => 'Diagnosis documented', 'ok' => $hasDiagnosis, 'critical' => true],
['label' => 'Clinical findings / assessment documented', 'ok' => $hasClinicalCourse, 'critical' => true],
['label' => 'Investigations/procedures documented (if applicable)', 'ok' => $hasProcedure, 'critical' => false],
['label' => 'Clinical condition / vitals documented', 'ok' => $hasCondition, 'critical' => true],
['label' => 'Treatment/medication plan documented', 'ok' => $hasMedicationPlan, 'critical' => true],
['label' => 'Follow-up or referral plan documented', 'ok' => $hasFollowUp, 'critical' => true],
['label' => 'Drug allergy / ADR history documented', 'ok' => $hasAllergyAdr, 'critical' => false],
['label' => 'Red-flag / emergency return advice documented', 'ok' => $hasRedFlags, 'critical' => false],
];
$criticalMissing = [];
$okCount = 0;
foreach ($items as $item) {
if (! empty($item['ok'])) {
$okCount++;
continue;
}
if (! empty($item['critical'])) {
$criticalMissing[] = (string) ($item['label'] ?? '');
}
}
return [
'ok_count' => $okCount,
'total_count' => count($items),
'critical_missing' => $criticalMissing,
'critical_missing_count' => count($criticalMissing),
];
}
private function resolveExistingColumn(array $fields, array $candidates): ?string
{
foreach ($candidates as $candidate) {
if (in_array($candidate, $fields, true)) {
return $candidate;
}
}
return null;
}
private function rowHasAnyNonEmptyValue(array $row, array $fields): bool
{
foreach ($fields as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
return true;
}
}
return false;
}
private function tableHasAnyNonEmptyValueByIpd(string $table, int $ipdId, array $preferredFields): bool
{
if ($ipdId <= 0 || ! $this->db->tableExists($table)) {
return false;
}
$fields = $this->db->getFieldNames($table) ?? [];
if (! in_array('ipd_id', $fields, true)) {
return false;
}
$selected = [];
foreach ($preferredFields as $field) {
if (in_array($field, $fields, true)) {
$selected[] = $field;
}
}
if ($selected === []) {
return false;
}
$rows = $this->db->table($table)
->select(implode(',', $selected))
->where('ipd_id', $ipdId)
->get()
->getResultArray();
foreach ($rows as $row) {
if ($this->rowHasAnyNonEmptyValue($row, $selected)) {
return true;
}
}
return false;
}
private function firstNonEmptyValueByIpd(string $table, int $ipdId, array $preferredFields): string
{
if ($ipdId <= 0 || ! $this->db->tableExists($table)) {
return '';
}
$fields = $this->db->getFieldNames($table) ?? [];
if (! in_array('ipd_id', $fields, true)) {
return '';
}
$selected = [];
foreach ($preferredFields as $field) {
if (in_array($field, $fields, true)) {
$selected[] = $field;
}
}
if ($selected === []) {
return '';
}
$builder = $this->db->table($table)->where('ipd_id', $ipdId);
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$row = $builder->get(1)->getRowArray() ?? [];
foreach ($selected as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
return $value;
}
}
return '';
}
private function getLatestOpdAllergySnapshotByPatient(int $patientId): array
{
if ($patientId <= 0 || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) {
return [];
}
$opdFields = $this->db->getFieldNames('opd_master') ?? [];
$rxFields = $this->db->getFieldNames('opd_prescription') ?? [];
if (! in_array('opd_id', $opdFields, true) || ! in_array('p_id', $opdFields, true) || ! in_array('opd_id', $rxFields, true)) {
return [];
}
$selects = [];
foreach (['drug_allergy_status', 'drug_allergy_details', 'adr_history'] as $field) {
if (in_array($field, $rxFields, true)) {
$selects[] = 'r.' . $field;
}
}
if ($selects === []) {
return [];
}
$builder = $this->db->table('opd_master o')
->select(implode(',', $selects), false)
->join('opd_prescription r', 'r.opd_id = o.opd_id', 'inner')
->where('o.p_id', $patientId);
if (in_array('apointment_date', $opdFields, true)) {
$builder->orderBy('o.apointment_date', 'DESC');
}
if (in_array('id', $rxFields, true)) {
$builder->orderBy('r.id', 'DESC');
}
$row = $builder->get(1)->getRowArray();
return is_array($row) ? $row : [];
}
}

View File

@ -15,6 +15,7 @@ class Template extends BaseController
'template.ct',
'template.mri',
'template.echo',
'template.discharge',
];
public function __construct()
@ -649,4 +650,197 @@ class Template extends BaseController
'mstTestKey' => $mstTestKey,
]);
}
private function ensureDischargeTemplateTable(): void
{
if ($this->db->tableExists('ipd_discharge_templates')) {
return;
}
$sql = "CREATE TABLE IF NOT EXISTS ipd_discharge_templates (
id INT NOT NULL AUTO_INCREMENT,
template_name VARCHAR(120) NOT NULL,
template_html LONGTEXT NOT NULL,
is_default TINYINT(1) NOT NULL DEFAULT 0,
status TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$this->db->query($sql);
}
private function defaultDischargeTemplateHtml(): string
{
return '<h3 style="margin:0 0 8px 0;">Discharge Summary</h3>'
. '<table style="width:100%;border-collapse:collapse;margin-bottom:10px;" border="1" cellpadding="6">'
. '<tr>'
. '<td><b>Patient</b>: {{PATIENT_NAME}}</td>'
. '<td><b>UHID</b>: {{UHID}}</td>'
. '<td><b>IPD</b>: {{IPD_CODE}}</td>'
. '</tr>'
. '<tr>'
. '<td><b>Age/Gender</b>: {{AGE_GENDER}}</td>'
. '<td><b>Admit Date</b>: {{ADMIT_DATE}}</td>'
. '<td><b>Discharge Date</b>: {{DISCHARGE_DATE}}</td>'
. '</tr>'
. '</table>'
. '<div>{{CONTENT}}</div>';
}
private function nabhDischargeTemplateHtml(): string
{
return '<h2 style="margin:0 0 10px 0;text-align:center;">DISCHARGE SUMMARY</h2>'
. '<table style="width:100%;border-collapse:collapse;margin-bottom:10px;" border="1" cellpadding="6">'
. '<tr>'
. '<td><b>Patient Name</b>: {{PATIENT_NAME}}</td>'
. '<td><b>UHID</b>: {{UHID}}</td>'
. '<td><b>IPD No.</b>: {{IPD_CODE}}</td>'
. '</tr>'
. '<tr>'
. '<td><b>Age/Gender</b>: {{AGE_GENDER}}</td>'
. '<td><b>Date of Admission</b>: {{ADMIT_DATE}}</td>'
. '<td><b>Date of Discharge</b>: {{DISCHARGE_DATE}}</td>'
. '</tr>'
. '<tr>'
. '<td colspan="3"><b>Prepared On</b>: {{CURRENT_DATE}}</td>'
. '</tr>'
. '</table>'
. '<div style="font-size:11px;color:#334155;margin-bottom:10px;">'
. 'NABH guidance note: Ensure diagnosis, procedures, clinical course, condition at discharge, medication with dose/duration, follow-up advice, red-flag signs, and emergency contact are documented.'
. '</div>'
. '<div style="margin-bottom:10px;">{{CONTENT}}</div>'
. '<h4 style="margin:12px 0 6px 0;">Counselling & Handover Confirmation</h4>'
. '<table style="width:100%;border-collapse:collapse;margin-bottom:10px;" border="1" cellpadding="6">'
. '<tr><td style="width:32%;">Medication explained to patient/attendant</td><td style="width:8%;"></td><td style="width:60%;">Remarks:</td></tr>'
. '<tr><td>Follow-up date and department explained</td><td></td><td>Next Visit: __________________</td></tr>'
. '<tr><td>Red-flag symptoms explained</td><td></td><td>Emergency Contact: __________________</td></tr>'
. '<tr><td>Diet and activity instructions explained</td><td></td><td></td></tr>'
. '</table>'
. '<table style="width:100%;border-collapse:collapse;margin-top:20px;" border="1" cellpadding="10">'
. '<tr>'
. '<td style="width:33%;vertical-align:bottom;">____________________<br>Consultant Name/Signature</td>'
. '<td style="width:33%;vertical-align:bottom;">____________________<br>Medical Officer Signature</td>'
. '<td style="width:34%;vertical-align:bottom;">____________________<br>Patient/Attendant Signature & Date</td>'
. '</tr>'
. '</table>';
}
private function ensureDefaultDischargeTemplateSeeded(): void
{
$this->ensureDischargeTemplateTable();
if (! $this->db->tableExists('ipd_discharge_templates')) {
return;
}
$table = $this->db->table('ipd_discharge_templates');
$count = (int) ($table->countAllResults() ?? 0);
if ($count === 0) {
$table->insert([
'template_name' => 'Default Discharge Template',
'template_html' => $this->defaultDischargeTemplateHtml(),
'is_default' => 1,
'status' => 1,
]);
}
$nabhExists = $this->db->table('ipd_discharge_templates')
->where('template_name', 'NABH Compliant Discharge Summary')
->get(1)
->getRowArray();
if (empty($nabhExists)) {
$table->insert([
'template_name' => 'NABH Compliant Discharge Summary',
'template_html' => $this->nabhDischargeTemplateHtml(),
'is_default' => 0,
'status' => 1,
]);
}
}
public function discharge_templates()
{
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
return $resp;
}
$this->ensureDefaultDischargeTemplateSeeded();
$notice = '';
$noticeType = 'success';
if (strtolower($this->request->getMethod()) === 'post') {
$id = (int) ($this->request->getPost('id') ?? 0);
$templateName = trim((string) ($this->request->getPost('template_name') ?? ''));
$templateHtml = (string) ($this->request->getPost('template_html') ?? '');
$isDefault = (int) ($this->request->getPost('is_default') ?? 0) === 1 ? 1 : 0;
$status = (int) ($this->request->getPost('status') ?? 1) === 1 ? 1 : 0;
if ($templateName === '' || trim($templateHtml) === '') {
$notice = 'Template name and template HTML are required.';
$noticeType = 'danger';
} else {
$table = $this->db->table('ipd_discharge_templates');
$data = [
'template_name' => $templateName,
'template_html' => $templateHtml,
'is_default' => $isDefault,
'status' => $status,
];
if ($id > 0) {
$table->where('id', $id)->update($data);
$notice = 'Template updated.';
} else {
$table->insert($data);
$id = (int) $this->db->insertID();
$notice = 'Template created.';
}
if ($isDefault === 1 && $id > 0) {
$this->db->table('ipd_discharge_templates')
->where('id <>', $id)
->update(['is_default' => 0]);
}
}
}
$editId = (int) ($this->request->getGet('edit') ?? 0);
$editRow = [];
if ($editId > 0) {
$editRow = $this->db->table('ipd_discharge_templates')
->where('id', $editId)
->get(1)
->getRowArray() ?? [];
}
$rows = $this->db->table('ipd_discharge_templates')
->orderBy('is_default', 'DESC')
->orderBy('id', 'ASC')
->get()
->getResultArray();
return view('Setting/Template/discharge_templates', [
'rows' => $rows,
'edit_row' => $editRow,
'notice' => $notice,
'notice_type' => $noticeType,
]);
}
public function discharge_template_delete(int $id)
{
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
return $resp;
}
$id = (int) $id;
if ($id > 0 && $this->db->tableExists('ipd_discharge_templates')) {
$this->db->table('ipd_discharge_templates')->where('id', $id)->delete();
}
return redirect()->to(base_url('setting/template/discharge_templates'));
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateIpdDischargeTemplatesTable extends Migration
{
private function hasTable(string $table): bool
{
$row = $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray();
return ! empty($row);
}
public function up()
{
if ($this->hasTable('ipd_discharge_templates')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'template_name' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => false,
],
'template_html' => [
'type' => 'LONGTEXT',
'null' => false,
],
'is_default' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'status' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('is_default');
$this->forge->addKey('status');
$this->forge->createTable('ipd_discharge_templates', true);
}
public function down()
{
$this->forge->dropTable('ipd_discharge_templates', true);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class IndianNameListSeeder extends Seeder
{
public function run()
{
if (! $this->db->tableExists('name_list')) {
$forge = \Config\Database::forge();
$forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => false,
],
]);
$forge->addKey('id', true);
$forge->addKey('name');
$forge->createTable('name_list', true);
}
$names = [
'Aarav', 'Aarush', 'Aditi', 'Akanksha', 'Akash', 'Akhil', 'Alok', 'Aman', 'Amar', 'Amit',
'Amrita', 'Anand', 'Ananya', 'Anil', 'Anjali', 'Ankit', 'Ankur', 'Ansh', 'Anshika', 'Anuj',
'Arjun', 'Arnav', 'Arun', 'Aryan', 'Asha', 'Ashish', 'Ayush', 'Bhavna', 'Bhavesh', 'Bhavya',
'Chandni', 'Charu', 'Chetan', 'Darpan', 'Deepa', 'Deepak', 'Dev', 'Devansh', 'Devesh', 'Dhruv',
'Diksha', 'Divya', 'Gaurav', 'Geeta', 'Gopal', 'Govind', 'Harish', 'Harsh', 'Harshita', 'Himanshu',
'Isha', 'Ishaan', 'Jatin', 'Jaya', 'Jyoti', 'Kabir', 'Kajal', 'Karan', 'Kartik', 'Kavita',
'Khushi', 'Kiran', 'Komal', 'Krishna', 'Kunal', 'Kusum', 'Lakshmi', 'Lokesh', 'Madhav', 'Madhuri',
'Mahesh', 'Manav', 'Manish', 'Meera', 'Megha', 'Mohan', 'Mohit', 'Mukesh', 'Nandini', 'Naveen',
'Neeraj', 'Neha', 'Nikhil', 'Nilesh', 'Niraj', 'Nisha', 'Nitin', 'Pankaj', 'Pooja', 'Pradeep',
'Prakash', 'Pranav', 'Prashant', 'Pratik', 'Preeti', 'Priya', 'Rahul', 'Raj', 'Rajat', 'Rajesh',
'Rakesh', 'Rani', 'Ravi', 'Rekha', 'Ritika', 'Rohan', 'Rohit', 'Sahil', 'Sakshi', 'Sameer',
'Sandeep', 'Sanjay', 'Sanjana', 'Sarthak', 'Satish', 'Seema', 'Shalini', 'Shankar', 'Shivam', 'Shreya',
'Shruti', 'Simran', 'Sonali', 'Sourabh', 'Suhani', 'Suman', 'Sunil', 'Suraj', 'Suresh', 'Swati',
'Tanvi', 'Tarun', 'Uday', 'Vaibhav', 'Varun', 'Vikas', 'Vinay', 'Vineet', 'Vishal', 'Vivek',
'Yash', 'Yashika', 'Yogesh', 'Zoya',
'Aggarwal', 'Agarwal', 'Arora', 'Bansal', 'Bhat', 'Bhatt', 'Bhardwaj', 'Chauhan', 'Chawla', 'Das',
'Dubey', 'Dwivedi', 'Garg', 'Goswami', 'Gupta', 'Jain', 'Joshi', 'Kapoor', 'Khanna', 'Khan',
'Kumar', 'Mishra', 'Nair', 'Pandey', 'Patel', 'Rao', 'Rawat', 'Saha', 'Saxena', 'Shah',
'Sharma', 'Shukla', 'Singh', 'Srivastava', 'Thakur', 'Tripathi', 'Tyagi', 'Taygi', 'Varma', 'Verma',
'Yadav'
];
$existingRows = $this->db->table('name_list')->select('name')->get()->getResultArray();
$existing = [];
foreach ($existingRows as $row) {
$existing[strtolower(trim((string) ($row['name'] ?? '')))] = true;
}
$batch = [];
foreach ($names as $name) {
$key = strtolower($name);
if (isset($existing[$key])) {
continue;
}
$batch[] = ['name' => $name];
}
if (! empty($batch)) {
$this->db->table('name_list')->insertBatch($batch);
}
}
}

View File

@ -215,6 +215,101 @@ class IpdBillingModel extends Model
->getResult();
}
public function getDoctorVisitFeeTypes(): array
{
if (! $this->db->tableExists('doc_ipd_fee_type')) {
return [];
}
return $this->db->table('doc_ipd_fee_type')
->select('id, fee_type')
->orderBy('fee_type', 'ASC')
->get()
->getResultArray();
}
public function getDoctorVisitFeeMap(array $doctorIds): array
{
if (empty($doctorIds) || ! $this->db->tableExists('doc_ipd_fee') || ! $this->db->tableExists('doc_ipd_fee_type')) {
return [];
}
$builder = $this->db->table('doc_ipd_fee d')
->select('d.doc_id, d.doc_fee_type as fee_type_id, d.doc_fee_desc, d.amount, t.fee_type')
->join('doc_ipd_fee_type t', 't.id = d.doc_fee_type', 'left')
->whereIn('d.doc_id', array_map('intval', $doctorIds));
if ($this->db->fieldExists('isdelete', 'doc_ipd_fee')) {
$builder->where('d.isdelete', 0);
}
$rows = $builder->orderBy('d.id', 'ASC')->get()->getResultArray();
$map = [];
foreach ($rows as $row) {
$docId = (int) ($row['doc_id'] ?? 0);
$feeTypeId = (int) ($row['fee_type_id'] ?? 0);
if ($docId <= 0 || $feeTypeId <= 0) {
continue;
}
$feeType = (string) ($row['fee_type'] ?? '');
$feeDesc = trim((string) ($row['doc_fee_desc'] ?? ''));
if ($feeDesc === '') {
$feeDesc = $feeType;
}
$map[$docId][$feeTypeId] = [
'fee_type_id' => $feeTypeId,
'fee_type' => $feeType,
'fee_desc' => $feeDesc,
'amount' => (float) ($row['amount'] ?? 0),
];
}
return $map;
}
public function getDoctorVisitFeeDetail(int $docId, int $feeTypeId = 0): ?array
{
if ($docId <= 0 || ! $this->db->tableExists('doc_ipd_fee') || ! $this->db->tableExists('doc_ipd_fee_type')) {
return null;
}
$builder = $this->db->table('doc_ipd_fee d')
->select('d.id, d.doc_id, d.doc_fee_type as fee_type_id, d.doc_fee_desc, d.amount, t.fee_type')
->join('doc_ipd_fee_type t', 't.id = d.doc_fee_type', 'left')
->where('d.doc_id', $docId);
if ($this->db->fieldExists('isdelete', 'doc_ipd_fee')) {
$builder->where('d.isdelete', 0);
}
if ($feeTypeId > 0) {
$builder->where('d.doc_fee_type', $feeTypeId);
}
$row = $builder->orderBy('d.id', 'ASC')->get()->getRowArray();
if (! is_array($row) || empty($row)) {
return null;
}
$feeType = (string) ($row['fee_type'] ?? '');
$feeDesc = trim((string) ($row['doc_fee_desc'] ?? ''));
if ($feeDesc === '') {
$feeDesc = $feeType;
}
return [
'id' => (int) ($row['id'] ?? 0),
'doc_id' => (int) ($row['doc_id'] ?? 0),
'fee_type_id' => (int) ($row['fee_type_id'] ?? 0),
'fee_type' => $feeType,
'fee_desc' => $feeDesc,
'amount' => (float) ($row['amount'] ?? 0),
];
}
public function getBankPaymentSources(): array
{
return $this->db->table('hospital_bank m')
@ -387,6 +482,26 @@ class IpdBillingModel extends Model
->getResult();
}
public function getNursingChargeHistory(int $ipdId): array
{
if (! $this->db->tableExists('nursing_ipd_charges')) {
return [];
}
$builder = $this->db->table('nursing_ipd_charges')
->select('charge_id, item_name, item_type, doctor_name, visit_date, visit_time, qty, rate, amount, remarks, include_in_bill')
->where('ipd_id', $ipdId);
if ($this->db->fieldExists('include_in_bill', 'nursing_ipd_charges')) {
$builder->where('include_in_bill', 1);
}
return $builder
->orderBy('charge_id', 'DESC')
->get()
->getResultArray();
}
private function baseIpdListQuery()
{
$builder = $this->db->table('ipd_master i');

View File

@ -296,11 +296,35 @@ class IpdModel extends Model
return 0;
}
$nursingChargeId = $this->extractNursingChargeIdFromComment((string) ($row['comment'] ?? ''));
if ($nursingChargeId > 0 && $this->db->tableExists('nursing_ipd_charges')) {
$nursingFields = $this->db->getFieldNames('nursing_ipd_charges');
$nursingUpdate = [];
if (in_array('include_in_bill', $nursingFields, true)) {
$nursingUpdate['include_in_bill'] = 0;
}
if (in_array('updated_by', $nursingFields, true)) {
$user = $this->getUserIdentity();
$nursingUpdate['updated_by'] = $user['name'] . ' [' . $user['id'] . '] D:' . date('Y-m-d H:i:s');
}
if (! empty($nursingUpdate)) {
$this->db->table('nursing_ipd_charges')
->where('charge_id', $nursingChargeId)
->update($nursingUpdate);
}
}
$user = $this->getUserIdentity();
$row['update_by_id'] = $user['id'];
$row['update_by'] = $user['name'];
$row['update_action'] = '1';
// Keep only columns that exist in the audit table to avoid schema drift failures.
$auditTableFields = $this->db->getFieldNames('ipd_invoice_item_update');
if (! empty($auditTableFields)) {
$row = array_intersect_key($row, array_flip($auditTableFields));
}
$inserted = $this->db->table('ipd_invoice_item_update')->insert($row);
if (! $inserted) {
return 0;
@ -310,6 +334,10 @@ class IpdModel extends Model
->where('id', $id)
->delete();
if ($this->db->affectedRows() < 1) {
return 0;
}
return 1;
}
@ -409,6 +437,19 @@ class IpdModel extends Model
];
}
private function extractNursingChargeIdFromComment(string $comment): int
{
if ($comment === '') {
return 0;
}
if (preg_match('/\[NURSING_CHARGE_ID:(\d+)\]/', $comment, $matches) !== 1) {
return 0;
}
return (int) ($matches[1] ?? 0);
}
private function getUserSignature(): string
{
$user = $this->getUserIdentity();

View File

@ -33,4 +33,54 @@ class OrganizationCaseModel extends Model
->where('id', $id)
->update($data);
}
public function getInvoiceRowsAndTotal(int $caseId): array
{
$sql1 = "select c.id AS id,c.case_id_code AS case_id_code,c.p_id AS p_id,
o.apointment_date AS s_Date,'OPD' AS Charge_type,0 AS Charge_type_id,
o.opd_id AS item_id,0 AS master_item_id,o.apointment_date AS Adate,
o.opd_fee_amount AS item_rate,1 AS item_qty,
date_format(o.apointment_date,'%d-%m-%Y') AS str_date,
concat('OPD Charge: Dr. ',o.doc_name) AS Description,
o.opd_code AS Code,o.opd_fee_amount AS Amount,'1' AS orgcode
from opd_master o
join organization_case_master c on o.insurance_case_id = c.id
where o.opd_status in (1,2) and c.id=" . $caseId .
" order by Adate";
$showinvoice1 = $this->db->query($sql1)->getResult();
$sql2 = "select c.id AS id,c.case_id_code AS case_id_code,c.p_id AS p_id,
i.inv_date AS s_Date,l.group_desc AS Charge_type,l.itype_id AS Charge_type_id,
t.id AS item_id,t.item_id AS master_item_id,i.inv_date AS Adate,
t.item_rate AS item_rate,t.item_qty AS item_qty,
date_format(i.inv_date,'%d-%m-%Y') AS str_date,
concat(t.item_name) AS Description,i.invoice_code AS Code,
t.item_amount AS Amount,t.org_code AS orgcode
from invoice_master i
join organization_case_master c on i.insurance_case_id = c.id
join invoice_item t on t.inv_master_id = i.id
join hc_item_type l on t.item_type = l.itype_id
left join hc_items_insurance it on t.item_id = it.hc_items_id and i.insurance_id = it.hc_insurance_id
where i.ipd_include = 1 and i.invoice_status = 1 and c.id=" . $caseId .
" order by Adate";
$showinvoice2 = $this->db->query($sql2)->getResult();
$rows = array_merge($showinvoice1, $showinvoice2);
usort($rows, static function ($a, $b): int {
$left = strtotime((string) ($a->s_Date ?? $a->Adate ?? ''));
$right = strtotime((string) ($b->s_Date ?? $b->Adate ?? ''));
return $left <=> $right;
});
$total = 0.0;
foreach ($rows as $row) {
$total += (float) ($row->Amount ?? 0);
}
return [
'rows' => $rows,
'total' => $total,
];
}
}

View File

@ -130,6 +130,87 @@ class PatientModel extends Model
return $rowSet;
}
public function getNameSuggestions(string $q): array
{
$term = strtolower(trim($q));
if ($term === '') {
return [];
}
$rows = [];
if ($this->db->tableExists('name_list')) {
try {
$rows = $this->db->table('name_list')
->select('name')
->like('name', $term, 'after')
->orderBy('name', 'ASC')
->limit(20)
->get()
->getResultArray();
} catch (\Throwable $e) {
$rows = [];
}
}
$rowSet = [];
foreach ($rows as $row) {
$name = trim((string) ($row['name'] ?? ''));
if ($name === '') {
continue;
}
$rowSet[] = [
'label' => $name,
'value' => $name,
];
}
if (!empty($rowSet)) {
return $rowSet;
}
$fallback = [
'Aarav', 'Aarush', 'Aditi', 'Akanksha', 'Akash', 'Akhil', 'Alok', 'Aman', 'Amar', 'Amit',
'Amrita', 'Anand', 'Ananya', 'Anil', 'Anjali', 'Ankit', 'Ankur', 'Ansh', 'Anshika', 'Anuj',
'Arjun', 'Arnav', 'Arun', 'Aryan', 'Asha', 'Ashish', 'Ayush', 'Bhavna', 'Bhavesh', 'Bhavya',
'Chandni', 'Charu', 'Chetan', 'Darpan', 'Deepa', 'Deepak', 'Dev', 'Devansh', 'Devesh', 'Dhruv',
'Diksha', 'Divya', 'Gaurav', 'Geeta', 'Gopal', 'Govind', 'Harish', 'Harsh', 'Harshita', 'Himanshu',
'Isha', 'Ishaan', 'Jatin', 'Jaya', 'Jyoti', 'Kabir', 'Kajal', 'Karan', 'Kartik', 'Kavita',
'Khushi', 'Kiran', 'Komal', 'Krishna', 'Kunal', 'Kusum', 'Lakshmi', 'Lokesh', 'Madhav', 'Madhuri',
'Mahesh', 'Manav', 'Manish', 'Meera', 'Megha', 'Mohan', 'Mohit', 'Mukesh', 'Nandini', 'Naveen',
'Neeraj', 'Neha', 'Nikhil', 'Nilesh', 'Niraj', 'Nisha', 'Nitin', 'Pankaj', 'Pooja', 'Pradeep',
'Prakash', 'Pranav', 'Prashant', 'Pratik', 'Preeti', 'Priya', 'Rahul', 'Raj', 'Rajat', 'Rajesh',
'Rakesh', 'Rani', 'Ravi', 'Rekha', 'Ritika', 'Rohan', 'Rohit', 'Sahil', 'Sakshi', 'Sameer',
'Sandeep', 'Sanjay', 'Sanjana', 'Sarthak', 'Satish', 'Seema', 'Shalini', 'Shankar', 'Shivam', 'Shreya',
'Shruti', 'Simran', 'Sonali', 'Sourabh', 'Suhani', 'Suman', 'Sunil', 'Suraj', 'Suresh', 'Swati',
'Tanvi', 'Tarun', 'Uday', 'Vaibhav', 'Varun', 'Vikas', 'Vinay', 'Vineet', 'Vishal', 'Vivek',
'Yash', 'Yashika', 'Yogesh', 'Zoya',
'Aggarwal', 'Agarwal', 'Arora', 'Bansal', 'Bhat', 'Bhatt', 'Bhardwaj', 'Chauhan', 'Chawla', 'Das',
'Dubey', 'Dwivedi', 'Garg', 'Goswami', 'Gupta', 'Jain', 'Joshi', 'Kapoor', 'Khanna', 'Khan',
'Kumar', 'Mishra', 'Nair', 'Pandey', 'Patel', 'Rao', 'Rawat', 'Saha', 'Saxena', 'Shah',
'Sharma', 'Shukla', 'Singh', 'Srivastava', 'Thakur', 'Tripathi', 'Tyagi', 'Taygi', 'Varma', 'Verma',
'Yadav'
];
$result = [];
foreach ($fallback as $name) {
if (stripos($name, $term) !== 0) {
continue;
}
$result[] = [
'label' => $name,
'value' => $name,
];
if (count($result) >= 20) {
break;
}
}
return $result;
}
public function insertRemark(array $data): int
{
$builder = $this->db->table('patient_remark');

View File

@ -63,12 +63,9 @@ $('#updatereport').click(function(){
{
var csrf_name = '<?= csrf_token() ?>';
var csrf_value = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
$.post('<?= base_url('billing/case/create_contingent_bill') ?>/'+caseid,{
"case_id": caseid,
[csrf_name]: csrf_value
}, function(data){
$.get('<?= base_url('Orgcase/create_contingent_bill') ?>/'+caseid, function(data){
notify('success','Please Attention',data);
load_form_div('<?= base_url('billing/case/contingent_bill') ?>/'+caseid,'org_CONTINGENT');
load_form_div('<?= base_url('Orgcase/contingent_bill') ?>/'+caseid,'org_CONTINGENT');
});
}
@ -76,12 +73,13 @@ $('#updatereport').click(function(){
$('#showreport').click(function(){
var caseid = $('#caseid').val();
load_report_div('<?= base_url('billing/case/contingent_bill') ?>/'+caseid+'/1','org_CONTINGENT');
var finalUrl = '<?= base_url('Orgcase/contingent_bill') ?>/' + caseid + '/1';
window.open(finalUrl, '_blank');
});
$('#editreport').click(function(){
var caseid = $('#caseid').val();
load_form_div('<?= base_url('billing/case/contingent_bill') ?>/'+caseid+'/0','org_CONTINGENT');
load_form_div('<?= base_url('Orgcase/contingent_bill') ?>/'+caseid+'/0','org_CONTINGENT');
});
</script>

View File

@ -6,6 +6,10 @@
<i class="bi bi-list"></i>
Charge Groups
</button>
<button class="btn btn-outline-primary" type="button" onclick="load_form_div('<?= base_url('nursing-bedside-items') ?>','maindiv','Clinical Procedure / Test Master');">
<i class="bi bi-heart-pulse"></i>
Clinical Procedure/Test Master
</button>
<button onclick="load_form_div('<?= base_url('item-ipd/add') ?>','maindiv','New Charge');" type="button" class="btn btn-primary">Add New Charge</button>
</div>
</div>

View File

@ -62,6 +62,12 @@
<span>IPD Charges</span>
</a>
</div>
<div class="col-6 col-md-4 col-lg-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('nursing-bedside-items') ?>','maindiv','Clinical Procedure / Test Master');">
<i class="bi bi-heart-pulse"></i>
<span>Clinical Procedure/Test</span>
</a>
</div>
<div class="col-6 col-md-4 col-lg-3">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('package/search') ?>','maindiv','Package IPD');">
<i class="bi bi-collection"></i>

View File

@ -0,0 +1,160 @@
<?php
$rows = $rows ?? [];
$edit = $edit_row ?? [];
$notice = (string) ($notice ?? '');
$noticeType = (string) ($notice_type ?? 'success');
$editId = (int) ($edit['id'] ?? 0);
$templateName = (string) ($edit['template_name'] ?? '');
$templateHtml = (string) ($edit['template_html'] ?? '<div>{{CONTENT}}</div>');
$isDefault = (int) ($edit['is_default'] ?? 0);
$status = (int) ($edit['status'] ?? 1);
?>
<section class="content">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0">IPD Discharge Templates</h6>
<a class="btn btn-sm btn-outline-secondary" href="javascript:load_form_div('<?= base_url('setting/template/discharge_templates') ?>','maindiv','IPD Discharge Template');">Reset</a>
</div>
<div class="card-body">
<?php if ($notice !== ''): ?>
<div class="alert alert-<?= esc($noticeType) ?> py-2 mb-3"><?= esc($notice) ?></div>
<?php endif; ?>
<div class="alert alert-info py-2 small">
Available placeholders: <code>{{CONTENT}}</code>, <code>{{PATIENT_NAME}}</code>, <code>{{UHID}}</code>, <code>{{IPD_CODE}}</code>,
<code>{{AGE_GENDER}}</code>, <code>{{ADMIT_DATE}}</code>, <code>{{DISCHARGE_DATE}}</code>, <code>{{CURRENT_DATE}}</code>.
</div>
<div class="alert alert-warning py-2 small">
NABH drafting checklist: include reason for admission, significant findings, diagnosis, procedures, course in hospital,
condition at discharge, discharge medication with dose/duration, follow-up plan, and warning signs/emergency contact.
</div>
<form method="post" action="<?= base_url('setting/template/discharge_templates') ?>" class="mb-4" id="discharge_template_form">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $editId ?>">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label small">Template Name</label>
<input type="text" name="template_name" class="form-control form-control-sm" value="<?= esc($templateName) ?>" required>
</div>
<div class="col-md-3">
<label class="form-label small">Status</label>
<select name="status" class="form-select form-select-sm">
<option value="1" <?= $status === 1 ? 'selected' : '' ?>>Active</option>
<option value="0" <?= $status === 0 ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="form-check mb-1">
<input class="form-check-input" type="checkbox" name="is_default" id="is_default" value="1" <?= $isDefault === 1 ? 'checked' : '' ?>>
<label class="form-check-label" for="is_default">Set as default</label>
</div>
</div>
<div class="col-12">
<label class="form-label small">Template HTML</label>
<textarea name="template_html" id="template_html_editor" rows="12" class="form-control" required><?= esc($templateHtml) ?></textarea>
<small class="text-muted">Use the editor to format layout. Use the <code>Source</code> button for raw HTML. Keep <code>{{CONTENT}}</code> in the template.</small>
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm"><?= $editId > 0 ? 'Update Template' : 'Create Template' ?></button>
<?php if ($editId > 0): ?>
<a class="btn btn-outline-secondary btn-sm" href="javascript:load_form_div('<?= base_url('setting/template/discharge_templates') ?>','maindiv','IPD Discharge Template');">Cancel Edit</a>
<?php endif; ?>
</div>
</form>
<div class="table-responsive">
<table class="table table-sm table-bordered align-middle">
<thead class="table-light">
<tr>
<th style="width:60px;">ID</th>
<th>Template Name</th>
<th style="width:90px;">Default</th>
<th style="width:90px;">Status</th>
<th style="width:180px;">Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="5" class="text-center text-muted">No templates found.</td></tr>
<?php else: ?>
<?php foreach ($rows as $row): ?>
<tr>
<td><?= (int) ($row['id'] ?? 0) ?></td>
<td><?= esc((string) ($row['template_name'] ?? '')) ?></td>
<td><?= (int) ($row['is_default'] ?? 0) === 1 ? 'Yes' : 'No' ?></td>
<td><?= (int) ($row['status'] ?? 0) === 1 ? 'Active' : 'Inactive' ?></td>
<td>
<a class="btn btn-outline-primary btn-sm" href="javascript:load_form_div('<?= base_url('setting/template/discharge_templates?edit=' . (int) ($row['id'] ?? 0)) ?>','maindiv','IPD Discharge Template');">Edit</a>
<a class="btn btn-outline-danger btn-sm" href="javascript:if(confirm('Delete this template?')) load_form_div('<?= base_url('setting/template/discharge_templates/delete/' . (int) ($row['id'] ?? 0)) ?>','maindiv','IPD Discharge Template');">Delete</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</section>
<script>
(function () {
var form = document.getElementById('discharge_template_form');
var editorFieldId = 'template_html_editor';
function initTemplateEditor() {
if (!window.CKEDITOR) {
return;
}
CKEDITOR.config.versionCheck = false;
CKEDITOR.config.removePlugins = '';
if (CKEDITOR.instances[editorFieldId]) {
CKEDITOR.instances[editorFieldId].destroy(true);
}
if (document.getElementById(editorFieldId)) {
CKEDITOR.replace(editorFieldId, {
height: 360,
toolbar: [
{ name: 'document', items: ['Source'] },
{ name: 'clipboard', items: ['Undo', 'Redo'] },
{ name: 'basicstyles', items: ['Bold', 'Italic', 'Underline', 'RemoveFormat'] },
{ name: 'paragraph', items: ['NumberedList', 'BulletedList', 'Outdent', 'Indent', 'Blockquote'] },
{ name: 'links', items: ['Link', 'Unlink'] },
{ name: 'insert', items: ['Table', 'HorizontalRule', 'SpecialChar'] },
{ name: 'styles', items: ['Format', 'Font', 'FontSize'] },
{ name: 'colors', items: ['TextColor', 'BGColor'] },
{ name: 'tools', items: ['Maximize'] }
]
});
}
}
function syncTemplateEditor() {
if (!window.CKEDITOR) {
return;
}
if (CKEDITOR.instances[editorFieldId]) {
CKEDITOR.instances[editorFieldId].updateElement();
}
}
initTemplateEditor();
if (form) {
form.addEventListener('submit', function () {
syncTemplateEditor();
});
}
})();
</script>

View File

@ -6,13 +6,14 @@ $canXray = $user && method_exists($user, 'can') ? $user->can('template.xray') :
$canCt = $user && method_exists($user, 'can') ? $user->can('template.ct') : false;
$canMri = $user && method_exists($user, 'can') ? $user->can('template.mri') : false;
$canEcho = $user && method_exists($user, 'can') ? $user->can('template.echo') : false;
$canDischarge = $user && method_exists($user, 'can') ? $user->can('template.discharge') : false;
$canOpdPrintTemplate = $user && method_exists($user, 'can') ? $user->can('billing.opd.edit') : false;
if (! $canOpdPrintTemplate && $user && method_exists($user, 'inGroup')) {
$canOpdPrintTemplate = $user->inGroup('OPDEdit', 'admin', 'superadmin', 'developer');
}
$hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $canMri || $canEcho;
$hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $canMri || $canEcho || $canDischarge;
?>
<section class="content">
<div class="pagetitle">
@ -194,6 +195,15 @@ $hasTemplateAccess = $canPathology || $canUltrasound || $canXray || $canCt || $c
</div>
<?php } ?>
<?php if ($canDischarge) { ?>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/template/discharge_templates') ?>','maindiv','IPD Discharge Template');">
<span class="tile-icon"><i class="bi bi-file-earmark-medical"></i></span>
<span>IPD Discharge Template</span>
</a>
</div>
<?php } ?>
</div>
<div class="row mt-4">

View File

@ -259,7 +259,7 @@
notify('error', 'Please Attention', data.error_text);
$("#RegisterPatient").prop("disabled", false);
} else {
load_form('<?= base_url('billing/patient/person_record') ?>/' + data.insertid);
load_form('<?= base_url('billing/patient/person_record') ?>/' + data.insertid, 'Patient Record');
}
}, 'json');
});
@ -273,7 +273,7 @@
});
$("#input_city").autocomplete({
source: "<?= base_url('Patient/city') ?>",
source: "<?= base_url('billing/patient/city') ?>",
minLength: 3,
autofocus: true,
select: function(event, ui) {
@ -325,7 +325,7 @@
})
.autocomplete({
source: function(request, response) {
$.getJSON("<?= base_url('Patient/get_name') ?>", {
$.getJSON("<?= base_url('billing/patient/get_name') ?>", {
term: extractLast(request.term)
}, response);
},
@ -363,7 +363,7 @@
})
.autocomplete({
source: function(request, response) {
$.getJSON("<?= base_url('Patient/get_name') ?>", {
$.getJSON("<?= base_url('billing/patient/get_name') ?>", {
term: extractLast(request.term)
}, response);
},

View File

@ -104,7 +104,11 @@
$checkbox_checked="";
$age_input_1='style="display: none;"';
$age_input_2='';
$DateofBirth=MysqlDate_to_str($data[0]->dob);
$DateofBirth='';
if (!empty($data[0]->dob)) {
$ts = strtotime((string) $data[0]->dob);
$DateofBirth = $ts ? date('Y-m-d', $ts) : '';
}
}
?>
<div class="col-md-3">
@ -138,14 +142,8 @@
<div id="age_input_2" <?=$age_input_2?>>
<div class="col-md-4">
<label> Date of Birth</label>
<div class="input-group date input-sm">
<div class="input-group-addon">
<i class="fa fa-calendar"></i>
</div>
<input class="form-control pull-right datepicker input-sm" name="datepicker_dob"
id="datepicker_dob" type="text" data-inputmask="'alias': 'dd/mm/yyyy'" data-mask
value="<?=$DateofBirth ?>" />
</div>
<input class="form-control input-sm" name="datepicker_dob"
id="datepicker_dob" type="date" value="<?=$DateofBirth ?>" />
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,17 @@ $ipd = $ipd_info ?? null;
$person = $person_info ?? null;
$ipdId = (int) ($ipd_id ?? 0);
$previewHtml = (string) ($content ?? '');
$renderedHtml = (string) ($rendered_content ?? $previewHtml);
$noticeText = (string) ($notice ?? '');
$noticeType = (string) ($notice_type ?? 'success');
$templateRows = $template_rows ?? [];
$selectedTemplateId = (int) ($selected_template_id ?? 0);
$nabhAudit = is_array($nabh_audit ?? null) ? $nabh_audit : [];
$nabhItems = is_array($nabhAudit['items'] ?? null) ? $nabhAudit['items'] : [];
$nabhCriticalMissing = is_array($nabhAudit['critical_missing'] ?? null) ? $nabhAudit['critical_missing'] : [];
$nabhCriticalMissingCount = (int) ($nabhAudit['critical_missing_count'] ?? 0);
$nabhOkCount = (int) ($nabhAudit['ok_count'] ?? 0);
$nabhTotalCount = (int) ($nabhAudit['total_count'] ?? count($nabhItems));
$patientName = trim((string) ($person->p_fname ?? ''));
$patientCode = trim((string) (
@ -46,21 +55,77 @@ if ($person) {
<div class="col-md-3"><strong>Discharge Date:</strong> <?= esc($ipd->str_discharge_date ?? '') ?></div>
</div>
<form method="post" action="<?= site_url('Ipd_discharge/preview_discharge_report/' . $ipdId) ?>">
<?= csrf_field() ?>
<div class="border rounded p-2 bg-white">
<textarea id="editor_Discharge_Preview" name="editor_Discharge_Preview" rows="16" class="form-control"><?= esc($previewHtml) ?></textarea>
<form method="get" action="<?= site_url('Ipd_discharge/preview_discharge_report/' . $ipdId) ?>" class="row g-2 mb-3">
<div class="col-md-6 col-lg-4">
<label class="form-label small mb-1"><strong>Discharge Template</strong></label>
<select class="form-select form-select-sm" name="tpl" id="tpl_selector">
<?php foreach ($templateRows as $tpl): ?>
<?php $tplId = (int) ($tpl['id'] ?? 0); ?>
<option value="<?= $tplId ?>" <?= $tplId === $selectedTemplateId ? 'selected' : '' ?>>
<?= esc((string) ($tpl['template_name'] ?? ('Template #' . $tplId))) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 col-lg-3 d-flex align-items-end">
<button type="submit" class="btn btn-outline-primary btn-sm">Apply Template</button>
</div>
</form>
<?php if (! empty($nabhItems)): ?>
<div class="card border-warning mb-3">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<strong>NABH Audit Mode</strong>
<span class="small">Completed: <?= $nabhOkCount ?>/<?= $nabhTotalCount ?></span>
</div>
<div class="card-body py-2">
<?php if ($nabhCriticalMissingCount > 0): ?>
<div class="alert alert-danger py-2 mb-2">
<strong>Critical sections missing (<?= $nabhCriticalMissingCount ?>):</strong>
<?= esc(implode(' | ', $nabhCriticalMissing)) ?>
</div>
<?php else: ?>
<div class="alert alert-success py-2 mb-2">All critical NABH checklist sections are present.</div>
<?php endif; ?>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0">
<thead>
<tr>
<th style="width:55%;">Checklist Item</th>
<th style="width:15%;">Type</th>
<th style="width:30%;">Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($nabhItems as $item): ?>
<?php
$ok = ! empty($item['ok']);
$critical = ! empty($item['critical']);
?>
<tr>
<td><?= esc((string) ($item['label'] ?? '')) ?></td>
<td><?= $critical ? 'Critical' : 'Recommended' ?></td>
<td><?= $ok ? 'OK' : ($critical ? 'Missing' : 'Needs review') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<div class="border rounded p-3 bg-white" style="min-height: 420px; overflow:auto;">
<?= $renderedHtml ?>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button type="submit" class="btn btn-success">Save Preview</button>
<button type="button" class="btn btn-primary" id="btn_create">Create Discharge Summary</button>
<button type="button" class="btn btn-outline-primary" id="btn_regen">Regenerate Summary</button>
<a class="btn btn-primary" id="btn_create" href="<?= site_url('Ipd_discharge/ipd_select/' . $ipdId) ?>">Back to Create Discharge Summary</a>
<button type="button" class="btn btn-danger" id="btn_show">Make File and Print on Letter Head</button>
<button type="button" class="btn btn-danger" id="btn_show2">Make File and Print On Plain Paper</button>
<button type="button" class="btn btn-outline-danger" id="btn_show3">New Print</button>
</div>
</form>
</div>
</div>
</section>
@ -72,59 +137,88 @@ if ($person) {
}
function loadOrRedirect(url, title) {
if (typeof load_form === 'function') {
load_form(url, title || 'Discharge');
return;
}
if (typeof load_form_div === 'function') {
load_form_div(url, 'maindiv', title || 'Discharge');
return;
}
window.location.href = url;
}
if (window.CKEDITOR) {
CKEDITOR.config.versionCheck = false;
// Global layout removes notification plugins; reset here to avoid dependency errors.
CKEDITOR.config.removePlugins = '';
if (CKEDITOR.instances.editor_Discharge_Preview) {
CKEDITOR.instances.editor_Discharge_Preview.destroy(true);
}
CKEDITOR.replace('editor_Discharge_Preview', {
height: 360
});
window.location.assign(url);
}
var ipdId = <?= (int) $ipdId ?>;
var nabhCriticalMissingCount = <?= (int) $nabhCriticalMissingCount ?>;
var nabhCriticalMissingItems = <?= json_encode(array_values($nabhCriticalMissing), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var btnCreate = document.getElementById('btn_create');
var btnRegen = document.getElementById('btn_regen');
var btnShow = document.getElementById('btn_show');
var btnShow2 = document.getElementById('btn_show2');
var btnShow3 = document.getElementById('btn_show3');
if (btnCreate) {
btnCreate.addEventListener('click', function() {
loadOrRedirect('<?= site_url('Ipd_discharge/ipd_select') ?>/' + ipdId, 'Create Discharge Summary');
});
function confirmNabhPrint() {
if (nabhCriticalMissingCount <= 0) {
return true;
}
if (btnRegen) {
btnRegen.addEventListener('click', function() {
loadOrRedirect('<?= site_url('Ipd_discharge/preview_discharge_report') ?>/' + ipdId + '?regen=1', 'Discharge Preview');
var detail = Array.isArray(nabhCriticalMissingItems) ? nabhCriticalMissingItems.join(' | ') : '';
var message = 'NABH Audit: critical sections are missing.\n\n' + detail + '\n\nDo you still want to print?';
return window.confirm(message);
}
if (btnCreate) {
btnCreate.addEventListener('click', function(e) {
if (typeof load_form !== 'function' && typeof load_form_div !== 'function') {
return;
}
// Prevent default only when SPA loader exists; otherwise native link works.
e.preventDefault();
loadOrRedirect('<?= site_url('Ipd_discharge/ipd_select') ?>/' + ipdId, 'Create Discharge Summary');
});
}
if (btnShow) {
btnShow.addEventListener('click', function() {
openUrl('<?= site_url('Ipd_discharge/show_discharge') ?>/' + ipdId + '/1');
if (!confirmNabhPrint()) {
return;
}
var tpl = document.getElementById('tpl_selector');
var tplId = tpl ? parseInt(tpl.value || '0', 10) : 0;
var url = '<?= site_url('Ipd_discharge/show_discharge') ?>/' + ipdId + '/1';
if (tplId > 0) {
url += '?tpl=' + tplId;
}
openUrl(url);
});
}
if (btnShow2) {
btnShow2.addEventListener('click', function() {
openUrl('<?= site_url('Ipd_discharge/show_discharge') ?>/' + ipdId + '/0');
if (!confirmNabhPrint()) {
return;
}
var tpl = document.getElementById('tpl_selector');
var tplId = tpl ? parseInt(tpl.value || '0', 10) : 0;
var url = '<?= site_url('Ipd_discharge/show_discharge') ?>/' + ipdId + '/0';
if (tplId > 0) {
url += '?tpl=' + tplId;
}
openUrl(url);
});
}
if (btnShow3) {
btnShow3.addEventListener('click', function() {
openUrl('<?= site_url('Ipd_discharge/show_file3') ?>/' + ipdId);
if (!confirmNabhPrint()) {
return;
}
var tpl = document.getElementById('tpl_selector');
var tplId = tpl ? parseInt(tpl.value || '0', 10) : 0;
var url = '<?= site_url('Ipd_discharge/show_file3') ?>/' + ipdId;
if (tplId > 0) {
url += '?tpl=' + tplId;
}
openUrl(url);
});
}
})();

View File

@ -5,10 +5,15 @@ $ipdId = (int) ($ipd_id ?? 0);
$printType = (int) ($print_type ?? 1);
$previewHtml = (string) ($content ?? '');
$patientName = trim((string) (($person->p_fname ?? '') . ' ' . ($person->p_lname ?? '')));
if ($patientName === '') {
$patientName = (string) ($person->p_fname ?? '');
}
$patientName = trim((string) ($person->p_fname ?? ''));
$patientCode = trim((string) (
$person->uhid
?? $person->UHID
?? $person->patient_code
?? $person->p_code
?? $person->reg_no
?? ''
));
$title = 'IPD Discharge Preview';
if ($printType === 0) {
@ -72,7 +77,7 @@ if ($printType === 0) {
<div class="meta">
<div><strong>IPD:</strong> <?= esc($ipd->ipd_code ?? $ipdId) ?></div>
<div><strong>Patient:</strong> <?= esc($patientName) ?></div>
<div><strong>UHID:</strong> <?= esc($person->p_code ?? '') ?></div>
<div><strong>UHID:</strong> <?= esc($patientCode) ?></div>
<div><strong>Date:</strong> <?= esc(date('d-m-Y h:i A')) ?></div>
</div>

View File

@ -7,6 +7,9 @@ $defaultPackageId = $hasPackage ? (int) ($packages[0]->id ?? 0) : 0;
$csrfName = csrf_token();
$csrfHash = csrf_hash();
$today = date('Y-m-d');
$bedsideItemsByCategory = $bedside_items_by_category ?? [];
$doctorVisitFeeTypes = $doctor_visit_fee_types ?? [];
$doctorVisitFeeMap = $doctor_visit_fee_map ?? [];
?>
<style>
@ -154,6 +157,134 @@ $today = date('Y-m-d');
<strong>Add Charges</strong>
</div>
<div class="card-body">
<div class="card mb-3 border-info">
<div class="card-header py-2"><strong>Bedside Clinical / Nursing Charge</strong></div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-3">
<label class="form-label">Item Type</label>
<select class="form-select form-select-sm" id="bedside_item_type">
<option value="">All</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Category</label>
<select class="form-select form-select-sm" id="bedside_category">
<option value="">Select</option>
<?php foreach (array_keys($bedsideItemsByCategory) as $categoryName) : ?>
<option value="<?= esc($categoryName) ?>"><?= esc($categoryName) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-5">
<label class="form-label">Item</label>
<select class="form-select form-select-sm" id="bedside_item_id">
<option value="">Select</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Quick Search</label>
<input type="text" class="form-control form-control-sm" id="bedside_item_search" placeholder="Search by code or name">
</div>
<div class="col-md-2">
<label class="form-label">Rate</label>
<input class="form-control form-control-sm" id="bedside_item_rate" value="0" type="number" step="0.01" />
</div>
<div class="col-md-2">
<label class="form-label">Qty</label>
<input class="form-control form-control-sm" id="bedside_item_qty" value="1" type="number" step="0.01" />
</div>
<div class="col-md-3">
<label class="form-label">Date</label>
<input class="form-control form-control-sm" id="bedside_item_date" value="<?= esc($today) ?>" type="date" />
</div>
<div class="col-md-3">
<label class="form-label">Doctor</label>
<select class="form-select form-select-sm" id="bedside_doc_id">
<option value="0">NONE</option>
<?php foreach ($doc_list ?? [] as $doc) : ?>
<option value="<?= esc($doc->id ?? '') ?>"><?= esc($doc->DocSpecName ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Comment</label>
<input class="form-control form-control-sm" id="bedside_comment" value="" type="text" />
</div>
<div class="col-md-12">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="ipdBedsideChargeAdd()">Add Bedside Charge</button>
</div>
</div>
</div>
</div>
<div class="card mb-3 border-primary">
<div class="card-header py-2"><strong>Doctor Visit Charge</strong></div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-5">
<label class="form-label">Doctor</label>
<select class="form-select form-select-sm" id="doctor_visit_doc_id">
<option value="">Select</option>
<?php foreach ($doc_list ?? [] as $doc) : ?>
<option value="<?= esc($doc->id ?? '') ?>"><?= esc($doc->DocSpecName ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Visit Type</label>
<select class="form-select form-select-sm" id="doctor_visit_fee_type">
<option value="">Select</option>
<?php foreach ($doctorVisitFeeTypes as $row) : ?>
<option value="<?= esc($row['id'] ?? 0) ?>"><?= esc($row['fee_type'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Rate</label>
<input class="form-control form-control-sm" id="doctor_visit_rate" value="0.00" type="number" step="0.01" />
</div>
<div class="col-md-5">
<label class="form-label">Description</label>
<input class="form-control form-control-sm" id="doctor_visit_fee_desc" value="" type="text" readonly />
</div>
<div class="col-md-2">
<label class="form-label">Qty</label>
<input class="form-control form-control-sm" id="doctor_visit_qty" value="1" type="number" step="0.01" />
</div>
<div class="col-md-3">
<label class="form-label">Date</label>
<input class="form-control form-control-sm" id="doctor_visit_date" value="<?= esc($today) ?>" type="date" />
</div>
<div class="col-md-2">
<label class="form-label">Time</label>
<input class="form-control form-control-sm" id="doctor_visit_time" type="time" />
</div>
<div class="col-md-2">
<label class="form-label">Duration (min)</label>
<input class="form-control form-control-sm" id="doctor_visit_duration" value="0" type="number" min="0" step="1" />
</div>
<div class="col-md-3">
<label class="form-label">Hospital/Clinic</label>
<input class="form-control form-control-sm" id="doctor_visit_hospital" value="" type="text" />
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="form-check mb-1">
<input class="form-check-input" type="checkbox" id="doctor_visit_outside" value="1">
<label class="form-check-label" for="doctor_visit_outside">Outside Doctor</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Comment</label>
<input class="form-control form-control-sm" id="doctor_visit_comment" value="" type="text" />
</div>
<div class="col-md-12">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="ipdDoctorVisitChargeAdd()">Add Doctor Visit</button>
</div>
</div>
</div>
</div>
<div class="accordion ipd-charge-accordion" id="ipdChargeAccordion">
<?php
$typeIndex = 0;
@ -232,7 +363,23 @@ $today = date('Y-m-d');
var ipdChargeCsrfHash = '<?= esc($csrfHash) ?>';
var ipdChargeBaseUrl = '<?= base_url() ?>';
var ipdChargeIpdId = <?= (int) $ipdId ?>;
var ipdChargeTabRefreshUrl = '<?= base_url('billing/ipd/panel/' . $ipdId . '/tab/ipd-charges') ?>';
var ipdChargeDefaultPackageId = <?= (int) $defaultPackageId ?>;
var bedsideItemsByCategory = <?= json_encode($bedsideItemsByCategory, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var doctorVisitFeeTypes = <?= json_encode($doctorVisitFeeTypes, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
var doctorVisitFeeMap = <?= json_encode($doctorVisitFeeMap, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
function getAllBedsideItems() {
var all = [];
Object.keys(bedsideItemsByCategory || {}).forEach(function (categoryName) {
var rows = bedsideItemsByCategory[categoryName] || [];
rows.forEach(function (row) {
all.push(row);
});
});
return all;
}
function ipdChargeSetRate(typeId) {
var select = document.getElementById('itype_name_id_' + typeId);
@ -245,6 +392,100 @@ $today = date('Y-m-d');
rateInput.value = rate || 0;
}
function bedsideRefreshTypeOptions() {
var allItems = getAllBedsideItems();
var selectedType = $('#bedside_item_type').val() || '';
var types = {};
allItems.forEach(function (item) {
var typeName = String(item.item_type || '').trim();
if (typeName !== '') {
types[typeName] = true;
}
});
var typeSelect = $('#bedside_item_type');
typeSelect.empty().append('<option value="">All</option>');
Object.keys(types).sort().forEach(function (typeName) {
typeSelect.append('<option value="' + typeName + '">' + typeName + '</option>');
});
if (selectedType !== '' && types[selectedType]) {
typeSelect.val(selectedType);
}
}
function bedsideRefreshCategories() {
var selectedType = $('#bedside_item_type').val() || '';
var selectedCategory = $('#bedside_category').val() || '';
var allItems = getAllBedsideItems();
var categories = {};
allItems.forEach(function (item) {
var typeName = String(item.item_type || '').trim();
if (selectedType !== '' && typeName !== selectedType) {
return;
}
var categoryName = String(item.category || '').trim();
if (categoryName === '') {
categoryName = 'General';
}
categories[categoryName] = true;
});
var categorySelect = $('#bedside_category');
categorySelect.empty().append('<option value="">Select</option>');
Object.keys(categories).sort().forEach(function (categoryName) {
categorySelect.append('<option value="' + categoryName + '">' + categoryName + '</option>');
});
if (selectedCategory !== '' && categories[selectedCategory]) {
categorySelect.val(selectedCategory);
}
}
function bedsideRefreshItems() {
var selectedType = $('#bedside_item_type').val() || '';
var category = $('#bedside_category').val() || '';
var search = ($('#bedside_item_search').val() || '').toLowerCase().trim();
var itemSelect = $('#bedside_item_id');
itemSelect.empty().append('<option value="">Select</option>');
var items = (bedsideItemsByCategory[category] || []).filter(function (item) {
var typeName = String(item.item_type || '').trim();
var itemName = String(item.item_name || '').toLowerCase();
var itemCode = String(item.item_code || '').toLowerCase();
if (search !== '' && itemName.indexOf(search) === -1 && itemCode.indexOf(search) === -1) {
return false;
}
return selectedType === '' || typeName === selectedType;
});
items.forEach(function(item) {
var rate = Number(item.default_rate || 0);
var label = (item.item_name || '') + ' [' + rate.toFixed(2) + ']';
var insuranceCode = String(item.insurance_code || '').trim();
if (insuranceCode !== '') {
label += ' {' + insuranceCode + '}';
}
var option = $('<option></option>')
.attr('value', item.item_id)
.attr('data-rate', rate)
.text(label);
itemSelect.append(option);
});
$('#bedside_item_rate').val(items.length > 0 ? Number(items[0].default_rate || 0).toFixed(2) : '0.00');
}
function bedsideSetRate() {
var selected = $('#bedside_item_id option:selected');
var rate = selected.data('rate');
$('#bedside_item_rate').val(rate !== undefined ? Number(rate).toFixed(2) : '0.00');
}
function ipdChargeAdd(typeId) {
var itemId = document.getElementById('itype_name_id_' + typeId).value;
var rate = document.getElementById('input_rate_' + typeId).value;
@ -269,7 +510,122 @@ $today = date('Y-m-d');
payload.doc_id = docId;
$.post(ipdChargeBaseUrl + 'billing/ipd/charge/add/' + ipdChargeIpdId, payload, function() {
load_form_div('<?= base_url('IpdNew/show_ipd_items/' . $ipdId . '/1') ?>', 'tab_charges_content');
load_form_div(ipdChargeTabRefreshUrl, 'tab_charges_content');
});
}
function ipdBedsideChargeAdd() {
var bedsideItemId = $('#bedside_item_id').val() || '';
if (!bedsideItemId) {
alert('Select bedside item.');
return;
}
var payload = {};
payload[ipdChargeCsrfName] = ipdChargeCsrfHash;
payload.bedside_item_id = bedsideItemId;
payload.item_rate = $('#bedside_item_rate').val() || 0;
payload.item_qty = $('#bedside_item_qty').val() || 1;
payload.item_date = $('#bedside_item_date').val() || '<?= esc($today) ?>';
payload.comment = $('#bedside_comment').val() || '';
payload.doc_id = $('#bedside_doc_id').val() || 0;
$.post(ipdChargeBaseUrl + 'billing/ipd/bedside-charge/add/' + ipdChargeIpdId, payload, function(resp) {
if (resp && resp.ok) {
load_form_div(ipdChargeTabRefreshUrl, 'tab_charges_content');
return;
}
alert((resp && resp.error) ? resp.error : 'Unable to add bedside charge');
}, 'json').fail(function(xhr) {
var msg = (xhr && xhr.responseJSON && xhr.responseJSON.error) ? xhr.responseJSON.error : 'Unable to add bedside charge';
alert(msg);
});
}
function doctorVisitRefreshTypes() {
var docId = parseInt($('#doctor_visit_doc_id').val() || '0', 10);
var selectedFeeType = $('#doctor_visit_fee_type').val() || '';
var feeTypeSelect = $('#doctor_visit_fee_type');
feeTypeSelect.empty().append('<option value="">Select</option>');
if (docId > 0 && doctorVisitFeeMap[docId]) {
var docFees = doctorVisitFeeMap[docId];
Object.keys(docFees).forEach(function (feeTypeId) {
var row = docFees[feeTypeId] || {};
var label = String(row.fee_type || '').trim();
if (label === '') {
label = 'Visit Type ' + feeTypeId;
}
feeTypeSelect.append('<option value="' + feeTypeId + '">' + label + '</option>');
});
} else {
(doctorVisitFeeTypes || []).forEach(function (row) {
feeTypeSelect.append('<option value="' + row.id + '">' + (row.fee_type || '') + '</option>');
});
}
if (selectedFeeType !== '' && feeTypeSelect.find('option[value="' + selectedFeeType + '"]').length > 0) {
feeTypeSelect.val(selectedFeeType);
}
}
function doctorVisitRefreshRate() {
var docId = parseInt($('#doctor_visit_doc_id').val() || '0', 10);
var feeTypeId = parseInt($('#doctor_visit_fee_type').val() || '0', 10);
var feeDetail = null;
if (docId > 0 && doctorVisitFeeMap[docId]) {
var docFees = doctorVisitFeeMap[docId];
if (feeTypeId > 0 && docFees[feeTypeId]) {
feeDetail = docFees[feeTypeId];
} else {
var firstKey = Object.keys(docFees)[0] || null;
if (firstKey && docFees[firstKey]) {
feeDetail = docFees[firstKey];
$('#doctor_visit_fee_type').val(String(firstKey));
}
}
}
if (feeDetail) {
$('#doctor_visit_rate').val(Number(feeDetail.amount || 0).toFixed(2));
$('#doctor_visit_fee_desc').val(feeDetail.fee_desc || feeDetail.fee_type || '');
return;
}
$('#doctor_visit_rate').val('0.00');
$('#doctor_visit_fee_desc').val('');
}
function ipdDoctorVisitChargeAdd() {
var docId = $('#doctor_visit_doc_id').val() || '';
if (!docId) {
alert('Select doctor.');
return;
}
var payload = {};
payload[ipdChargeCsrfName] = ipdChargeCsrfHash;
payload.doc_id = docId;
payload.fee_type_id = $('#doctor_visit_fee_type').val() || 0;
payload.item_rate = $('#doctor_visit_rate').val() || 0;
payload.item_qty = $('#doctor_visit_qty').val() || 1;
payload.item_date = $('#doctor_visit_date').val() || '<?= esc($today) ?>';
payload.visit_time = $('#doctor_visit_time').val() || '';
payload.duration = $('#doctor_visit_duration').val() || 0;
payload.hospital_clinic = $('#doctor_visit_hospital').val() || '';
payload.is_outside_doctor = $('#doctor_visit_outside').is(':checked') ? 1 : 0;
payload.comment = $('#doctor_visit_comment').val() || '';
$.post(ipdChargeBaseUrl + 'billing/ipd/doctor-visit/add/' + ipdChargeIpdId, payload, function(resp) {
if (resp && resp.ok) {
load_form_div(ipdChargeTabRefreshUrl, 'tab_charges_content');
return;
}
alert((resp && resp.error) ? resp.error : 'Unable to add doctor visit charge');
}, 'json').fail(function(xhr) {
var msg = (xhr && xhr.responseJSON && xhr.responseJSON.error) ? xhr.responseJSON.error : 'Unable to add doctor visit charge';
alert(msg);
});
}
@ -283,7 +639,7 @@ $today = date('Y-m-d');
payload.item_rate = rate;
$.post(ipdChargeBaseUrl + 'billing/ipd/charge/update/' + itemId, payload, function() {
load_form_div('<?= base_url('IpdNew/show_ipd_items/' . $ipdId . '/1') ?>', 'tab_charges_content');
load_form_div(ipdChargeTabRefreshUrl, 'tab_charges_content');
});
}
@ -295,8 +651,15 @@ $today = date('Y-m-d');
var payload = {};
payload[ipdChargeCsrfName] = ipdChargeCsrfHash;
$.post(ipdChargeBaseUrl + 'billing/ipd/charge/delete/' + itemId, payload, function() {
load_form_div('<?= base_url('IpdNew/show_ipd_items/' . $ipdId . '/1') ?>', 'tab_charges_content');
$.post(ipdChargeBaseUrl + 'billing/ipd/charge/delete/' + itemId, payload, function(resp) {
if (resp && resp.ok) {
load_form_div(ipdChargeTabRefreshUrl, 'tab_charges_content');
return;
}
alert((resp && resp.error) ? resp.error : 'Unable to remove charge item');
}, 'json').fail(function(xhr) {
var msg = (xhr && xhr.responseJSON && xhr.responseJSON.error) ? xhr.responseJSON.error : 'Unable to remove charge item';
alert(msg);
});
}
@ -306,7 +669,26 @@ $today = date('Y-m-d');
payload.package_id = checkbox.checked ? ipdChargeDefaultPackageId : 0;
$.post(ipdChargeBaseUrl + 'billing/ipd/charge/package/' + itemId, payload, function() {
load_form_div('<?= base_url('IpdNew/show_ipd_items/' . $ipdId . '/1') ?>', 'tab_charges_content');
load_form_div(ipdChargeTabRefreshUrl, 'tab_charges_content');
});
}
$('#bedside_item_type').on('change', function () {
bedsideRefreshCategories();
bedsideRefreshItems();
});
$('#bedside_category').on('change', bedsideRefreshItems);
$('#bedside_item_search').on('input', bedsideRefreshItems);
$('#bedside_item_id').on('change', bedsideSetRate);
$('#doctor_visit_doc_id').on('change', function () {
doctorVisitRefreshTypes();
doctorVisitRefreshRate();
});
$('#doctor_visit_fee_type').on('change', doctorVisitRefreshRate);
bedsideRefreshTypeOptions();
bedsideRefreshCategories();
bedsideRefreshItems();
doctorVisitRefreshTypes();
doctorVisitRefreshRate();
</script>

View File

@ -9,6 +9,22 @@
</div>
<section class="section">
<div class="card mb-3">
<div class="card-body pt-3">
<div class="row g-2 align-items-center">
<div class="col-md-8">
<input type="text" id="medical-master-search" class="form-control" placeholder="Search links (e.g. exp, gst, stock, invoice, bank)" list="medical-master-suggestions" autocomplete="off">
<datalist id="medical-master-suggestions"></datalist>
</div>
<div class="col-md-4 text-md-end">
<button type="button" class="btn btn-outline-secondary btn-sm" id="medical-master-clear">Clear</button>
</div>
</div>
<small class="text-muted d-block mt-2">Type a keyword to filter links. Matching links are highlighted.</small>
<div id="medical-master-search-empty" class="alert alert-warning mt-2 mb-0 py-2" style="display:none;">No matching links found.</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<div class="card">
@ -16,52 +32,52 @@
<div class="card-body pt-3">
<ul class="list-group list-group-flush">
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/purchase_invoice_report') ?>','medical-main','Purchase Invoice :Pharmacy');">Purchase Invoice</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/purchase_invoice_report') ?>','medical-main','Purchase Invoice :Pharmacy');">Purchase Invoice</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/supplier_account') ?>','medical-main','Supplier Account :Pharmacy');">Supplier Account</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/supplier_account') ?>','medical-main','Supplier Account :Pharmacy');">Supplier Account</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Payment_Medical') ?>','medical-main','Med. Payment Edit :Pharmacy');">Med. Payment Edit</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Payment_Medical') ?>','medical-main','Med. Payment Edit :Pharmacy');">Med. Payment Edit</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Payment_Medical/payment_log') ?>','medical-main','Med. Payment Edit Logs :Pharmacy');">Med. Payment Edit Logs</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Payment_Medical/payment_log') ?>','medical-main','Med. Payment Edit Logs :Pharmacy');">Med. Payment Edit Logs</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/invoice_item_log') ?>','medical-main','Med. Invoice Item Update Logs :Pharmacy');">Med. Invoice Item Update Logs</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/invoice_item_log') ?>','medical-main','Med. Invoice Item Update Logs :Pharmacy');">Med. Invoice Item Update Logs</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/store_stock') ?>','medical-main','Stock Report :Pharmacy');">Stock Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/store_stock') ?>','medical-main','Stock Report :Pharmacy');">Stock Report</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/stock_details') ?>','medical-main','Item Stock Statement :Pharmacy');">Item Stock Statement</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/stock_details') ?>','medical-main','Item Stock Statement :Pharmacy');">Item Stock Statement</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/ipd_discharge') ?>','medical-main','IPD Discharge Report :Pharmacy');">IPD Discharge Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/ipd_discharge') ?>','medical-main','IPD Discharge Report :Pharmacy');">IPD Discharge Report</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_daily_med_sale') ?>','medical-main','Day wise Medicine Sale :Pharmacy');">Day wise Medicine Sale</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_daily_med_sale') ?>','medical-main','Day wise Medicine Sale :Pharmacy');">Day wise Medicine Sale</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_daily_med_sale_doc') ?>','medical-main','Day wise & Doc Wise Medicine Sale :Pharmacy');">Day wise &amp; Doc Wise Medicine Sale</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_daily_med_sale_doc') ?>','medical-main','Day wise & Doc Wise Medicine Sale :Pharmacy');">Day wise &amp; Doc Wise Medicine Sale</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_company_med_sale') ?>','medical-main','Company Wise Medicine :Pharmacy');">Company Wise Medicine</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_company_med_sale') ?>','medical-main','Company Wise Medicine :Pharmacy');">Company Wise Medicine</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/expire_stock') ?>','medical-main','Expiry Medicine :Pharmacy');">Expiry Medicine</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/expire_stock') ?>','medical-main','Expiry Medicine :Pharmacy');">Expiry Medicine</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/master_report/lost-medicine') ?>','medical-main','Lost Medicine :Pharmacy');">Lost Medicine</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/master_report/lost-medicine') ?>','medical-main','Lost Medicine :Pharmacy');">Lost Medicine</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Short_medicine') ?>','medical-main','Short Medicine List :Pharmacy');">Short Medicine List</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Short_medicine') ?>','medical-main','Short Medicine List :Pharmacy');">Short Medicine List</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/stocktransfer') ?>','medical-main','Stock Transfer :Pharmacy');">Stock Transfer</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/stocktransfer') ?>','medical-main','Stock Transfer :Pharmacy');">Stock Transfer</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/merge_product') ?>','medical-main','Product Merge :Pharmacy');">Product Merge</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/merge_product') ?>','medical-main','Product Merge :Pharmacy');">Product Merge</a>
</li>
</ul>
</div>
@ -74,22 +90,22 @@
<div class="card-body pt-3">
<ul class="list-group list-group-flush">
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/Invoice_Med_Draft') ?>','medical-main','Old Invoice :Pharmacy');">Old Invoice</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/Invoice_Med_Draft') ?>','medical-main','Old Invoice :Pharmacy');">Old Invoice</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_4') ?>','medical-main','IPD Sale Report :Pharmacy');">IPD Sale Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_4') ?>','medical-main','IPD Sale Report :Pharmacy');">IPD Sale Report</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_backpanel/Report_IPD_CreditBills') ?>','medical-main','IPD Invoice Bill Type :Pharmacy');">IPD Invoice Bill Type</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_backpanel/Report_IPD_CreditBills') ?>','medical-main','IPD Invoice Bill Type :Pharmacy');">IPD Invoice Bill Type</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_backpanel/Org_Bills') ?>','medical-main','OPD Org Report :Pharmacy');">OPD Org Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_backpanel/Org_Bills') ?>','medical-main','OPD Org Report :Pharmacy');">OPD Org Report</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/master_report/opd-cash-pending') ?>','medical-main','OPD CASH Pending Report :Pharmacy');">OPD CASH Pending Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/master_report/opd-cash-pending') ?>','medical-main','OPD CASH Pending Report :Pharmacy');">OPD CASH Pending Report</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/master_report/opd-old-balance-paid') ?>','medical-main','OPD Old Balance Paid Report :Pharmacy');">OPD Old Balance Paid Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/master_report/opd-old-balance-paid') ?>','medical-main','OPD Old Balance Paid Report :Pharmacy');">OPD Old Balance Paid Report</a>
</li>
</ul>
</div>
@ -102,16 +118,16 @@
<div class="card-body pt-3">
<ul class="list-group list-group-flush">
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_5') ?>','medical-main','Invoice List for GST :Pharmacy');">Invoice List for GST</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_5') ?>','medical-main','Invoice List for GST :Pharmacy');">Invoice List for GST</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_3') ?>','medical-main','GST Report :Pharmacy');">GST Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_3') ?>','medical-main','GST Report :Pharmacy');">GST Report</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical/master_report/bank-ledger') ?>','medical-main','Bank Ledger :Pharmacy');">Bank Ledger</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical/master_report/bank-ledger') ?>','medical-main','Bank Ledger :Pharmacy');">Bank Ledger</a>
</li>
<li class="list-group-item px-0">
<a href="javascript:load_form_div('<?= base_url('Medical_Report/Report_gst_1') ?>','medical-main','Purchase GST Report :Pharmacy');">Purchase GST Report</a>
<a class="medical-master-link" href="javascript:load_form_div('<?= base_url('Medical_Report/Report_gst_1') ?>','medical-main','Purchase GST Report :Pharmacy');">Purchase GST Report</a>
</li>
</ul>
</div>
@ -121,3 +137,119 @@
<div id="medical-main" class="mt-3"></div>
</section>
<style>
.medical-master-match {
background-color: #fff8d9;
border-radius: 4px;
}
</style>
<script>
(function () {
var searchInput = document.getElementById('medical-master-search');
var clearBtn = document.getElementById('medical-master-clear');
var emptyBox = document.getElementById('medical-master-search-empty');
var suggestBox = document.getElementById('medical-master-suggestions');
if (!searchInput || !clearBtn || !emptyBox || !suggestBox) {
return;
}
var keywordMap = {
'Purchase Invoice': 'purchase bill buy vendor supplier invoice pi',
'Supplier Account': 'supplier vendor ledger account payable',
'Med. Payment Edit': 'payment edit correction cash bank',
'Med. Payment Edit Logs': 'payment logs audit history',
'Med. Invoice Item Update Logs': 'invoice item logs update history',
'Stock Report': 'stock inventory qty quantity balance',
'Item Stock Statement': 'stock statement item inventory detail',
'IPD Discharge Report': 'ipd discharge in patient report',
'Day wise Medicine Sale': 'day daily date wise sale',
'Day wise & Doc Wise Medicine Sale': 'doctor doc day wise sale',
'Company Wise Medicine': 'company manufacturer medicine sale',
'Expiry Medicine': 'expiry exp expired near expiry',
'Lost Medicine': 'lost missing damage wastage',
'Short Medicine List': 'short shortage out of stock low stock',
'Stock Transfer': 'transfer move stock branch',
'Product Merge': 'merge duplicate combine product',
'Old Invoice': 'old previous invoice',
'IPD Sale Report': 'ipd sale report in patient',
'IPD Invoice Bill Type': 'ipd bill type credit cash',
'OPD Org Report': 'opd org organisation corporate',
'OPD CASH Pending Report': 'opd cash pending due',
'OPD Old Balance Paid Report': 'opd old balance paid receipt',
'Invoice List for GST': 'gst invoice tax',
'GST Report': 'gst tax summary',
'Bank Ledger': 'bank ledger statement transaction',
'Purchase GST Report': 'purchase gst tax input'
};
var links = Array.prototype.slice.call(document.querySelectorAll('.medical-master-link'));
var cards = Array.prototype.slice.call(document.querySelectorAll('section.section .row.g-3 > .col-lg-4'));
function normalize(text) {
return (text || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
}
function buildSuggestions() {
var pool = {};
links.forEach(function (link) {
var title = link.textContent.trim();
var keywords = keywordMap[title] || '';
var hay = normalize(title + ' ' + keywords).split(' ');
hay.forEach(function (word) {
if (word.length >= 3) {
pool[word] = true;
}
});
});
Object.keys(pool).sort().slice(0, 120).forEach(function (term) {
var option = document.createElement('option');
option.value = term;
suggestBox.appendChild(option);
});
}
function applyFilter() {
var q = normalize(searchInput.value);
var hasAny = false;
links.forEach(function (link) {
var title = normalize(link.textContent);
var keywords = normalize(keywordMap[link.textContent.trim()] || '');
var haystack = title + ' ' + keywords;
var matched = q === '' || haystack.indexOf(q) !== -1;
var item = link.closest('li');
if (!item) {
return;
}
item.style.display = matched ? '' : 'none';
item.classList.toggle('medical-master-match', matched && q !== '');
if (matched) {
hasAny = true;
}
});
cards.forEach(function (cardCol) {
var visibleItems = cardCol.querySelectorAll('li.list-group-item:not([style*="display: none"])');
cardCol.style.display = visibleItems.length > 0 ? '' : 'none';
});
emptyBox.style.display = (!hasAny && q !== '') ? '' : 'none';
}
searchInput.addEventListener('input', applyFilter);
clearBtn.addEventListener('click', function () {
searchInput.value = '';
applyFilter();
searchInput.focus();
});
buildSuggestions();
applyFilter();
})();
</script>

View File

@ -80,6 +80,12 @@ if (! $canPharmacy && $user && method_exists($user, 'inGroup')) {
<span>IPD Patient List</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/index') ?>','Report Panel')">
<i class="bi bi-grid-1x2"></i>
<span>Report Panel</span>
</a>
</li>
<li class="nav-heading">Doctor's Work</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('/opd/appointment') ?>','OPD Appointment List')">
@ -123,6 +129,7 @@ if (! $canPharmacy && $user && method_exists($user, 'inGroup')) {
<?php } ?>
<li class="nav-heading">Reports</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report')">
<i class="bi bi-graph-up"></i>
@ -141,6 +148,12 @@ if (! $canPharmacy && $user && method_exists($user, 'inGroup')) {
<span>Insurance Credit </span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/nabh_audit_report') ?>','NABH Audit Report')">
<i class="bi bi-clipboard2-check"></i>
<span>NABH Audit Report</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('org-packing') ?>','Organization Packing')">
<i class="bi bi-box-seam"></i>

View File

@ -0,0 +1,93 @@
<?php
$doctors = $doctors ?? [];
?>
<section class="content">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Billing Operations Report</h5>
</div>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-5">
<label class="form-label">Date Range</label>
<div class="d-flex gap-2">
<input type="datetime-local" class="form-control" id="billing_start">
<input type="datetime-local" class="form-control" id="billing_end">
</div>
</div>
<div class="col-md-5">
<label class="form-label">Doctor (OPD)</label>
<select class="form-control select2" id="billing_doctor" data-placeholder="All Doctors">
<option value="0">All Doctors</option>
<?php foreach ($doctors as $row): ?>
<option value="<?= esc((string) ($row->doc_name ?? '')) ?>"><?= esc((string) ($row->doc_name ?? '')) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex gap-2">
<button type="button" class="btn btn-primary w-100" id="billing_show">Show</button>
<button type="button" class="btn btn-outline-primary w-100" id="billing_export">Export</button>
</div>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<div id="billing_report_result" class="table-responsive">Select filters and click Show.</div>
</div>
</div>
</section>
<script>
(function() {
function pad(v) {
return v < 10 ? '0' + v : String(v);
}
function toInputValue(date) {
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
+ 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
function toRangeValue(value, fallback) {
var finalValue = value || fallback;
if (finalValue.length === 16) {
return finalValue + ':00';
}
return finalValue;
}
var now = new Date();
var start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
var end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 0);
document.getElementById('billing_start').value = toInputValue(start);
document.getElementById('billing_end').value = toInputValue(end);
function buildUrl(withExport) {
var startVal = toRangeValue(document.getElementById('billing_start').value, toInputValue(start));
var endVal = toRangeValue(document.getElementById('billing_end').value, toInputValue(end));
var dateRange = startVal + 'S' + endVal;
var doctor = document.getElementById('billing_doctor').value || '0';
var url = '<?= base_url('Report/billing_operations_report_data') ?>/'
+ encodeURIComponent(dateRange) + '/' + encodeURIComponent(doctor);
if (withExport) {
url += '/1';
}
return url;
}
document.getElementById('billing_show').addEventListener('click', function() {
load_form_div(buildUrl(false), 'billing_report_result');
});
document.getElementById('billing_export').addEventListener('click', function() {
window.open(buildUrl(true), '_blank');
});
$('.select2').select2({
theme: 'bootstrap4',
width: '100%'
});
})();
</script>

View File

@ -0,0 +1,178 @@
<?php
$opdRows = $opd_rows ?? [];
$invoiceSummary = $invoice_summary ?? [];
$invoiceRows = $invoice_rows ?? [];
$ipdSummary = $ipd_summary ?? [];
$ipdPaymentRows = $ipd_payment_rows ?? [];
?>
<div class="mb-3 small text-muted">
Date: <strong><?= esc((string) ($min_range ?? '')) ?></strong> to <strong><?= esc((string) ($max_range ?? '')) ?></strong>
<?php if (! empty($doctor_filter)): ?>
| Doctor: <strong><?= esc((string) $doctor_filter) ?></strong>
<?php endif; ?>
</div>
<div class="row g-3 mb-3">
<div class="col-md-3">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">OPD Doctors</div>
<div class="h5 mb-0"><?= count($opdRows) ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">Total Invoices</div>
<div class="h5 mb-0"><?= (int) ($invoiceSummary['invoice_count'] ?? 0) ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">Invoice Amount</div>
<div class="h5 mb-0"><?= number_format((float) ($invoiceSummary['total_invoice_amount'] ?? 0), 2) ?></div>
</div>
</div>
<div class="col-md-3">
<div class="border rounded p-2 bg-light">
<div class="small text-muted">IPD Net Received</div>
<div class="h5 mb-0"><?= number_format((float) ($ipdSummary['net_received'] ?? 0), 2) ?></div>
</div>
</div>
</div>
<h6>1) OPD Done By Doctors</h6>
<table class="table table-sm table-bordered align-middle mb-4">
<thead>
<tr>
<th style="width:60px;">#</th>
<th>Doctor</th>
<th style="width:160px;">Total OPDs</th>
</tr>
</thead>
<tbody>
<?php if (empty($opdRows)): ?>
<tr><td colspan="3" class="text-center text-muted">No OPD data found.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach ($opdRows as $row): ?>
<tr>
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row->doc_name ?? '')) ?></td>
<td><?= (int) ($row->total_opd ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<h6>2) Charges Invoice / Tests / Amount / Received</h6>
<div class="table-responsive mb-2">
<table class="table table-sm table-bordered align-middle">
<thead>
<tr>
<th>Total Tests</th>
<th>Total Test Qty</th>
<th>Total Test Amount</th>
<th>Total Received</th>
<th>Total Refund</th>
<th>Net Received</th>
</tr>
</thead>
<tbody>
<tr>
<td><?= (int) ($invoiceSummary['total_test_count'] ?? 0) ?></td>
<td><?= number_format((float) ($invoiceSummary['total_test_qty'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($invoiceSummary['total_test_amount'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($invoiceSummary['total_received'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($invoiceSummary['total_refund'] ?? 0), 2) ?></td>
<td><?= number_format(((float) ($invoiceSummary['total_received'] ?? 0)) - ((float) ($invoiceSummary['total_refund'] ?? 0)), 2) ?></td>
</tr>
</tbody>
</table>
</div>
<table class="table table-sm table-bordered align-middle mb-4">
<thead>
<tr>
<th>#</th>
<th>Invoice</th>
<th>Date</th>
<th>Test Count</th>
<th>Test Amount</th>
<th>Invoice Amount</th>
<th>Received</th>
<th>Pending</th>
</tr>
</thead>
<tbody>
<?php if (empty($invoiceRows)): ?>
<tr><td colspan="8" class="text-center text-muted">No invoice data found.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach ($invoiceRows as $row): ?>
<tr>
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row->inv_name ?? ('INV#' . ($row->id ?? '')))) ?></td>
<td><?= esc((string) ($row->inv_date ?? '')) ?></td>
<td><?= (int) ($row->test_count ?? 0) ?></td>
<td><?= number_format((float) ($row->test_amount ?? 0), 2) ?></td>
<td><?= number_format((float) ($row->invoice_amount ?? 0), 2) ?></td>
<td><?= number_format((float) ($row->net_received ?? 0), 2) ?></td>
<td><?= number_format((float) ($row->pending_amount ?? 0), 2) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<h6>3) IPD Related Payment Received</h6>
<div class="table-responsive mb-2">
<table class="table table-sm table-bordered align-middle">
<thead>
<tr>
<th>IPD Cases</th>
<th>Total Received</th>
<th>Total Refund</th>
<th>Net Received</th>
</tr>
</thead>
<tbody>
<tr>
<td><?= (int) ($ipdSummary['ipd_count'] ?? 0) ?></td>
<td><?= number_format((float) ($ipdSummary['total_received'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($ipdSummary['total_refund'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($ipdSummary['net_received'] ?? 0), 2) ?></td>
</tr>
</tbody>
</table>
</div>
<table class="table table-sm table-bordered align-middle">
<thead>
<tr>
<th>#</th>
<th>IPD Code</th>
<th>Patient</th>
<th>Received</th>
<th>Refund</th>
<th>Net</th>
</tr>
</thead>
<tbody>
<?php if (empty($ipdPaymentRows)): ?>
<tr><td colspan="6" class="text-center text-muted">No IPD payment data found.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach ($ipdPaymentRows as $row): ?>
<?php
$received = (float) ($row->total_received ?? 0);
$refund = (float) ($row->total_refund ?? 0);
?>
<tr>
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row->ipd_code ?? ('IPD#' . ($row->ipd_id ?? '')))) ?></td>
<td><?= esc((string) ($row->patient_name ?? '')) ?></td>
<td><?= number_format($received, 2) ?></td>
<td><?= number_format($refund, 2) ?></td>
<td><?= number_format($received - $refund, 2) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>

194
app/Views/report/index.php Normal file
View File

@ -0,0 +1,194 @@
<section class="content report-hub">
<div class="report-hub-head mb-3">
<h2 class="report-hub-title mb-1">Report Panel</h2>
<p class="report-hub-sub mb-0">Quick access to billing, diagnosis, insurance, and operational report sections.</p>
</div>
<div class="row g-3">
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-blue"><i class="bi bi-bar-chart-line"></i></span>
<h5 class="report-card-title mb-0">Reports</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/nabh_audit_report') ?>','NABH Audit Report');"><i class="bi bi-arrow-right-short"></i> NABH Audit Report</a></li>
</ul>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-cyan"><i class="bi bi-clipboard2-pulse"></i></span>
<h5 class="report-card-title mb-0">Reports Diagnosis</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/diagnosis_report') ?>','Diagnosis Report');"><i class="bi bi-arrow-right-short"></i> Diagnosis Report</a></li>
</ul>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-green"><i class="bi bi-hospital"></i></span>
<h5 class="report-card-title mb-0">Reports IPD</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/insurance_ipd_report') ?>','Insurance Case IPD');"><i class="bi bi-arrow-right-short"></i> Insurance Case IPD</a></li>
</ul>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-indigo"><i class="bi bi-shield-check"></i></span>
<h5 class="report-card-title mb-0">Reports Insurance Credit</h5>
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/insurance_ipd_report') ?>','Insurance Case IPD');"><i class="bi bi-arrow-right-short"></i> Insurance Case IPD</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/insurance_opd_report') ?>','Insurance Case OPD');"><i class="bi bi-arrow-right-short"></i> Insurance Case OPD</a></li>
<li><a href="javascript:load_form('<?= base_url('org-packing') ?>','Org. OPD Packing');"><i class="bi bi-arrow-right-short"></i> Or. OPD Packing</a></li>
</ul>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card report-card h-100 report-card-muted">
<div class="card-body">
<div class="report-card-head">
<span class="report-icon report-icon-slate"><i class="bi bi-file-earmark-text"></i></span>
<h5 class="report-card-title mb-0">Reports Document</h5>
</div>
<div class="report-empty">Document reports are not migrated yet.</div>
</div>
</div>
</div>
</div>
</section>
<style>
.report-hub {
--hub-bg: #f4f6fb;
--hub-border: #e3e8f2;
--hub-text: #2f3b52;
--hub-sub: #69758c;
--hub-link: #1f4d90;
--hub-link-hover: #0d6efd;
--hub-shadow: 0 10px 22px rgba(30, 48, 84, 0.08);
background: linear-gradient(145deg, #f7f9fd 0%, #edf2f9 100%);
border: 1px solid var(--hub-border);
border-radius: 14px;
padding: 18px;
}
.report-hub-title {
color: #24334f;
font-weight: 700;
letter-spacing: 0.2px;
}
.report-hub-sub {
color: var(--hub-sub);
font-size: 0.92rem;
}
.report-card {
border: 1px solid var(--hub-border);
border-radius: 12px;
box-shadow: 0 2px 0 rgba(255, 255, 255, 0.9) inset;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
background: #fff;
}
.report-card:hover {
transform: translateY(-4px);
border-color: #cdd8ea;
box-shadow: var(--hub-shadow);
}
.report-card-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.report-card-title {
color: var(--hub-text);
font-size: 1.03rem;
font-weight: 600;
}
.report-icon {
width: 34px;
height: 34px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.report-icon-blue { background: #e7f0ff; color: #1f5dd0; }
.report-icon-cyan { background: #e3f9fa; color: #0f8f9a; }
.report-icon-green { background: #e7f9ef; color: #1d9960; }
.report-icon-indigo { background: #ece9ff; color: #4b49b6; }
.report-icon-slate { background: #eef1f6; color: #6b7280; }
.report-links {
list-style: none;
padding: 0;
margin: 0;
}
.report-links li + li {
margin-top: 6px;
}
.report-links a {
color: var(--hub-link);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 500;
line-height: 1.35;
}
.report-links a:hover {
color: var(--hub-link-hover);
}
.report-links i {
font-size: 1rem;
}
.report-card-muted {
background: #fcfdff;
}
.report-empty {
color: #7a8598;
font-size: 0.94rem;
padding-top: 2px;
}
@media (max-width: 767.98px) {
.report-hub {
padding: 14px;
}
}
</style>

View File

@ -0,0 +1,96 @@
<section class="content">
<div class="card">
<div class="card-header">
<h5 class="mb-0">NABH Audit Report</h5>
</div>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label">Date Range</label>
<div class="d-flex gap-2">
<input type="datetime-local" class="form-control" id="report_start">
<input type="datetime-local" class="form-control" id="report_end">
</div>
</div>
<div class="col-md-3">
<label class="form-label">Module</label>
<select class="form-control" id="module_filter">
<option value="all">All</option>
<option value="ipd">IPD Discharge</option>
<option value="opd">OPD Consult</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Compliance Status</label>
<select class="form-control" id="status_filter">
<option value="all">All</option>
<option value="critical-missing">Critical Missing</option>
<option value="compliant">Compliant</option>
</select>
</div>
<div class="col-md-2 d-grid gap-2">
<button type="button" class="btn btn-primary" id="show_report">Show</button>
<button type="button" class="btn btn-outline-primary" id="export_report">Export</button>
</div>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<div id="report_result" class="table-responsive">Select filters and click Show.</div>
</div>
</div>
</section>
<script>
(function() {
function pad(value) {
return value < 10 ? '0' + value : value;
}
function toInputValue(date) {
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
+ 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
function toRangeValue(value, fallback) {
if (!value) {
value = fallback;
}
if (value.length === 16) {
return value + ':00';
}
return value;
}
var now = new Date();
var start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
var end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 0);
document.getElementById('report_start').value = toInputValue(start);
document.getElementById('report_end').value = toInputValue(end);
function buildQuery() {
var startVal = toRangeValue(document.getElementById('report_start').value, toInputValue(start));
var endVal = toRangeValue(document.getElementById('report_end').value, toInputValue(end));
var dateRange = startVal + 'S' + endVal;
var module = document.getElementById('module_filter').value || 'all';
var status = document.getElementById('status_filter').value || 'all';
return '<?= base_url('Report/nabh_audit_report_data') ?>/'
+ encodeURIComponent(dateRange) + '/'
+ encodeURIComponent(module) + '/'
+ encodeURIComponent(status);
}
document.getElementById('show_report').addEventListener('click', function() {
load_form_div(buildQuery(), 'report_result');
});
document.getElementById('export_report').addEventListener('click', function() {
window.open(buildQuery() + '/1', '_blank');
});
})();
</script>

View File

@ -0,0 +1,97 @@
<?php
$rows = $rows ?? [];
$summary = $summary ?? [];
$minRange = $min_range ?? '';
$maxRange = $max_range ?? '';
$moduleFilter = $module_filter ?? 'all';
$statusFilter = $status_filter ?? 'all';
$overall = $summary['all'] ?? ['total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0];
$ipd = $summary['ipd'] ?? ['total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0];
$opd = $summary['opd'] ?? ['total' => 0, 'compliant' => 0, 'critical_missing' => 0, 'avg_completion' => 0];
?>
<div class="mb-3">
<p><strong>Date Range:</strong> <?= esc($minRange) ?> to <?= esc($maxRange) ?></p>
<p><strong>Module:</strong> <?= esc(strtoupper((string) $moduleFilter)) ?>, <strong>Status:</strong> <?= esc((string) $statusFilter) ?></p>
</div>
<div class="row g-2 mb-3">
<div class="col-md-4">
<div class="border rounded p-2">
<div><strong>Overall</strong></div>
<div>Total: <?= (int) ($overall['total'] ?? 0) ?></div>
<div>Compliant: <?= (int) ($overall['compliant'] ?? 0) ?></div>
<div>Critical Missing: <?= (int) ($overall['critical_missing'] ?? 0) ?></div>
<div>Avg Completion: <?= number_format((float) ($overall['avg_completion'] ?? 0), 1) ?>%</div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-2">
<div><strong>IPD Discharge</strong></div>
<div>Total: <?= (int) ($ipd['total'] ?? 0) ?></div>
<div>Compliant: <?= (int) ($ipd['compliant'] ?? 0) ?></div>
<div>Critical Missing: <?= (int) ($ipd['critical_missing'] ?? 0) ?></div>
<div>Avg Completion: <?= number_format((float) ($ipd['avg_completion'] ?? 0), 1) ?>%</div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded p-2">
<div><strong>OPD Consult</strong></div>
<div>Total: <?= (int) ($opd['total'] ?? 0) ?></div>
<div>Compliant: <?= (int) ($opd['compliant'] ?? 0) ?></div>
<div>Critical Missing: <?= (int) ($opd['critical_missing'] ?? 0) ?></div>
<div>Avg Completion: <?= number_format((float) ($opd['avg_completion'] ?? 0), 1) ?>%</div>
</div>
</div>
</div>
<?php if (empty($rows)) : ?>
<div class="alert alert-info">No audit data found for selected criteria.</div>
<?php else : ?>
<table class="table table-bordered table-sm">
<thead class="table-light">
<tr>
<th style="width: 50px;">#</th>
<th>Module</th>
<th>Encounter</th>
<th>Patient</th>
<th>Date Time</th>
<th class="text-end">Completion</th>
<th class="text-end">Critical Missing</th>
<th>Missing Critical Items</th>
</tr>
</thead>
<tbody>
<?php $index = 1; ?>
<?php foreach ($rows as $row) : ?>
<?php
$criticalMissingCount = (int) ($row['critical_missing_count'] ?? 0);
$rowClass = $criticalMissingCount > 0 ? 'table-warning' : 'table-success';
$encounterLabel = trim((string) ($row['encounter_code'] ?? ''));
if ($encounterLabel === '') {
$encounterLabel = '#' . (int) ($row['encounter_id'] ?? 0);
}
$patientLabel = trim((string) ($row['patient_name'] ?? ''));
$patientCode = trim((string) ($row['patient_code'] ?? ''));
if ($patientCode !== '') {
$patientLabel = $patientCode . ' - ' . $patientLabel;
}
?>
<tr class="<?= esc($rowClass) ?>">
<td><?= $index++ ?></td>
<td><?= esc((string) ($row['module_label'] ?? '')) ?></td>
<td><?= esc($encounterLabel) ?></td>
<td><?= esc($patientLabel) ?></td>
<td><?= esc((string) ($row['encounter_datetime'] ?? '')) ?></td>
<td class="text-end">
<?= (int) ($row['ok_count'] ?? 0) ?>/<?= (int) ($row['total_count'] ?? 0) ?>
(<?= number_format((float) ($row['completion_percent'] ?? 0), 1) ?>%)
</td>
<td class="text-end"><?= $criticalMissingCount ?></td>
<td><?= esc((string) ($row['critical_missing_labels'] ?? '-')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>

View File

@ -3,6 +3,117 @@
use CodeIgniter\Boot;
use Config\Paths;
/*
*---------------------------------------------------------------
* EMERGENCY FILESYSTEM PREPARE (PRE-BOOTSTRAP)
*---------------------------------------------------------------
* Usage: /?fs_prepare=1&key=YOUR_SETUP_KEY
* Runs before CodeIgniter boot so it works even when cache init fails.
*/
if (isset($_GET['fs_prepare']) && (string) $_GET['fs_prepare'] === '1') {
header('Content-Type: text/plain; charset=utf-8');
$projectRoot = dirname(__DIR__);
$envPath = $projectRoot . DIRECTORY_SEPARATOR . '.env';
$envKey = '';
if (is_readable($envPath)) {
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (is_array($lines)) {
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '' || strpos($line, '#') === 0) {
continue;
}
if (stripos($line, 'setup.dbToolsKey') !== 0) {
continue;
}
$parts = explode('=', $line, 2);
$envKey = trim((string) ($parts[1] ?? ''), " \t\n\r\0\x0B\"'");
break;
}
}
}
$requestKey = trim((string) ($_GET['key'] ?? ''));
if ($envKey === '') {
http_response_code(403);
echo "setup.dbToolsKey is not configured in .env\n";
exit(1);
}
if ($requestKey === '' || !hash_equals($envKey, $requestKey)) {
http_response_code(403);
echo "Invalid or missing key. Use /?fs_prepare=1&key=YOUR_KEY\n";
exit(1);
}
$paths = [
$projectRoot . DIRECTORY_SEPARATOR . 'writable',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'cache',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'logs',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'session',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'uploads',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'debugbar',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'tmp',
$projectRoot . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'uploads',
];
$created = [];
$writableOk = [];
$failed = [];
foreach ($paths as $path) {
if (!is_dir($path)) {
if (@mkdir($path, 0775, true)) {
$created[] = $path;
}
}
if (is_dir($path)) {
@chmod($path, 0775);
$probe = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.write_test_' . str_replace('.', '', uniqid('', true)) . '.tmp';
$written = @file_put_contents($probe, 'ok');
if ($written !== false) {
@unlink($probe);
$writableOk[] = $path;
} else {
$failed[] = $path;
}
} else {
$failed[] = $path;
}
}
echo "Filesystem preparation complete\n";
echo "Project root: {$projectRoot}\n";
echo "Created dirs: " . count($created) . "\n";
echo "Writable OK: " . count($writableOk) . "/" . count($paths) . "\n\n";
if (!empty($failed)) {
echo "FAILED paths (run on server shell):\n";
foreach (array_values(array_unique($failed)) as $item) {
echo " - {$item}\n";
}
$writablePath = $projectRoot . '/writable';
$uploadsPath = $projectRoot . '/public/uploads';
echo "\nCommands:\n";
echo "sudo chown -R www-data:www-data \"{$writablePath}\" \"{$uploadsPath}\"\n";
echo "sudo find \"{$writablePath}\" -type d -exec chmod 775 {} \\;\n";
echo "sudo find \"{$writablePath}\" -type f -exec chmod 664 {} \\;\n";
echo "sudo chmod -R 775 \"{$uploadsPath}\"\n";
} else {
echo "All required paths are writable.\n";
}
exit(0);
}
/*
*---------------------------------------------------------------
* CHECK PHP VERSION

119
public/setup_fs.php Normal file
View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=utf-8');
$projectRoot = dirname(__DIR__);
$envPath = $projectRoot . DIRECTORY_SEPARATOR . '.env';
$envKey = '';
if (is_readable($envPath)) {
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (is_array($lines)) {
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '' || strpos($line, '#') === 0) {
continue;
}
if (stripos($line, 'setup.dbToolsKey') !== 0) {
continue;
}
$parts = explode('=', $line, 2);
$envKey = trim((string) ($parts[1] ?? ''), " \t\n\r\0\x0B\"'");
break;
}
}
}
$requestKey = trim((string) ($_GET['key'] ?? ''));
if ($envKey === '') {
http_response_code(403);
echo "setup.dbToolsKey is not configured in .env\n";
exit(1);
}
if ($requestKey === '' || !hash_equals($envKey, $requestKey)) {
http_response_code(403);
echo "Invalid or missing key. Use: /setup_fs.php?key=YOUR_KEY\n";
exit(1);
}
$paths = [
$projectRoot . DIRECTORY_SEPARATOR . 'writable',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'cache',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'logs',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'session',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'uploads',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'debugbar',
$projectRoot . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR . 'tmp',
$projectRoot . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'uploads',
];
$created = [];
$chmodOk = [];
$writableOk = [];
$failed = [];
foreach ($paths as $path) {
if (!is_dir($path)) {
if (@mkdir($path, 0775, true)) {
$created[] = $path;
}
}
if (is_dir($path)) {
if (@chmod($path, 0775)) {
$chmodOk[] = $path;
}
$probe = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.write_test_' . str_replace('.', '', uniqid('', true)) . '.tmp';
$written = @file_put_contents($probe, 'ok');
if ($written !== false) {
@unlink($probe);
$writableOk[] = $path;
} else {
$failed[] = $path;
}
} else {
$failed[] = $path;
}
}
echo "Filesystem preparation complete\n";
echo "Project root: {$projectRoot}\n";
echo "Created dirs: " . count($created) . "\n";
echo "chmod attempted: " . count($chmodOk) . "\n";
echo "Writable OK: " . count($writableOk) . "/" . count($paths) . "\n\n";
if (!empty($created)) {
echo "Created:\n";
foreach ($created as $item) {
echo " - {$item}\n";
}
echo "\n";
}
if (!empty($failed)) {
echo "FAILED paths (need server shell fix):\n";
foreach (array_values(array_unique($failed)) as $item) {
echo " - {$item}\n";
}
$writablePath = $projectRoot . '/writable';
$uploadsPath = $projectRoot . '/public/uploads';
echo "\nRun these on server shell (example www-data):\n";
echo "sudo chown -R www-data:www-data \"{$writablePath}\" \"{$uploadsPath}\"\n";
echo "sudo find \"{$writablePath}\" -type d -exec chmod 775 {} \\;\n";
echo "sudo find \"{$writablePath}\" -type f -exec chmod 664 {} \\;\n";
echo "sudo chmod -R 775 \"{$uploadsPath}\"\n";
} else {
echo "All required paths are writable.\n";
}
echo "\nNext:\n";
echo "1) Open /setup/db-tools?key=... again\n";
echo "2) Delete this file after success: public/setup_fs.php\n";

2
spark
View File

@ -28,7 +28,7 @@ use Config\Paths;
*/
// Refuse to run when called from php-cgi
if (str_starts_with(PHP_SAPI, 'cgi')) {
if (strpos(PHP_SAPI, 'cgi') === 0) {
exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n");
}