Add patient search autofill for demographics

This commit is contained in:
sas.fajri
2026-04-13 20:37:49 +07:00
parent 15f4383304
commit d3e9b4eafd
2 changed files with 144 additions and 3 deletions

132
server.js
View File

@@ -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] || "";

View File

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