diff --git a/app/Controllers/Billing/Ipd.php b/app/Controllers/Billing/Ipd.php index 0ebd3eb..1aaa9af 100644 --- a/app/Controllers/Billing/Ipd.php +++ b/app/Controllers/Billing/Ipd.php @@ -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), + ], ]); } diff --git a/app/Controllers/Medical.php b/app/Controllers/Medical.php index 0aabc26..f544c9b 100644 --- a/app/Controllers/Medical.php +++ b/app/Controllers/Medical.php @@ -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 diff --git a/app/Views/billing/ipd/panel_medical_bills.php b/app/Views/billing/ipd/panel_medical_bills.php index 23d5d05..194c0fe 100644 --- a/app/Views/billing/ipd/panel_medical_bills.php +++ b/app/Views/billing/ipd/panel_medical_bills.php @@ -26,18 +26,19 @@ $csrfHash = csrf_hash(); inv_date ?? '') ?> net_amount ?? 0), 2) ?> - ipd_credit_type ?? 0) === 1; ?> -
- ipd_credit_type ?? 1) === 1; ?> +
+ + - +
@@ -65,33 +66,104 @@ $csrfHash = csrf_hash(); var ipdMedicalBillsUpdateUrl = ''; var ipdMedicalBillsDetailsUrl = ''; - 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; diff --git a/app/Views/billing/ipd/panel_package.php b/app/Views/billing/ipd/panel_package.php index 5768cb1..2b61377 100644 --- a/app/Views/billing/ipd/panel_package.php +++ b/app/Views/billing/ipd/panel_package.php @@ -324,7 +324,7 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? [];
Pharmacy Bills Package Mapping - Unchecked = In Package, Checked = Include in Bill + Select whether pharmacy bill is covered In Package or billed to IPD
@@ -335,14 +335,14 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? []; Invoice Code Date Amount - Include in Bill + Bill Type - ipd_credit_type ?? 1) === 1 ? 'checked' : ''; ?> + ipd_credit_type ?? 1) === 1; ?> @@ -356,14 +356,10 @@ $ayushmanChecklistItems = $ayushman_checklist_items ?? []; inv_date ?? '')) ?> net_amount ?? 0), 2)) ?> -
- - /> -
+ @@ -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; }); } diff --git a/app/Views/medical/invoice_edit.php b/app/Views/medical/invoice_edit.php index c75e62c..2e9c4b3 100644 --- a/app/Views/medical/invoice_edit.php +++ b/app/Views/medical/invoice_edit.php @@ -36,18 +36,66 @@
-
- - -
- - + 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 !== ''); + ?> +
+ + + +
+ +
-
- - + +
+ +
+ + +
- + +
+ + Saved + +
+
+ +
+ Doctor : doc_name ?: 'Not Specified') ?> +
@@ -210,11 +258,11 @@
- +
- +
@@ -250,7 +298,7 @@ - + @@ -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 = 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 = ' ' + (text || 'Error'); + } else { + saveIndicator.classList.add('bg-success-subtle', 'text-success', 'border-success-subtle'); + saveIndicator.innerHTML = ' ' + (text || 'Saved'); + } + setTimeout(function () { + if (saveIndicator) saveIndicator.classList.add('d-none'); + }, 2500); + } + + function updateDoctorAjax(docId, docName, onSuccess) { + var csrfInput = document.querySelector('input[name=""]'); + var csrfHash = csrfInput ? csrfInput.value : ''; + + var body = new URLSearchParams(); + body.append('', csrfHash); + body.append('med_invoice_id', String(invId)); + body.append('doc_id', String(docId)); + body.append('doc_name', String(docName || '')); + + fetch('', { + 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=""]').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(); + } + }); + } + })(); })(); diff --git a/app/Views/medical/invoice_master_edit.php b/app/Views/medical/invoice_master_edit.php index 214b2dd..b372397 100644 --- a/app/Views/medical/invoice_master_edit.php +++ b/app/Views/medical/invoice_master_edit.php @@ -164,23 +164,51 @@
-
-
- - - - -
-
- - Cancel +
+ 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 !== ''); + ?> +
+
+ + + + +
+ +
+ + +
+ +
+ + Cancel +
@@ -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; + } + }); + } })(); diff --git a/app/Views/medical/medical_invoice_list.php b/app/Views/medical/medical_invoice_list.php index 66fa65e..e4e559a 100644 --- a/app/Views/medical/medical_invoice_list.php +++ b/app/Views/medical/medical_invoice_list.php @@ -96,9 +96,9 @@ 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.');