Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions app/assets/sass/_misc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,7 @@ body.js-enabled .app-no-js-only {
}

.app-align-right {
float: right;
* {
text-align: right;
}
text-align: right;
}

.app-suppress-link-styles * {
Expand Down
68 changes: 48 additions & 20 deletions app/lib/generators/mammogram-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ const INCOMPLETE_MAMMOGRAPHY_REASONS = [

// Follow-up appointment options
const INCOMPLETE_MAMMOGRAPHY_FOLLOW_UP_OPTIONS = [
'Yes, as soon as possible',
'Yes, when physical condition improves',
'No, best possible images taken'
"Yes, record as 'to be recalled'",
"No, record as 'partial mammography'"
]

const STANDARD_VIEWS = [
Expand Down Expand Up @@ -213,46 +212,75 @@ const generateMammogramImages = ({
if (isSeedData && hasMissingViews) {
const reason = faker.helpers.arrayElement(INCOMPLETE_MAMMOGRAPHY_REASONS)
const followUp = weighted.select({
'Yes, as soon as possible': 0.4,
'Yes, when physical condition improves': 0.3,
'No, best possible images taken': 0.3
"Yes, record as 'to be recalled'": 0.4,
"No, record as 'partial mammography'": 0.6
})

incompleteMammographyData = {
isIncompleteMammography: 'yes',
incompleteMammographyReason: reason,
isIncompleteMammography: ['yes'],
incompleteMammographyReasons: [reason],
incompleteMammographyFollowUpAppointment: followUp
}

// Add optional details for 'Other' reason or randomly for some cases
if (reason === 'Other' || Math.random() < 0.3) {
incompleteMammographyData.incompleteMammographyReasonDetails =
faker.helpers.arrayElement([
'Participant became unwell during the appointment.',
'Equipment malfunction prevented completion.',
'Participant had to leave urgently.',
'Severe pain prevented further imaging.',
'Implants made positioning very difficult.'
'Participant became unwell during the appointment',
'Equipment malfunction prevented completion',
'Participant had to leave urgently',
'Severe pain prevented further imaging',
'Implants made positioning very difficult'
])
}

// Add details if follow-up depends on physical condition improving
if (followUp === 'Yes, when physical condition improves') {
// Add details if follow-up is YES
if (followUp === "Yes, record as 'to be recalled'") {
incompleteMammographyData.incompleteMammographyFollowUpAppointmentDetails =
faker.helpers.arrayElement([
'Participant has a shoulder injury that should heal in 4-6 weeks.',
'Recent surgery - needs 3 months recovery before rebooking.',
'Mobility issues - suggest booking at hospital site with better accessibility.',
'Chest infection - rebook when recovered.',
'Arm in a cast - expected to be removed in 2 weeks.'
'Participant has a shoulder injury that should heal in 4-6 week.',
'Recent surgery - needs 3 months recovery before rebooking',
'Mobility issues - suggest booking at hospital site with better accessibility',
'Chest infection - rebook when recovered',
'Arm in a cast - expected to be removed in 2 weeks'
])
}
}

// Generate imperfect but best possible data (sometimes)
let imperfectData = {}
if (isSeedData && Math.random() < 0.1) {
imperfectData.isImperfectButBestPossible = ['yes']
}

// Generate notes for reader
// Used for imperfect images to explain why, or occasionally for other reasons
let notesData = {}
if (isSeedData) {
if (imperfectData.isImperfectButBestPossible) {
notesData.notesForReader = faker.helpers.arrayElement([
'Patient found positioning difficult due to shoulder pain',
'Kyphosis made positioning challenging',
'Ideally would have got more tissue but patient unable to tolerate',
'Skin folds present due to weight loss',
'Best possible images achieved'
])
} else if (Math.random() < 0.05) {
notesData.notesForReader = faker.helpers.arrayElement([
'Mole on right breast',
'Pacemaker present',
'Previous surgery scars noted',
'Patient anxious but compliant'
])
}
}

return {
accessionBase,
views,
...incompleteMammographyData,
...imperfectData,
...notesData,
metadata: {
totalImages,
standardViewsCompleted: Object.keys(views).length === 4,
Expand Down
9 changes: 8 additions & 1 deletion app/lib/utils/reading.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ const getReadingProgress = function (
hasPreviousUserReadable: !!previousUserReadableEvent,
nextUserReadableId: nextUserReadableEvent?.id || null,
previousUserReadableId: previousUserReadableEvent?.id || null,
// Whether user has already read the previous/next event (for review page links)
previousUserHasRead: previousUserReadableEvent
? userHasReadEvent(previousUserReadableEvent, currentUserId)
: false,
nextUserHasRead: nextUserReadableEvent
? userHasReadEvent(nextUserReadableEvent, currentUserId)
: false,
// Skipped events
skippedEvents,
isCurrentSkipped: skippedEvents.includes(currentEventId),
Expand Down Expand Up @@ -1046,7 +1053,7 @@ const createReadingBatch = (data, options) => {
name,
clinicId,
batchId = null, // Allow custom batch ID
limit = 50,
limit = 25,
filters = {}
} = options

Expand Down
28 changes: 21 additions & 7 deletions app/lib/utils/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ const STATUS_GROUPS = {
'event_attended_not_screened',
'event_cancelled'
],
active: ['event_scheduled', 'event_checked_in', 'event_in_progress', 'event_paused'],
active: [
'event_scheduled',
'event_checked_in',
'event_in_progress',
'event_paused'
],
eligible_for_reading: ['event_complete', 'event_partially_screened']
}

Expand Down Expand Up @@ -227,6 +232,8 @@ const getStatusTagColour = (status) => {
'normal': 'green',
'recall_for_assessment': 'red',
'technical_recall': 'orange',
'clinical_recall': 'yellow',
'abnormal': 'red',

// Image status
'available': 'green',
Expand All @@ -252,13 +259,12 @@ const getStatusTagColour = (status) => {
'partial_second_read': 'blue',
'mixed_reads': 'yellow',
'mixed_with_arbitration': 'yellow',
'imperfect': 'orange',
'incomplete': 'orange',

'no_events': 'grey',

// Outcomes
'normal': 'green',
'recall_for_assessment': 'red',
'technical_recall': 'grey',
'arbitration': 'orange',
'completed_(blind)': 'grey',

Expand Down Expand Up @@ -290,7 +296,13 @@ const getStatusText = (status) => {
event_did_not_attend: 'Did not attend',
event_attended_not_screened: 'Attended not screened',
event_cancelled: 'Cancelled',
event_rescheduled: 'Reschedule requested'
event_rescheduled: 'Reschedule requested',

// Image reading opinions
technical_recall: 'Technical recall',
clinical_recall: 'Clinical recall',
recall_for_assessment: 'Recall for assessment',
abnormal: 'Abnormal'

// "technical-recall": 'Technical recall',
// "recall-for-assesment": 'Recall for assessment',
Expand All @@ -305,11 +317,13 @@ const filterEventsByStatus = (events, filter) => {
case 'checked-in':
return events.filter((e) => e.status === 'event_checked_in')
case 'in-progress':
return events.filter((e) => e.status === 'event_in_progress' || e.status === 'event_paused')
return events.filter(
(e) => e.status === 'event_in_progress' || e.status === 'event_paused'
)
case 'complete':
return events.filter((e) => isFinal(e))
case 'remaining':
return events.filter((e) => isActive(e))
return events.filter((e) => hasNotStarted(e))
default:
return events
}
Expand Down
Loading