fix(billing & pharmacy): fix IPD medical bill package classification, default empty qty, and doctor ajax selection

- IPD Billing: replace ambiguous 'In Package' switch with explicit 'Include in IPD Bill (Normal)' / 'In Package (Covered)' dropdown selector in Medical Bills and Package tabs
- IPD Billing: return recalculated summary metrics from credit-type endpoint and update dashboard cards in real-time
- Pharmacy: classify IPD bills using ipd_credit_type instead of group_invoice_id to correctly show Package: No for credit bills and add to Total Credit
- Pharmacy Billing: default item quantity to blank instead of 0 to allow direct typing without backspacing, with validation against 0/empty submissions
- Pharmacy Invoice: add outside doctor/consulting doctor selection and auto-save via AJAX
This commit is contained in:
DevSoftTech Server 2026-10-25 14:15:42 +05:30
parent 6cc34057b5
commit 51c06304d0
7 changed files with 437 additions and 88 deletions

View File

@ -2318,12 +2318,22 @@ class Ipd extends BaseController
}
$this->recalculateIpdBillingSafely($ipdId, 'medical-bill.credit-type');
$updatedIpd = $this->db->table('ipd_master')->where('id', $ipdId)->get()->getRow();
return $this->response->setJSON([
'ok' => true,
'invoice_id' => $invoiceId,
'ipd_id' => $ipdId,
'ipd_credit_type' => $creditType,
'summary' => [
'charge_amount' => number_format((float) ($updatedIpd->charge_amount ?? 0), 2),
'med_amount' => number_format((float) ($updatedIpd->med_amount ?? 0), 2),
'net_amount' => number_format((float) ($updatedIpd->net_amount ?? 0), 2),
'total_paid_amount' => number_format((float) ($updatedIpd->total_paid_amount ?? 0), 2),
'balance_amount' => number_format((float) ($updatedIpd->balance_amount ?? 0), 2),
'cash_med_amount' => number_format((float) ($updatedIpd->cash_med_amount ?? 0), 2),
'med_paid' => number_format((float) ($updatedIpd->med_paid ?? 0), 2),
],
]);
}

View File

@ -1590,11 +1590,26 @@ class Medical extends BaseController
return $this->invoiceUpdateResponse(false, 'Not Done');
}
if (! $this->db->tableExists('invoice_med_master')) {
return $this->invoiceUpdateResponse(false, 'Invoice table missing');
}
$invoice = $this->db->table('invoice_med_master')->where('id', $invoiceId)->get()->getRow();
if (! $invoice) {
return $this->invoiceUpdateResponse(false, 'Invoice not found');
}
if ($this->isInvoiceFinalized($invoice)) {
return $this->invoiceUpdateResponse(false, 'Invoice is finalized');
}
if ($docId > 0 && $this->db->tableExists('doctor_master')) {
$doc = $this->db->table('doctor_master')->where('id', $docId)->get()->getRow();
if ($doc) {
$docName = (string) ($doc->p_fname ?? $docName);
$docName = trim((string) ($doc->p_fname ?? $docName));
}
} elseif ($docId < 0) {
$docId = 0;
}
$ok = $this->updateInvoiceMasterByFields($invoiceId, [
@ -2329,8 +2344,8 @@ class Medical extends BaseController
$legacyBalanceTotal += $balance;
$ipdCredit = (int) ($row->ipd_credit ?? 0);
$groupInvoiceId = (int) ($row->group_invoice_id ?? 0);
if ($ipdCredit > 0 && $groupInvoiceId > 0) {
$ipdCreditType = (int) ($row->ipd_credit_type ?? 1);
if ($ipdCredit > 0 && $ipdCreditType === 0) {
$packageTotal += $net;
} elseif ($ipdCredit > 0) {
$creditTotal += $net;
@ -4075,6 +4090,33 @@ class Medical extends BaseController
}
}
$docList = [];
if ($this->db->tableExists('doctor_master')) {
$docBuilder = $this->db->table('doctor_master');
$docFields = $this->db->getFieldNames('doctor_master') ?? [];
if (in_array('active', $docFields, true)) {
$docBuilder->where('active', 1);
}
$docList = $docBuilder->orderBy('p_fname', 'ASC')->get()->getResult();
$invDocId = (int) ($invoice->doc_id ?? 0);
if ($invDocId > 0) {
$found = false;
foreach ($docList as $d) {
if ((int) ($d->id ?? 0) === $invDocId) {
$found = true;
break;
}
}
if (! $found) {
$docRow = $this->db->table('doctor_master')->where('id', $invDocId)->get()->getRow();
if ($docRow) {
$docList[] = $docRow;
}
}
}
}
return [
'invoice' => $invoice,
'items' => $items,
@ -4083,7 +4125,8 @@ class Medical extends BaseController
'totals' => $totals,
'oldInvoices' => $oldInvoices,
'isFinalized' => $this->isInvoiceFinalized($invoice),
'message' => (string) $this->request->getGet('msg'),
'message' => (string) ($this->request ? $this->request->getGet('msg') : ''),
'docList' => $docList,
];
}
@ -10131,11 +10174,11 @@ class Medical extends BaseController
ELSE 0
END) AS ipd_cash_amount,
SUM(CASE
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.group_invoice_id,0)=0 THEN " . $netExpr . "
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.ipd_credit_type,1)=1 THEN " . $netExpr . "
ELSE 0
END) AS ipd_credit_amount,
SUM(CASE
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.group_invoice_id,0)>0 THEN " . $netExpr . "
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.ipd_credit_type,1)=0 THEN " . $netExpr . "
ELSE 0
END) AS ipd_package_amount
FROM invoice_med_master m
@ -10173,10 +10216,10 @@ class Medical extends BaseController
$billCondition = ' AND IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=0';
break;
case 4:
$billCondition = ' AND IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.group_invoice_id,0)=0';
$billCondition = ' AND IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.ipd_credit_type,1)=1';
break;
case 5:
$billCondition = ' AND IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.group_invoice_id,0)>0';
$billCondition = ' AND IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.ipd_credit_type,1)=0';
break;
case 6:
$billCondition = ' AND IFNULL(m.sale_return,0)=1';
@ -10220,8 +10263,8 @@ class Medical extends BaseController
WHEN IFNULL(m.ipd_id,0)=0 AND IFNULL(m.case_id,0)=0 AND IFNULL(m.case_credit,0)=0 THEN 'OPD CASH'
WHEN IFNULL(m.ipd_id,0)=0 AND IFNULL(m.case_id,0)>0 THEN 'OPD ORG'
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=0 THEN 'IPD CASH'
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.group_invoice_id,0)=0 THEN 'IPD CREDIT'
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.group_invoice_id,0)>0 THEN 'IPD PACKAGE'
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.ipd_credit_type,1)=1 THEN 'IPD CREDIT'
WHEN IFNULL(m.ipd_id,0)>0 AND IFNULL(m.ipd_credit,0)=1 AND IFNULL(m.ipd_credit_type,1)=0 THEN 'IPD PACKAGE'
ELSE 'OTHER'
END AS bill_type,
" . $orgNameExpr . " AS org_name

View File

@ -26,18 +26,19 @@ $csrfHash = csrf_hash();
<td><?= esc($row->inv_date ?? '') ?></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"
<?php $includeInBill = (int) ($row->ipd_credit_type ?? 1) === 1; ?>
<div class="d-flex align-items-center gap-2">
<select
class="form-select form-select-sm"
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 ?>">
style="width: auto; min-width: 210px; font-weight: 600;"
onchange="ipdMedicalBillsChangeType(this, <?= (int) $row->id ?>)">
<option value="1" <?= $includeInBill ? 'selected' : '' ?>>✓ Include in IPD Bill (Normal)</option>
<option value="0" <?= ! $includeInBill ? 'selected' : '' ?>>📦 In Package (Covered)</option>
</select>
<span id="badge-bill-type-<?= (int) $row->id ?>" class="badge <?= $includeInBill ? 'bg-success-subtle text-success border border-success' : 'bg-warning-subtle text-warning border border-warning' ?>">
<?= $includeInBill ? 'Include in Bill' : 'In Package' ?>
</label>
</span>
</div>
</td>
<td>
@ -65,33 +66,104 @@ $csrfHash = csrf_hash();
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 + '"]');
function ipdMedicalBillsChangeType(select, invoiceId) {
var val = parseInt(select.value, 10);
var badge = document.getElementById('badge-bill-type-' + invoiceId);
var payload = {
ipd_id: ipdMedicalBillsIpdId,
ipd_credit_type: checkbox.checked ? 1 : 0
ipd_credit_type: val
};
payload[ipdMedicalBillsCsrfName] = ipdMedicalBillsCsrfHash;
checkbox.disabled = true;
select.disabled = true;
if (badge) {
badge.className = 'badge bg-secondary-subtle text-secondary border border-secondary';
badge.textContent = 'Updating...';
}
$.post(ipdMedicalBillsUpdateUrl + '/' + invoiceId, payload, function(resp) {
if (!resp || !resp.ok) {
checkbox.checked = !checkbox.checked;
select.value = val === 1 ? '0' : '1';
alert((resp && resp.error) ? resp.error : 'Unable to update medical bill type.');
return;
}
label.textContent = checkbox.checked ? 'Include in Bill' : 'In Package';
var isIncluded = val === 1;
if (badge) {
badge.className = isIncluded
? 'badge bg-success-subtle text-success border border-success'
: 'badge bg-warning-subtle text-warning border border-warning';
badge.textContent = isIncluded ? 'Include in Bill' : 'In Package';
}
// Real-time update top dashboard summary numbers
if (resp.summary) {
ipdMedicalBillsUpdateDashboardCards(resp.summary);
}
// Invalidate Bill Details tab so it loads fresh calculated data when clicked
var billPanel = document.getElementById('tab_bill_content');
if (billPanel) {
billPanel.innerHTML = 'Loading...';
}
if (typeof window.notify === 'function') {
window.notify('success', 'Medical Bill', isIncluded ? 'Bill marked as Include in IPD Bill' : 'Bill marked as In Package');
}
}).fail(function(xhr) {
checkbox.checked = !checkbox.checked;
select.value = val === 1 ? '0' : '1';
var message = xhr.responseJSON && xhr.responseJSON.error
? xhr.responseJSON.error
: 'Unable to update medical bill type.';
alert(message);
}).always(function() {
checkbox.disabled = false;
select.disabled = false;
});
}
function ipdMedicalBillsUpdateDashboardCards(s) {
var orangeCard = document.querySelector('.ipd-summary-card.orange');
if (orangeCard) {
var spans = orangeCard.querySelectorAll('span');
spans.forEach(function(sp) {
var txt = sp.textContent || '';
if (txt.indexOf('Charges') >= 0 && s.charge_amount !== undefined) {
sp.textContent = 'Charges : ' + s.charge_amount;
} else if (txt.indexOf('Pharmacy Cr. IPD') >= 0 && s.med_amount !== undefined) {
sp.textContent = 'Pharmacy Cr. IPD : ' + s.med_amount;
} else if (txt.indexOf('Net Amount') >= 0 && s.net_amount !== undefined) {
sp.textContent = 'Net Amount : ' + s.net_amount;
}
});
}
var greenCard = document.querySelector('.ipd-summary-card.green');
if (greenCard) {
var gSpans = greenCard.querySelectorAll('span');
gSpans.forEach(function(sp) {
var txt = sp.textContent || '';
if (txt.indexOf('Total Paid') >= 0 && s.total_paid_amount !== undefined) {
sp.textContent = 'Total Paid : ' + s.total_paid_amount;
} else if (txt.indexOf('Balance') >= 0 && s.balance_amount !== undefined) {
sp.textContent = 'Balance : ' + s.balance_amount;
}
});
}
var blueCard = document.querySelector('.ipd-summary-card.blue');
if (blueCard) {
var bSpans = blueCard.querySelectorAll('span');
bSpans.forEach(function(sp) {
var txt = sp.textContent || '';
if (txt.indexOf('Pharmacy Bill') >= 0 && s.cash_med_amount !== undefined) {
sp.textContent = 'Pharmacy Bill : ' + s.cash_med_amount;
} else if (txt.indexOf('Paid Amount') >= 0 && s.med_paid !== undefined) {
sp.textContent = 'Paid Amount : ' + s.med_paid;
}
});
}
}
function ipdMedicalBillsShowInvoice(button, invoiceId) {
var details = $('#ipd-medical-bill-details');
button.disabled = true;

View File

@ -324,7 +324,7 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? [];
<div class="card border-top border-3 border-info mt-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Pharmacy Bills Package Mapping</strong>
<span class="small text-muted">Unchecked = In Package, Checked = Include in Bill</span>
<span class="small text-muted">Select whether pharmacy bill is covered In Package or billed to IPD</span>
</div>
<div class="card-body">
<div class="table-responsive">
@ -335,14 +335,14 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? [];
<th>Invoice Code</th>
<th>Date</th>
<th class="text-end">Amount</th>
<th class="text-center">Include in Bill</th>
<th class="text-center">Bill Type</th>
</tr>
</thead>
<tbody>
<?php if (! empty($medical_bills)) : ?>
<?php $medicalSrNo = 1; ?>
<?php foreach ($medical_bills as $medicalBill) : ?>
<?php $includeInBillChecked = (int) ($medicalBill->ipd_credit_type ?? 1) === 1 ? 'checked' : ''; ?>
<?php $includeInBill = (int) ($medicalBill->ipd_credit_type ?? 1) === 1; ?>
<tr>
<td><?= $medicalSrNo++ ?></td>
<td>
@ -356,14 +356,10 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? [];
<td><?= esc((string) ($medicalBill->inv_date ?? '')) ?></td>
<td class="text-end"><?= esc(number_format((float) ($medicalBill->net_amount ?? 0), 2)) ?></td>
<td class="text-center">
<div class="form-check d-inline-flex justify-content-center mb-0">
<input
class="form-check-input"
type="checkbox"
onchange="ipdMedicalBillToggleType(this, <?= (int) ($medicalBill->id ?? 0) ?>)"
<?= $includeInBillChecked ?>
/>
</div>
<select class="form-select form-select-sm d-inline-block" style="width: auto; min-width: 210px; font-weight: 600;" onchange="ipdPackageMedicalBillChangeType(this, <?= (int) ($medicalBill->id ?? 0) ?>)">
<option value="1" <?= $includeInBill ? 'selected' : '' ?>>✓ Include in IPD Bill (Normal)</option>
<option value="0" <?= ! $includeInBill ? 'selected' : '' ?>>📦 In Package (Covered)</option>
</select>
</td>
</tr>
<?php endforeach; ?>
@ -539,23 +535,31 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? [];
});
}
function ipdMedicalBillToggleType(checkbox, invoiceId) {
function ipdPackageMedicalBillChangeType(select, invoiceId) {
var val = parseInt(select.value, 10);
var payload = {};
payload[ipdPackageCsrfName] = ipdPackageCsrfHash;
payload.ipd_id = ipdPackageIpdId;
payload.ipd_credit_type = checkbox.checked ? 1 : 0;
payload.ipd_credit_type = val;
select.disabled = true;
$.post(ipdMedicalBillCreditTypeUrlBase + '/' + invoiceId, payload, function(resp) {
if (!resp || !resp.ok) {
checkbox.checked = !checkbox.checked;
select.value = val === 1 ? '0' : '1';
alert((resp && resp.error) ? resp.error : 'Unable to update pharmacy bill option.');
return;
}
// Update dashboard summary cards if function exists
if (resp.summary && typeof ipdMedicalBillsUpdateDashboardCards === 'function') {
ipdMedicalBillsUpdateDashboardCards(resp.summary);
}
ipdPackageReload();
}).fail(function(xhr) {
checkbox.checked = !checkbox.checked;
select.value = val === 1 ? '0' : '1';
var msg = (xhr && xhr.responseJSON && xhr.responseJSON.error) ? xhr.responseJSON.error : 'Unable to update pharmacy bill option.';
alert(msg);
}).always(function() {
select.disabled = false;
});
}

View File

@ -36,18 +36,66 @@
</div>
<?php if (empty($isFinalized)): ?>
<form method="post" action="<?= base_url('Medical/go_final') ?>" class="row g-2 mb-3">
<?= csrf_field() ?>
<input type="hidden" name="inv_id" value="<?= (int)($invoice->id ?? 0) ?>">
<div class="col-md-2">
<label class="form-label form-label-sm mb-1">Doctor ID</label>
<input type="number" name="doc_id" value="<?= esc($invoice->doc_id ?? 0) ?>" class="form-control form-control-sm" min="0" step="1">
<?php
$currentDocId = (int) ($invoice->doc_id ?? 0);
$currentDocName = trim((string) ($invoice->doc_name ?? ''));
$docFound = false;
if (!empty($docList) && $currentDocId > 0) {
foreach ($docList as $d) {
if ((int) ($d->id ?? 0) === $currentDocId) {
$docFound = true;
break;
}
}
}
// If not found in hospital doctors list but doctor name exists, treat as outside doctor
$isOtherDoc = (!$docFound && ($currentDocId === 0 || $currentDocId === 999999) && $currentDocName !== '');
?>
<div class="row g-2 mb-3 align-items-end" id="invoice-doctor-panel">
<input type="hidden" name="doc_id" id="hidden_doc_id" value="<?= $currentDocId ?>">
<input type="hidden" name="doc_name" id="hidden_doc_name" value="<?= esc($currentDocName) ?>">
<div class="col-md-4 col-sm-6">
<label class="form-label form-label-sm mb-1 fw-bold text-secondary">
<i class="bi bi-person-badge"></i> Doctor
</label>
<select id="invoice_doc_select" class="form-select form-select-sm">
<option value="">-- Select Doctor --</option>
<?php foreach (($docList ?? []) as $doc): ?>
<?php
$dId = (int) ($doc->id ?? 0);
$dName = trim((string) ($doc->p_fname ?? ('Doctor #' . $dId)));
$isSelected = (!$isOtherDoc && $currentDocId === $dId);
?>
<option value="<?= $dId ?>" data-name="<?= esc($dName) ?>" <?= $isSelected ? 'selected' : '' ?>>
<?= esc($dName) ?>
</option>
<?php endforeach; ?>
<option value="other" <?= $isOtherDoc ? 'selected' : '' ?>>Outside Doctor / Other</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label form-label-sm mb-1">Doctor Name</label>
<input type="text" name="doc_name" value="<?= esc($invoice->doc_name ?? '') ?>" class="form-control form-control-sm">
<div class="col-md-4 col-sm-6" id="wrap_doc_other" style="<?= $isOtherDoc ? '' : 'display: none;' ?>">
<label class="form-label form-label-sm mb-1 fw-bold text-secondary">
<i class="bi bi-pencil-square"></i> Outside Doctor Name
</label>
<div class="input-group input-group-sm">
<input type="text" id="input_doc_other" class="form-control form-control-sm" placeholder="Enter Doctor Name" value="<?= $isOtherDoc ? esc($currentDocName) : '' ?>">
<button type="button" id="btn_save_doc_other" class="btn btn-outline-primary btn-sm" title="Save Doctor">Save</button>
</div>
</div>
</form>
<div class="col-auto">
<span id="doc_save_indicator" class="badge bg-success-subtle text-success border border-success-subtle py-1 px-2 d-none">
<i class="bi bi-check-circle-fill"></i> Saved
</span>
</div>
</div>
<?php else: ?>
<div class="small mb-2 text-muted">
<strong>Doctor :</strong> <span class="badge bg-light text-dark border"><?= esc($invoice->doc_name ?: 'Not Specified') ?></span>
</div>
<?php endif; ?>
<div id="show_item_list" class="table-responsive mb-3">
@ -210,11 +258,11 @@
</div>
<div class="col-md-4">
<label class="form-label form-label-sm mb-1">Qty</label>
<input class="form-control form-control-sm" name="qty" id="input_product_qty" placeholder="Qty Like No. of Tab." type="number" min="0" step="1" value="0" autocomplete="off">
<input class="form-control form-control-sm" name="qty" id="input_product_qty" placeholder="Qty" type="number" min="1" step="1" value="" autocomplete="off" required>
</div>
<div class="col-md-3">
<label class="form-label form-label-sm mb-1">Disc %</label>
<input class="form-control form-control-sm" name="disc_per" id="input_disc" placeholder="Discount %" type="number" min="0" max="100" $11" value="0" autocomplete="off">
<input class="form-control form-control-sm" name="disc_per" id="input_disc" placeholder="Discount %" type="number" min="0" max="100" value="0" autocomplete="off">
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-primary w-100" type="submit" id="additem" disabled>Add</button>
@ -250,7 +298,7 @@
<input type="hidden" name="inv_id" value="<?= (int)($invoice->id ?? 0) ?>">
<input type="hidden" name="stock_id" value="<?= (int)($row->stock_id ?? 0) ?>">
<input type="number" name="qty" value="1" min="1" step="1" class="form-control form-control-sm" style="width:80px;">
<input type="number" name="disc_per" value="0" min="0" max="100" $11" class="form-control form-control-sm" style="width:90px;" title="Disc %">
<input type="number" name="disc_per" value="0" min="0" max="100" class="form-control form-control-sm" style="width:90px;" title="Disc %">
<button class="btn btn-sm btn-primary" type="submit">Add</button>
</form>
</td>
@ -545,6 +593,7 @@
addBtn.disabled = !item.l_ss_no;
}
if (qtyInput) {
qtyInput.value = '';
qtyInput.focus();
}
}
@ -737,11 +786,13 @@
}
var qtyInput = document.getElementById('input_product_qty');
var qtyValue = parseFloat((qtyInput && qtyInput.value) ? qtyInput.value : '0');
if (!qtyValue || qtyValue <= 0) {
var rawQty = (qtyInput && qtyInput.value) ? qtyInput.value.trim() : '';
var qtyValue = parseFloat(rawQty || '0');
if (!rawQty || isNaN(qtyValue) || qtyValue <= 0) {
event.preventDefault();
alert('Enter valid Qty greater than 0.');
alert('Please enter valid Qty greater than 0.');
if (qtyInput) {
qtyInput.value = '';
qtyInput.focus();
}
return;
@ -774,5 +825,129 @@
if (addBtn) {
addBtn.accessKey = 'i';
}
// Doctor Select & Outside Doctor AJAX save handler
(function () {
var docSelect = document.getElementById('invoice_doc_select');
var wrapOther = document.getElementById('wrap_doc_other');
var inputOther = document.getElementById('input_doc_other');
var btnSaveOther = document.getElementById('btn_save_doc_other');
var hiddenDocId = document.getElementById('hidden_doc_id');
var hiddenDocName = document.getElementById('hidden_doc_name');
var saveIndicator = document.getElementById('doc_save_indicator');
var invId = <?= (int) ($invoice->id ?? 0) ?>;
if (!docSelect || !invId) {
return;
}
function showSaveStatus(text, isError) {
if (!saveIndicator) return;
saveIndicator.classList.remove('d-none', 'bg-success-subtle', 'text-success', 'border-success-subtle', 'bg-danger-subtle', 'text-danger', 'border-danger-subtle');
if (isError) {
saveIndicator.classList.add('bg-danger-subtle', 'text-danger', 'border-danger-subtle');
saveIndicator.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + (text || 'Error');
} else {
saveIndicator.classList.add('bg-success-subtle', 'text-success', 'border-success-subtle');
saveIndicator.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + (text || 'Saved');
}
setTimeout(function () {
if (saveIndicator) saveIndicator.classList.add('d-none');
}, 2500);
}
function updateDoctorAjax(docId, docName, onSuccess) {
var csrfInput = document.querySelector('input[name="<?= csrf_token() ?>"]');
var csrfHash = csrfInput ? csrfInput.value : '';
var body = new URLSearchParams();
body.append('<?= csrf_token() ?>', csrfHash);
body.append('med_invoice_id', String(invId));
body.append('doc_id', String(docId));
body.append('doc_name', String(docName || ''));
fetch('<?= base_url('Medical/update_doctor') ?>', {
method: 'POST',
body: body,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(function (res) {
return res.json();
}).then(function (res) {
if (res && res.csrfHash) {
document.querySelectorAll('input[name="<?= csrf_token() ?>"]').forEach(function (inp) {
inp.value = res.csrfHash;
});
}
if (res && res.status) {
if (hiddenDocId) hiddenDocId.value = docId;
if (hiddenDocName) hiddenDocName.value = docName;
showSaveStatus(docId === 0 ? 'Outside Doctor Saved' : 'Doctor Updated', false);
if (typeof onSuccess === 'function') onSuccess();
} else {
showSaveStatus((res && res.remark) ? res.remark : 'Save Failed', true);
}
}).catch(function () {
showSaveStatus('Network Error', true);
});
}
docSelect.addEventListener('change', function () {
var val = docSelect.value;
if (val === 'other') {
if (wrapOther) {
wrapOther.style.display = '';
}
if (inputOther) {
inputOther.focus();
if (inputOther.value.trim() !== '') {
updateDoctorAjax(0, inputOther.value.trim());
}
}
} else if (val === '') {
if (wrapOther) {
wrapOther.style.display = 'none';
}
updateDoctorAjax(0, '');
} else {
if (wrapOther) {
wrapOther.style.display = 'none';
}
var opt = docSelect.options[docSelect.selectedIndex];
var docName = opt ? (opt.getAttribute('data-name') || opt.text.trim()) : '';
updateDoctorAjax(parseInt(val, 10), docName);
}
});
function saveOtherDoctor() {
if (!inputOther) return;
var otherName = inputOther.value.trim();
if (!otherName) {
inputOther.focus();
showSaveStatus('Enter Doctor Name', true);
return;
}
updateDoctorAjax(0, otherName);
}
if (btnSaveOther) {
btnSaveOther.addEventListener('click', saveOtherDoctor);
}
if (inputOther) {
inputOther.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
saveOtherDoctor();
}
});
inputOther.addEventListener('blur', function () {
if (docSelect.value === 'other' && inputOther.value.trim() !== '') {
saveOtherDoctor();
}
});
}
})();
})();
</script>

View File

@ -164,23 +164,51 @@
</div>
<div class="card mb-0">
<div class="card-body d-flex flex-wrap gap-2">
<div class="flex-grow-1">
<label class="form-label">Doctor</label>
<select class="form-select" id="doc-select">
<option value="">Select</option>
<?php foreach (($docList ?? []) as $doc): ?>
<option value="<?= esc($doc->id ?? '') ?>" data-name="<?= esc($doc->p_fname ?? '') ?>" <?= ((int)($invoice->doc_id ?? 0) === (int)($doc->id ?? 0)) ? 'selected' : '' ?>>
<?= esc($doc->p_fname ?? ('Doctor #' . ($doc->id ?? ''))) ?>
</option>
<?php endforeach; ?>
</select>
<input type="hidden" name="doc_id" id="doc_id" value="<?= (int)($invoice->doc_id ?? 0) ?>">
<input type="hidden" name="doc_name" id="doc_name" value="<?= esc($invoice->doc_name ?? '') ?>">
</div>
<div class="d-flex align-items-end gap-2">
<button type="submit" class="btn btn-success">Update Invoice Header</button>
<a class="btn btn-outline-secondary" href="javascript:load_form_div('<?= base_url('Medical/invoice_edit/' . (int)($invoice->id ?? 0)) ?>','medical-main');">Cancel</a>
<div class="card-body">
<?php
$masterDocId = (int) ($invoice->doc_id ?? 0);
$masterDocName = trim((string) ($invoice->doc_name ?? ''));
$masterDocFound = false;
if (!empty($docList) && $masterDocId > 0) {
foreach ($docList as $d) {
if ((int) ($d->id ?? 0) === $masterDocId) {
$masterDocFound = true;
break;
}
}
}
$masterIsOtherDoc = (!$masterDocFound && ($masterDocId === 0 || $masterDocId === 999999) && $masterDocName !== '');
?>
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">Doctor</label>
<select class="form-select" id="doc-select">
<option value="">-- Select Doctor --</option>
<?php foreach (($docList ?? []) as $doc): ?>
<?php
$dId = (int) ($doc->id ?? 0);
$dName = trim((string) ($doc->p_fname ?? ('Doctor #' . $dId)));
$isSelected = (!$masterIsOtherDoc && $masterDocId === $dId);
?>
<option value="<?= $dId ?>" data-name="<?= esc($dName) ?>" <?= $isSelected ? 'selected' : '' ?>>
<?= esc($dName) ?>
</option>
<?php endforeach; ?>
<option value="other" <?= $masterIsOtherDoc ? 'selected' : '' ?>>Outside Doctor / Other</option>
</select>
<input type="hidden" name="doc_id" id="doc_id" value="<?= $masterDocId ?>">
<input type="hidden" name="doc_name" id="doc_name" value="<?= esc($masterDocName) ?>">
</div>
<div class="col-md-4" id="wrap_master_doc_other" style="<?= $masterIsOtherDoc ? '' : 'display: none;' ?>">
<label class="form-label">Outside Doctor Name</label>
<input type="text" class="form-control" id="master_doc_other_input" placeholder="Enter Doctor Name" value="<?= $masterIsOtherDoc ? esc($masterDocName) : '' ?>">
</div>
<div class="col-md-4 d-flex align-items-end gap-2">
<button type="submit" class="btn btn-success">Update Invoice Header</button>
<a class="btn btn-outline-secondary" href="javascript:load_form_div('<?= base_url('Medical/invoice_edit/' . (int)($invoice->id ?? 0)) ?>','medical-main');">Cancel</a>
</div>
</div>
</div>
</div>
@ -419,20 +447,37 @@
(function () {
var select = document.getElementById('doc-select');
var wrapOther = document.getElementById('wrap_master_doc_other');
var otherInput = document.getElementById('master_doc_other_input');
var docIdInput = document.getElementById('doc_id');
var docNameInput = document.getElementById('doc_name');
if (!select) {
return;
}
select.addEventListener('change', function () {
var option = select.options[select.selectedIndex];
var docIdInput = document.getElementById('doc_id');
var docNameInput = document.getElementById('doc_name');
if (docIdInput) {
docIdInput.value = option ? (option.value || 0) : 0;
function syncDoctor() {
var val = select.value;
if (val === 'other') {
if (wrapOther) wrapOther.style.display = '';
if (docIdInput) docIdInput.value = 0;
if (docNameInput && otherInput) docNameInput.value = otherInput.value.trim();
if (otherInput) otherInput.focus();
} else {
if (wrapOther) wrapOther.style.display = 'none';
var option = select.options[select.selectedIndex];
if (docIdInput) docIdInput.value = option ? (option.value || 0) : 0;
if (docNameInput) docNameInput.value = option ? (option.getAttribute('data-name') || '') : '';
}
if (docNameInput) {
docNameInput.value = option ? (option.getAttribute('data-name') || '') : '';
}
});
}
select.addEventListener('change', syncDoctor);
if (otherInput) {
otherInput.addEventListener('input', function () {
if (select.value === 'other' && docNameInput) {
docNameInput.value = otherInput.value.trim();
if (docIdInput) docIdInput.value = 0;
}
});
}
})();
</script>

View File

@ -96,9 +96,9 @@
<?php
$invoiceId = (int) ($row->id ?? 0);
$ipdCredit = (int)($row->ipd_credit ?? 0);
$groupInvoiceId = (int)($row->group_invoice_id ?? 0);
$ipdCreditType = (int)($row->ipd_credit_type ?? 1);
$creditText = ($ipdCredit > 0) ? 'Credit' : 'CASH';
$packageText = ($ipdCredit > 0 && $groupInvoiceId > 0) ? 'Yes' : 'No';
$packageText = ($ipdCredit > 0 && $ipdCreditType === 0) ? 'Yes' : 'No';
$editMeta = $invoiceEditMeta[$invoiceId] ?? ['can_edit' => true, 'reason' => ''];
$canEditRow = !empty($editMeta['can_edit']);
$editReason = (string) ($editMeta['reason'] ?? 'Invoice is view-only.');