Add placeholder panel to document print settings; autocomplete off on med inputs; localStorage medicine cache

This commit is contained in:
DevSoftTech Server 2026-04-09 03:43:53 +05:30
parent 5f35d22f86
commit 441ae0668f
2 changed files with 167 additions and 5 deletions

View File

@ -106,17 +106,110 @@ $columnsReady = (bool) ($columns_ready ?? false);
<div class="alert alert-secondary py-2 small mb-2">
If Header HTML is blank, default hospital header logic is used based on print mode.
</div>
<div class="card border-info mb-3">
<div class="card-header bg-info bg-opacity-10 py-1 d-flex justify-content-between align-items-center">
<span class="fw-semibold small text-info">Available Placeholders — click any to insert into focused textarea</span>
<span class="text-muted small" id="doc_tpl_focus_label">Focus a textarea below first</span>
</div>
<div class="card-body p-2">
<div class="mb-1"><span class="badge bg-secondary me-1">Hospital</span>
<?php foreach ([
'{{H_Name}}' => 'Hospital Name',
'{{H_address_1}}' => 'Address Line 1',
'{{H_address_2}}' => 'Address Line 2',
'{{H_phone_No}}' => 'Phone No.',
'{{H_Email}}' => 'Email',
'{{H_logo}}' => 'Logo filename',
'{{H_logo_abs}}' => 'Logo absolute path',
'{{hospital_logo_html}}' => 'Logo <img> tag',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-info btn-sm py-0 px-1 me-1 mb-1 doc-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
<div class="mb-1"><span class="badge bg-secondary me-1">Patient / IPD</span>
<?php foreach ([
'{{PATIENT_NAME}}' => 'Full patient name',
'{{UHID}}' => 'Patient UHID',
'{{IPD_CODE}}' => 'IPD admission number',
'{{ADMIT_DATE}}' => 'Date of admission',
'{{AGE_GENDER}}' => 'Age & gender combined',
'{{age_sex}}' => 'Age/sex short form',
'{{phoneno}}' => 'Patient phone',
'{{DOCTORS}}' => 'Treating doctors',
'{{doctor_name}}' => 'Primary doctor name',
'{{doctor_sign_html}}' => 'Doctor signature img',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-success btn-sm py-0 px-1 me-1 mb-1 doc-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
<div class="mb-1"><span class="badge bg-secondary me-1">Date / Page</span>
<?php foreach ([
'{{CURRENT_DATE}}' => 'Today\'s date',
'{{CURRENT_DATETIME}}' => 'Today date + time',
'{{print_time}}' => 'Print timestamp',
'{PAGENO}' => 'mPDF: current page',
'{nbpg}' => 'mPDF: total pages',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-warning btn-sm py-0 px-1 me-1 mb-1 doc-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
<div class="mb-0"><span class="badge bg-secondary me-1">QR / Barcode</span>
<?php foreach ([
'{{qr_content}}' => 'QR raw content string',
'{{qr_code_html}}' => 'QR code <img> tag',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 me-1 mb-1 doc-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="row g-2">
<div class="col-12">
<label class="form-label small">Header HTML / mPDF tags</label>
<textarea class="form-control" name="header_html" rows="6" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['header_html'] ?? '')) ?></textarea>
<textarea class="form-control doc-ph-target" name="header_html" rows="6" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['header_html'] ?? '')) ?></textarea>
</div>
<div class="col-12">
<label class="form-label small">Footer HTML / mPDF tags</label>
<textarea class="form-control" name="footer_html" rows="4" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['footer_html'] ?? '')) ?></textarea>
<textarea class="form-control doc-ph-target" name="footer_html" rows="4" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['footer_html'] ?? '')) ?></textarea>
</div>
</div>
<script>
(function () {
var activeTarget = null;
document.querySelectorAll('.doc-ph-target').forEach(function (ta) {
ta.addEventListener('focus', function () {
activeTarget = ta;
var label = document.getElementById('doc_tpl_focus_label');
if (label) {
label.textContent = 'Inserting into: ' + (ta.name || ta.id || 'textarea');
label.className = 'small text-success fw-semibold';
}
});
});
document.querySelectorAll('.doc-ph-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var ph = btn.getAttribute('data-ph') || '';
if (!ph) { return; }
if (!activeTarget) {
alert('Click inside a Header or Footer textarea first, then click a placeholder.');
return;
}
var start = activeTarget.selectionStart || 0;
var end = activeTarget.selectionEnd || 0;
var val = activeTarget.value;
activeTarget.value = val.substring(0, start) + ph + val.substring(end);
activeTarget.selectionStart = activeTarget.selectionEnd = start + ph.length;
activeTarget.focus();
});
});
})();
</script>
<div class="mt-3 d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm" <?= ! $columnsReady ? 'disabled' : '' ?>>Save Settings</button>
</div>

View File

@ -777,7 +777,7 @@
<div class="row g-3 align-items-end mb-2">
<div class="col-md-8">
<label class="form-label fw-semibold mb-1">Prescribed:</label>
<input type="text" class="form-control" id="med_name" list="medicine_suggest" placeholder="Search / enter medicine name">
<input type="text" class="form-control" id="med_name" list="medicine_suggest" autocomplete="off" placeholder="Search / enter medicine name">
<datalist id="medicine_suggest"></datalist>
</div>
<div class="col-md-4">
@ -1019,7 +1019,7 @@
<div class="col-md-7">
<div class="fw-semibold mb-2">Add Medicines</div>
<div class="mb-2">
<input type="text" class="form-control form-control-sm" id="crgc_med_name" list="crgc_med_suggest" placeholder="Medicine name">
<input type="text" class="form-control form-control-sm" id="crgc_med_name" list="crgc_med_suggest" autocomplete="off" placeholder="Medicine name">
<datalist id="crgc_med_suggest"></datalist>
</div>
<div class="mb-2">
@ -1114,6 +1114,9 @@
var medicineSuggestRows = [];
var medicineSearchCache = {};
var medicineSearchCacheTtlMs = 120000;
var medicineSearchLocalDbKey = 'opd_medicine_search_cache_v1';
var medicineSearchCacheMaxEntries = 250;
var medicineSearchLocalDbReady = false;
var medInputTimer = null;
var crgcMedInputTimer = null;
var medicineDoseMasterCache = { dose: [], when: [], freq: [], where: [] };
@ -1181,6 +1184,69 @@
return (scope || 'all') + '|' + (query || '').trim().toUpperCase();
}
function pruneMedicineSearchCacheStore(nowMs) {
var now = nowMs || Date.now();
var keys = Object.keys(medicineSearchCache || {});
if (!keys.length) {
medicineSearchCache = {};
return;
}
var rows = [];
keys.forEach(function(k) {
var item = medicineSearchCache[k] || {};
var ts = Number(item.ts || 0);
if (!ts || (now - ts) > medicineSearchCacheTtlMs) {
return;
}
rows.push({ key: k, ts: ts, rows: Array.isArray(item.rows) ? item.rows : [] });
});
rows.sort(function(a, b) { return b.ts - a.ts; });
if (rows.length > medicineSearchCacheMaxEntries) {
rows = rows.slice(0, medicineSearchCacheMaxEntries);
}
var next = {};
rows.forEach(function(item) {
next[item.key] = { ts: item.ts, rows: item.rows };
});
medicineSearchCache = next;
}
function persistMedicineSearchCacheToLocalDb() {
try {
pruneMedicineSearchCacheStore(Date.now());
localStorage.setItem(medicineSearchLocalDbKey, JSON.stringify({
saved_at: Date.now(),
entries: medicineSearchCache
}));
} catch (e) {
// Ignore storage quota/private-mode failures; memory cache still works.
}
}
function ensureMedicineSearchLocalDbLoaded() {
if (medicineSearchLocalDbReady) {
return;
}
medicineSearchLocalDbReady = true;
try {
var raw = localStorage.getItem(medicineSearchLocalDbKey);
if (!raw) {
return;
}
var parsed = JSON.parse(raw);
var entries = parsed && parsed.entries ? parsed.entries : {};
if (entries && typeof entries === 'object') {
medicineSearchCache = entries;
pruneMedicineSearchCacheStore(Date.now());
}
} catch (e) {
medicineSearchCache = {};
}
}
function fetchMedicineSuggestions(query, scope, done) {
var q = (query || '').trim();
if (q.length < 2) {
@ -1188,6 +1254,8 @@
return;
}
ensureMedicineSearchLocalDbLoaded();
var useScope = (scope || 'all').toString();
var cacheKey = getMedicineSearchCacheKey(q, useScope);
var now = Date.now();
@ -1199,7 +1267,8 @@
apiGet('<?= base_url('Opd_prescription/medicine_search') ?>?q=' + encodeURIComponent(q) + '&scope=' + encodeURIComponent(useScope) + '&limit=10', function(data) {
var rows = (data && data.rows) ? data.rows : [];
medicineSearchCache[cacheKey] = { ts: now, rows: rows };
medicineSearchCache[cacheKey] = { ts: Date.now(), rows: rows };
persistMedicineSearchCacheToLocalDb();
done(rows);
});
}