Add IPD medical bill type controls

Filter IPD pharmacy credit bills, add package/include toggles with admission validation, and provide an inline invoice detail view.
This commit is contained in:
DevSoftTech Server 2026-08-08 08:33:53 +05:30
parent 561c0d9fc3
commit ee87802af1
6 changed files with 207 additions and 13 deletions

View File

@ -459,6 +459,7 @@ $routes->group('billing', function($routes) {
$routes->post('ipd/nursing-charge/delete/(:num)/(:num)', 'Billing\\Ipd::deleteNursingCharge/$1/$2');
$routes->post('ipd/charge/package/(:num)', 'Billing\\Ipd::updateIpdChargePackage/$1');
$routes->post('ipd/package/add/(:num)', 'Billing\\Ipd::addIpdPackage/$1');
$routes->get('ipd/medical-bill/(:num)/(:num)', 'Billing\\Ipd::medicalBillDetails/$1/$2');
$routes->post('ipd/medical-bill/credit-type/(:num)', 'Billing\\Ipd::updateMedicalBillCreditType/$1');
$routes->get('ipd/panel/(:num)/ayushman/search', 'Billing\Ipd::searchAyushmanPackages/$1');
$routes->post('ipd/panel/(:num)/ayushman/map/(:num)', 'Billing\Ipd::saveAyushmanPackageMapping/$1/$2');

View File

@ -1976,6 +1976,28 @@ class Ipd extends BaseController
return $this->response->setJSON(['ok' => $result === 1]);
}
public function medicalBillDetails(int $ipdId, int $invoiceId)
{
$permission = $this->requireAnyPermission([
'billing.access',
'billing.ipd.invoice',
]);
if ($permission) {
return $permission;
}
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setBody('Invalid request');
}
$details = $this->ipdModel->getMedicalBillDetails($ipdId, $invoiceId);
if (empty($details)) {
return $this->response->setStatusCode(404)->setBody('Medical bill not found for this admission.');
}
return view('billing/ipd/panel_medical_bill_details', $details);
}
public function updateMedicalBillCreditType(int $invoiceId)
{
$permission = $this->requireAnyPermission([
@ -1999,14 +2021,21 @@ class Ipd extends BaseController
return $this->response->setStatusCode(422)->setJSON(['ok' => false, 'error' => 'Medical bill credit type column not available']);
}
$ipdId = (int) ($this->request->getPost('ipd_id') ?? 0);
if ($ipdId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['ok' => false, 'error' => 'IPD admission is required']);
}
$invoice = $this->db->table('invoice_med_master')
->select('id,ipd_id')
->select('id,ipd_id,ipd_credit')
->where('id', $invoiceId)
->where('ipd_id', $ipdId)
->where('ipd_credit', 1)
->get(1)
->getRowArray();
if (empty($invoice)) {
return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'Medical bill not found']);
return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'IPD credit bill not found for this admission']);
}
$creditType = (int) ($this->request->getPost('ipd_credit_type') ?? 0) === 1 ? 1 : 0;
@ -2014,25 +2043,30 @@ class Ipd extends BaseController
'ipd_credit_type' => $creditType,
];
if (in_array('ipd_credit', $fields, true)) {
$update['ipd_credit'] = 1;
}
if (in_array('ipd_credit_type_update', $fields, true)) {
$user = function_exists('auth') ? auth()->user() : null;
$userId = (int) ($user->id ?? 0);
$userName = (string) ($user->username ?? $user->email ?? 'user');
$update['ipd_credit_type_update'] = $userName . '[' . $userId . '] D:' . date('d-m-Y h:i:s');
$typeLabel = $creditType === 1 ? 'Include in Bill' : 'In Package';
$update['ipd_credit_type_update'] = $typeLabel . ' by ' . $userName . '[' . $userId . '] D:' . date('d-m-Y H:i:s');
}
$this->db->table('invoice_med_master')
$updated = $this->db->table('invoice_med_master')
->where('id', $invoiceId)
->where('ipd_id', $ipdId)
->where('ipd_credit', 1)
->update($update);
if (! $updated) {
return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => 'Unable to update medical bill type']);
}
$this->recalculateIpdBillingSafely($ipdId, 'medical-bill.credit-type');
return $this->response->setJSON([
'ok' => true,
'invoice_id' => $invoiceId,
'ipd_id' => (int) ($invoice['ipd_id'] ?? 0),
'ipd_id' => $ipdId,
'ipd_credit_type' => $creditType,
]);
}

View File

@ -677,12 +677,37 @@ class IpdBillingModel extends Model
public function getMedicalBills(int $ipdId): array
{
return $this->db->table('invoice_med_master')
->where('ipd_id >', 0)
->where('ipd_id', $ipdId)
->where('ipd_credit', 1)
->orderBy('id', 'DESC')
->get()
->getResult();
}
public function getMedicalBillDetails(int $ipdId, int $invoiceId): array
{
$invoice = $this->db->table('invoice_med_master')
->where('id', $invoiceId)
->where('ipd_id', $ipdId)
->where('ipd_credit', 1)
->get(1)
->getRow();
if ($invoice === null) {
return [];
}
return [
'invoice' => $invoice,
'items' => $this->db->table('inv_med_item')
->where('inv_med_id', $invoiceId)
->orderBy('id', 'ASC')
->get()
->getResult(),
];
}
public function getBillDetails(int $ipdId): array
{
return $this->db->table('ipd_master')

View File

@ -0,0 +1,56 @@
<?php
/** @var object $invoice */
/** @var array<int, object> $items */
$invoiceCode = (string) ($invoice->inv_med_code ?? $invoice->id ?? '');
?>
<div class="card mb-0">
<div class="card-header d-flex align-items-center justify-content-between">
<strong>Invoice ID: <?= esc($invoiceCode) ?></strong>
<a href="<?= site_url('Medical/invoice_print/' . (int) $invoice->id) ?>" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-printer"></i> Print
</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped mb-0 align-middle">
<thead>
<tr>
<th>#</th>
<th>Item Name</th>
<th class="text-end">Qty.</th>
<th class="text-end">Price</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<?php if (! empty($items)) : ?>
<?php foreach ($items as $index => $item) : ?>
<?php
$itemName = trim((string) ($item->item_Name ?? $item->item_name ?? ''));
$formulation = trim((string) ($item->formulation ?? ''));
?>
<tr>
<td><?= $index + 1 ?></td>
<td><?= esc($itemName . ($formulation !== '' ? ' [' . $formulation . ']' : '')) ?></td>
<td class="text-end"><?= esc($item->qty ?? '') ?></td>
<td class="text-end"><?= number_format((float) ($item->price ?? 0), 2) ?></td>
<td class="text-end"><?= number_format((float) ($item->tamount ?? 0), 2) ?></td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="5" class="text-center text-muted">No invoice items found.</td>
</tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr>
<th colspan="4" class="text-end">Total</th>
<th class="text-end"><?= number_format((float) ($invoice->net_amount ?? 0), 2) ?></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>

View File

@ -1,3 +1,9 @@
<?php
$ipdId = (int) ($ipd_info->id ?? 0);
$csrfName = csrf_token();
$csrfHash = csrf_hash();
?>
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead>
@ -6,7 +12,8 @@
<th>Invoice Code</th>
<th>Date</th>
<th class="text-end">Amount</th>
<th>Status</th>
<th>Bill Type</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@ -17,15 +24,85 @@
<td><?= $srNo++ ?></td>
<td><?= esc($row->inv_med_code ?? $row->id ?? '') ?></td>
<td><?= esc($row->inv_date ?? '') ?></td>
<td class="text-end"><?= esc($row->net_amount ?? '') ?></td>
<td><?= ((int) ($row->payment_status ?? 0) > 0) ? 'Paid' : 'Pending' ?></td>
<td class="text-end"><?= number_format((float) ($row->net_amount ?? 0), 2) ?></td>
<td>
<?php $includeInBill = (int) ($row->ipd_credit_type ?? 0) === 1; ?>
<div class="form-check form-switch mb-0">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="medical-bill-type-<?= (int) $row->id ?>"
<?= $includeInBill ? 'checked' : '' ?>
onchange="ipdMedicalBillsToggleType(this, <?= (int) $row->id ?>)">
<label class="form-check-label" for="medical-bill-type-<?= (int) $row->id ?>">
<?= $includeInBill ? 'Include in Bill' : 'In Package' ?>
</label>
</div>
</td>
<td>
<button type="button" class="btn btn-sm btn-primary" onclick="ipdMedicalBillsShowInvoice(this, <?= (int) $row->id ?>)">
Show Invoice
</button>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="5" class="text-center text-muted">No medical bills found.</td>
<td colspan="6" class="text-center text-muted">No IPD credit medical bills found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div id="ipd-medical-bill-details" class="mt-3"></div>
<script>
var ipdMedicalBillsIpdId = <?= $ipdId ?>;
var ipdMedicalBillsCsrfName = '<?= esc($csrfName) ?>';
var ipdMedicalBillsCsrfHash = '<?= esc($csrfHash) ?>';
var ipdMedicalBillsUpdateUrl = '<?= site_url('billing/ipd/medical-bill/credit-type') ?>';
var ipdMedicalBillsDetailsUrl = '<?= site_url('billing/ipd/medical-bill') ?>';
function ipdMedicalBillsToggleType(checkbox, invoiceId) {
var label = document.querySelector('label[for="' + checkbox.id + '"]');
var payload = {
ipd_id: ipdMedicalBillsIpdId,
ipd_credit_type: checkbox.checked ? 1 : 0
};
payload[ipdMedicalBillsCsrfName] = ipdMedicalBillsCsrfHash;
checkbox.disabled = true;
$.post(ipdMedicalBillsUpdateUrl + '/' + invoiceId, payload, function(resp) {
if (!resp || !resp.ok) {
checkbox.checked = !checkbox.checked;
alert((resp && resp.error) ? resp.error : 'Unable to update medical bill type.');
return;
}
label.textContent = checkbox.checked ? 'Include in Bill' : 'In Package';
}).fail(function(xhr) {
checkbox.checked = !checkbox.checked;
var message = xhr.responseJSON && xhr.responseJSON.error
? xhr.responseJSON.error
: 'Unable to update medical bill type.';
alert(message);
}).always(function() {
checkbox.disabled = false;
});
}
function ipdMedicalBillsShowInvoice(button, invoiceId) {
var details = $('#ipd-medical-bill-details');
button.disabled = true;
details.html('<div class="text-muted p-3">Loading invoice...</div>');
$.get(ipdMedicalBillsDetailsUrl + '/' + ipdMedicalBillsIpdId + '/' + invoiceId, function(html) {
details.html(html);
}).fail(function(xhr) {
details.html('<div class="alert alert-danger">' + (xhr.responseText || 'Unable to load invoice details.') + '</div>');
}).always(function() {
button.disabled = false;
});
}
</script>

View File

@ -542,6 +542,7 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? [];
function ipdMedicalBillToggleType(checkbox, invoiceId) {
var payload = {};
payload[ipdPackageCsrfName] = ipdPackageCsrfHash;
payload.ipd_id = ipdPackageIpdId;
payload.ipd_credit_type = checkbox.checked ? 1 : 0;
$.post(ipdMedicalBillCreditTypeUrlBase + '/' + invoiceId, payload, function(resp) {