`;
+}
+
+async function rpLoadPatientsForPicker() {
+ try {
+ rpPatientsCache = await Api.getPatients();
+ const select = document.getElementById('rp_patientSelect');
+ if (select) {
+ select.innerHTML = rpPatientsCache.map(p => ``).join('');
+ }
+ } catch { /* patient picker just won't populate; add flow will fail loudly instead */ }
+}
+
+// Fills the three reference dropdowns on the Practitioner Role form. `row` is
+// passed on Edit so the record's current practitioner / organization /
+// location come back selected instead of resetting to the first option.
+async function rpLoadPractitionerRoleDropdowns(row = null) {
+ const look = await rpEnsureLookups(true);
+
+ const fill = (fieldKey, list, labelFn, selected) => {
+ const el = document.getElementById(`rp_field_${fieldKey}`);
+ if (!el || el.tagName !== 'SELECT') return;
+
+ el.innerHTML = '' +
+ list.map(item => {
+ const id = item.id ?? item.Id ?? '';
+ const label = labelFn(item) || id;
+ const isSel = selected && String(selected) === String(id) ? ' selected' : '';
+ return ``;
+ }).join('');
+
+ if (selected) el.value = String(selected);
+ };
+
+ fill('practitionerId', look.practitioners, rpPractitionerLabel, row?.practitionerId);
+ fill('organizationId', look.organizations, o => o.name, row?.organizationId);
+ fill('locationId', look.locations, l => l.name, row?.locationId);
+}
+// A short banner at the top of the form: icon, what the resource is, and how
+// the score is worked out for risk assessments. Injected once and reused.
+const RP_FORM_INTRO = {
+ riskassessments: {
+ icon: '📊',
+ title: 'Clinical risk assessment',
+ body: 'Tick the factors that apply. The percentage and risk level are calculated from their weights, and the note is written for you.'
+ },
+ servicerequests: { icon: '📄', title: 'Service request', body: 'The identifier is generated automatically from the resource name and patient id.' },
+ careplans: { icon: '📋', title: 'Care plan', body: 'Period covers the whole plan; the activity schedule covers the next step.' },
+ conditions: { icon: '🩺', title: 'Condition', body: 'Use an ICD-10 code (e.g. E11.9) and its display name.' },
+ observations: { icon: '📈', title: 'Observation', body: 'Use a LOINC code (e.g. 8480-6). Put the measured value in the note.' }
+};
+
+function rpRenderFormIntro(config, mode) {
+ const host = document.getElementById('rp_formIntro');
+ if (!host) return;
+
+ const meta = RP_FORM_INTRO[config.key];
+ if (!meta) { host.innerHTML = ''; host.classList.add('d-none'); return; }
+
+ host.classList.remove('d-none');
+ host.innerHTML = `
+
+
${meta.icon}
+
+
${rpEscapeHtml(meta.title)}
+
${meta.body}
+
+
`;
+}
+
+async function rpOpenAddModal() {
+
+ rpEditingRowId = null;
+ rpEditingPatientId = null;
+ const config = window.RESOURCE_PAGE_CONFIG;
+ document.getElementById('rp_modalTitle').textContent = `Add ${config.title.replace(/s$/, '')}`;
+ rpRenderFormIntro(config, 'add');
+ // Resources that don't hang off a patient (organizations, locations,
+ // practitioners, CDS rules, users) shouldn't show a patient dropdown at all.
+ document.getElementById('rp_patientPickerRow')
+ .classList.toggle('d-none', rpIsStandalone(config));
+ document.getElementById('rp_fieldsBody').innerHTML = config.fields
+ .filter(f => !RP_AUTO_FIELDS.includes(f.key))
+ .filter(f => !(rpNormKey(config.key) === 'riskassessment' && f.key === 'noteText'))
+ .map(f => rpFieldInputHtml(f, null)).join('')
+ + (rpNormKey(config.key) === 'riskassessment' ? rpRiskFactorsHtml() : '');
+
+ if (rpNormKey(config.key) === 'riskassessment') {
+ rpRiskPatientId = null; // Add mode follows the dropdown
+ rpWireRiskForm();
+ }
+
+ if (rpNormKey(config.key) === 'practitionerrole') {
+ try { await rpLoadPractitionerRoleDropdowns(); }
+ catch (err) { rpShowToast(`Could not load reference lists: ${err.message}`, 'danger'); }
+ }
+
+ rpAddModal.show();
+}
+
+// Keeps the live total in sync, and re-renders the reproductive questions
+// when a different patient is picked (they only apply to female patients).
+function rpWireRiskForm() {
+ document.querySelectorAll('.rp-risk-check')
+ .forEach(el => el.addEventListener('change', rpUpdateRiskPreview));
+
+ document.getElementById('rp_riskFreeText')
+ ?.addEventListener('input', rpUpdateRiskPreview);
+
+ const picker = document.getElementById('rp_patientSelect');
+ if (picker && !picker.dataset.riskWired) {
+ picker.dataset.riskWired = '1';
+ picker.addEventListener('change', () => {
+ if (window.RESOURCE_PAGE_CONFIG?.key !== 'riskassessments') return;
+
+ // Preserve what's already ticked across the re-render.
+ const checked = [...document.querySelectorAll('.rp-risk-check:checked')]
+ .map(el => el.dataset.label);
+ const extra = document.getElementById('rp_riskFreeText')?.value || '';
+
+ const host = document.getElementById('rp_riskScorePreview')?.closest('.col-12');
+ if (!host) return;
+
+ // Rebuild just the questionnaire portion.
+ const fields = document.getElementById('rp_fieldsBody');
+ const keep = [...fields.children].filter(c => !c.querySelector('.rp-risk-check, #rp_riskScoreChip, #rp_riskFreeText'));
+ fields.innerHTML = '';
+ keep.forEach(c => fields.appendChild(c));
+ fields.insertAdjacentHTML('beforeend', rpRiskFactorsHtml(checked, extra));
+ rpWireRiskForm();
+ });
+ }
+
+ rpUpdateRiskPreview();
+}
+
+async function rpOpenEditModal(row) {
+ rpEditingRowId = row.id;
+ rpEditingPatientId = rpRowPatientId(row);
+ const config = window.RESOURCE_PAGE_CONFIG;
+ const key = rpNormKey(config.key);
+
+ // Standalone records are identified by their own name, not a patient's.
+ const label = rpIsStandalone(config)
+ ? rpStandaloneTitle(config, row, 0)
+ : '';
+ document.getElementById('rp_modalTitle').textContent =
+ `Edit ${config.title.replace(/s$/, '')}${label ? ` — ${label}` : ''}`;
+
+ rpRenderFormIntro(config, 'edit');
+ document.getElementById('rp_patientPickerRow').classList.add('d-none');
+ if (key === 'riskassessment') rpRiskPatientId = rpRowPatientId(row);
+
+ const split = key === 'riskassessment'
+ ? rpSplitRiskNote(row.noteText)
+ : null;
+
+ document.getElementById('rp_fieldsBody').innerHTML = config.fields
+ .filter(f => !RP_AUTO_FIELDS.includes(f.key))
+ .filter(f => !(key === 'riskassessment' && f.key === 'noteText'))
+ .map(f => rpFieldInputHtml(f, row[f.key])).join('')
+ + (split ? rpRiskFactorsHtml(split.known, split.other) : '');
+
+ if (key === 'riskassessment') rpWireRiskForm();
+
+ // Reference dropdowns have to be rebuilt after the fields are drawn, with
+ // this record's values reselected.
+ if (key === 'practitionerrole') {
+ try { await rpLoadPractitionerRoleDropdowns(row); }
+ catch (err) { rpShowToast(`Could not load reference lists: ${err.message}`, 'danger'); }
+ }
+
+ rpAddModal.show();
+}
+
+async function rpSubmitModal(e) {
+ e.preventDefault();
+ const config = window.RESOURCE_PAGE_CONFIG;
+ const payload = {};
+ for (const f of config.fields) {
+
+ const el =
+ document.getElementById(`rp_field_${f.key}`);
+
+ console.log(f.key, el);
+
+ if (!el) {
+ // Auto-generated fields have no input; everything else is a real bug.
+ if (!RP_AUTO_FIELDS.includes(f.key)) console.error(`Missing field: ${f.key}`);
+ continue;
+ }
+
+ let v = el.value;
+
+ if (f.type === 'number') v = v === '' ? null : parseFloat(v);
+ if (f.type === 'boolean' || f.key === 'active' || f.key === 'enabled' || f.key === 'isActive')
+{
+ v = v === 'true' || v === true;
+}
+ payload[f.key] = v === '' ? null : v;
+ }
+
+ if (rpNormKey(config.key) === 'riskassessment') {
+ // The note is assembled from the ticked factors so the weights are always
+ // well-formed and the score parses correctly.
+ payload.noteText = rpBuildRiskNote();
+ }
+
+ try {
+ if (config.key === 'conditions') {
+
+ const practitioners = await Api.getPractitioners();
+
+console.log("Practitioners:", practitioners);
+
+if (practitioners.length > 0) {
+
+ payload.recorderId = practitioners[0].id;
+ payload.asserterId = practitioners[0].id;
+}
+
+console.log("Payload after practitioner:", payload);
+
+ payload.clinicalStatusDisplay =
+ payload.clinicalStatusCode;
+
+ payload.verificationStatusDisplay =
+ payload.verificationStatusCode;
+
+ payload.severityDisplay =
+ payload.severityCode;
+}
+if (config.key === 'observations') {
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+
+ payload.performerPractitionerId =
+ practitioners[0].id;
+
+ payload.noteAuthorPractitionerId =
+ practitioners[0].id;
+ }
+
+ payload.noteTime =
+ new Date().toISOString();
+}
+
+const patientSelect =
+document.getElementById('rp_patientSelect');
+const patientId =
+patientSelect ? patientSelect.value : null;
+
+if (config.key === 'observations') {
+
+ console.log("Selected patientId:", patientId);
+console.log("Patients Cache:", rpPatientsCache);
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+
+ payload.performerPractitionerId =
+ practitioners[0].id;
+
+ payload.noteAuthorPractitionerId =
+ practitioners[0].id;
+ }
+
+ payload.noteTime =
+ new Date().toISOString();
+}
+
+if (config.key === 'allergies') {
+
+ if (config.key === 'allergies') {
+
+ payload.patientId = patientId;
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+
+ payload.recorderPractitionerId =
+ practitioners[0].id;
+ }
+
+ payload.asserterPatientId =
+ patientId;
+}
+}
+
+if (config.key === 'servicerequests') {
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+
+ payload.requesterPractitionerId =
+ practitioners[0].id;
+
+ payload.noteAuthorPractitionerId =
+ practitioners[0].id;
+ }
+
+ payload.noteTime =
+ new Date().toISOString();
+
+ payload.patientId =
+ patientId;
+}
+
+if (config.key === 'riskassessments') {
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+
+ payload.performerPractitionerId =
+ practitioners[0].id;
+
+ payload.noteAuthorPractitionerId =
+ practitioners[0].id;
+ }
+
+ payload.noteTime =
+ new Date().toISOString();
+}
+
+if (config.key === 'careplans') {
+
+ console.log("Selected Patient:", patientId);
+
+ payload.subjectPatientId = patientId;
+ payload.authorPatientId = patientId;
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+ payload.activityPerformerPractitionerId =
+ practitioners[0].id;
+ }
+
+ console.log("CAREPLAN PAYLOAD", payload);
+}
+
+
+ // Identifiers are set on create only; editing keeps the original.
+ if (!rpEditingRowId) {
+ const picker = document.getElementById('rp_patientSelect');
+ const pid = (!rpIsStandalone(config) && picker) ? picker.value : null;
+ const ids = rpBuildIdentifiers(config, pid);
+
+ if (config.fields.some(f => f.key === 'identifierSystem')) {
+ payload.identifierSystem = ids.identifierSystem;
+ }
+ if (config.fields.some(f => f.key === 'identifierValue')) {
+ payload.identifierValue = ids.identifierValue;
+ }
+ }
+
+ if (rpEditingRowId) {
+ await config.update(rpEditingPatientId, rpEditingRowId, payload);
+ rpShowToast(`${config.title} updated.`);
+ } else {
+
+ const patientSelect =
+ document.getElementById('rp_patientSelect');
+
+
+
+ const patientId =
+ patientSelect ? patientSelect.value : null;
+
+ if (config.key === 'allergies') {
+
+ const practitioners =
+ await Api.getPractitioners();
+
+ if (practitioners.length > 0) {
+
+ payload.recorderPractitionerId =
+ practitioners[0].id;
+ }
+
+ payload.asserterPatientId =
+ patientId;
+}
+
+if (
+ !rpIsStandalone(config) &&
+ !RP_STANDALONE_KEYS.includes(rpNormKey(config.key)) &&
+ !patientId
+)
+{
+ rpShowToast('Select a patient first.', 'danger');
+ return;
+}
+ // Standalone resources have no patient dropdown — sending '' as the id
+ // makes the API build a URL like /patients//organizations.
+ const ownerId = rpIsStandalone(config) ? null : (patientId || null);
+ await config.add(ownerId, payload);
+
+ rpShowToast(`${config.title} added.`);
+}
+
+ rpAddModal.hide();
+ await rpLoadGrid();
+ } catch (err) {
+ rpShowToast(err.message, 'danger');
+ }
+}
+
+async function rpDeleteRow(row) {
+ const config = window.RESOURCE_PAGE_CONFIG;
+ const noun = config.title.replace(/s$/, '').toLowerCase();
+
+ // "…for undefined" is what this used to say on a standalone record, which
+ // has no patient. Name the record itself instead.
+ const subject = rpIsStandalone(config)
+ ? `"${rpStandaloneTitle(config, row, 0)}"`
+ : `for ${row.patientName || 'this patient'}`;
+
+ if (!confirm(`Delete this ${noun} ${subject}?`)) return;
+ try {
+ await config.remove(
+ rpRowPatientId(row),
+ row.id
+);
+ rpShowToast('Deleted.');
+ await rpLoadGrid();
+ } catch (err) {
+ rpShowToast(err.message, 'danger');
+ }
+}
+
+// ---------- Card rendering ----------
+// Rows are shown as avatar cards with a count and a View button instead of a
+// flat table. Patient-scoped resources group every row for one patient into a
+// single card; standalone resources (organizations, practitioners, ...) get
+// one card each. The old table is kept in the DOM but hidden, so the existing
+// markup on every page still works untouched.
+
+
+// The card styles are injected from here rather than relying on
+// css/styles.css. Every previous round showed the external stylesheet not
+// being picked up (avatars rendered as full-width bars because .rp-avatar
+// had no size), so the layout now ships with the script that draws it.
+const RP_CARD_STYLES = `
+.rp-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+ align-items: start;
+}
+.rp-card {
+ background: #fff;
+ border: 1px solid #e3e9e9;
+ border-radius: 14px;
+ padding: 1.4rem 1rem 1.2rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: .7rem;
+ box-shadow: 0 1px 3px rgba(16,40,40,.06);
+ transition: box-shadow .12s ease, transform .12s ease;
+}
+.rp-card:hover { box-shadow: 0 6px 18px rgba(16,40,40,.12); transform: translateY(-2px); }
+
+.rp-avatar {
+ width: 64px;
+ height: 64px;
+ min-width: 64px;
+ min-height: 64px;
+ flex: 0 0 64px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-weight: 700;
+ font-size: 1.15rem;
+ letter-spacing: .5px;
+ line-height: 1;
+}
+.rp-card-name {
+ font-weight: 600;
+ font-size: 1rem;
+ color: #1f2d2d;
+ line-height: 1.25;
+ max-width: 100%;
+ word-break: break-word;
+}
+.rp-card-badge {
+ font-size: .78rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .4px;
+ border-radius: 999px;
+ padding: .2rem .7rem;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ background: #eef3f3;
+ color: #5f7373;
+}
+.rp-card-badge.is-high { background: #fdecea; color: #c0392b; }
+.rp-card-badge.is-warning { background: #fdf3e3; color: #b9770e; }
+.rp-card-badge.is-normal { background: #eafaf1; color: #1e8449; }
+.rp-card-badge.is-info { background: #e8f2fb; color: #2471a3; }
+
+.rp-card-sub { font-size: .78rem; color: #5f7373; }
+.rp-card-actions { display: flex; gap: .35rem; flex-wrap: wrap; justify-content: center; }
+.rp-card-view {
+ display: inline-block;
+ font-size: .85rem;
+ padding: .3rem 1rem;
+ border: 1px solid #0f6e6e;
+ color: #0f6e6e;
+ background: #fff;
+ border-radius: 8px;
+ cursor: pointer;
+ text-decoration: none;
+ line-height: 1.4;
+}
+.rp-card-view:hover { background: #0f6e6e; color: #fff !important; }
+
+/* --- Risk score chips (grid + detail table) --- */
+.rp-score-chip {
+ display: inline-block; min-width: 52px; text-align: center;
+ font-weight: 700; font-size: .95rem; padding: .25rem .55rem;
+ border-radius: 8px; line-height: 1.2;
+}
+.rp-score-chip.is-high { background: #fdecea; color: #c0392b; }
+.rp-score-chip.is-moderate { background: #fdf3e3; color: #b9770e; }
+.rp-score-chip.is-low { background: #eafaf1; color: #1e8449; }
+.rp-score-level {
+ font-size: .68rem; text-transform: uppercase; letter-spacing: .5px;
+ color: #5f7373; margin-top: .2rem; text-align: center;
+}
+
+/* --- Single-record detail panel (organizations, locations, practitioners) --- */
+.rp-detail-head {
+ display: flex; align-items: center; gap: .9rem;
+ padding-bottom: .9rem; margin-bottom: .9rem;
+ border-bottom: 1px solid #eef3f3;
+}
+.rp-detail-avatar {
+ width: 52px; height: 52px; min-width: 52px; border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ color: #fff; font-weight: 700; font-size: 1rem; line-height: 1;
+}
+.rp-detail-name { font-weight: 700; font-size: 1.05rem; color: #1f2d2d; }
+.rp-detail-sub {
+ font-size: .74rem; text-transform: uppercase; letter-spacing: .5px;
+ color: #5f7373; margin-top: .15rem;
+}
+.rp-detail-grid {
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: .75rem 1.25rem; margin: 0;
+}
+.rp-detail-item {
+ border-bottom: 1px solid #f2f6f6; padding-bottom: .5rem;
+}
+.rp-detail-item dt {
+ font-size: .7rem; text-transform: uppercase; letter-spacing: .5px;
+ font-weight: 600; color: #5f7373; margin-bottom: .15rem;
+}
+.rp-detail-item dd {
+ margin: 0; font-size: .92rem; color: #1f2d2d; word-break: break-word;
+}
+.rp-detail-actions {
+ display: flex; gap: .5rem; justify-content: flex-end;
+ margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid #eef3f3;
+}
+
+/* --- Notes rendered as bullets instead of one long line --- */
+.rp-note-list { margin: 0; padding-left: 1.05rem; font-size: .85rem; }
+.rp-note-list li { margin-bottom: .15rem; }
+
+/* --- Add / Edit form chrome --- */
+#rp_modal .modal-header {
+ background: linear-gradient(135deg, #0a4f4f, #0f6e6e);
+ color: #fff; border-bottom: none;
+}
+#rp_modal .modal-header .modal-title { font-weight: 700; }
+#rp_modal .modal-header .btn-close { filter: invert(1) grayscale(1) brightness(2); }
+#rp_modal .modal-body { padding-top: 1.1rem; }
+#rp_modal .form-label {
+ font-size: .74rem; text-transform: uppercase; letter-spacing: .5px;
+ font-weight: 600; color: #5f7373; margin-bottom: .25rem;
+}
+#rp_modal .form-control,
+#rp_modal .form-select {
+ border-radius: 9px; border-color: #dfe7e7; padding: .5rem .75rem;
+}
+#rp_modal .form-control:focus,
+#rp_modal .form-select:focus {
+ border-color: #0f6e6e; box-shadow: 0 0 0 .18rem rgba(15,110,110,.15);
+}
+#rp_modal .modal-footer { border-top: 1px solid #eef3f3; }
+
+.rp-form-intro {
+ display: flex; gap: .8rem; align-items: flex-start;
+ background: #f2f8f8; border: 1px solid #d9e8e8;
+ border-radius: 12px; padding: .8rem .9rem; margin-bottom: 1rem;
+}
+.rp-form-intro-icon { font-size: 1.5rem; line-height: 1; }
+.rp-form-intro-title { font-weight: 700; color: #0a4f4f; margin-bottom: .15rem; }
+.rp-form-intro-body { font-size: .82rem; color: #5f7373; line-height: 1.45; }
+/* --- Risk factor questionnaire inside the Add/Edit form --- */
+.rp-risk-score {
+ display: flex; align-items: center; gap: .6rem;
+ background: #f2f8f8; border: 1px solid #d9e8e8;
+ border-radius: 10px; padding: .55rem .8rem; margin-bottom: .3rem;
+}
+.rp-risk-score-label { font-weight: 600; color: #0a4f4f; font-size: .9rem; }
+.rp-risk-group {
+ font-size: .74rem; text-transform: uppercase; letter-spacing: .5px;
+ font-weight: 700; color: #0a4f4f; margin: .6rem 0 .35rem;
+}
+.rp-risk-item {
+ display: flex; align-items: center; gap: .6rem;
+ padding: .45rem .6rem; border: 1px solid #e6ecec; border-radius: 9px;
+ margin-bottom: .35rem; cursor: pointer; font-size: .9rem;
+ transition: background .1s ease, border-color .1s ease;
+}
+.rp-risk-item:hover { background: #f6fafa; border-color: #cfe0e0; }
+.rp-risk-item input { margin: 0; flex-shrink: 0; }
+.rp-risk-item span:first-of-type { flex: 1; }
+.rp-risk-weight {
+ font-size: .74rem; font-weight: 700; color: #5f7373;
+ background: #eef3f3; border-radius: 999px; padding: .1rem .45rem;
+}
+.rp-risk-item input:checked ~ .rp-risk-weight { background: #d9ece4; color: #1e8449; }
+.rp-risk-hint { font-size: .76rem; color: #5f7373; margin-top: .3rem; line-height: 1.45; }
+.rp-risk-hint code {
+ background: #f2f8f8; border: 1px solid #dfe7e7; border-radius: 4px;
+ padding: .02rem .25rem; color: #0f6e6e;
+}
+
+.rp-form-intro-body code {
+ background: #fff; border: 1px solid #dfe7e7; border-radius: 5px;
+ padding: .05rem .3rem; color: #0f6e6e; font-size: .78rem;
+}
+
+/* Long add-forms (Practitioner, Location, Encounter) must scroll inside the
+ dialog instead of running off the bottom of the screen. */
+#rp_modal .modal-content,
+#rp_detailModal .modal-content { max-height: 90vh; }
+#rp_modal .modal-body,
+#rp_detailModal .modal-body { max-height: 70vh; overflow-y: auto; }
+#rp_modal .modal-body::-webkit-scrollbar,
+#rp_detailModal .modal-body::-webkit-scrollbar { width: 12px; }
+#rp_modal .modal-body::-webkit-scrollbar-track,
+#rp_detailModal .modal-body::-webkit-scrollbar-track { background: #e3e9e9; border-radius: 6px; }
+#rp_modal .modal-body::-webkit-scrollbar-thumb,
+#rp_detailModal .modal-body::-webkit-scrollbar-thumb {
+ background: #1a1a1a; border-radius: 6px; border: 2px solid #e3e9e9;
+}
+#rp_modal .modal-body,
+#rp_detailModal .modal-body { scrollbar-width: thin; scrollbar-color: #1a1a1a #e3e9e9; }
+`;
+
+function rpInjectCardStyles() {
+ if (document.getElementById('rp-card-styles')) return;
+ const style = document.createElement('style');
+ style.id = 'rp-card-styles';
+ style.textContent = RP_CARD_STYLES;
+ document.head.appendChild(style);
+}
+
+// Colour the badge from its own text, the way a status pill should read.
+function rpBadgeClass(text) {
+ const t = (text || '').toLowerCase();
+ if (/high|critical|severe|urgent|stat/.test(t)) return 'is-high';
+ if (/warning|moderate|on-hold|pending/.test(t)) return 'is-warning';
+ if (/normal|low|active|final|completed|confirmed/.test(t)) return 'is-normal';
+ if (/info/.test(t)) return 'is-info';
+ return '';
+}
+
+const RP_AVATAR_COLOURS = [
+ '#2e86ab', '#1e8449', '#d68910', '#7d3c98', '#c0392b',
+ '#117864', '#a04000', '#5499c7', '#b7950b', '#34495e'
+];
+
+function rpInitials(name) {
+ return (name || '')
+ .split(' ')
+ .filter(Boolean)
+ .filter(part => !/^(dr|mr|mrs|ms|miss)\.?$/i.test(part))
+ .slice(0, 2)
+ .map(p => p[0].toUpperCase())
+ .join('') || '?';
+}
+
+// Same name always gets the same colour.
+function rpAvatarColour(name) {
+ let hash = 0;
+ for (const ch of (name || '')) hash = (hash * 31 + ch.charCodeAt(0)) % 100000;
+ return RP_AVATAR_COLOURS[hash % RP_AVATAR_COLOURS.length];
+}
+
+// The badge under the name: whatever the config nominates, otherwise the
+// first column that isn't the patient's name.
+function rpCardBadge(config, rows, title) {
+ if (config.cardBadge) return config.cardBadge(rows);
+
+ // Risk assessments show the patient's highest score, not just a status.
+ if (rpNormKey(config.key) === 'riskassessment') {
+ const best = rows
+ .map(r => rpParseRiskNote(r.noteText).score)
+ .reduce((a, b) => Math.max(a, b), 0);
+ return `${rpRiskLevel(best)} · ${best}%`;
+ }
+
+ const row = rows[0];
+
+ // Standalone records: show a second, different detail — the name is already
+ // the card title, so repeating it in the badge tells the user nothing.
+ if (rpIsStandalone(config)) {
+ const subFn = RP_CARD_SUBTITLE[rpNormKey(config.key)];
+ const sub = subFn ? subFn(row) : '';
+ if (sub) return sub;
+
+ const col = (config.columns || []).find(c =>
+ c.key !== 'patientName' &&
+ row?.[c.key] !== null && row?.[c.key] !== undefined && row?.[c.key] !== '' &&
+ String(row[c.key]) !== String(title));
+ return col ? String(row[col.key]) : '';
+ }
+
+ const col = config.columns.find(c => c.key !== 'patientName');
+ if (!col) return '';
+ const value = row?.[col.key];
+ return value === null || value === undefined || value === '' ? '' : String(value);
+}
+
+// ---------- Identifier generation ----------
+// IdentifierSystem / IdentifierValue are [Required] on several models, so the
+// forms used to ask for them and a blank box meant "Validation failed". They
+// are generated here instead:
+// system = http://clinicalinsightspro.in//
+// value = - (e.g. SerReq-pat-002)
+// A suffix is added if that value is already taken, so values stay unique.
+const RP_IDENTIFIER_PREFIX = {
+ conditions: 'Cond',
+ observations: 'Obs',
+ allergies: 'Alg',
+ medications: 'Med',
+ encounters: 'Enc',
+ servicerequests: 'SerReq',
+ riskassessments: 'Risk',
+ careplans: 'CarePlan',
+ cdsalerts: 'Alert',
+ organizations: 'Org',
+ locations: 'Loc',
+ practitioners: 'Prac',
+ practitionerroles: 'PracRole',
+ cdsrules: 'Rule'
+};
+
+// Fields the user should never have to fill in.
+const RP_AUTO_FIELDS = ['identifierSystem', 'identifierValue'];
+
+// ---------- Risk factor questionnaire ----------
+// The note is what actually stores the factors, but typing
+// "Current smoker (+10); ..." by hand is error-prone — one wrong bracket and
+// the score comes out wrong. So the Add/Edit form asks the same tick-box
+// questions the dashboard does, and assembles the note from the answers.
+// Weights match RiskScoringService exactly, so both routes agree.
+const RP_RISK_FACTORS = [
+ { group: 'General health', label: 'Current smoker', weight: 10 },
+ { group: 'General health', label: 'Family history of heart disease', weight: 8 },
+ { group: 'General health', label: 'Prior hospitalization', weight: 12 },
+ { group: 'General health', label: 'Resting heart rate over 100 bpm', weight: 8 },
+ { group: 'General health', label: 'BMI 30 or above', weight: 8 },
+ { group: 'General health', label: 'Pain score 7 or above', weight: 5 },
+
+ { group: 'Reproductive health', label: 'Currently pregnant', weight: 6, femaleOnly: true },
+ { group: 'Reproductive health', label: 'Currently breastfeeding', weight: 4, femaleOnly: true },
+
+ { group: 'Conditions & medications', label: 'Active condition reported as not well controlled', weight: 10 },
+ { group: 'Conditions & medications', label: 'New chronic condition diagnosed in the past 6 months', weight: 10 },
+ { group: 'Conditions & medications', label: 'Reported missed medication doses', weight: 8 },
+ { group: 'Conditions & medications', label: 'History of severe/anaphylactic allergic reaction', weight: 10 }
+];
+
+// Gender of whoever is picked in the patient dropdown, so the reproductive
+// questions only appear for female patients — same rule as the dashboard.
+// Whose gender gates the reproductive questions. On Add that's the dropdown;
+// on Edit the dropdown is hidden and stale, so the row's own patient wins.
+let rpRiskPatientId = null;
+
+function rpSelectedGender() {
+ const id = rpRiskPatientId
+ || document.getElementById('rp_patientSelect')?.value
+ || null;
+ if (!id) return '';
+ const patient = rpPatientsCache.find(p => p.id === id);
+ return (patient?.gender || '').toLowerCase();
+}
+
+function rpRiskFactorsHtml(checkedLabels = [], freeText = '') {
+ const gender = rpSelectedGender();
+ const visible = RP_RISK_FACTORS.filter(f => !f.femaleOnly || gender === 'female');
+
+ const groups = [];
+ visible.forEach(f => {
+ let g = groups.find(x => x.name === f.group);
+ if (!g) { g = { name: f.group, items: [] }; groups.push(g); }
+ g.items.push(f);
+ });
+
+ return `
+