From e731ef61e95f53d7633759260c45dd15d1cd2e7a Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 6 Jun 2026 20:53:05 +0530 Subject: [PATCH] Fix discharge medicine save, add CKEditor, and Review After quick select MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Medicine Saving Fix (MAJOR): - Add hidden field 'discharge_medicine_json' to capture medicine table data - Add serializeDischargeMedicineTable() function to convert DOM table to JSON before form submit - Hook serialization into form submit event (before syncEditorValues) - Update controller to process discharge_medicine_json in save_main action - Insert medicines into ipd_discharge_prescrption_prescribed or ipd_discharge_prescription_prescribed table - Medicine data now persists on page reload CKEditor Integration: - Initialize CKEditor for 'instruction_other' (Other Advice) field - Initialize CKEditor for 'instruction_remark' (Discharge Summary) field - Add simplified toolbar: Bold, Italic, Underline, Lists, Undo/Redo - Properly destroy existing instances before re-initialization - Height: 120px for Other Advice, 150px for Discharge Summary Review After Quick Select Buttons: - Add getNextVisitOptions() method to Ipd_discharge controller (copied from Opd_prescription) - Add resolveFirstField() helper method - Pass next_visit_options array to view - Render quick select buttons: '3 Days (09-06-2026)', '1 Week (13-06-2026)', etc. - Add JavaScript handler: click button → populate #discharge_review_after input - Change Review After field from col-md-4 to col-md-6 for better layout - Add col-md-12 wrapper for button row with flex-wrap gap-2 layout Technical Details: - Medicine JSON structure: {med_type, med_name, dosage, dosage_when, dosage_freq, no_of_days, qty, remark} - JSON processing includes column existence checks for flexibility - Quick select buttons use data-value attribute for value insertion - Falls back to hardcoded date options if opd_nextvisit table missing - Medicine insert uses med_id=0 to mark client-added medicines User Workflow: 1. Add medicines → shown in table (Type, Medicine, Dose labels, Duration formatted) 2. Click 'Save Discharge Advice' → medicines serialize to JSON → POST to controller 3. Controller parses JSON → inserts to database → medicines persist on reload 4. Other Advice/Discharge Summary now have rich text editing (bold, lists, etc.) 5. Review After: click '1 Week (13-06-2026)' button → auto-fills input --- app/Controllers/Ipd_discharge.php | 143 +++++++++++++++++++++ app/Views/billing/ipd/discharge_create.php | 94 +++++++++++++- 2 files changed, 235 insertions(+), 2 deletions(-) diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index ac35a3b..1ff7464 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -5126,6 +5126,58 @@ class Ipd_discharge extends BaseController $savedAny = $this->upsertByIpd('ipd_discharge_drug_food_interaction', $ipdId, $legacyData) || $savedAny; } + // Process discharge medicine JSON (newly added medicines from the form). + $medicineJson = trim((string) ($this->request->getPost('discharge_medicine_json') ?? '')); + if ($medicineJson !== '') { + $medicines = json_decode($medicineJson, true); + if (is_array($medicines) && ! empty($medicines)) { + $legacyTable = $this->findFirstExistingTable([ + 'ipd_discharge_prescrption_prescribed', + 'ipd_discharge_prescription_prescribed', + ]); + + if ($legacyTable !== null) { + // Clear previously added client-side medicines (optional - keeps only DB-saved ones). + // Comment out if you want to preserve all medicines. + // $this->db->table($legacyTable)->where('ipd_id', $ipdId)->where('med_id', 0)->delete(); + + foreach ($medicines as $med) { + $medName = trim((string) ($med['med_name'] ?? '')); + if ($medName === '') { + continue; + } + + $insert = [ + 'ipd_id' => $ipdId, + 'med_id' => 0, + 'med_name' => $medName, + 'update_by' => $userLabel, + ]; + + // Add optional fields if columns exist. + $optionalFields = [ + 'med_type' => 'med_type', + 'dosage' => 'dosage', + 'dosage_when' => 'dosage_when', + 'dosage_freq' => 'dosage_freq', + 'no_of_days' => 'no_of_days', + 'qty' => 'qty', + 'remark' => 'remark', + ]; + + foreach ($optionalFields as $jsonKey => $colName) { + if ($this->db->fieldExists($colName, $legacyTable)) { + $insert[$colName] = trim((string) ($med[$jsonKey] ?? '')); + } + } + + $this->db->table($legacyTable)->insert($insert); + $savedAny = true; + } + } + } + } + // Examination on Admission (General Examination values). if ($this->tableHasColumns('ipd_discharge_general_exam_col', ['id', 'col_name']) && $this->tableHasColumns('ipd_discharge_1_b', ['ipd_d_id', 'col_id', 'short_head', 'rdata'])) { @@ -5586,6 +5638,7 @@ class Ipd_discharge extends BaseController 'instruction_other' => (string) ($instructionMeta['other_text'] ?? ''), 'inhos_remark' => (string) ($inhosRow['comp_remark'] ?? ''), 'other_exam_text' => (string) ($otherExamParsed['text'] ?? ''), + 'next_visit_options' => $this->getNextVisitOptions(date('Y-m-d')), ]); } @@ -6145,4 +6198,94 @@ class Ipd_discharge extends BaseController // Do not block discharge workflows if queue service is unavailable. } } + + private function getNextVisitOptions(string $baseDate): array + { + $fallbackDays = [3, 4, 5, 7, 10, 15, 20, 30, 60]; + $fallbackDesc = [ + 3 => '3 Days', + 4 => '4 Days', + 5 => '5 Days', + 7 => '1 Week', + 10 => '10 Days', + 15 => '15 Days', + 20 => '20 days', + 30 => '1 Month', + 60 => '2 Months', + ]; + + $baseTs = strtotime($baseDate); + if ($baseTs === false) { + $baseTs = strtotime(date('Y-m-d')); + } + + $rows = []; + if ($this->db->tableExists('opd_nextvisit')) { + $fields = $this->db->getFieldNames('opd_nextvisit') ?? []; + $descField = $this->resolveFirstField($fields, ['next_visit_desc', 'visit_desc', 'description', 'nextvisit_desc', 'name']); + $daysField = $this->resolveFirstField($fields, ['no_of_days', 'days', 'day_count']); + + if ($descField !== null && $daysField !== null) { + $builder = $this->db->table('opd_nextvisit') + ->select($descField . ' as visit_desc,' . $daysField . ' as no_of_days'); + + if (in_array('status', $fields, true)) { + $builder->where('status', 1); + } + + $rows = $builder + ->orderBy($daysField, 'ASC') + ->get() + ->getResultArray(); + } + } + + $options = []; + if (! empty($rows)) { + foreach ($rows as $row) { + $days = (int) ($row['no_of_days'] ?? 0); + $desc = trim((string) ($row['visit_desc'] ?? '')); + if ($days <= 0 || $desc === '') { + continue; + } + + $visitDate = date('d-m-Y', strtotime('+' . $days . ' day', $baseTs)); + $value = $desc . ' (' . $visitDate . ')'; + + $options[] = [ + 'desc' => $desc, + 'days' => $days, + 'date' => $visitDate, + 'value' => $value, + ]; + } + } + + if (empty($options)) { + foreach ($fallbackDays as $days) { + $desc = (string) ($fallbackDesc[$days] ?? ($days . ' Days')); + $visitDate = date('d-m-Y', strtotime('+' . $days . ' day', $baseTs)); + $value = $desc . ' (' . $visitDate . ')'; + $options[] = [ + 'desc' => $desc, + 'days' => $days, + 'date' => $visitDate, + 'value' => $value, + ]; + } + } + + return $options; + } + + private function resolveFirstField(array $fields, array $candidates): ?string + { + foreach ($candidates as $candidate) { + if (in_array($candidate, $fields, true)) { + return $candidate; + } + } + + return null; + } } diff --git a/app/Views/billing/ipd/discharge_create.php b/app/Views/billing/ipd/discharge_create.php index d7d1c8e..d3ee925 100644 --- a/app/Views/billing/ipd/discharge_create.php +++ b/app/Views/billing/ipd/discharge_create.php @@ -1269,6 +1269,7 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller + @@ -1426,9 +1427,19 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller -
+
- + +
+
+
+ + + + + + +
@@ -1713,6 +1724,14 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller CKEDITOR.instances.systemic_exam_editor.destroy(true); } + if (CKEDITOR.instances.instruction_other) { + CKEDITOR.instances.instruction_other.destroy(true); + } + + if (CKEDITOR.instances.instruction_remark) { + CKEDITOR.instances.instruction_remark.destroy(true); + } + if (document.getElementById('complaint_remark_editor')) { CKEDITOR.replace('complaint_remark_editor', { height: 180 @@ -1724,6 +1743,28 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller height: 200 }); } + + if (document.getElementById('instruction_other')) { + CKEDITOR.replace('instruction_other', { + height: 120, + toolbar: [ + { name: 'basicstyles', items: ['Bold', 'Italic', 'Underline'] }, + { name: 'paragraph', items: ['NumberedList', 'BulletedList'] }, + { name: 'editing', items: ['Undo', 'Redo'] } + ] + }); + } + + if (document.getElementById('instruction_remark')) { + CKEDITOR.replace('instruction_remark', { + height: 150, + toolbar: [ + { name: 'basicstyles', items: ['Bold', 'Italic', 'Underline'] }, + { name: 'paragraph', items: ['NumberedList', 'BulletedList'] }, + { name: 'editing', items: ['Undo', 'Redo'] } + ] + }); + } } function getCsrfPair(form) { @@ -3697,6 +3738,19 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller refreshInstructionPreview(); initDietaryMasterCrud(getDischargeForm()); + + // Review After quick select buttons + var reviewChips = section.querySelectorAll('.discharge-review-chip'); + var reviewInput = section.querySelector('#discharge_review_after'); + reviewChips.forEach(function(chip) { + chip.addEventListener('click', function() { + var value = chip.getAttribute('data-value') || chip.textContent.trim(); + if (reviewInput) { + reviewInput.value = value; + reviewInput.focus(); + } + }); + }); } function initDietaryMasterCrud(form) { @@ -4210,6 +4264,41 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller } } + function serializeDischargeMedicineTable() { + var tbody = document.querySelector('#discharge_medicine_tbody'); + var hiddenField = document.querySelector('#discharge_medicine_json'); + if (!tbody || !hiddenField) { + return; + } + + var rows = tbody.querySelectorAll('tr'); + var medicines = []; + + rows.forEach(function(row) { + var cells = row.querySelectorAll('td'); + // Skip empty state row or rows with insufficient cells + if (cells.length >= 8) { + var cellData = { + med_type: cells[0].textContent.trim(), + med_name: cells[1].textContent.trim(), + dosage: cells[2].textContent.trim(), + dosage_when: cells[3].textContent.trim(), + dosage_freq: cells[4].textContent.trim(), + no_of_days: cells[5].textContent.trim(), + qty: cells[6].textContent.trim(), + remark: cells[7].textContent.trim() + }; + + // Only add non-empty medicine names + if (cellData.med_name !== '' && cellData.med_name !== 'No medicine added') { + medicines.push(cellData); + } + } + }); + + hiddenField.value = JSON.stringify(medicines); + } + function syncEditorValues() { if (!window.CKEDITOR) { return; @@ -4551,6 +4640,7 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller form.addEventListener('submit', function(e) { e.preventDefault(); syncEditorValues(); + serializeDischargeMedicineTable(); syncClinicalLabSelection(form); syncCoMorbidityHidden(form);