diff --git a/App.tsx b/App.tsx index 93a4f65..b772fae 100644 --- a/App.tsx +++ b/App.tsx @@ -298,7 +298,7 @@ export function AnaestheticLogApp() { if (!open) cancelPatientSelect(); }} title="Switch patient?" - message={`You have unsaved changes in your current testing session.${selectedPatient ? ` Current: ${selectedPatient.lastName ? `${selectedPatient.lastName.toUpperCase()}, ${selectedPatient.firstName}` : selectedPatient.firstName} (MRN: ${selectedPatient.mrn || '—'}, DOB: ${selectedPatient.dob || 'not recorded'}).` : ''}${pendingPatientSelection ? ` Target: ${pendingPatientSelection.patient.lastName ? `${pendingPatientSelection.patient.lastName.toUpperCase()}, ${pendingPatientSelection.patient.firstName}` : pendingPatientSelection.patient.firstName} (MRN: ${pendingPatientSelection.patient.mrn || '—'}, DOB: ${pendingPatientSelection.patient.dob || 'not recorded'}).` : ''} Switching patients will discard these changes. This cannot be undone.`} + message={`You have unsaved changes in your current testing session.${selectedPatient ? ` Current: ${selectedPatient.lastName ? `${selectedPatient.lastName.toUpperCase()}, ${selectedPatient.firstName}` : selectedPatient.firstName} (REDCap ID: ${selectedPatient.mrn || '—'}, DOB: ${selectedPatient.dob || 'not recorded'}).` : ''}${pendingPatientSelection ? ` Target: ${pendingPatientSelection.patient.lastName ? `${pendingPatientSelection.patient.lastName.toUpperCase()}, ${pendingPatientSelection.patient.firstName}` : pendingPatientSelection.patient.firstName} (REDCap ID: ${pendingPatientSelection.patient.mrn || '—'}, DOB: ${pendingPatientSelection.patient.dob || 'not recorded'}).` : ''} Switching patients will discard these changes. This cannot be undone.`} confirmLabel="Switch patient" cancelLabel="Cancel" variant="danger" diff --git a/e2e/direct-entry.spec.ts b/e2e/direct-entry.spec.ts index 2736d61..eedc0f2 100644 --- a/e2e/direct-entry.spec.ts +++ b/e2e/direct-entry.spec.ts @@ -24,7 +24,7 @@ test.describe('Home quick-start entry points', () => { await expect(page).toHaveURL(/\/testing$/); await expect(page.getByRole('heading', { name: 'Allergy Testing', exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Patient Identity', exact: true })).toBeVisible(); - await expect(page.getByLabel(/MRN/i)).toBeEditable(); + await expect(page.getByLabel(/REDCap ID/i)).toBeEditable(); await expect(page.getByLabel(/First Name/i)).toBeEditable(); await expect(page.getByLabel(/Last Name/i)).toBeEditable(); await expect(page.getByLabel(/Date of Birth/i)).toBeEditable(); @@ -35,7 +35,7 @@ test.describe('Home quick-start entry points', () => { // section 0, where each field shows its own inline error message (not a link). await page.getByRole('button', { name: /7\.\s*Review and save/i }).click(); await page.getByRole('button', { name: 'Save Clinical Record', exact: true }).first().click(); - await expect(page.getByText('MRN is required')).toBeVisible(); + await expect(page.getByText('REDCap ID is required')).toBeVisible(); await expect(page.getByText('First name is required')).toBeVisible(); await expect(page.getByText('Last name is required')).toBeVisible(); }); @@ -48,7 +48,7 @@ test.describe('Home quick-start entry points', () => { await expect(page).toHaveURL(/\/testing$/); await expect(page.getByRole('heading', { name: 'Allergy Testing', exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Patient Identity', exact: true })).toBeVisible(); - await expect(page.getByLabel(/MRN/i)).toBeEditable(); + await expect(page.getByLabel(/REDCap ID/i)).toBeEditable(); await expect(page.getByLabel(/Patient identity/i)).toHaveCount(0); }); diff --git a/e2e/testing-day.spec.ts b/e2e/testing-day.spec.ts index 96e870a..fb7407d 100644 --- a/e2e/testing-day.spec.ts +++ b/e2e/testing-day.spec.ts @@ -117,7 +117,7 @@ test.describe('Testing Day Flow', () => { await page.waitForLoadState('networkidle'); await dismissHelpModal(page); - await page.getByLabel(/^mrn/i).fill('1'); + await page.getByLabel(/^REDCap ID/i).fill('1'); await page.getByLabel(/first name/i).fill('Wei'); await page.getByLabel(/last name/i).fill('Chen'); @@ -138,7 +138,7 @@ test.describe('Testing Day Flow', () => { await expect(page.getByText(/Draft saved/)).toBeVisible({ timeout: 5000 }); await expect(page.getByLabel(/first name/i)).toHaveValue('Wei'); await expect(page.getByLabel(/last name/i)).toHaveValue('Chen'); - await expect(page.getByLabel(/^mrn/i)).toHaveValue('1'); + await expect(page.getByLabel(/^REDCap ID/i)).toHaveValue('1'); // Section 1's field values are also preserved in the draft. await page.getByRole('button', { name: 'Next Section', exact: true }).click(); diff --git a/src/core/screens/LogScreen.tsx b/src/core/screens/LogScreen.tsx index 100e1ed..18a1cfe 100644 --- a/src/core/screens/LogScreen.tsx +++ b/src/core/screens/LogScreen.tsx @@ -142,7 +142,7 @@ export function LogScreen({ const errors = { firstName: selectedPatient.firstName.trim() ? '' : 'First name is required.', lastName: selectedPatient.lastName.trim() ? '' : 'Last name is required.', - mrn: selectedPatient.mrn.trim() ? '' : 'MRN is required.', + mrn: selectedPatient.mrn.trim() ? '' : 'REDCap ID is required.', }; setManualPatientErrors(errors); if (Object.values(errors).some(Boolean)) return; @@ -430,7 +430,7 @@ export function LogScreen({ if (!open) setPendingPatientToSelect(null); }} title="Switch patient?" - message={`You have unsaved changes in your current testing session.${selectedPatient ? ` Current: ${selectedPatient.lastName ? `${selectedPatient.lastName.toUpperCase()}, ${selectedPatient.firstName}` : selectedPatient.firstName} (MRN: ${selectedPatient.mrn || '—'}, DOB: ${selectedPatient.dob || 'not recorded'}).` : ''}${pendingPatientToSelect ? ` Target: ${pendingPatientToSelect.lastName ? `${pendingPatientToSelect.lastName.toUpperCase()}, ${pendingPatientToSelect.firstName}` : pendingPatientToSelect.firstName} (MRN: ${pendingPatientToSelect.mrn || '—'}, DOB: ${pendingPatientToSelect.dob || 'not recorded'}).` : ''} Switching patients will discard these changes. This cannot be undone.`} + message={`You have unsaved changes in your current testing session.${selectedPatient ? ` Current: ${selectedPatient.lastName ? `${selectedPatient.lastName.toUpperCase()}, ${selectedPatient.firstName}` : selectedPatient.firstName} (REDCap ID: ${selectedPatient.mrn || '—'}, DOB: ${selectedPatient.dob || 'not recorded'}).` : ''}${pendingPatientToSelect ? ` Target: ${pendingPatientToSelect.lastName ? `${pendingPatientToSelect.lastName.toUpperCase()}, ${pendingPatientToSelect.firstName}` : pendingPatientToSelect.firstName} (REDCap ID: ${pendingPatientToSelect.mrn || '—'}, DOB: ${pendingPatientToSelect.dob || 'not recorded'}).` : ''} Switching patients will discard these changes. This cannot be undone.`} confirmLabel="Switch patient" cancelLabel="Cancel" variant="danger" @@ -501,7 +501,7 @@ export function LogScreen({
{patientName}
{redact(data.mrn)}
- Note: Patient direct identifiers (name, MRN, DOB, contacts) are omitted. + Note: Patient direct identifiers (name, REDCap ID, DOB, contacts) are omitted.
{redactedFullName}
MRN
+REDCap ID
{redact(data.mrn)}
REDCap Record ID
+REDCap Record ID (secondary)
{redact(patient.redcapId)}
- MRN: {formData.mrn || '—'} + REDCap ID: {formData.mrn || '—'}
Visit Date: {formData.visitDate || '—'}
diff --git a/src/features/testing/components/TestingLogForm.test.tsx b/src/features/testing/components/TestingLogForm.test.tsx
index 47b75cd..8c036fb 100644
--- a/src/features/testing/components/TestingLogForm.test.tsx
+++ b/src/features/testing/components/TestingLogForm.test.tsx
@@ -147,7 +147,7 @@ describe('TestingLogForm (Indexed Workflow)', () => {
it('renders editable identity fields when isDirectEntry is true', () => {
render(
-
URN:
+ REDCap ID:
{patient.mrn}
diff --git a/src/features/testing/services/TestingService.test.ts b/src/features/testing/services/TestingService.test.ts
index 86f83c8..6407ca1 100644
--- a/src/features/testing/services/TestingService.test.ts
+++ b/src/features/testing/services/TestingService.test.ts
@@ -59,7 +59,7 @@ describe('TestingService', () => {
expect(result.isValid).toBe(false);
expect(result.errors).toEqual([
- 'MRN is required',
+ 'REDCap ID is required',
'First name is required',
'Last name is required',
'Visit date is required',
diff --git a/src/features/testing/services/TestingService.ts b/src/features/testing/services/TestingService.ts
index b9b2806..fb0cdb1 100644
--- a/src/features/testing/services/TestingService.ts
+++ b/src/features/testing/services/TestingService.ts
@@ -14,7 +14,7 @@ export class TestingService {
// Required fields
if (!formData.mrn?.trim()) {
- errors.push('MRN is required');
+ errors.push('REDCap ID is required');
}
if (!formData.firstName?.trim()) {
errors.push('First name is required');
diff --git a/src/lib/sentry.test.ts b/src/lib/sentry.test.ts
index be94d11..674bbab 100644
--- a/src/lib/sentry.test.ts
+++ b/src/lib/sentry.test.ts
@@ -18,6 +18,38 @@ describe('scrubPhiFromEvent', () => {
expect(result.exception?.values?.[0].value).not.toContain('12345');
});
+ it('scrubs REDCap IDs and REDCap Record IDs from exception messages', () => {
+ const event1 = {
+ type: undefined,
+ exception: {
+ values: [{ value: 'Failed to save REDCap ID: 12345 for patient' }],
+ },
+ } as SentryEvent;
+ const result1 = scrubPhiFromEvent(event1);
+ expect(result1.exception?.values?.[0].value).toContain('REDCap ID: [REDACTED]');
+ expect(result1.exception?.values?.[0].value).not.toContain('12345');
+
+ const event2 = {
+ type: undefined,
+ exception: {
+ values: [{ value: 'Failed to save REDCap Record ID: 67890 for patient' }],
+ },
+ } as SentryEvent;
+ const result2 = scrubPhiFromEvent(event2);
+ expect(result2.exception?.values?.[0].value).toContain('REDCap Record ID: [REDACTED]');
+ expect(result2.exception?.values?.[0].value).not.toContain('67890');
+
+ const event3 = {
+ type: undefined,
+ exception: {
+ values: [{ value: 'Failed to save REDCap Record ID (secondary): 99999 for patient' }],
+ },
+ } as SentryEvent;
+ const result3 = scrubPhiFromEvent(event3);
+ expect(result3.exception?.values?.[0].value).toContain('REDCap Record ID: [REDACTED]');
+ expect(result3.exception?.values?.[0].value).not.toContain('99999');
+ });
+
it('scrubs record IDs from event messages', () => {
const event = { type: undefined, message: 'lookup failed for REC-441' } as SentryEvent;
@@ -39,12 +71,19 @@ describe('scrubPhiFromEvent', () => {
it('scrubs string values from extras', () => {
const event = {
type: undefined,
- extra: { note: 'MRN: 99', componentStack: 'at Foo' },
+ extra: {
+ note: 'MRN: 99',
+ redcapNote: 'REDCap ID: 101',
+ recordNote: 'REDCap Record ID: 202',
+ componentStack: 'at Foo',
+ },
} as SentryEvent;
const result = scrubPhiFromEvent(event);
expect(result.extra?.note).toBe('MRN: [REDACTED]');
+ expect(result.extra?.redcapNote).toBe('REDCap ID: [REDACTED]');
+ expect(result.extra?.recordNote).toBe('REDCap Record ID: [REDACTED]');
expect(result.extra?.componentStack).toBe('at Foo');
});
@@ -63,12 +102,14 @@ describe('scrubPhiFromEvent', () => {
it('scrubs MRNs from breadcrumb messages', () => {
const event = {
type: undefined,
- breadcrumbs: [{ message: 'MRN: 7' }],
+ breadcrumbs: [{ message: 'MRN: 7' }, { message: 'REDCap ID: 8' }, { message: 'REDCap Record ID: 9' }],
} as SentryEvent;
const result = scrubPhiFromEvent(event);
expect(result.breadcrumbs?.[0].message).toBe('MRN: [REDACTED]');
+ expect(result.breadcrumbs?.[1].message).toBe('REDCap ID: [REDACTED]');
+ expect(result.breadcrumbs?.[2].message).toBe('REDCap Record ID: [REDACTED]');
});
it('passes through events without scrubbed fields', () => {
diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts
index 5855051..565dd44 100644
--- a/src/lib/sentry.ts
+++ b/src/lib/sentry.ts
@@ -19,6 +19,8 @@ async function loadSentry(): Promise