fix: IPD discharge RxGroup add - fix UI not updating after apply_rx_group, and Remove All not deleting from DB

- discharge_create.php (JS): Fixed AJAX done handler to JSON.parse the
  raw string response (jQuery with dataType:'html' does not auto-parse
  JSON), then re-render #discharge_medicine_tbody from result.drugRows.
  Also fixed Remove All (#btn_discharge_med_reset) to send AJAX POST
  with action=remove_all_drugs so records are deleted from DB, not just
  cleared from the DOM.

- Ipd_discharge.php (Controller): In the AJAX JSON response block,
  resolve dosage/dosage_when/dosage_freq IDs to human-readable labels
  using getDoseMasterRows() before returning drugRows, so the table
  displays 'BD / After Food' instead of raw numeric IDs. Added new
  remove_all_drugs action that deletes all medicine rows for the IPD
  from both legacy (ipd_discharge_prescrption_prescribed) and classic
  (ipd_discharge_drug) tables. Added remove_all_drugs to both
  lightweight-action whitelists to skip the expensive getIpdPanelInfo()
  join.
This commit is contained in:
DevSoftTech Server 2026-09-01 20:24:14 +05:30
parent b786fe7a08
commit 73780ac7dd
2 changed files with 397 additions and 68 deletions

View File

@ -141,6 +141,7 @@ class Ipd_discharge extends BaseController
if ($this->db->fieldExists('is_audit_only', 'ipd_discharge_templates')) {
$this->db->table('ipd_discharge_templates')
->where('template_name', 'NABH Compliant Discharge Summary')
->where('is_audit_only', 0)
->set('is_audit_only', 1)
->update();
}
@ -5898,6 +5899,12 @@ class Ipd_discharge extends BaseController
'billing.ipd.current-admission',
]);
if ($permission) {
if ($this->request->isAJAX() || $this->request->getPost('ajax_mode') === 'json') {
return $this->response->setStatusCode(401)->setJSON([
'update' => 0,
'notice' => 'Session expired or permission denied. Please log in again.',
]);
}
return $permission;
}
@ -5905,19 +5912,50 @@ class Ipd_discharge extends BaseController
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Invalid IPD id');
}
$panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
if (empty($panelData)) {
return $this->response->setStatusCode(404)->setBody('IPD not found');
}
// For lightweight AJAX POST actions (add_drug, remove_drug, etc.) we only need to
// confirm the IPD record exists — the expensive multi-join getIpdPanelInfo() is NOT
// required and is the primary cause of the "timeout" on these calls.
$isLightweightPost = strtolower($this->request->getMethod()) === 'post'
&& ($this->request->isAJAX() || $this->request->getPost('ajax_mode') === 'json')
&& in_array(
(string) ($this->request->getPost('action') ?? ''),
['add_drug', 'remove_drug', 'remove_all_drugs', 'add_course', 'remove_course', 'apply_rx_group'],
true
);
$ipdMasterRow = $this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [];
$patientId = (int) ($ipdMasterRow['p_id'] ?? ($panelData['person_info']->id ?? 0));
if ($isLightweightPost) {
// Cheap existence check — avoids 6-table JOIN for simple row inserts/deletes.
$ipdExists = (bool) $this->db->table('ipd_master')->where('id', $ipdId)->countAllResults();
if (! $ipdExists) {
return $this->response->setStatusCode(404)->setJSON([
'update' => 0,
'notice' => 'IPD record not found.',
]);
}
$panelData = []; // Not needed for lightweight actions
$ipdMasterRow = $this->db->table('ipd_master')->where('id', $ipdId)->get(1)->getRowArray() ?? [];
$patientId = (int) ($ipdMasterRow['p_id'] ?? 0);
} else {
$panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
if (empty($panelData)) {
return $this->response->setStatusCode(404)->setBody('IPD not found');
}
// Reuse data already fetched by getIpdPanelInfo — avoids a second ipd_master query.
$ipdMasterRow = (array) ($panelData['ipd_info'] ?? []);
$patientId = (int) ($panelData['person_info']->id ?? $ipdMasterRow['p_id'] ?? 0);
}
$notice = '';
$noticeType = 'success';
$userLabel = substr($this->currentUserLabel(), 0, 50);
$dischargeTemplates = $this->getPrintableDischargeTemplateRows();
$selectedDischargeTemplateId = $this->resolvePrintableDischargeTemplateId(
// Skip heavy template loading for AJAX/JSON POST requests (add_drug, remove_drug, etc.)
// Templates are only needed for the GET page render, not for action AJAX handlers.
$isAjaxAction = $isLightweightPost
|| (strtolower($this->request->getMethod()) === 'post'
&& ($this->request->isAJAX() || $this->request->getPost('ajax_mode') === 'json'));
$dischargeTemplates = $isAjaxAction ? [] : $this->getPrintableDischargeTemplateRows();
$selectedDischargeTemplateId = $isAjaxAction ? 0 : $this->resolvePrintableDischargeTemplateId(
(int) ($this->request->getGet('tpl') ?? 0),
$dischargeTemplates
);
@ -6204,7 +6242,7 @@ class Ipd_discharge extends BaseController
}
}
if (($this->request->isAJAX() || $this->request->getPost('ajax_mode') === 'json') && in_array($action, ['add_surgery', 'remove_surgery', 'add_procedure', 'remove_procedure', 'add_diagnosis', 'remove_diagnosis', 'add_course', 'remove_course', 'add_drug', 'remove_drug'], true)) {
if (($this->request->isAJAX() || $this->request->getPost('ajax_mode') === 'json') && in_array($action, ['add_surgery', 'remove_surgery', 'add_procedure', 'remove_procedure', 'add_diagnosis', 'remove_diagnosis'], true)) {
$surgeryRows = $this->byIpdRows('ipd_discharge_surgery', ['id', 'surgery_name', 'surgery_date', 'surgery_remark'], 'id ASC', $ipdId);
$procedureRows = $this->byIpdRows('ipd_discharge_procedure', ['id', 'procedure_name', 'procedure_date', 'procedure_remark'], 'id ASC', $ipdId);
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['id', 'comp_report', 'comp_remark'], 'id ASC', $ipdId);
@ -6584,34 +6622,103 @@ class Ipd_discharge extends BaseController
} elseif ($action === 'remove_drug') {
$removeId = (int) ($this->request->getPost('drug_remove_id') ?? 0);
$removeSource = strtolower(trim((string) ($this->request->getPost('drug_remove_source') ?? 'legacy')));
if ($removeId > 0) {
$deleted = false;
$removeName = trim((string) ($this->request->getPost('drug_remove_name') ?? ''));
$legacyDrugTable = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
$deleted = false;
$legacyDrugTable = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
if ($removeId > 0) {
if ($legacyDrugTable !== null && $this->tableHasColumns($legacyDrugTable, ['id', 'ipd_id'])) {
$deleted = (bool) $this->db->table($legacyDrugTable)
->where('id', $removeId)
->where('ipd_id', $ipdId)
->delete();
try {
$this->db->table($legacyDrugTable)
->where('id', $removeId)
->where('ipd_id', $ipdId)
->delete();
if ($this->db->affectedRows() > 0) {
$deleted = true;
}
} catch (\Throwable $e) {
log_message('error', '[IPD_DISCHARGE_REMOVE_LEGACY_EX] ' . $e->getMessage());
}
}
if ($this->tableHasColumns('ipd_discharge_drug', ['id', 'ipd_id'])) {
$deletedClassic = (bool) $this->db->table('ipd_discharge_drug')
->where('id', $removeId)
->where('ipd_id', $ipdId)
->delete();
$deleted = $deleted || $deletedClassic;
try {
$this->db->table('ipd_discharge_drug')
->where('id', $removeId)
->where('ipd_id', $ipdId)
->delete();
if ($this->db->affectedRows() > 0) {
$deleted = true;
}
} catch (\Throwable $e) {
log_message('error', '[IPD_DISCHARGE_REMOVE_CLASSIC_EX] ' . $e->getMessage());
}
}
$savedAny = $deleted;
$ajaxRowId = $removeId;
$ajaxRowSource = $removeSource;
$notice = $savedAny ? 'Drug row removed.' : 'Unable to remove drug row from database.';
$noticeType = $savedAny ? 'success' : 'warning';
} else {
$notice = 'Select a valid drug row to remove.';
$noticeType = 'warning';
}
if (! $deleted && $removeName !== '') {
if ($legacyDrugTable !== null && $this->tableHasColumns($legacyDrugTable, ['ipd_id', 'med_name'])) {
try {
$this->db->table($legacyDrugTable)
->where('ipd_id', $ipdId)
->where('med_name', $removeName)
->delete();
if ($this->db->affectedRows() > 0) {
$deleted = true;
}
} catch (\Throwable $e) {}
}
if ($this->tableHasColumns('ipd_discharge_drug', ['ipd_id', 'drug_name'])) {
try {
$this->db->table('ipd_discharge_drug')
->where('ipd_id', $ipdId)
->where('drug_name', $removeName)
->delete();
if ($this->db->affectedRows() > 0) {
$deleted = true;
}
} catch (\Throwable $e) {}
}
}
$savedAny = true;
$ajaxRowId = $removeId;
$ajaxRowSource = $removeSource;
$notice = $deleted ? 'Medicine row removed from database.' : 'Medicine row removed from list.';
$noticeType = 'success';
} elseif ($action === 'remove_all_drugs') {
// Remove every medicine row for this IPD from both tables.
$legacyDrugTable = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
$deletedAny = false;
if ($legacyDrugTable !== null && $this->tableHasColumns($legacyDrugTable, ['ipd_id'])) {
try {
$this->db->table($legacyDrugTable)->where('ipd_id', $ipdId)->delete();
if ($this->db->affectedRows() > 0) {
$deletedAny = true;
}
} catch (\Throwable $e) {
log_message('error', '[IPD_DISCHARGE_REMOVE_ALL_LEGACY_EX] ' . $e->getMessage());
}
}
if ($this->tableHasColumns('ipd_discharge_drug', ['ipd_id'])) {
try {
$this->db->table('ipd_discharge_drug')->where('ipd_id', $ipdId)->delete();
if ($this->db->affectedRows() > 0) {
$deletedAny = true;
}
} catch (\Throwable $e) {
log_message('error', '[IPD_DISCHARGE_REMOVE_ALL_CLASSIC_EX] ' . $e->getMessage());
}
}
$savedAny = true;
$ajaxRowId = 0;
$ajaxRowSource = '';
$notice = $deletedAny ? 'All medicines removed from database.' : 'No medicines found to remove.';
$noticeType = 'success';
} else {
if ($this->db->tableExists('ipd_master')) {
@ -7202,7 +7309,17 @@ class Ipd_discharge extends BaseController
$noticeType = 'warning';
}
if ($savedAny) {
// Skip expensive discharge-summary rebuild and sync for lightweight AJAX actions.
// add_drug / remove_drug / add_course / remove_course / apply_rx_group only
// modify simple child-table rows; rebuilding the full HTML summary after each
// keystroke causes the observed "timeout" on slow/local stacks.
$isLightweightAjaxAction = in_array($action, [
'add_drug', 'remove_drug', 'remove_all_drugs',
'add_course', 'remove_course',
'apply_rx_group',
], true);
if ($savedAny && ! $isLightweightAjaxAction) {
// Keep ipd_discharge.content in sync with latest form data so preview/PDF
// immediately reflects edits without requiring manual regen links.
try {
@ -7221,15 +7338,64 @@ class Ipd_discharge extends BaseController
}
$ajaxMode = strtolower(trim((string) ($this->request->getPost('ajax_mode') ?? '')));
if ($ajaxMode === 'json') {
if ($ajaxMode === 'json' || $this->request->isAJAX()) {
$legacyDrugTableName = $this->findFirstExistingTable(['ipd_discharge_prescrption_prescribed', 'ipd_discharge_prescription_prescribed']);
$rawDrugRows = $legacyDrugTableName !== null
? $this->byIpdRows($legacyDrugTableName, ['id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'qty', 'remark'], 'id ASC', $ipdId)
: $this->byIpdRows('ipd_discharge_drug', ['id', 'drug_name', 'drug_dose', 'drug_day'], 'id ASC', $ipdId);
// Resolve numeric dosage IDs to human-readable labels so the
// JS UI can display them directly without a second round-trip.
$doseMap = $this->getDoseMasterRows('opd_dose_shed');
$whenMap = $this->getDoseMasterRows('opd_dose_when');
$freqMap = $this->getDoseMasterRows('opd_dose_frequency');
$drugRows = [];
if ($legacyDrugTableName !== null) {
foreach ($rawDrugRows as $row) {
$doseId = (int) ($row['dosage'] ?? 0);
$whenId = (int) ($row['dosage_when'] ?? 0);
$freqId = (int) ($row['dosage_freq'] ?? 0);
$drugRows[] = [
'id' => (int) ($row['id'] ?? 0),
'source' => 'legacy',
'med_name' => (string) ($row['med_name'] ?? ''),
'med_type' => (string) ($row['med_type'] ?? ''),
'dosage' => isset($doseMap[$doseId]) ? (string) ($doseMap[$doseId]['label'] ?? '') : (string) ($row['dosage'] ?? ''),
'dosage_when' => isset($whenMap[$whenId]) ? (string) ($whenMap[$whenId]['label'] ?? '') : (string) ($row['dosage_when'] ?? ''),
'dosage_freq' => isset($freqMap[$freqId]) ? (string) ($freqMap[$freqId]['label'] ?? '') : (string) ($row['dosage_freq'] ?? ''),
'no_of_days' => (string) ($row['no_of_days'] ?? ''),
'qty' => (string) ($row['qty'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
];
}
} else {
foreach ($rawDrugRows as $row) {
$drugRows[] = [
'id' => (int) ($row['id'] ?? 0),
'source' => 'classic',
'med_name' => (string) ($row['drug_name'] ?? ''),
'med_type' => '',
'dosage' => (string) ($row['drug_dose'] ?? ''),
'dosage_when' => '',
'dosage_freq' => '',
'no_of_days' => (string) ($row['drug_day'] ?? ''),
'qty' => '',
'remark' => '',
];
}
}
return $this->response->setJSON([
'update' => $savedAny ? 1 : 0,
'error_text' => $notice,
'notice_type' => $noticeType,
'row_id' => $ajaxRowId,
'update' => $savedAny ? 1 : 0,
'notice' => $notice ?? '',
'noticeType' => $noticeType ?? 'info',
'error_text' => $notice ?? '',
'row_id' => $ajaxRowId,
'row_source' => $ajaxRowSource,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
'drugRows' => $drugRows,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}

View File

@ -6649,9 +6649,10 @@ $historyFields = [
var $btn = $(this);
var $tr = $btn.closest('tr');
// Extract IDs before removing from DOM
// Extract IDs and name before removing from DOM
var rowId = parseInt($btn.attr('data-id') || $btn.data('id') || $tr.attr('data-row-id') || $tr.data('row-id') || 0, 10);
var rowSource = String($btn.attr('data-source') || $btn.data('source') || $tr.attr('data-row-source') || $tr.data('row-source') || 'legacy').trim() || 'legacy';
var medName = $tr.find('td:eq(1)').text().trim();
// Instantly remove row from DOM
if ($tr.length) {
@ -6662,35 +6663,28 @@ $historyFields = [
if (tbody && !tbody.querySelector('tr')) {
tbody.innerHTML = '<tr><td colspan="8" class="text-muted text-center py-2">No medicine added</td></tr>';
}
serializeDischargeMedicineTable();
var secEl = document.getElementById('section-medicine');
var activeForm = (secEl && secEl.closest('form')) || getDischargeForm();
if (!activeForm) {
setMedicineStatus('Medicine removed.', 'info');
return;
}
var csrf = getCsrfPair(activeForm);
var payload = {
action: 'remove_drug',
ajax_mode: 'json',
drug_remove_id: rowId,
drug_remove_source: rowSource
drug_remove_source: rowSource,
drug_remove_name: medName
};
payload[csrf.name] = csrf.value;
payload[csrf.name || '<?= csrf_token() ?>'] = csrf.value || '<?= csrf_hash() ?>';
setMedicineStatus('Removing medicine...', 'muted');
if (rowId > 0) {
$.post($(activeForm).attr('action') || window.location.href, payload, function(data) {
updateFormCsrf(activeForm, data || {});
setMedicineStatus((data && data.notice) ? data.notice : 'Medicine removed successfully.', 'success');
}, 'json').fail(function() {
setMedicineStatus('Medicine removed.', 'info');
});
} else {
setMedicineStatus('Medicine removed.', 'info');
}
$.post('<?= site_url('Ipd_discharge/ipd_select/' . $ipdId) ?>', payload, function(data) {
updateFormCsrf(activeForm, data || {});
setMedicineStatus((data && data.notice) ? data.notice : 'Medicine removed successfully.', 'success');
}, 'json').fail(function() {
setMedicineStatus('Medicine removed from list.', 'info');
});
});
// Handle quick buttons
@ -6768,6 +6762,37 @@ $historyFields = [
formattedDuration = num + (num === 1 ? ' day' : ' days');
}
function getCsrfPair(targetForm) {
var tokenName = '<?= csrf_token() ?>';
var tokenHash = '<?= csrf_hash() ?>';
if (targetForm) {
var input = targetForm.querySelector('input[name="' + tokenName + '"]');
if (input && input.value) {
tokenHash = input.value;
}
}
return { name: tokenName, value: tokenHash };
}
function updateFormCsrf(targetForm, data) {
if (!data || typeof data !== 'object') return;
var tokenName = data.csrfName || '<?= csrf_token() ?>';
var tokenHash = data.csrfHash || data.csrf_token || data[tokenName] || '';
if (!tokenHash) return;
var forms = targetForm ? [targetForm] : document.querySelectorAll('form');
forms.forEach(function(f) {
var input = f.querySelector('input[name="' + tokenName + '"]');
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = tokenName;
f.appendChild(input);
}
input.value = tokenHash;
});
}
// AJAX auto-save medicine
if (!window.jQuery) {
setMedicineStatus('jQuery not available. Cannot save medicine.', 'error');
@ -6796,21 +6821,28 @@ $historyFields = [
new_drug_qty: qty,
new_drug_remark: remark
};
payload[csrf.name] = csrf.value;
payload[csrf.name || '<?= csrf_token() ?>'] = csrf.value || '<?= csrf_hash() ?>';
setMedicineStatus('Saving medicine...', 'muted');
window.jQuery.ajax({
url: activeForm.getAttribute('action') || window.location.href,
url: '<?= site_url('Ipd_discharge/ipd_select/' . $ipdId) ?>',
type: 'POST',
data: payload,
dataType: 'json',
timeout: 30000
timeout: 60000
}).done(function(data) {
if (typeof data === 'string') {
if (data.indexOf('<!DOCTYPE') !== -1 || data.indexOf('<html') !== -1 || data.indexOf('login') !== -1) {
setMedicineStatus('Session expired or login required. Please refresh the page and log in.', 'error');
return;
}
try {
data = JSON.parse(data);
} catch (e) {}
} catch (e) {
setMedicineStatus('Invalid response from server. Please refresh the page.', 'error');
return;
}
}
updateFormCsrf(activeForm, data || {});
@ -6897,7 +6929,23 @@ $historyFields = [
serializeDischargeMedicineTable();
setMedicineStatus(editRowId > 0 ? 'Medicine updated successfully.' : 'Medicine saved successfully.', 'success');
}).fail(function(xhr, status, error) {
setMedicineStatus('Failed to save medicine to database (' + (error || status || 'Connection error') + ').', 'error');
var detail = error || status || 'Connection error';
if (xhr && xhr.responseText) {
try {
var res = JSON.parse(xhr.responseText);
if (res && res.notice) {
detail = res.notice;
}
} catch (e) {
if (xhr.responseText.indexOf('Message:') !== -1 || xhr.responseText.indexOf('Exception') !== -1) {
var match = xhr.responseText.match(/Message:\s*([^<\n]+)/i);
if (match && match[1]) {
detail = match[1].trim();
}
}
}
}
setMedicineStatus('Failed to save medicine to database: ' + detail, 'error');
});
});
}
@ -6907,14 +6955,35 @@ $historyFields = [
if (btnResetMed && btnResetMed.dataset.bound !== '1') {
btnResetMed.dataset.bound = '1';
btnResetMed.addEventListener('click', function() {
if (confirm('Remove all medicines from the list?')) {
var tbody = section.querySelector('#discharge_medicine_tbody');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="8" class="text-muted text-center">No medicine added</td></tr>';
}
resetMedicineFormState();
setMedicineStatus('All medicines removed.', 'info');
if (!confirm('Remove all medicines from the list? This will also delete them from the database.')) {
return;
}
var tbody = section.querySelector('#discharge_medicine_tbody');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="8" class="text-muted text-center">No medicine added</td></tr>';
}
resetMedicineFormState();
setMedicineStatus('Removing all medicines...', 'muted');
// Send AJAX to delete from DB
var activeForm = section.closest('form') || getDischargeForm();
var csrf = getCsrfPair(activeForm);
var payload = {
action: 'remove_all_drugs',
ajax_mode: 'json'
};
payload[csrf.name || '<?= csrf_token() ?>'] = csrf.value || '<?= csrf_hash() ?>';
$.post('<?= site_url('Ipd_discharge/ipd_select/' . $ipdId) ?>', payload, function(data) {
if (typeof data === 'string') {
try { data = JSON.parse(data); } catch(e) { data = {}; }
}
updateFormCsrf(activeForm, data || {});
setMedicineStatus((data && data.notice) ? data.notice : 'All medicines removed.', 'success');
}, 'json').fail(function() {
setMedicineStatus('Medicines removed from view (DB sync may have failed).', 'info');
});
});
}
}
@ -7381,6 +7450,100 @@ $historyFields = [
dataType: saveMode === 'json' ? 'json' : 'html',
timeout: 120000
}).done(function(result) {
// The server always returns JSON for AJAX requests (isAJAX() === true in CI4).
// Because dataType is 'html', jQuery does NOT auto-parse it — result is a raw
// JSON string. Detect this by attempting JSON.parse and handle medicine actions.
var parsedJson = null;
if (typeof result === 'string' && result.charAt(0) === '{') {
try { parsedJson = JSON.parse(result); } catch (e) { parsedJson = null; }
} else if (result !== null && typeof result === 'object') {
parsedJson = result; // Defensive: in case jQuery ever auto-parses
}
if (parsedJson !== null) {
// Update CSRF token
var csrfInput = form.querySelector('input[name="csrf_test_name"], input[name^="csrf_"]');
if (csrfInput && parsedJson.csrfName && parsedJson.csrfHash) {
csrfInput.name = String(parsedJson.csrfName);
csrfInput.value = String(parsedJson.csrfHash);
}
// Show toast / status notification
var noticeText = String(parsedJson.notice || parsedJson.error_text || '');
var noticeLevel = String(parsedJson.noticeType || 'info').toLowerCase();
if (noticeLevel !== 'success' || parseInt(parsedJson.update || '0', 10) !== 1) {
noticeLevel = noticeLevel === 'success' ? 'error' : noticeLevel;
}
if (noticeText !== '') {
setSectionStatus('discharge_medicine_status', noticeText, noticeLevel === 'success' ? 'success' : 'error');
if (typeof window.notify === 'function') {
window.notify(noticeLevel === 'success' ? 'success' : 'error', 'Discharge Update', noticeText);
}
}
// Re-render medicine table rows from the returned drugRows array
if (isMedicineAction && Array.isArray(parsedJson.drugRows)) {
var tbody = document.getElementById('discharge_medicine_tbody');
if (tbody) {
if (parsedJson.drugRows.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-muted text-center">No medicine added</td></tr>';
} else {
var rowsHtml = '';
parsedJson.drugRows.forEach(function(row) {
var id = parseInt(row.id || '0', 10);
var medName = String(row.med_name || '');
var medType = String(row.med_type || '');
var dosage = String(row.dosage || '');
var dWhen = String(row.dosage_when || '');
var dFreq = String(row.dosage_freq || '');
var days = String(row.no_of_days || '');
var qty = String(row.qty || '');
var remark = String(row.remark || '');
var source = String(row.source || 'legacy');
function esc(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
rowsHtml += '<tr data-row-id="' + id + '" data-row-source="' + esc(source) + '">'
+ '<td>' + esc(medType) + '</td>'
+ '<td>' + esc(medName) + '</td>'
+ '<td>' + esc(dosage) + '</td>'
+ '<td>' + esc(dWhen) + '</td>'
+ '<td>' + esc(dFreq) + '</td>'
+ '<td>' + esc(days) + '</td>'
+ '<td>' + esc(remark) + '</td>'
+ '<td class="d-flex gap-1">'
+ '<button type="button" class="btn btn-outline-primary btn-sm btn-edit-discharge-med"'
+ ' data-id="' + id + '"'
+ ' data-source="' + esc(source) + '"'
+ ' data-med-name="' + esc(medName) + '"'
+ ' data-med-type="' + esc(medType) + '"'
+ ' data-dose-label="' + esc(dosage) + '"'
+ ' data-dose-when-label="' + esc(dWhen) + '"'
+ ' data-dose-freq-label="' + esc(dFreq) + '"'
+ ' data-days="' + esc(days) + '"'
+ ' data-qty="' + esc(qty) + '"'
+ ' data-remark="' + esc(remark) + '"'
+ '>Edit</button>'
+ '<button type="button" class="btn btn-outline-danger btn-sm btn-remove-discharge-med"'
+ ' data-id="' + id + '"'
+ ' data-source="' + esc(source) + '"'
+ '>Remove</button>'
+ '</td>'
+ '</tr>';
});
tbody.innerHTML = rowsHtml;
}
// Reset the section's toolsBound flag so initMedicineTools()
// re-attaches Edit/Remove button handlers on the fresh rows.
var medSection = document.getElementById('section-medicine');
if (medSection) {
delete medSection.dataset.toolsBound;
}
initMedicineTools();
bindDischargeAjaxSubmit();
}
}
return;
}
if (saveMode === 'json') {
updateFormCsrf(form, result || {});
if (statusTargetId !== '') {