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({
({ ...prev, mrn: '' })); }} - placeholder="Medical Record Number..." + placeholder="REDCap ID..." aria-invalid={!!manualPatientErrors.mrn} aria-describedby={manualPatientErrors.mrn ? 'manual-mrn-error' : undefined} /> @@ -523,14 +523,14 @@ export function LogScreen({
onManualDetailChange('redcapId', e.target.value)} - placeholder="REDCap ID..." + placeholder="Secondary ID..." />
diff --git a/src/features/dashboard/components/Dashboard.test.tsx b/src/features/dashboard/components/Dashboard.test.tsx index b504a03..e6a2590 100644 --- a/src/features/dashboard/components/Dashboard.test.tsx +++ b/src/features/dashboard/components/Dashboard.test.tsx @@ -193,7 +193,7 @@ describe('Dashboard', () => { it('filters patients by search term', async () => { render(); - const searchInput = screen.getByPlaceholderText(/Search by Name, MRN/i); + const searchInput = screen.getByPlaceholderText(/Search by Name, REDCap ID/i); fireEvent.change(searchInput, { target: { value: 'John' } }); await waitFor(() => { @@ -205,7 +205,7 @@ describe('Dashboard', () => { it('filters patients by MRN', async () => { render(); - const searchInput = screen.getByPlaceholderText(/Search by Name, MRN/i); + const searchInput = screen.getByPlaceholderText(/Search by Name, REDCap ID/i); fireEvent.change(searchInput, { target: { value: 'MRN001' } }); await waitFor(() => { @@ -217,7 +217,7 @@ describe('Dashboard', () => { it('clears search when input is cleared', async () => { render(); - const searchInput = screen.getByPlaceholderText(/Search by Name, MRN/i); + const searchInput = screen.getByPlaceholderText(/Search by Name, REDCap ID/i); fireEvent.change(searchInput, { target: { value: 'John' } }); await waitFor(() => { @@ -583,7 +583,7 @@ describe('Dashboard', () => { it('has proper ARIA labels on search input', () => { render(); - const searchInput = screen.getByPlaceholderText(/Search by Name, MRN/i); + const searchInput = screen.getByPlaceholderText(/Search by Name, REDCap ID/i); expect(searchInput).toHaveAttribute('aria-label'); }); @@ -597,7 +597,7 @@ describe('Dashboard', () => { it('is keyboard navigable', () => { render(); - const searchInput = screen.getByPlaceholderText(/Search by Name, MRN/i); + const searchInput = screen.getByPlaceholderText(/Search by Name, REDCap ID/i); searchInput.focus(); expect(searchInput).toHaveFocus(); diff --git a/src/features/dashboard/components/PatientTable.tsx b/src/features/dashboard/components/PatientTable.tsx index 6a46842..ca1bb97 100644 --- a/src/features/dashboard/components/PatientTable.tsx +++ b/src/features/dashboard/components/PatientTable.tsx @@ -220,8 +220,8 @@ const PatientTable: React.FC = ({
@@ -106,7 +106,7 @@ export function ClinicalContextBar({
Name
{formattedName}
-
MRN
+
REDCap ID
{mrn ? redact(mrn) : '—'}
DOB
@@ -146,7 +146,7 @@ export function ClinicalContextBar({ - MRN {mrn ? redact(mrn) : '—'} + REDCap ID {mrn ? redact(mrn) : '—'} diff --git a/src/features/patients/components/PatientIdentityBar.test.tsx b/src/features/patients/components/PatientIdentityBar.test.tsx index 39a023c..1d55c3f 100644 --- a/src/features/patients/components/PatientIdentityBar.test.tsx +++ b/src/features/patients/components/PatientIdentityBar.test.tsx @@ -37,7 +37,7 @@ describe('PatientIdentityBar', () => { render(); const identityBar = screen.getByLabelText('Patient identity'); - expect(identityBar).toHaveTextContent('DOE, Jane·MRN MrN00aB1·DOB not recorded'); + expect(identityBar).toHaveTextContent('DOE, Jane·REDCap ID MrN00aB1·DOB not recorded'); expect(screen.queryByText(/^Reaction/)).not.toBeInTheDocument(); }); diff --git a/src/features/reports/components/ClinicalReport.tsx b/src/features/reports/components/ClinicalReport.tsx index 2c5679c..ead73ba 100644 --- a/src/features/reports/components/ClinicalReport.tsx +++ b/src/features/reports/components/ClinicalReport.tsx @@ -76,7 +76,7 @@ const ClinicalReport: React.FC = ({ data, activeReportSaved

{patientName}

- +

{redact(data.mrn)}

diff --git a/src/features/reports/components/OutboundActionDialog.test.tsx b/src/features/reports/components/OutboundActionDialog.test.tsx index ecf8f8f..4604163 100644 --- a/src/features/reports/components/OutboundActionDialog.test.tsx +++ b/src/features/reports/components/OutboundActionDialog.test.tsx @@ -68,8 +68,8 @@ describe('OutboundActionDialog', () => { expect(screen.getByText(/Transmitted Field Categories:/i)).toBeInTheDocument(); expect(screen.getByText(/Tested drug names and skin\/intradermal test/i)).toBeInTheDocument(); - expect(screen.getAllByText(/Patient direct identifiers \(name, MRN, DOB, contacts\) are omitted/i).length).toBeGreaterThanOrEqual(1); - expect(screen.getByText(/De-identified \(patient direct identifiers: name, MRN, DOB, contacts omitted\)/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Patient direct identifiers \(name, REDCap ID, DOB, contacts\) are omitted/i).length).toBeGreaterThanOrEqual(1); + expect(screen.getByText(/De-identified \(patient direct identifiers: name, REDCap ID, DOB, contacts omitted\)/i)).toBeInTheDocument(); expect(screen.queryByText(/Fully De-identified/i)).not.toBeInTheDocument(); expect(screen.queryByText(/anonymised/i)).not.toBeInTheDocument(); expect(screen.getByText(/This record has already been submitted/i)).toBeInTheDocument(); diff --git a/src/features/reports/components/OutboundActionDialog.tsx b/src/features/reports/components/OutboundActionDialog.tsx index 02e978c..c41d5bb 100644 --- a/src/features/reports/components/OutboundActionDialog.tsx +++ b/src/features/reports/components/OutboundActionDialog.tsx @@ -110,9 +110,9 @@ export function OutboundActionDialog({ icon: , confirmLabel: researchAlreadySubmitted ? 'Already Submitted' : 'Confirm & Submit to Research Registry', destination: propDestination || 'ANZTADC Secure Research Database', - disclosure: propDisclosureMode || 'De-identified (patient direct identifiers: name, MRN, DOB, contacts omitted)', + disclosure: propDisclosureMode || 'De-identified (patient direct identifiers: name, REDCap ID, DOB, contacts omitted)', description: - 'Confirm transmission of de-identified clinical testing results to the anaesthetic reaction registry. Patient direct identifiers (name, MRN, DOB, contacts) are omitted prior to transmission.', + 'Confirm transmission of de-identified clinical testing results to the anaesthetic reaction registry. Patient direct identifiers (name, REDCap ID, DOB, contacts) are omitted prior to transmission.', }; } }; @@ -167,7 +167,7 @@ export function OutboundActionDialog({ {patientName} - MRN: + REDCap ID: {mrn} @@ -206,7 +206,7 @@ export function OutboundActionDialog({ ))}

- Note: Patient direct identifiers (name, MRN, DOB, contacts) are omitted. + Note: Patient direct identifiers (name, REDCap ID, DOB, contacts) are omitted.

)} diff --git a/src/features/reports/components/PowerchartLetter.tsx b/src/features/reports/components/PowerchartLetter.tsx index e0899ec..7765e36 100644 --- a/src/features/reports/components/PowerchartLetter.tsx +++ b/src/features/reports/components/PowerchartLetter.tsx @@ -69,12 +69,12 @@ const PowerchartLetter: React.FC = ({ data, patient, acti

{redactedFullName}

-

MRN

+

REDCap ID

{redact(data.mrn)}

{patient?.redcapId && patient.redcapId !== data.mrn && (
-

REDCap Record ID

+

REDCap Record ID (secondary)

{redact(patient.redcapId)}

)} diff --git a/src/features/reports/components/ReportPrintIdentity.test.tsx b/src/features/reports/components/ReportPrintIdentity.test.tsx index 9ea576e..3683bf1 100644 --- a/src/features/reports/components/ReportPrintIdentity.test.tsx +++ b/src/features/reports/components/ReportPrintIdentity.test.tsx @@ -16,11 +16,11 @@ describe('ReportPrintIdentity', () => { ); // Top print header - expect(screen.getByText('Wei Chen · MRN MRN-12345 · DOB 01/05/1985')).toBeInTheDocument(); + expect(screen.getByText('Wei Chen · REDCap ID MRN-12345 · DOB 01/05/1985')).toBeInTheDocument(); expect(screen.getByText('Anaesthetic Testing Report')).toBeInTheDocument(); // Bottom print footer - expect(screen.getByText('Wei Chen · MRN MRN-12345')).toBeInTheDocument(); + expect(screen.getByText('Wei Chen · REDCap ID MRN-12345')).toBeInTheDocument(); expect(screen.getByText('Date of report: 15/03/2024')).toBeInTheDocument(); }); @@ -33,7 +33,7 @@ describe('ReportPrintIdentity', () => { /> ); - expect(screen.getAllByText('John Doe · MRN MRN-999')).toHaveLength(2); + expect(screen.getAllByText('John Doe · REDCap ID MRN-999')).toHaveLength(2); expect(screen.getByText('Patient Handout')).toBeInTheDocument(); expect(screen.getByText(/Date of report:/)).toBeInTheDocument(); }); diff --git a/src/features/reports/components/ReportPrintIdentity.tsx b/src/features/reports/components/ReportPrintIdentity.tsx index b7cca3b..ab91da7 100644 --- a/src/features/reports/components/ReportPrintIdentity.tsx +++ b/src/features/reports/components/ReportPrintIdentity.tsx @@ -16,7 +16,7 @@ export const ReportPrintIdentity: React.FC = ({ reportTitle, requestDate, }) => { - const patientIdentifier = `${patientName} · MRN ${mrn}${dob ? ` · DOB ${formatDate(dob)}` : ''}`; + const patientIdentifier = `${patientName} · REDCap ID ${mrn}${dob ? ` · DOB ${formatDate(dob)}` : ''}`; const footerDate = requestDate || new Date().toISOString(); return ( @@ -26,7 +26,7 @@ export const ReportPrintIdentity: React.FC = ({ {reportTitle}
- {patientName} · MRN {mrn} + {patientName} · REDCap ID {mrn} Date of report: {formatDate(footerDate)}
diff --git a/src/features/reports/components/ReportsPrintSafety.test.tsx b/src/features/reports/components/ReportsPrintSafety.test.tsx index bc48fc7..9c6370e 100644 --- a/src/features/reports/components/ReportsPrintSafety.test.tsx +++ b/src/features/reports/components/ReportsPrintSafety.test.tsx @@ -26,20 +26,20 @@ describe('reports print safety', () => { const { rerender } = renderReport(); expect(screen.getByRole('heading', { level: 2, name: 'Anaesthetic Testing Report' })).toBeInTheDocument(); - expect(screen.getAllByText(/Wei Chen · MRN MRN-42 · DOB 01\/05\/1980/).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Wei Chen · REDCap ID MRN-42 · DOB 01\/05\/1980/).length).toBeGreaterThan(0); expect(screen.getByText('Date of Birth')).toBeInTheDocument(); expect(screen.getAllByText('01/05/1980').length).toBeGreaterThan(0); rerender(); expect(screen.getByRole('heading', { level: 2, name: 'Allergy Testing Results' })).toBeInTheDocument(); - expect(screen.getAllByText(/Wei Chen · MRN MRN-42 · DOB 01\/05\/1980/).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Wei Chen · REDCap ID MRN-42 · DOB 01\/05\/1980/).length).toBeGreaterThan(0); expect(screen.getByText('Date of Birth')).toBeInTheDocument(); expect(screen.getAllByText('01/05/1980').length).toBeGreaterThan(0); rerender(); expect(screen.getByRole('heading', { level: 2, name: 'Anaesthetic Allergy Clinic' })).toBeInTheDocument(); expect(screen.getByText('Powerchart Letter')).toBeInTheDocument(); - expect(screen.getAllByText(/Wei Chen · MRN MRN-42 · DOB 01\/05\/1980/).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Wei Chen · REDCap ID MRN-42 · DOB 01\/05\/1980/).length).toBeGreaterThan(0); }); it('uses black-and-white distinguishable clinical challenge badges', () => { diff --git a/src/features/testing/components/ControlsSection.tsx b/src/features/testing/components/ControlsSection.tsx index 2686d9a..dffc612 100644 --- a/src/features/testing/components/ControlsSection.tsx +++ b/src/features/testing/components/ControlsSection.tsx @@ -39,7 +39,7 @@ export function VisitDetailsSection({
onInputChange('mrn', e.target.value)} - placeholder="Record number..." + placeholder="REDCap ID..." aria-invalid={!!mrnError} aria-describedby={mrnError ? 'patient-mrn-error' : undefined} /> @@ -150,7 +150,7 @@ export function VisitDetailsSection({
-
MRN
+
REDCap ID
{formData.mrn}
diff --git a/src/features/testing/components/ReviewSaveSection.test.tsx b/src/features/testing/components/ReviewSaveSection.test.tsx index bdfc3eb..a7e882a 100644 --- a/src/features/testing/components/ReviewSaveSection.test.tsx +++ b/src/features/testing/components/ReviewSaveSection.test.tsx @@ -92,7 +92,7 @@ describe('ReviewSaveSection', () => { it('renders validation errors and jumps to corresponding sections', () => { const onJump = vi.fn(); const errors = [ - { message: 'MRN is required', fieldId: 'mrn-input' }, + { message: 'REDCap ID is required', fieldId: 'mrn-input' }, { message: 'Challenge drug must be selected', fieldId: 'challenge-drug' }, { message: 'Drug test panel required', fieldId: 'drug-panel' }, { message: 'Plan is required', fieldId: 'plan-text' }, @@ -110,7 +110,7 @@ describe('ReviewSaveSection', () => { expect(screen.getByText(/Please fix the following issues before saving:/i)).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /MRN is required/i })); + fireEvent.click(screen.getByRole('button', { name: /REDCap ID is required/i })); expect(onJump).toHaveBeenCalledWith(0, 'mrn-input'); fireEvent.click(screen.getByRole('button', { name: /Challenge drug must be selected/i })); diff --git a/src/features/testing/components/ReviewSaveSection.tsx b/src/features/testing/components/ReviewSaveSection.tsx index a751252..0d87387 100644 --- a/src/features/testing/components/ReviewSaveSection.tsx +++ b/src/features/testing/components/ReviewSaveSection.tsx @@ -68,7 +68,7 @@ export const ReviewSaveSection: React.FC = ({ type="button" onClick={() => { const lower = error.message.toLowerCase(); - if (lower.includes('mrn') || lower.includes('name') || lower.includes('visit')) { + if (lower.includes('mrn') || lower.includes('redcap') || lower.includes('name') || lower.includes('visit')) { onJumpToSection(0, error.fieldId); } else if (lower.includes('challenge')) { onJumpToSection(2, error.fieldId); @@ -110,7 +110,7 @@ export const ReviewSaveSection: React.FC = ({ Name: {formData.lastName || '—'}, {formData.firstName || '—'}

- 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(); - expect(screen.getByLabelText(/MRN/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/REDCap ID/i)).toBeInTheDocument(); expect(screen.getByLabelText(/First Name/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Last Name/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Date of Birth/i)).toBeInTheDocument(); @@ -163,7 +163,7 @@ describe('TestingLogForm (Indexed Workflow)', () => { }; render(); - fireEvent.change(screen.getByLabelText(/^MRN/i), { target: { value: 'MRN999' } }); + fireEvent.change(screen.getByLabelText(/^REDCap ID/i), { target: { value: 'MRN999' } }); fireEvent.change(screen.getByLabelText(/^First Name/i), { target: { value: 'Alice' } }); fireEvent.change(screen.getByLabelText(/^Last Name/i), { target: { value: 'Smith' } }); fireEvent.change(screen.getByLabelText(/Date of Birth/i), { target: { value: '1985-05-12' } }); @@ -320,7 +320,7 @@ describe('TestingLogForm (Indexed Workflow)', () => { expect(screen.getByText('Patient Identity')).toBeInTheDocument(); }); - fireEvent.change(screen.getByLabelText(/^MRN/i), { target: { value: 'MRN999' } }); + fireEvent.change(screen.getByLabelText(/^REDCap ID/i), { target: { value: 'MRN999' } }); fireEvent.change(screen.getByLabelText(/^First Name/i), { target: { value: 'Alice' } }); fireEvent.change(screen.getByLabelText(/^Last Name/i), { target: { value: 'Smith' } }); fireEvent.click(screen.getByRole('button', { name: /Save Clinical Record/i })); @@ -345,7 +345,7 @@ describe('TestingLogForm (Indexed Workflow)', () => { expect(screen.getByText('Patient Identity')).toBeInTheDocument(); }); - fireEvent.change(screen.getByLabelText(/^MRN/i), { target: { value: 'MRN999' } }); + fireEvent.change(screen.getByLabelText(/^REDCap ID/i), { target: { value: 'MRN999' } }); fireEvent.change(screen.getByLabelText(/^First Name/i), { target: { value: 'Alice' } }); fireEvent.change(screen.getByLabelText(/^Last Name/i), { target: { value: 'Smith' } }); fireEvent.click(screen.getByRole('button', { name: /Save Clinical Record/i })); diff --git a/src/features/testing/components/TestingLogForm.tsx b/src/features/testing/components/TestingLogForm.tsx index 4025396..3a9f57c 100644 --- a/src/features/testing/components/TestingLogForm.tsx +++ b/src/features/testing/components/TestingLogForm.tsx @@ -75,7 +75,7 @@ const TestingLogForm: React.FC = ({ const toValidationLink = (message: string): ValidationErrorLink => { const lower = message.toLowerCase(); - if (lower.includes('mrn')) return { message, fieldId: 'patient-mrn' }; + if (lower.includes('mrn') || lower.includes('redcap id')) return { message, fieldId: 'patient-mrn' }; if (lower.includes('first name')) return { message, fieldId: 'patient-first-name' }; if (lower.includes('last name')) return { message, fieldId: 'patient-last-name' }; if (lower.includes('visit date')) return { message, fieldId: 'visit-date' }; @@ -86,7 +86,7 @@ const TestingLogForm: React.FC = ({ const getInvalidSectionIndex = (errors: string[]): number => { for (const error of errors) { const lower = error.toLowerCase(); - if (lower.includes('mrn') || lower.includes('first name') || lower.includes('last name') || lower.includes('visit date')) { + if (lower.includes('mrn') || lower.includes('redcap id') || lower.includes('first name') || lower.includes('last name') || lower.includes('visit date')) { return 0; // Patient and visit } if (lower.includes('drug') || lower.includes('test panel')) { diff --git a/src/features/testing/components/TestingLogFormSections.test.tsx b/src/features/testing/components/TestingLogFormSections.test.tsx index e09e835..bbc068f 100644 --- a/src/features/testing/components/TestingLogFormSections.test.tsx +++ b/src/features/testing/components/TestingLogFormSections.test.tsx @@ -100,7 +100,7 @@ describe('SaveActionSection', () => { it('renders validation error summary when errors exist', () => { const errors = [ - { message: 'MRN is required', fieldId: 'patient-mrn' }, + { message: 'REDCap ID is required', fieldId: 'patient-mrn' }, { message: 'Plan is required', fieldId: 'clinical-plan' }, ]; render( @@ -112,7 +112,7 @@ describe('SaveActionSection', () => { ); expect(screen.getByRole('alert')).toBeInTheDocument(); - expect(screen.getByText('MRN is required')).toBeInTheDocument(); + expect(screen.getByText('REDCap ID is required')).toBeInTheDocument(); expect(screen.getByText('Plan is required')).toBeInTheDocument(); }); }); diff --git a/src/features/testing/components/TestingPlanPrintView.tsx b/src/features/testing/components/TestingPlanPrintView.tsx index e02658c..d62fb33 100644 --- a/src/features/testing/components/TestingPlanPrintView.tsx +++ b/src/features/testing/components/TestingPlanPrintView.tsx @@ -233,7 +233,7 @@ const TestingPlanPrintView = ({ patient, data, drugCategories, onProceed }: Test - + 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 { * for structured identifiers, not a substitute for never logging PHI. */ const PHI_PATTERNS: Array<[RegExp, string]> = [ + [/REDCap Record ID(?:\s*\(secondary\))?:?\s*[\w-]+/gi, 'REDCap Record ID: [REDACTED]'], + [/REDCap ID:?\s*[\w-]+/gi, 'REDCap ID: [REDACTED]'], [/MRN:?\s*[\w-]+/gi, 'MRN: [REDACTED]'], [/\bREC-\d+\b/g, '[RECORD-ID]'], [/\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/g, '[DATE]'], // DOB-shaped dates diff --git a/src/shared/utils/reportExporter.test.ts b/src/shared/utils/reportExporter.test.ts index 0f0ac9c..ea9c324 100644 --- a/src/shared/utils/reportExporter.test.ts +++ b/src/shared/utils/reportExporter.test.ts @@ -181,7 +181,7 @@ describe('formatClinicalReportAsText', () => { }), () => '[redacted]'); expect(text).toContain('Patient: [redacted]'); - expect(text).toContain('MRN: [redacted]'); + expect(text).toContain('REDCap ID: [redacted]'); expect(text).toContain('DOB: [redacted]'); expect(text).not.toContain('Alice Smith'); expect(text).not.toContain('MRN-999'); diff --git a/src/shared/utils/reportExporter.ts b/src/shared/utils/reportExporter.ts index 8c8536f..e4c34bb 100644 --- a/src/shared/utils/reportExporter.ts +++ b/src/shared/utils/reportExporter.ts @@ -26,7 +26,7 @@ export function formatClinicalReportAsText(data: LogFormData, redact?: (value: s lines.push('Clinical Immunology & Allergy — Royal Prince Alfred Hospital'); lines.push(''); lines.push(`Patient: ${redact ? redact(`${data.firstName} ${data.lastName}`) : `${data.firstName} ${data.lastName}`}`); - lines.push(`MRN: ${redact ? redact(data.mrn) : data.mrn}`); + lines.push(`REDCap ID: ${redact ? redact(data.mrn) : data.mrn}`); lines.push(`DOB: ${data.dob ? (redact ? redact(formatDate(data.dob)) : formatDate(data.dob)) : 'Unknown'}`); lines.push(`Visit Date: ${data.visitDate ? new Date(data.visitDate).toLocaleDateString('en-AU') : 'Unknown'}`); lines.push(''); diff --git a/src/shared/utils/testingPlanFormatter.test.ts b/src/shared/utils/testingPlanFormatter.test.ts index 9508e9b..d40ac49 100644 --- a/src/shared/utils/testingPlanFormatter.test.ts +++ b/src/shared/utils/testingPlanFormatter.test.ts @@ -47,8 +47,8 @@ describe('testingPlanFormatter', () => { // Patient Details expect(result).toContain('Name: Wei Chen'); - expect(result).toContain('MRN: MRN-12345'); - expect(result).toContain('REDCap ID: REDCAP-999'); + expect(result).toContain('REDCap ID: MRN-12345'); + expect(result).toContain('REDCap Record ID (secondary): REDCAP-999'); expect(result).toContain('DOB: 15/06/1985'); expect(result).toContain('Gender: Female'); expect(result).toContain('Date of Reaction: 10/02/2024'); @@ -203,15 +203,15 @@ describe('testingPlanFormatter', () => { expect(result).not.toContain('*** URGENT'); }); - it('omits REDCap ID when it matches MRN or is missing', () => { + it('omits REDCap Record ID (secondary) when it matches REDCap ID / MRN or is missing', () => { const patientSame = createMockPatient({ mrn: 'MRN-100', redcapId: 'MRN-100' }); const planData = createMockTestingPlanData(); const resultSame = formatTestingPlanAsText(patientSame, planData, sampleDrugCategories); - expect(resultSame).not.toContain('REDCap ID:'); + expect(resultSame).not.toContain('REDCap Record ID (secondary):'); const patientNone = createMockPatient({ mrn: 'MRN-200', redcapId: undefined }); const resultNone = formatTestingPlanAsText(patientNone, planData, sampleDrugCategories); - expect(resultNone).not.toContain('REDCap ID:'); + expect(resultNone).not.toContain('REDCap Record ID (secondary):'); }); it('handles missing DOB and gender gracefully', () => { diff --git a/src/shared/utils/testingPlanFormatter.ts b/src/shared/utils/testingPlanFormatter.ts index 2a287b7..d3a26fe 100644 --- a/src/shared/utils/testingPlanFormatter.ts +++ b/src/shared/utils/testingPlanFormatter.ts @@ -23,9 +23,9 @@ export function formatTestingPlanAsText( lines.push('PATIENT DETAILS'); lines.push('---------------'); lines.push(`Name: ${patient.firstName} ${patient.lastName}`); - lines.push(`MRN: ${patient.mrn}`); + lines.push(`REDCap ID: ${patient.mrn}`); if (patient.redcapId && patient.redcapId !== patient.mrn) { - lines.push(`REDCap ID: ${patient.redcapId}`); + lines.push(`REDCap Record ID (secondary): ${patient.redcapId}`); } lines.push(`DOB: ${patient.dob ? new Date(patient.dob).toLocaleDateString('en-AU') : 'Unknown'}`); lines.push(`Gender: ${patient.gender ?? 'Unknown'}`);
URN:REDCap ID: {patient.mrn}