Add patient search autofill for demographics
This commit is contained in:
132
server.js
132
server.js
@@ -244,6 +244,40 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
||||
var modalRoot = document.getElementById('modal-root');
|
||||
if (modalRoot) modalRoot.innerHTML = '';
|
||||
}
|
||||
|
||||
function normalizeDateForInput(value) {
|
||||
var text = String(value || '').trim();
|
||||
if (!text || text === 'null' || text === 'undefined') return '';
|
||||
var iso = text.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);
|
||||
if (iso) return iso[0];
|
||||
var slash = text.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);
|
||||
if (slash) return slash[3] + '-' + slash[2] + '-' + slash[1];
|
||||
var dash = text.match(/^(\\d{2})-(\\d{2})-(\\d{4})$/);
|
||||
if (dash) return dash[3] + '-' + dash[2] + '-' + dash[1];
|
||||
return '';
|
||||
}
|
||||
|
||||
function fillPatientFromPick(button) {
|
||||
var form = document.querySelector('[data-order-form]');
|
||||
if (!form || !button) return;
|
||||
var fields = {
|
||||
patient_name: button.getAttribute('data-patient-name') || '',
|
||||
patient_dob: normalizeDateForInput(button.getAttribute('data-patient-dob') || ''),
|
||||
patient_nik: button.getAttribute('data-patient-nik') || '',
|
||||
patient_hp: button.getAttribute('data-patient-hp') || '',
|
||||
patient_address: button.getAttribute('data-patient-address') || '',
|
||||
};
|
||||
Object.keys(fields).forEach(function (name) {
|
||||
var field = form.querySelector('[name="' + name.replace(/"/g, '\\"') + '"]');
|
||||
if (!field) return;
|
||||
field.value = fields[name];
|
||||
field.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
syncOrderDraft(form);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
document.addEventListener('input', function (event) {
|
||||
var form = isOrderField(event.target);
|
||||
if (!form) return;
|
||||
@@ -270,6 +304,12 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
||||
injectOrderDraftPayload(form);
|
||||
});
|
||||
document.addEventListener('click', function (event) {
|
||||
var patientPick = event.target.closest && event.target.closest('[data-patient-pick]');
|
||||
if (patientPick) {
|
||||
event.preventDefault();
|
||||
fillPatientFromPick(patientPick);
|
||||
return;
|
||||
}
|
||||
var printTrigger = event.target.closest && event.target.closest('[data-action="print-order"]');
|
||||
if (printTrigger) {
|
||||
event.preventDefault();
|
||||
@@ -793,9 +833,12 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
|
||||
demografi: `
|
||||
<div class="stack">
|
||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
${panelHeader("Mandatory", "These fields are required or expected by the backend before save.")}
|
||||
${panelHeader("Mandatory", "These fields are required before save.")}
|
||||
<div class="topbar-actions" style="justify-content:flex-start; margin-bottom:12px">
|
||||
<button class="btn btn-secondary" type="button" hx-get="/fragments/modals/patient-search" hx-target="#modal-root" hx-swap="innerHTML">Search patient</button>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<label class="field"><span>Patient name</span><input name="patient_name" placeholder="Fajar Anugrah" required /></label>
|
||||
<label class="field"><span>Patient name</span><input name="patient_name" required /></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
||||
@@ -804,7 +847,7 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
|
||||
<label class="field"><span>Date of birth</span><input name="patient_dob" type="date" /></label>
|
||||
<label class="field"><span>NIK</span><input name="patient_nik" placeholder="16 digits if available" /></label>
|
||||
<label class="field"><span>Phone</span><input name="patient_hp" placeholder="08xxxxxxxxxx" /></label>
|
||||
<label class="field"><span>Address</span><input name="patient_address" placeholder="Bandung" /></label>
|
||||
<label class="field"><span>Address</span><input name="patient_address" /></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1651,6 +1694,18 @@ function normalizeDesktopOrder(raw, index = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePatientOrder(raw, index = 0) {
|
||||
return {
|
||||
id: raw?.order_id || raw?.id || raw?.order_patient_id || "",
|
||||
patient: raw?.order_name || raw?.patient_name || raw?.name || "",
|
||||
orderDob: raw?.order_dob || "",
|
||||
orderNik: raw?.order_nik || "",
|
||||
orderHp: raw?.order_hp || "",
|
||||
orderAddress: raw?.order_address || "",
|
||||
updated: raw?.order_date || raw?.updated_at || raw?.updated || "",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeResult(raw, index = 0) {
|
||||
const status = raw?.status || raw?.result_status || raw?.order_status || "Pending";
|
||||
const detailsSource = raw?.details || raw?.items || raw?.order_details || [];
|
||||
@@ -1777,6 +1832,24 @@ async function loadDesktopOrders(session, { search = "", month = "", year = "",
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPatientSearch(session, search = "") {
|
||||
try {
|
||||
const payload = await apiPost(
|
||||
"/order/search_order_pasien_by_doktorid",
|
||||
{
|
||||
token: session.token,
|
||||
OrderPatientM_DoctorID: session.doctorId || sampleLogin.doctorId,
|
||||
search: String(search || ""),
|
||||
},
|
||||
session.token,
|
||||
);
|
||||
const rows = extractArray(payload) || [];
|
||||
return Array.isArray(rows) ? rows.map((row, index) => normalizePatientOrder(row, index)).filter((item) => item.id || item.patient) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadResults(session, search = "") {
|
||||
const term = String(search || "").trim();
|
||||
try {
|
||||
@@ -2079,6 +2152,53 @@ function fragmentPesanKhusus(orderId) {
|
||||
`;
|
||||
}
|
||||
|
||||
async function fragmentPatientSearch(session, search = "") {
|
||||
const patients = await loadPatientSearch(session, search);
|
||||
return `
|
||||
<div class="modal-shell" role="dialog" aria-modal="true" aria-labelledby="patient-search-title">
|
||||
<div class="modal-backdrop" data-modal-close></div>
|
||||
<section class="modal-card panel">
|
||||
${panelHeader("Search patient", "Pick an existing patient and the demographic fields will fill automatically.", `<button class="btn btn-secondary" type="button" data-modal-close>Close</button>`)}
|
||||
<form class="card" hx-get="/fragments/modals/patient-search" hx-target="#modal-root" hx-swap="innerHTML">
|
||||
<label class="field">
|
||||
<span>Search name</span>
|
||||
<input name="search" value="${escapeHtml(search)}" placeholder="Type patient name" />
|
||||
</label>
|
||||
<div class="topbar-actions" style="justify-content:flex-start; margin-top:12px">
|
||||
<button class="btn btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<div style="height:14px"></div>
|
||||
${
|
||||
patients.length
|
||||
? `<div class="mini-list">${patients
|
||||
.map(
|
||||
(patient) => `
|
||||
<button
|
||||
class="mini-item"
|
||||
type="button"
|
||||
data-patient-pick="true"
|
||||
data-patient-name="${escapeHtml(patient.patient || "")}"
|
||||
data-patient-dob="${escapeHtml(patient.orderDob || "")}"
|
||||
data-patient-nik="${escapeHtml(patient.orderNik || "")}"
|
||||
data-patient-hp="${escapeHtml(patient.orderHp || "")}"
|
||||
data-patient-address="${escapeHtml(patient.orderAddress || "")}"
|
||||
>
|
||||
<div>
|
||||
<strong>${escapeHtml(patient.patient || "Unknown patient")}</strong>
|
||||
<p>${escapeHtml(patient.orderNik || "-")} · ${escapeHtml(patient.orderAddress || "-")}</p>
|
||||
</div>
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
.join("")}</div>`
|
||||
: emptyState("No patients found", "Search by patient name to load matching records.")
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function fragmentResultDetail(session, resultId) {
|
||||
const result = await loadResultDetail(session, resultId);
|
||||
return `
|
||||
@@ -2349,6 +2469,12 @@ async function renderRoute(req, res, url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/fragments/modals/patient-search" && isGet) {
|
||||
if (!requireAuth(req, res)) return;
|
||||
html(res, 200, await fragmentPatientSearch(session, query.search || ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.startsWith("/fragments/results/detail/") && isGet) {
|
||||
if (!requireAuth(req, res)) return;
|
||||
const resultId = path.split("/")[4] || "";
|
||||
|
||||
15
styles.css
15
styles.css
@@ -950,6 +950,21 @@ tr:last-child td {
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.mini-item[data-patient-pick] {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.mini-item[data-patient-pick]:hover {
|
||||
background: rgba(185, 28, 28, 0.06);
|
||||
border-color: rgba(185, 28, 28, 0.14);
|
||||
}
|
||||
|
||||
.mini-item strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
|
||||
Reference in New Issue
Block a user