Fix discharge medicine save, add CKEditor, and Review After quick select

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
This commit is contained in:
DevSoftTech Server 2026-06-06 20:53:05 +05:30
parent e229ae3df2
commit e731ef61e9
2 changed files with 235 additions and 2 deletions

View File

@ -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;
}
}

View File

@ -1269,6 +1269,7 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
</table>
</div>
<input type="hidden" name="drug_remove_id" id="drug_remove_id" value="0">
<input type="hidden" name="discharge_medicine_json" id="discharge_medicine_json" value="">
</div>
<!-- Add Medicine Form -->
@ -1426,9 +1427,19 @@ $allergyStatusNoKnown = in_array($allergyStatusNormalized, ['no known drug aller
<label class="form-label">Discharge Summary</label>
<textarea class="form-control" name="instruction_remark" id="instruction_remark" rows="3"><?= esc((string) ($instruction_remark ?? '')) ?></textarea>
</div>
<div class="col-md-4">
<div class="col-md-6">
<label class="form-label">Review After (days/text)</label>
<input type="text" class="form-control" name="review_after" value="<?= esc((string) ($review_after ?? '')) ?>">
<input type="text" class="form-control" name="review_after" id="discharge_review_after" value="<?= esc((string) ($review_after ?? '')) ?>" placeholder="e.g. 5 Days">
</div>
<div class="col-md-12">
<div class="d-flex flex-wrap gap-2">
<?php $nextVisitOptions = is_array($next_visit_options ?? null) ? $next_visit_options : []; ?>
<?php foreach ($nextVisitOptions as $nextVisitOpt) : ?>
<?php $nextVisitValue = trim((string) ($nextVisitOpt['value'] ?? '')); ?>
<?php if ($nextVisitValue === '') { continue; } ?>
<button type="button" class="btn btn-outline-secondary btn-sm discharge-review-chip" data-value="<?= esc($nextVisitValue) ?>"><?= esc($nextVisitValue) ?></button>
<?php endforeach; ?>
</div>
</div>
<div class="col-md-12 d-flex justify-content-end">
<button type="submit" class="btn btn-outline-success btn-sm" name="action" value="save_main" data-reload-section="section-instructions">Save Discharge Advice</button>
@ -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);