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');
|
var modalRoot = document.getElementById('modal-root');
|
||||||
if (modalRoot) modalRoot.innerHTML = '';
|
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) {
|
document.addEventListener('input', function (event) {
|
||||||
var form = isOrderField(event.target);
|
var form = isOrderField(event.target);
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
@@ -270,6 +304,12 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
|||||||
injectOrderDraftPayload(form);
|
injectOrderDraftPayload(form);
|
||||||
});
|
});
|
||||||
document.addEventListener('click', function (event) {
|
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"]');
|
var printTrigger = event.target.closest && event.target.closest('[data-action="print-order"]');
|
||||||
if (printTrigger) {
|
if (printTrigger) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -793,9 +833,12 @@ function renderOrderForm(step, stepKey = "demografi", fppTests = [], mouId = "")
|
|||||||
demografi: `
|
demografi: `
|
||||||
<div class="stack">
|
<div class="stack">
|
||||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
<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">
|
<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>
|
</div>
|
||||||
<div class="panel" style="padding:0; box-shadow:none; background:transparent; border:0">
|
<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>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>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>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>
|
</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) {
|
function normalizeResult(raw, index = 0) {
|
||||||
const status = raw?.status || raw?.result_status || raw?.order_status || "Pending";
|
const status = raw?.status || raw?.result_status || raw?.order_status || "Pending";
|
||||||
const detailsSource = raw?.details || raw?.items || raw?.order_details || [];
|
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 = "") {
|
async function loadResults(session, search = "") {
|
||||||
const term = String(search || "").trim();
|
const term = String(search || "").trim();
|
||||||
try {
|
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) {
|
async function fragmentResultDetail(session, resultId) {
|
||||||
const result = await loadResultDetail(session, resultId);
|
const result = await loadResultDetail(session, resultId);
|
||||||
return `
|
return `
|
||||||
@@ -2349,6 +2469,12 @@ async function renderRoute(req, res, url) {
|
|||||||
return;
|
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 (path.startsWith("/fragments/results/detail/") && isGet) {
|
||||||
if (!requireAuth(req, res)) return;
|
if (!requireAuth(req, res)) return;
|
||||||
const resultId = path.split("/")[4] || "";
|
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);
|
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 {
|
.mini-item strong {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user