diff --git a/i18n/en.pot b/i18n/en.pot index 096a85fc..174a29b1 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-09-17T14:21:36.444Z\n" -"PO-Revision-Date: 2024-09-17T14:21:36.444Z\n" +"POT-Creation-Date: 2024-09-25T12:32:54.534Z\n" +"PO-Revision-Date: 2024-09-25T12:32:54.534Z\n" msgid "" msgstr "" @@ -68,16 +68,6 @@ msgid "" "does not match the auto-calculated" msgstr "" -msgid "" -"Module 1 totals with missing sum or sum that does not match the " -"auto-calculated" -msgstr "" - -msgid "" -"Module 1 (Subnational single entry) totals with missing sum or sum that " -"does not match the auto-calculated" -msgstr "" - msgid "Metadata Admin Report" msgstr "" @@ -132,7 +122,6 @@ msgstr "" msgid "Username" msgstr "" -msgid "Template Groups" msgid "Template Groups" msgstr "" @@ -222,6 +211,43 @@ msgid "" "= not done" msgstr "" +msgid "CSY Audit Filters - Operative Care" +msgstr "" + +msgid "Mortality (operative and 24hr) among low-risk patients (ASA score 1-2) " +msgstr "" + +msgid "Mortality (operative and 24hr) in patients with 0 comorbidities " +msgstr "" + +msgid "Mortality (operative and 24hr) in patients who undergo C-section ​" +msgstr "" + +msgid "Number of prior facilities is >1 and case urgency is Emergent" +msgstr "" + +msgid "All cases where the Safe Surgery checklist was not performed" +msgstr "" + +msgid "All cases of OR/OT mortality" +msgstr "" + +msgid "Emergent case and time to OR/OT > 6 hours (admission time-operative time)" +msgstr "" + +msgid "" +"All cases of mortality where the category of surgical or anesthesia " +"provider is not a specialist" +msgstr "" + +msgid "All cases without pulse oximetry used" +msgstr "" + +msgid "" +"Any intra-operative complications in patients with ASA 1-2 or 0 " +"co-morbidities" +msgstr "" + msgid "CSY Audit Filters - Trauma Care" msgstr "" @@ -464,9 +490,6 @@ msgstr "" msgid "Select File" msgstr "" -msgid "Select File" -msgstr "" - msgid "Version has been successfully patched" msgstr "" @@ -876,3 +899,71 @@ msgstr "" msgctxt "Facility Dispo = Morgue or Died" msgid "ETA_EU Dispo = Morgue or Died or ETA" msgstr "" + +msgctxt "ASA Functional Status Score == ASA 1 or ASA 2" +msgid "" +"(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || " +"CSY_OP_Disposition 24 Hours After Surgery == Deceased) && CSY_OP" +msgstr "" + +msgctxt "" +"Disposition 24 Hours After Surgery == Deceased) && Number of Major Medical " +"Comorbidities == 0" +msgid "(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || CSY_OP" +msgstr "" + +msgctxt "SurgicalIntervention 5 == Caesarean Section )" +msgid "" +"(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || " +"CSY_OP_Disposition 24 Hours After Surgery == Deceased) && " +"(CSY_OP_SurgicalIntervention == Caesarean Section || " +"CSY_OP_SurgicalIntervention 2 == Caesarean Section || " +"CSY_OP_SurgicalIntervention 3 == Caesarean Section || " +"CSY_OP_SurgicalIntervention 4 == Caesarean Section || CSY_OP" +msgstr "" + +msgctxt "Urgency of Surgery == Acute emergency, needed within 6 hours (Emergent)" +msgid "ETA_Facility Transfers > 1 && CSY_OP" +msgstr "" + +msgctxt "Safe Surgery Check List Used == No" +msgid "CSY_OP" +msgstr "" + +msgctxt "Disposition on Leaving Operating Theatre == Deceased" +msgid "CSY_OP" +msgstr "" + +msgctxt "" +"Urgency of Surgery == Acute emergency, needed within 6 hours (Emergent) && " +"(Arrival Date and Time - Date and Time of Operating Theatre Arrival > 6 " +"hours)" +msgid "CSY_OP" +msgstr "" + +msgctxt "" +"Category of Surgical Provider 3 ≠ Surgeon with Specialty in Surgery " +"Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician))" +msgid "" +"(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || " +"CSY_OP_Disposition 24 Hours After Surgery == Deceased) && ((CSY_OP_Category " +"of Surgical Provider ≠ Surgeon with Specialty in Surgery Performed or " +"Primary Anaesthesia type ≠ Specialist Anaesthesia Physician) || " +"(CSY_OP_Category of Surgical Provider 2 ≠ Surgeon with Specialty in Surgery " +"Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician) " +"|| (CSY_OP" +msgstr "" + +msgctxt "Monitoring Used Intra-operatively 5 ≠ Pulse oximeter " +msgid "" +"CSY_OP_Monitoring Used Intra-operatively ≠ Pulse oximeter && " +"CSY_OP_Monitoring Used Intra-operatively 2 ≠ Pulse oximeter && " +"CSY_OP_Monitoring Used Intra-operatively 3 ≠ Pulse oximeter && " +"CSY_OP_Monitoring Used Intra-operatively 4 ≠ Pulse oximeter && CSY_OP" +msgstr "" + +msgctxt "Major Medical Comorbidities == 0)" +msgid "" +"If CSY_OP_Intra-operative complication has value && (CSY_OP_ASA Functional " +"Status Score == ASA 1 or ASA 2 || ETA" +msgstr "" diff --git a/i18n/es.po b/i18n/es.po index c948db53..0e8f1b41 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2024-09-17T14:37:32.167Z\n" +"POT-Creation-Date: 2024-09-25T12:27:30.865Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -68,16 +68,6 @@ msgid "" "not match the auto-calculated" msgstr "" -msgid "" -"Module 1 totals with missing sum or sum that does not match the auto-" -"calculated" -msgstr "" - -msgid "" -"Module 1 (Subnational single entry) totals with missing sum or sum that does " -"not match the auto-calculated" -msgstr "" - msgid "Metadata Admin Report" msgstr "" @@ -132,7 +122,6 @@ msgstr "" msgid "Username" msgstr "" -msgid "Template Groups" msgid "Template Groups" msgstr "" @@ -222,6 +211,44 @@ msgid "" "not done" msgstr "" +msgid "CSY Audit Filters - Operative Care" +msgstr "" + +msgid "Mortality (operative and 24hr) among low-risk patients (ASA score 1-2) " +msgstr "" + +msgid "Mortality (operative and 24hr) in patients with 0 comorbidities " +msgstr "" + +msgid "Mortality (operative and 24hr) in patients who undergo C-section ​" +msgstr "" + +msgid "Number of prior facilities is >1 and case urgency is Emergent" +msgstr "" + +msgid "All cases where the Safe Surgery checklist was not performed" +msgstr "" + +msgid "All cases of OR/OT mortality" +msgstr "" + +msgid "" +"Emergent case and time to OR/OT > 6 hours (admission time-operative time)" +msgstr "" + +msgid "" +"All cases of mortality where the category of surgical or anesthesia provider " +"is not a specialist" +msgstr "" + +msgid "All cases without pulse oximetry used" +msgstr "" + +msgid "" +"Any intra-operative complications in patients with ASA 1-2 or 0 co-" +"morbidities" +msgstr "" + msgid "CSY Audit Filters - Trauma Care" msgstr "" @@ -465,9 +492,6 @@ msgstr "" msgid "Select File" msgstr "" -msgid "Select File" -msgstr "" - msgid "Version has been successfully patched" msgstr "" @@ -814,7 +838,6 @@ msgstr "" msgid "Occupation" msgstr "" -msgstr "" msgid "Practising" msgstr "" @@ -879,6 +902,75 @@ msgctxt "Facility Dispo = Morgue or Died" msgid "ETA_EU Dispo = Morgue or Died or ETA" msgstr "" +msgctxt "ASA Functional Status Score == ASA 1 or ASA 2" +msgid "" +"(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || " +"CSY_OP_Disposition 24 Hours After Surgery == Deceased) && CSY_OP" +msgstr "" + +msgctxt "" +"Disposition 24 Hours After Surgery == Deceased) && Number of Major Medical " +"Comorbidities == 0" +msgid "(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || CSY_OP" +msgstr "" + +msgctxt "SurgicalIntervention 5 == Caesarean Section )" +msgid "" +"(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || " +"CSY_OP_Disposition 24 Hours After Surgery == Deceased) && " +"(CSY_OP_SurgicalIntervention == Caesarean Section || " +"CSY_OP_SurgicalIntervention 2 == Caesarean Section || " +"CSY_OP_SurgicalIntervention 3 == Caesarean Section || " +"CSY_OP_SurgicalIntervention 4 == Caesarean Section || CSY_OP" +msgstr "" + +msgctxt "" +"Urgency of Surgery == Acute emergency, needed within 6 hours (Emergent)" +msgid "ETA_Facility Transfers > 1 && CSY_OP" +msgstr "" + +msgctxt "Safe Surgery Check List Used == No" +msgid "CSY_OP" +msgstr "" + +msgctxt "Disposition on Leaving Operating Theatre == Deceased" +msgid "CSY_OP" +msgstr "" + +msgctxt "" +"Urgency of Surgery == Acute emergency, needed within 6 hours (Emergent) && " +"(Arrival Date and Time - Date and Time of Operating Theatre Arrival > 6 " +"hours)" +msgid "CSY_OP" +msgstr "" + +msgctxt "" +"Category of Surgical Provider 3 ≠ Surgeon with Specialty in Surgery " +"Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician))" +msgid "" +"(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || " +"CSY_OP_Disposition 24 Hours After Surgery == Deceased) && ((CSY_OP_Category " +"of Surgical Provider ≠ Surgeon with Specialty in Surgery Performed or " +"Primary Anaesthesia type ≠ Specialist Anaesthesia Physician) || " +"(CSY_OP_Category of Surgical Provider 2 ≠ Surgeon with Specialty in Surgery " +"Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician) || " +"(CSY_OP" +msgstr "" + +msgctxt "Monitoring Used Intra-operatively 5 ≠ Pulse oximeter " +msgid "" +"CSY_OP_Monitoring Used Intra-operatively ≠ Pulse oximeter && " +"CSY_OP_Monitoring Used Intra-operatively 2 ≠ Pulse oximeter && " +"CSY_OP_Monitoring Used Intra-operatively 3 ≠ Pulse oximeter && " +"CSY_OP_Monitoring Used Intra-operatively 4 ≠ Pulse oximeter && CSY_OP" +msgstr "" + +msgctxt "Major Medical Comorbidities == 0)" +msgid "" +"If CSY_OP_Intra-operative complication has value && (CSY_OP_ASA Functional " +"Status Score == ASA 1 or ASA 2 || ETA" +msgstr "" + #~ msgid "Add" #~ msgstr "Añadir" diff --git a/package.json b/package.json index eb0933a7..fa26559f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,8 @@ "two-factor-monitoring-build": "REACT_APP_DHIS2_BASE_URL='' REACT_APP_DHIS2_AUTH='' yarn two-factor-monitoring-build-folder && rm -f $npm_package_name.zip && cd build && rm -f manifest.json mal-favicon.ico && zip --quiet -r ../$npm_package_name.zip *", "csy-audit-emergency-build-folder": "rm -rf build/ && d2-manifest package.json manifest.webapp --manifest.version=$npm_package_reportVersions_csy_audit_emergency && react-scripts build && yarn run csy-audit-emergency-manifest && cp -r i18n icon.png build", "csy-audit-emergency-build": "REACT_APP_DHIS2_BASE_URL='' REACT_APP_DHIS2_AUTH='' yarn csy-audit-emergency-build-folder && rm -f $npm_package_name.zip && cd build && rm -f manifest.json mal-favicon.ico && zip --quiet -r ../$npm_package_name.zip *", + "csy-audit-operative-build-folder": "rm -rf build/ && d2-manifest package.json manifest.webapp --manifest.version=$npm_package_reportVersions_csy_audit_operative && react-scripts build && yarn run csy-audit-operative-manifest && cp -r i18n icon.png build", + "csy-audit-operative-build": "REACT_APP_DHIS2_BASE_URL='' REACT_APP_DHIS2_AUTH='' yarn csy-audit-operative-build-folder && rm -f $npm_package_name.zip && cd build && rm -f manifest.json mal-favicon.ico && zip --quiet -r ../$npm_package_name.zip *", "csy-audit-trauma-build-folder": "rm -rf build/ && d2-manifest package.json manifest.webapp --manifest.version=$npm_package_reportVersions_csy_audit_trauma && react-scripts build && yarn run csy-audit-trauma-manifest && cp -r i18n icon.png build", "csy-audit-trauma-build": "REACT_APP_DHIS2_BASE_URL='' REACT_APP_DHIS2_AUTH='' yarn csy-audit-trauma-build-folder && rm -f $npm_package_name.zip && cd build && rm -f manifest.json mal-favicon.ico && zip --quiet -r ../$npm_package_name.zip *", "csy-summary-mortality-build-folder": "rm -rf build/ && d2-manifest package.json manifest.webapp --manifest.version=$npm_package_reportVersions_csy_summary_mortality && react-scripts build && yarn run csy-summary-mortality-manifest && cp -r i18n icon.png build", @@ -94,6 +96,7 @@ "authorities-monitoring-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_authorities_monitoring", "two-factor-monitoring-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_twofactor_monitoring", "csy-audit-emergency-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_csy_audit_emergency", + "csy-audit-operative-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_csy_audit_operative", "csy-audit-trauma-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_csy_audit_trauma", "csy-summary-mortality-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_csy_summary_mortality", "csy-summary-patient-manifest": "d2-manifest package.json build/manifest.webapp --manifest.version=$npm_package_reportVersions_csy_summary_patient", @@ -123,6 +126,7 @@ "two-factor-monitoring": "1.0.0", "authorities-monitoring": "1.0.0", "csy-audit-emergency": "1.0.0", + "csy-audit-operative": "1.0.0", "csy-audit-trauma": "1.0.0", "csy-summary-mortality": "1.0.0", "csy-summary-patient": "1.0.0", @@ -208,4 +212,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/compositionRoot.ts b/src/compositionRoot.ts index 710e5a61..198ed612 100644 --- a/src/compositionRoot.ts +++ b/src/compositionRoot.ts @@ -37,6 +37,7 @@ import { GetSubscriptionUseCase } from "./domain/reports/mal-data-subscription/u import { GetMonitoringUseCase as GetSubscriptionMonitoringUseCase } from "./domain/reports/mal-data-subscription/usecases/GetMonitoringUseCase"; import { SaveMonitoringUseCase as SaveSubscriptionMonitoringUseCase } from "./domain/reports/mal-data-subscription/usecases/SaveMonitoringUseCase"; import { AuditItemD2Repository as CSYAuditEmergencyD2Repository } from "./data/reports/csy-audit-emergency/AuditItemD2Repository"; +import { AuditItemD2Repository as CSYAuditOperativeD2Repository } from "./data/reports/csy-audit-operative/AuditItemD2Repository"; import { GetAuditEmergencyUseCase } from "./domain/reports/csy-audit-emergency/usecases/GetAuditEmergencyUseCase"; import { SaveAuditEmergencyUseCase } from "./domain/reports/csy-audit-emergency/usecases/SaveAuditEmergencyUseCase"; import { GetAuditTraumaUseCase } from "./domain/reports/csy-audit-trauma/usecases/GetAuditTraumaUseCase"; @@ -104,10 +105,12 @@ import { GetMonitoringTwoFactorColumnsUseCase } from "./domain/reports/twofactor import { SaveMonitoringTwoFactorUseCase } from "./domain/reports/twofactor-monitoring/usecases/SaveMonitoringTwoFactorUseCase"; import { MonitoringTwoFactorD2Repository } from "./data/reports/twofactor-monitoring/MonitoringTwoFactorD2Repository"; import { GetOrgUnitsWithChildrenUseCase } from "./domain/reports/glass-data-submission/usecases/GetOrgUnitsWithChildrenUseCase"; +import { GetAuditOperativeUseCase } from "./domain/reports/csy-audit-operative/usecases/GetAuditOperativeUseCase"; export function getCompositionRoot(api: D2Api) { const configRepository = new Dhis2ConfigRepository(api, getReportType()); const csyAuditEmergencyRepository = new CSYAuditEmergencyD2Repository(api); + const csyAuditOperativeRepository = new CSYAuditOperativeD2Repository(api); const csyAuditTraumaRepository = new CSYAuditTraumaD2Repository(api); const dataCommentsRepository = new NHWADataCommentsDefaultRepository(api); const dataApprovalRepository = new NHWADataApprovalDefaultRepository(api); @@ -174,6 +177,9 @@ export function getCompositionRoot(api: D2Api) { get: new GetAuditEmergencyUseCase(csyAuditEmergencyRepository), save: new SaveAuditEmergencyUseCase(csyAuditEmergencyRepository), }), + auditOperative: getExecute({ + get: new GetAuditOperativeUseCase(csyAuditOperativeRepository), + }), auditTrauma: getExecute({ get: new GetAuditTraumaUseCase(csyAuditTraumaRepository), save: new SaveAuditTraumaUseCase(csyAuditTraumaRepository), diff --git a/src/data/common/Dhis2ConfigRepository.ts b/src/data/common/Dhis2ConfigRepository.ts index 3b2cb29d..88b05561 100644 --- a/src/data/common/Dhis2ConfigRepository.ts +++ b/src/data/common/Dhis2ConfigRepository.ts @@ -58,6 +58,12 @@ const base = { constantCode: "", approvalWorkflows: { namePrefix: "" }, }, + auditOperative: { + dataSets: { namePrefix: "NONE", nameExcluded: /-APVD$/ }, + sqlViewNames: [], + constantCode: "", + approvalWorkflows: { namePrefix: "" }, + }, auditTrauma: { dataSets: { namePrefix: "NONE", nameExcluded: /-APVD$/ }, sqlViewNames: [], diff --git a/src/data/reports/csy-audit-operative/AuditItemD2Repository.ts b/src/data/reports/csy-audit-operative/AuditItemD2Repository.ts new file mode 100644 index 00000000..0b3b62ef --- /dev/null +++ b/src/data/reports/csy-audit-operative/AuditItemD2Repository.ts @@ -0,0 +1,381 @@ +import { emptyPage, paginate, PaginatedObjects } from "../../../domain/common/entities/PaginatedObjects"; +import { D2Api } from "../../../types/d2-api"; +import { AuditItem, AuditType } from "../../../domain/reports/csy-audit-operative/entities/AuditItem"; +import { + AuditItemRepository, + AuditOptions, +} from "../../../domain/reports/csy-audit-operative/repositories/AuditRepository"; +import { getOrgUnitIdsFromPaths } from "../../../domain/common/entities/OrgUnit"; +import { promiseMap } from "../../../utils/promises"; +import { getEventQueryString } from "../../common/entities/AuditAnalytics"; +import { + AuditAnalyticsData, + AuditAnalyticsResponse, + buildRefs, +} from "../../../domain/common/entities/AuditAnalyticsResponse"; +import { Maybe } from "../../../types/utils"; +import { Id } from "../../../domain/common/entities/Base"; +import _ from "lodash"; + +export class AuditItemD2Repository implements AuditItemRepository { + constructor(private api: D2Api) {} + + async get(options: AuditOptions): Promise> { + const { paging, year, orgUnitPaths, quarter, auditType } = options; + const period = !quarter ? year : `${year}${quarter}`; + const orgUnitIds = getOrgUnitIdsFromPaths(orgUnitPaths); + + if (_.isEmpty(orgUnitIds)) return emptyPage; + + const auditItems = await this.getAuditItems(auditType, orgUnitIds, period); + + return paginate(auditItems, paging); + } + + private async getAuditItems(auditType: AuditType, orgUnitIds: string[], period: string): Promise { + const queryStrings = auditQueryStrings[auditType]; + + const analyticsResponse = await promiseMap(queryStrings, async queryString => { + const { programs, programStages } = metadata; + const query = `${queryString}&dimension=${metadata.dataElements.etaRegistryId}`; + + const eventQueryString = getEventQueryString( + programs.operativeCareProgramId, + programStages.operativeCareProgramStageId, + orgUnitIds.join(";"), + period, + query + ); + + const analyticsResponse = await this.api.get(eventQueryString).getData(); + + return new AuditAnalyticsData(analyticsResponse); + }); + + return this.getAuditItemsByAuditType(auditType, analyticsResponse); + } + + private getColumnValues(data: Maybe, id: Id): string[] { + return data ? data.getColumnValues(id) : []; + } + + private getAuditItemsByAuditType(auditType: AuditType, data: AuditAnalyticsData[]): AuditItem[] { + return buildRefs(this.getMatchedIds(auditType, data)); + } + + private getMatchedIds(auditType: AuditType, data: AuditAnalyticsData[]): string[] { + const { arrivalDateId, etaRegistryId, firstOTDateId } = metadata.dataElements; + + switch (auditType) { + case "lowRiskMortality": { + const [deceasedDispoData, postSurgeryDispoData, asaScoreData] = data; + + const deceasedDispoIds = this.getColumnValues(deceasedDispoData, etaRegistryId); + const postSurgeryDispoIds = this.getColumnValues(postSurgeryDispoData, etaRegistryId); + const asaScoreIds = this.getColumnValues(asaScoreData, etaRegistryId); + + const matchedDispoIds = _.union(deceasedDispoIds, postSurgeryDispoIds); + + return _.intersection(matchedDispoIds, asaScoreIds); + } + case "zeroComorbidityMortality": { + const [deceasedDispoData, postSurgeryDispoData, medicalComorbiditiesData] = data; + + const deceasedDispoIds = this.getColumnValues(deceasedDispoData, etaRegistryId); + const postSurgeryDispoIds = this.getColumnValues(postSurgeryDispoData, etaRegistryId); + const medicalComorbiditiesIds = this.getColumnValues(medicalComorbiditiesData, etaRegistryId); + + const matchedDispoIds = _.union(deceasedDispoIds, postSurgeryDispoIds); + + return _.intersection(matchedDispoIds, medicalComorbiditiesIds); + } + case "cSectionMortality": { + const [ + deceasedDispoData, + postSurgeryDispoData, + surgicalInterventionData, + surgicalIntervention2Data, + surgicalIntervention3Data, + surgicalIntervention4Data, + surgicalIntervention5Data, + ] = data; + + const deceasedDispoIds = this.getColumnValues(deceasedDispoData, etaRegistryId); + const postSurgeryDispoIds = this.getColumnValues(postSurgeryDispoData, etaRegistryId); + const matchedDispoIds = _.union(deceasedDispoIds, postSurgeryDispoIds); + + const surgicalInterventionIds = this.getColumnValues(surgicalInterventionData, etaRegistryId); + const surgicalIntervention2Ids = this.getColumnValues(surgicalIntervention2Data, etaRegistryId); + const surgicalIntervention3Ids = this.getColumnValues(surgicalIntervention3Data, etaRegistryId); + const surgicalIntervention4Ids = this.getColumnValues(surgicalIntervention4Data, etaRegistryId); + const surgicalIntervention5Ids = this.getColumnValues(surgicalIntervention5Data, etaRegistryId); + const matchedSurgicalInterventionIds = _.union( + surgicalInterventionIds, + surgicalIntervention2Ids, + surgicalIntervention3Ids, + surgicalIntervention4Ids, + surgicalIntervention5Ids + ); + + return _.intersection(matchedDispoIds, matchedSurgicalInterventionIds); + } + case "emergentCase": { + const [etaFacilityTransfersData, urgencyOfSurgeryData] = data; + + const etaFacilityTransfersIds = this.getColumnValues(etaFacilityTransfersData, etaRegistryId); + const urgencyOfSurgeryIds = this.getColumnValues(urgencyOfSurgeryData, etaRegistryId); + + return _.intersection(etaFacilityTransfersIds, urgencyOfSurgeryIds); + } + case "surgeryChecklist": { + const [surgeryChecklistData] = data; + const surgeryChecklistIds = this.getColumnValues(surgeryChecklistData, etaRegistryId); + + return surgeryChecklistIds; + } + case "otMortality": { + const [deceasedDispoData] = data; + const deceasedDispoIds = this.getColumnValues(deceasedDispoData, etaRegistryId); + + return deceasedDispoIds; + } + case "acuteEmergentCase": { + const [urgencyOfSurgeryData, arrivalDateData, firstOTDateData] = data; + if (!arrivalDateData || !firstOTDateData) return []; + + const urgencyOfSurgeryIds = this.getColumnValues(urgencyOfSurgeryData, etaRegistryId); + + const dateIds = this.getColumnValues(arrivalDateData, etaRegistryId); + const arrivalDateIds = this.getColumnValues(arrivalDateData, arrivalDateId); + const firstOTDateIds = this.getColumnValues(firstOTDateData, firstOTDateId); + const arrivalDates = _.map(arrivalDateIds, arrivalDate => new Date(arrivalDate).getTime()); + const firstOTDates = _.map(firstOTDateIds, firstOTDate => new Date(firstOTDate).getTime()); + + const timeDiffIds = _.compact( + _.filter(_.zip(dateIds, arrivalDates, firstOTDates), ([, arrivalDate, firstOTDate]) => { + const timeDifferenceInHours = 6; + const arrivalTime = arrivalDate ?? 0; + const firstOTTime = firstOTDate ?? 0; + + return firstOTTime - arrivalTime > convertHoursToMilliseconds(timeDifferenceInHours); + }).map(([id]) => id) + ); + + return _.intersection(urgencyOfSurgeryIds, timeDiffIds); + } + case "nonSpecialistMortality": { + const [ + deceasedDispoData, + postSurgeryDispoData, + anaesthesiaProviderData, + surgicalProviderCategoryData, + surgicalProviderCategory2Data, + surgicalProviderCategory3Data, + ] = data; + + const deceasedDispoIds = this.getColumnValues(deceasedDispoData, etaRegistryId); + const postSurgeryDispoIds = this.getColumnValues(postSurgeryDispoData, etaRegistryId); + const matchedDispoIds = _.union(deceasedDispoIds, postSurgeryDispoIds); + + const anaesthesiaProviderIds = this.getColumnValues(anaesthesiaProviderData, etaRegistryId); + const surgicalProviderCategoryIds = this.getColumnValues(surgicalProviderCategoryData, etaRegistryId); + const surgicalProviderCategory2Ids = this.getColumnValues(surgicalProviderCategory2Data, etaRegistryId); + const surgicalProviderCategory3Ids = this.getColumnValues(surgicalProviderCategory3Data, etaRegistryId); + const matchedNonSpecialistSurgicalCategoryIds = _.union( + anaesthesiaProviderIds, + surgicalProviderCategoryIds, + surgicalProviderCategory2Ids, + surgicalProviderCategory3Ids + ); + + return _.intersection(matchedDispoIds, matchedNonSpecialistSurgicalCategoryIds); + } + case "pulseOximetry": { + const [ + intraOperativeData, + intraOperative2Data, + intraOperative3Data, + intraOperative4Data, + intraOperative5Data, + ] = data; + + const intraOperativeIds = this.getColumnValues(intraOperativeData, etaRegistryId); + const intraOperative2Ids = this.getColumnValues(intraOperative2Data, etaRegistryId); + const intraOperative3Ids = this.getColumnValues(intraOperative3Data, etaRegistryId); + const intraOperative4Ids = this.getColumnValues(intraOperative4Data, etaRegistryId); + const intraOperative5Ids = this.getColumnValues(intraOperative5Data, etaRegistryId); + + return _.intersection( + intraOperativeIds, + intraOperative2Ids, + intraOperative3Ids, + intraOperative4Ids, + intraOperative5Ids + ); + } + case "intraOperativeComplications": { + const [ + intraOperativeComplicationData, + intraOperativeComplication2Data, + intraOperativeComplication3Data, + intraOperativeComplication4Data, + intraOperativeComplication5Data, + asaScoreData, + medicalComorbiditiesData, + ] = data; + + const intraOperativeComplicationIds = this.getColumnValues( + intraOperativeComplicationData, + etaRegistryId + ); + const intraOperativeComplication2Ids = this.getColumnValues( + intraOperativeComplication2Data, + etaRegistryId + ); + const intraOperativeComplication3Ids = this.getColumnValues( + intraOperativeComplication3Data, + etaRegistryId + ); + const intraOperativeComplication4Ids = this.getColumnValues( + intraOperativeComplication4Data, + etaRegistryId + ); + const intraOperativeComplication5Ids = this.getColumnValues( + intraOperativeComplication5Data, + etaRegistryId + ); + const intraOperativeComplicationsMatchedIds = _.union( + intraOperativeComplicationIds, + intraOperativeComplication2Ids, + intraOperativeComplication3Ids, + intraOperativeComplication4Ids, + intraOperativeComplication5Ids + ); + + const asaScoreIds = this.getColumnValues(asaScoreData, etaRegistryId); + const medicalComorbiditiesIds = this.getColumnValues(medicalComorbiditiesData, etaRegistryId); + const matchedIds = _.union(asaScoreIds, medicalComorbiditiesIds); + + return _.intersection(intraOperativeComplicationsMatchedIds, matchedIds); + } + default: + return []; + } + } +} + +const metadata = { + programs: { + operativeCareProgramId: "Cd144iCAheH", + }, + programStages: { + operativeCareProgramStageId: "fR7MnAYI7qO", + }, + dataElements: { + etaRegistryId: "QStbireWKjW", + leavingTheatreDispoId: "RFHUqfwttmQ", + postSurgeryDispoId: "HY1cx8VUKTJ", + functionalStatusScoreId: "DDMhyMh8Akg", + majorMedicalComorbiditiesId: "VFHOGGrm6U2", + surgicalIntervention: "ErKjmQCZbX6", + surgicalIntervention2: "htjb6oO289L", + surgicalIntervention3: "Fz9M9wisOLE", + surgicalIntervention4: "qYqAIU3egh1", + surgicalIntervention5: "vlIZHCoDicY", + etaFacilityTransfersId: "KnO9B1STfzZ", + urgencyOfSurgeryId: "rb99kcPzmP8", + safeSurgeryChecklistId: "yfhmIPel90Z", + arrivalDateId: "E7Ijomzpk7n", + firstOTDateId: "IBstQUA3RPC", + surgicalProviderCategoryId: "iZfmQzsgdBb", + surgicalProviderCategory2Id: "t5C6iIrp0Y4", + surgicalProviderCategory3Id: "Oj0rFoQJysS", + anaesthesiaProviderId: "piS3HyOQpZo", + intraOperativeId: "B07nT1jiYrt", + intraOperative2Id: "IFzupi1AWLu", + intraOperative3Id: "ttLGITjPyS5", + intraOperative4Id: "RYBoWWsrVWi", + intraOperative5Id: "C2RIUnwcfy7", + intraOperativeComplication: "iWPQ5idqXeS", + intraOperativeComplication2: "DSxubU0ucPo", + intraOperativeComplication3: "xvVSTJgbG78", + intraOperativeComplication4: "r2CDCnH86zY", + intraOperativeComplication5: "RscNs7nxlwM", + }, + // option set codes + optionSets: { + leavingTheatreDispoDeceased: "3", + postSurgeryDispoDeceased: "3", + asa1FunctionalStatusScore: "1", + asa2FunctionalStatusScore: "2", + cSectionSurgicalIntervention: "19", + acuteEmergency: "1", + noSafeSurgery: "0", + surgeonSpecialist: "1", + anaestheticPhysicianSpecialist: "1", + pulseOximiterMonitoring: "1", + }, +}; + +const { dataElements, optionSets } = metadata; + +const auditQueryStrings: Record = { + lowRiskMortality: [ + `dimension=${dataElements.leavingTheatreDispoId}:EQ:${optionSets.leavingTheatreDispoDeceased}`, + `dimension=${dataElements.postSurgeryDispoId}:EQ:${optionSets.postSurgeryDispoDeceased}`, + `dimension=${dataElements.functionalStatusScoreId}:IN:${optionSets.asa1FunctionalStatusScore};${optionSets.asa2FunctionalStatusScore}`, + ], + zeroComorbidityMortality: [ + `dimension=${dataElements.leavingTheatreDispoId}:EQ:${optionSets.leavingTheatreDispoDeceased}`, + `dimension=${dataElements.postSurgeryDispoId}:EQ:${optionSets.postSurgeryDispoDeceased}`, + `dimension=${dataElements.majorMedicalComorbiditiesId}:EQ:0`, + ], + cSectionMortality: [ + `dimension=${dataElements.leavingTheatreDispoId}:EQ:${optionSets.leavingTheatreDispoDeceased}`, + `dimension=${dataElements.postSurgeryDispoId}:EQ:${optionSets.postSurgeryDispoDeceased}`, + `dimension=${dataElements.surgicalIntervention}:EQ:${optionSets.cSectionSurgicalIntervention}`, + `dimension=${dataElements.surgicalIntervention2}:EQ:${optionSets.cSectionSurgicalIntervention}`, + `dimension=${dataElements.surgicalIntervention3}:EQ:${optionSets.cSectionSurgicalIntervention}`, + `dimension=${dataElements.surgicalIntervention4}:EQ:${optionSets.cSectionSurgicalIntervention}`, + `dimension=${dataElements.surgicalIntervention5}:EQ:${optionSets.cSectionSurgicalIntervention}`, + ], + emergentCase: [ + `dimension=${dataElements.etaFacilityTransfersId}:GT:1`, + `dimension=${dataElements.urgencyOfSurgeryId}:EQ:${optionSets.acuteEmergency}`, + ], + surgeryChecklist: [`dimension=${dataElements.safeSurgeryChecklistId}:EQ:${optionSets.noSafeSurgery}`], + otMortality: [`dimension=${dataElements.leavingTheatreDispoId}:EQ:${optionSets.leavingTheatreDispoDeceased}`], + acuteEmergentCase: [ + `dimension=${dataElements.urgencyOfSurgeryId}:EQ:${optionSets.acuteEmergency}`, + `dimension=${dataElements.arrivalDateId}`, + `dimension=${dataElements.firstOTDateId}`, + ], + nonSpecialistMortality: [ + `dimension=${dataElements.leavingTheatreDispoId}:EQ:${optionSets.leavingTheatreDispoDeceased}`, + `dimension=${dataElements.postSurgeryDispoId}:EQ:${optionSets.postSurgeryDispoDeceased}`, + `dimension=${dataElements.anaesthesiaProviderId}:NE:${optionSets.anaestheticPhysicianSpecialist}`, + `dimension=${dataElements.surgicalProviderCategoryId}:NE:${optionSets.surgeonSpecialist}`, + `dimension=${dataElements.surgicalProviderCategory2Id}:NE:${optionSets.surgeonSpecialist}`, + `dimension=${dataElements.surgicalProviderCategory3Id}:NE:${optionSets.surgeonSpecialist}`, + ], + pulseOximetry: [ + `dimension=${dataElements.intraOperativeId}:NE:${optionSets.pulseOximiterMonitoring}`, + `dimension=${dataElements.intraOperative2Id}:NE:${optionSets.pulseOximiterMonitoring}`, + `dimension=${dataElements.intraOperative3Id}:NE:${optionSets.pulseOximiterMonitoring}`, + `dimension=${dataElements.intraOperative4Id}:NE:${optionSets.pulseOximiterMonitoring}`, + `dimension=${dataElements.intraOperative5Id}:NE:${optionSets.pulseOximiterMonitoring}`, + ], + intraOperativeComplications: [ + `dimension=${dataElements.intraOperativeComplication}:NE:NV`, + `dimension=${dataElements.intraOperativeComplication2}:NE:NV`, + `dimension=${dataElements.intraOperativeComplication3}:NE:NV`, + `dimension=${dataElements.intraOperativeComplication4}:NE:NV`, + `dimension=${dataElements.intraOperativeComplication5}:NE:NV`, + `dimension=${dataElements.functionalStatusScoreId}:IN:${optionSets.asa1FunctionalStatusScore};${optionSets.asa2FunctionalStatusScore}`, + `dimension=${dataElements.majorMedicalComorbiditiesId}:EQ:0`, + ], +}; + +function convertHoursToMilliseconds(hours: number): number { + return hours * 60 * 60 * 1000; +} diff --git a/src/domain/common/entities/AuditAnalyticsResponse.ts b/src/domain/common/entities/AuditAnalyticsResponse.ts index a5df6655..8220ad85 100644 --- a/src/domain/common/entities/AuditAnalyticsResponse.ts +++ b/src/domain/common/entities/AuditAnalyticsResponse.ts @@ -17,9 +17,12 @@ export class AuditAnalyticsData { public getColumnValues(columnId: string): string[] { const columnIndex = this.getColumnIndex(columnId); - const values = _(this.rows) - .map(row => row[columnIndex]) - .compact() + + const values: string[] = _(this.rows) + .map(row => { + const cellValue = row[columnIndex]; + return cellValue ? cellValue : ""; + }) .value(); return values; diff --git a/src/domain/common/entities/ReportType.ts b/src/domain/common/entities/ReportType.ts index 6ad9b420..51664417 100644 --- a/src/domain/common/entities/ReportType.ts +++ b/src/domain/common/entities/ReportType.ts @@ -5,6 +5,7 @@ export type ReportType = | "glass" | "glass-admin" | "auditEmergency" + | "auditOperative" | "auditTrauma" | "summary-patient" | "summary-mortality" diff --git a/src/domain/reports/csy-audit-operative/entities/AuditItem.ts b/src/domain/reports/csy-audit-operative/entities/AuditItem.ts new file mode 100644 index 00000000..38b94fd8 --- /dev/null +++ b/src/domain/reports/csy-audit-operative/entities/AuditItem.ts @@ -0,0 +1,15 @@ +export interface AuditItem { + registerId: string; +} + +export type AuditType = + | "lowRiskMortality" + | "zeroComorbidityMortality" + | "cSectionMortality" + | "emergentCase" + | "surgeryChecklist" + | "otMortality" + | "acuteEmergentCase" + | "nonSpecialistMortality" + | "pulseOximetry" + | "intraOperativeComplications"; diff --git a/src/domain/reports/csy-audit-operative/repositories/AuditRepository.ts b/src/domain/reports/csy-audit-operative/repositories/AuditRepository.ts new file mode 100644 index 00000000..f4c44843 --- /dev/null +++ b/src/domain/reports/csy-audit-operative/repositories/AuditRepository.ts @@ -0,0 +1,15 @@ +import { PaginatedObjects, Paging, Sorting } from "../../../common/entities/PaginatedObjects"; +import { AuditItem, AuditType } from "../entities/AuditItem"; + +export interface AuditItemRepository { + get(options: AuditOptions): Promise>; +} + +export interface AuditOptions { + paging: Paging; + sorting: Sorting; + orgUnitPaths: string[]; + auditType: AuditType; + year: string; + quarter?: string; +} diff --git a/src/domain/reports/csy-audit-operative/usecases/GetAuditOperativeUseCase.ts b/src/domain/reports/csy-audit-operative/usecases/GetAuditOperativeUseCase.ts new file mode 100644 index 00000000..c7b97d4f --- /dev/null +++ b/src/domain/reports/csy-audit-operative/usecases/GetAuditOperativeUseCase.ts @@ -0,0 +1,11 @@ +import { PaginatedObjects } from "../../../common/entities/PaginatedObjects"; +import { AuditItem } from "../entities/AuditItem"; +import { AuditOptions, AuditItemRepository } from "../repositories/AuditRepository"; + +export class GetAuditOperativeUseCase { + constructor(private auditRepository: AuditItemRepository) {} + + execute(options: AuditOptions): Promise> { + return this.auditRepository.get(options); + } +} diff --git a/src/webapp/reports/Reports.tsx b/src/webapp/reports/Reports.tsx index 36fc602f..412425a7 100644 --- a/src/webapp/reports/Reports.tsx +++ b/src/webapp/reports/Reports.tsx @@ -18,6 +18,7 @@ import AuthoritiesMonitoringReport from "./authorities-monitoring/AuthoritiesMon import GLASSAdminReport from "./glass-admin/GLASSAdminReport"; import { TwoFactorMonitoringReport } from "./two-factor-monitor/TwoFactorMonitoringReport"; import i18n from "../../locales"; +import CSYAuditOperativeReport from "./csy-audit-operative/CSYAuditOperativeReport"; const widget = process.env.REACT_APP_REPORT_VARIANT || ""; @@ -47,6 +48,9 @@ const Component: React.FC = () => { case "csy-audit-trauma": { return ; } + case "csy-audit-operative": { + return ; + } case "csy-summary-patient": { return ; } diff --git a/src/webapp/reports/csy-audit-emergency/AuditViewModel.ts b/src/webapp/reports/csy-audit-emergency/AuditViewModel.ts index b3374775..faf6e16d 100644 --- a/src/webapp/reports/csy-audit-emergency/AuditViewModel.ts +++ b/src/webapp/reports/csy-audit-emergency/AuditViewModel.ts @@ -1,4 +1,4 @@ -import { AuditItem } from "../../../domain/reports/csy-audit-trauma/entities/AuditItem"; +import { AuditItem } from "../../../domain/reports/csy-audit-emergency/entities/AuditItem"; export interface AuditViewModel { id: string; diff --git a/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/CSYAuditEmergencyList.tsx b/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/CSYAuditEmergencyList.tsx index 1b579708..0aab71f5 100644 --- a/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/CSYAuditEmergencyList.tsx +++ b/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/CSYAuditEmergencyList.tsx @@ -28,7 +28,7 @@ export const CSYAuditEmergencyList: React.FC = React.memo(() => {

- Audit Definition: {auditDefinition} + {i18n.t("Audit Definition:")} {auditDefinition}

diff --git a/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/useAuditReport.tsx b/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/useAuditReport.tsx index c8f19d11..f912d842 100644 --- a/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/useAuditReport.tsx +++ b/src/webapp/reports/csy-audit-emergency/csy-audit-emergency-list/useAuditReport.tsx @@ -1,5 +1,4 @@ import { useSnackbar, TableSorting, TablePagination, TableGlobalAction } from "@eyeseetea/d2-ui-components"; -import _ from "lodash"; import { useState, useMemo } from "react"; import { useAppContext } from "../../../contexts/app-context"; import { useReload } from "../../../utils/use-reload"; @@ -8,6 +7,7 @@ import { auditTypeItems, Filter, FilterOptions } from "./Filters"; import { emptyPage, PaginatedObjects, Sorting } from "../../../../domain/common/entities/PaginatedObjects"; import StorageIcon from "@material-ui/icons/Storage"; import { AuditItem } from "../../../../domain/reports/csy-audit-emergency/entities/AuditItem"; +import { useSelectablePeriods } from "../../../utils/selectablePeriods"; interface AuditReportState { auditDefinition: string; @@ -42,10 +42,7 @@ export function useAuditReport(filters: Filter): AuditReportState { const snackbar = useSnackbar(); const [sorting, setSorting] = useState>(); - const selectablePeriods = useMemo(() => { - const currentYear = new Date().getFullYear(); - return _.range(currentYear - 10, currentYear + 1).map(year => year.toString()); - }, []); + const selectablePeriods = useSelectablePeriods(startYear); const filterOptions = useMemo(() => getFilterOptions(selectablePeriods), [selectablePeriods]); const auditDefinition = @@ -109,3 +106,5 @@ function getFilterOptions(selectablePeriods: string[]): FilterOptions { periods: selectablePeriods, }; } + +const startYear = 2014; diff --git a/src/webapp/reports/csy-audit-operative/AuditViewModel.ts b/src/webapp/reports/csy-audit-operative/AuditViewModel.ts new file mode 100644 index 00000000..87f682fe --- /dev/null +++ b/src/webapp/reports/csy-audit-operative/AuditViewModel.ts @@ -0,0 +1,15 @@ +import { AuditItem } from "../../../domain/reports/csy-audit-operative/entities/AuditItem"; + +export interface AuditViewModel { + id: string; + registerId: string; +} + +export function getAuditViews(items: AuditItem[]): AuditViewModel[] { + return items.map((item, i) => { + return { + id: i.toString(), + registerId: item.registerId, + }; + }); +} diff --git a/src/webapp/reports/csy-audit-operative/CSYAuditOperativeReport.tsx b/src/webapp/reports/csy-audit-operative/CSYAuditOperativeReport.tsx new file mode 100644 index 00000000..25c420a2 --- /dev/null +++ b/src/webapp/reports/csy-audit-operative/CSYAuditOperativeReport.tsx @@ -0,0 +1,23 @@ +import { Typography, makeStyles } from "@material-ui/core"; +import i18n from "../../../locales"; +import { CSYAuditOperativeList } from "./csy-audit-operative-list/CSYAuditOperativeList"; + +const CSYAuditOperativeReport: React.FC = () => { + const classes = useStyles(); + + return ( +
+ + {i18n.t("CSY Audit Filters - Operative Care")} + + + +
+ ); +}; + +const useStyles = makeStyles({ + wrapper: { padding: 20 }, +}); + +export default CSYAuditOperativeReport; diff --git a/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/CSYAuditOperativeList.tsx b/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/CSYAuditOperativeList.tsx new file mode 100644 index 00000000..aa1b397f --- /dev/null +++ b/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/CSYAuditOperativeList.tsx @@ -0,0 +1,47 @@ +import React, { useMemo, useState } from "react"; +import { Filter, Filters } from "./Filters"; +import { ObjectsList, TableConfig, useObjectsTable } from "@eyeseetea/d2-ui-components"; +import { AuditViewModel } from "../AuditViewModel"; +import { useAuditReport } from "./useAuditReport"; +import i18n from "@eyeseetea/d2-ui-components/locales"; + +export const CSYAuditOperativeList: React.FC = React.memo(() => { + const [filters, setFilters] = useState(() => getEmptyDataValuesFilter()); + const { auditDefinition, downloadCsv, filterOptions, initialSorting, paginationOptions, getRows } = + useAuditReport(filters); + + const baseConfig: TableConfig = useMemo( + () => ({ + columns: [{ name: "registerId", text: i18n.t("Register ID"), sortable: true }], + actions: [], + initialSorting: initialSorting, + paginationOptions: paginationOptions, + }), + [initialSorting, paginationOptions] + ); + + const tableProps = useObjectsTable(baseConfig, getRows); + + return ( + + {...tableProps} onChangeSearch={undefined} globalActions={[downloadCsv]}> +
+ +

+ {i18n.t("Audit Definition:")} {auditDefinition} +

+
+ +
+ ); +}); + +function getEmptyDataValuesFilter(): Filter { + return { + auditType: "lowRiskMortality", + orgUnitPaths: [], + year: (new Date().getFullYear() - 1).toString(), + periodType: "yearly", + quarter: undefined, + }; +} diff --git a/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/Filters.tsx b/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/Filters.tsx new file mode 100644 index 00000000..748b2784 --- /dev/null +++ b/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/Filters.tsx @@ -0,0 +1,229 @@ +import React, { useMemo, useState } from "react"; +import { OrgUnitsFilterButton } from "../../../components/org-units-filter/OrgUnitsFilterButton"; +import { useAppContext } from "../../../contexts/app-context"; +import { Id } from "../../../../domain/common/entities/Base"; +import styled from "styled-components"; +import { Dropdown, DropdownProps } from "@eyeseetea/d2-ui-components"; +import i18n from "../../../../locales"; +import _ from "lodash"; +import { AuditType } from "../../../../domain/reports/csy-audit-operative/entities/AuditItem"; + +export interface FiltersProps { + values: Filter; + options: FilterOptions; + onChange: React.Dispatch>; +} + +export interface Filter { + auditType: AuditType; + orgUnitPaths: Id[]; + periodType: string; + year: string; + quarter?: string; +} + +export type FilterOptions = { + periods: string[]; +}; + +export const Filters: React.FC = React.memo(props => { + const { config, api } = useAppContext(); + const { values: filter, options: filterOptions, onChange } = props; + + const [periodType, setPerType] = useState("yearly"); + const rootIds = React.useMemo( + () => + _(config.currentUser.orgUnits) + .map(ou => ou.id) + .value(), + [config] + ); + + const yearItems = useMemoOptionsFromStrings(filterOptions.periods); + + const setAuditType = React.useCallback( + auditType => { + onChange(filter => ({ ...filter, auditType: auditType as AuditType })); + }, + [onChange] + ); + + const setQuarterPeriod = React.useCallback( + quarterPeriod => { + onChange(filter => ({ ...filter, quarter: quarterPeriod ?? "" })); + }, + [onChange] + ); + + const setPeriodType = React.useCallback( + periodType => { + setPerType(periodType ?? "yearly"); + setQuarterPeriod(periodType !== "yearly" ? "Q1" : undefined); + + onChange(filter => ({ ...filter, periodType: periodType ?? "yearly" })); + }, + [onChange, setQuarterPeriod] + ); + + const setYear = React.useCallback( + year => { + onChange(filter => ({ ...filter, year: year ?? "" })); + }, + [onChange] + ); + + return ( + + + + onChange({ ...filter, orgUnitPaths: paths })} + selectableLevels={[1, 2, 3, 4, 5, 6, 7]} + /> + + + + + + {periodType === "quarterly" && ( + <> + + + )} + + ); +}); + +const quarterPeriodItems = [ + { value: "Q1", text: i18n.t("Jan - March") }, + { value: "Q2", text: i18n.t("April - June") }, + { value: "Q3", text: i18n.t("July - September") }, + { value: "Q4", text: i18n.t("October - December") }, +]; + +const periodTypeItems = [ + { value: "yearly", text: i18n.t("Yearly") }, + { value: "quarterly", text: i18n.t("Quarterly") }, +]; + +export const auditTypeItems = [ + { + value: "lowRiskMortality", + text: i18n.t("Mortality (operative and 24hr) among low-risk patients (ASA score 1-2) "), + auditDefinition: i18n.t( + "(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || CSY_OP_Disposition 24 Hours After Surgery == Deceased) && CSY_OP_ASA Functional Status Score == ASA 1 or ASA 2" + ), + }, + { + value: "zeroComorbidityMortality", + text: i18n.t("Mortality (operative and 24hr) in patients with 0 comorbidities "), + auditDefinition: i18n.t( + "(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || CSY_OP_Disposition 24 Hours After Surgery == Deceased) && Number of Major Medical Comorbidities == 0" + ), + }, + { + value: "cSectionMortality", + text: i18n.t("Mortality (operative and 24hr) in patients who undergo C-section ​"), + auditDefinition: i18n.t( + "(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || CSY_OP_Disposition 24 Hours After Surgery == Deceased) && (CSY_OP_SurgicalIntervention == Caesarean Section || CSY_OP_SurgicalIntervention 2 == Caesarean Section || CSY_OP_SurgicalIntervention 3 == Caesarean Section || CSY_OP_SurgicalIntervention 4 == Caesarean Section || CSY_OP_SurgicalIntervention 5 == Caesarean Section )" + ), + }, + { + value: "emergentCase", + text: i18n.t("Number of prior facilities is >1 and case urgency is Emergent"), + auditDefinition: i18n.t( + "ETA_Facility Transfers > 1 && CSY_OP_Urgency of Surgery == Acute emergency, needed within 6 hours (Emergent)" + ), + }, + { + value: "surgeryChecklist", + text: i18n.t("All cases where the Safe Surgery checklist was not performed"), + auditDefinition: i18n.t("CSY_OP_Safe Surgery Check List Used == No"), + }, + { + value: "otMortality", + text: i18n.t("All cases of OR/OT mortality"), + auditDefinition: i18n.t("CSY_OP_Disposition on Leaving Operating Theatre == Deceased"), + }, + { + value: "acuteEmergentCase", + text: i18n.t("Emergent case and time to OR/OT > 6 hours (admission time-operative time)"), + auditDefinition: i18n.t( + "CSY_OP_Urgency of Surgery == Acute emergency, needed within 6 hours (Emergent) && (Arrival Date and Time - Date and Time of Operating Theatre Arrival > 6 hours)" + ), + }, + { + value: "nonSpecialistMortality", + text: i18n.t( + "All cases of mortality where the category of surgical or anesthesia provider is not a specialist" + ), + auditDefinition: i18n.t( + "(CSY_OP_Disposition on Leaving Operating Theatre == Deceased || CSY_OP_Disposition 24 Hours After Surgery == Deceased) && ((CSY_OP_Category of Surgical Provider ≠ Surgeon with Specialty in Surgery Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician) || (CSY_OP_Category of Surgical Provider 2 ≠ Surgeon with Specialty in Surgery Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician) || (CSY_OP_Category of Surgical Provider 3 ≠ Surgeon with Specialty in Surgery Performed or Primary Anaesthesia type ≠ Specialist Anaesthesia Physician))" + ), + }, + { + value: "pulseOximetry", + text: i18n.t("All cases without pulse oximetry used"), + auditDefinition: i18n.t( + "CSY_OP_Monitoring Used Intra-operatively ≠ Pulse oximeter && CSY_OP_Monitoring Used Intra-operatively 2 ≠ Pulse oximeter && CSY_OP_Monitoring Used Intra-operatively 3 ≠ Pulse oximeter && CSY_OP_Monitoring Used Intra-operatively 4 ≠ Pulse oximeter && CSY_OP_Monitoring Used Intra-operatively 5 ≠ Pulse oximeter " + ), + }, + { + value: "intraOperativeComplications", + text: i18n.t("Any intra-operative complications in patients with ASA 1-2 or 0 co-morbidities"), + auditDefinition: i18n.t( + "If CSY_OP_Intra-operative complication has value && (CSY_OP_ASA Functional Status Score == ASA 1 or ASA 2 || ETA_Major Medical Comorbidities == 0)" + ), + }, +]; + +function useMemoOptionsFromStrings(options: string[]) { + return useMemo(() => { + return options.map(option => ({ value: option, text: option })); + }, [options]); +} + +const Container = styled.div` + display: flex; + gap: 1rem; + flex-wrap: wrap; +`; + +const AuditTypeDropdown = styled(Dropdown)` + margin-left: -10px; + width: 600px; +`; + +const SingleDropdownStyled = styled(Dropdown)` + margin-left: -10px; + width: 180px; +`; + +type SingleDropdownHandler = DropdownProps["onChange"]; diff --git a/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/useAuditReport.tsx b/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/useAuditReport.tsx new file mode 100644 index 00000000..358ef38c --- /dev/null +++ b/src/webapp/reports/csy-audit-operative/csy-audit-operative-list/useAuditReport.tsx @@ -0,0 +1,132 @@ +import { useSnackbar, TableSorting, TablePagination, TableGlobalAction } from "@eyeseetea/d2-ui-components"; +import { useState, useMemo } from "react"; +import { useAppContext } from "../../../contexts/app-context"; +import { useReload } from "../../../utils/use-reload"; +import { emptyPage, PaginatedObjects, Sorting } from "../../../../domain/common/entities/PaginatedObjects"; +import StorageIcon from "@material-ui/icons/Storage"; +import { AuditItem } from "../../../../domain/reports/csy-audit-operative/entities/AuditItem"; +import { AuditViewModel, getAuditViews } from "../AuditViewModel"; +import { auditTypeItems, Filter, FilterOptions } from "./Filters"; +import { CsvWriterDataSource } from "../../../../data/common/CsvWriterCsvDataSource"; +import { CsvData } from "../../../../data/common/CsvDataSource"; +import { downloadFile } from "../../../../data/common/utils/download-file"; +import { useSelectablePeriods } from "../../../utils/selectablePeriods"; + +interface AuditReportState { + auditDefinition: string; + downloadCsv: TableGlobalAction; + filterOptions: FilterOptions; + initialSorting: TableSorting; + paginationOptions: { + pageSizeOptions: number[]; + pageSizeInitialValue: number; + }; + getRows: ( + search: string, + paging: TablePagination, + sorting: TableSorting + ) => Promise>; +} + +const initialSorting = { + field: "registerId" as const, + order: "asc" as const, +}; + +const paginationOptions = { + pageSizeOptions: [10, 20, 50], + pageSizeInitialValue: 10, +}; + +export function useAuditReport(filters: Filter): AuditReportState { + const { compositionRoot } = useAppContext(); + + const [reloadKey, _reload] = useReload(); + const snackbar = useSnackbar(); + const [sorting, setSorting] = useState>(); + + const selectablePeriods = useSelectablePeriods(startYear); + const filterOptions = useMemo(() => getFilterOptions(selectablePeriods), [selectablePeriods]); + + const auditDefinition = + auditTypeItems.find(auditTypeItem => auditTypeItem.value === filters.auditType)?.auditDefinition ?? ""; + + const getRows = useMemo( + () => async (_search: string, paging: TablePagination, sorting: TableSorting) => { + const { pager, objects } = await compositionRoot.auditOperative + .get({ + paging: { page: paging.page, pageSize: paging.pageSize }, + sorting: getSortingFromTableSorting(sorting), + ...filters, + }) + .catch(error => { + snackbar.error(error.message); + return emptyPage; + }); + + setSorting(sorting); + console.debug("Reloading", reloadKey); + return { pager, objects: getAuditViews(objects) }; + }, + [compositionRoot.auditOperative, filters, reloadKey, snackbar] + ); + + const downloadCsv: TableGlobalAction = { + name: "downloadCsv", + text: "Download CSV", + icon: , + onClick: async () => { + if (!sorting) return; + const { objects: auditItems } = await compositionRoot.auditOperative.get({ + paging: { page: 1, pageSize: 100000 }, + sorting: getSortingFromTableSorting(sorting), + ...filters, + }); + + downloadAuditReport("audit-report.csv", auditItems); + }, + }; + + return { + auditDefinition, + filterOptions, + initialSorting, + paginationOptions, + getRows, + downloadCsv, + }; +} + +export function getSortingFromTableSorting(sorting: TableSorting): Sorting { + return { + field: sorting.field === "id" ? "registerId" : sorting.field, + direction: sorting.order, + }; +} + +async function downloadAuditReport(filename: string, items: AuditItem[]): Promise { + const headers = csvFields.map(field => ({ id: field, text: field })); + const rows = items.map( + (dataValue): AuditItemRow => ({ + registerId: dataValue.registerId, + }) + ); + const timestamp = new Date().toISOString(); + const csvDataSource = new CsvWriterDataSource(); + const csvData: CsvData = { headers, rows }; + const csvContents = `Time: ${timestamp}\n` + csvDataSource.toString(csvData); + + await downloadFile(csvContents, filename, "text/csv"); +} + +const csvFields = ["registerId"] as const; +type CsvField = typeof csvFields[number]; +type AuditItemRow = Record; + +function getFilterOptions(selectablePeriods: string[]): FilterOptions { + return { + periods: selectablePeriods, + }; +} + +const startYear = 2014; diff --git a/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/CSYAuditTraumaList.tsx b/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/CSYAuditTraumaList.tsx index 6343b229..eec22bcc 100644 --- a/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/CSYAuditTraumaList.tsx +++ b/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/CSYAuditTraumaList.tsx @@ -27,7 +27,7 @@ export const CSYAuditTraumaList: React.FC = React.memo(() => {

- Audit Definition: {auditDefinition} + {i18n.t("Audit Definition:")} {auditDefinition}

diff --git a/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/useAuditReport.tsx b/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/useAuditReport.tsx index b443ab7b..579132be 100644 --- a/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/useAuditReport.tsx +++ b/src/webapp/reports/csy-audit-trauma/csy-audit-trauma-list/useAuditReport.tsx @@ -1,5 +1,4 @@ import { useSnackbar, TableSorting, TablePagination, TableGlobalAction } from "@eyeseetea/d2-ui-components"; -import _ from "lodash"; import { useState, useMemo } from "react"; import { useAppContext } from "../../../contexts/app-context"; import { useReload } from "../../../utils/use-reload"; @@ -8,6 +7,7 @@ import { auditTypeItems, Filter, FilterOptions } from "./Filters"; import { emptyPage, PaginatedObjects, Sorting } from "../../../../domain/common/entities/PaginatedObjects"; import StorageIcon from "@material-ui/icons/Storage"; import { AuditItem } from "../../../../domain/reports/csy-audit-trauma/entities/AuditItem"; +import { useSelectablePeriods } from "../../../utils/selectablePeriods"; interface AuditReportState { auditDefinition: string; @@ -42,10 +42,7 @@ export function useAuditReport(filters: Filter): AuditReportState { const snackbar = useSnackbar(); const [sorting, setSorting] = useState>(); - const selectablePeriods = useMemo(() => { - const currentYear = new Date().getFullYear(); - return _.range(currentYear - 10, currentYear + 1).map(year => year.toString()); - }, []); + const selectablePeriods = useSelectablePeriods(startYear); const filterOptions = useMemo(() => getFilterOptions(selectablePeriods), [selectablePeriods]); const auditDefinition = @@ -109,3 +106,5 @@ function getFilterOptions(selectablePeriods: string[]): FilterOptions { periods: selectablePeriods, }; } + +const startYear = 2014; diff --git a/src/webapp/reports/csy-summary-mortality/csy-summary-mortality-list/useSummaryReport.tsx b/src/webapp/reports/csy-summary-mortality/csy-summary-mortality-list/useSummaryReport.tsx index c5e943a5..753d9424 100644 --- a/src/webapp/reports/csy-summary-mortality/csy-summary-mortality-list/useSummaryReport.tsx +++ b/src/webapp/reports/csy-summary-mortality/csy-summary-mortality-list/useSummaryReport.tsx @@ -7,7 +7,7 @@ import { emptyPage, PaginatedObjects, Sorting } from "../../../../domain/common/ import { SummaryItem } from "../../../../domain/reports/csy-summary-mortality/entities/SummaryItem"; import { Filter, FilterOptions } from "./Filters"; import StorageIcon from "@material-ui/icons/Storage"; -import _ from "lodash"; +import { useSelectablePeriods } from "../../../utils/selectablePeriods"; interface SummaryReportState { downloadCsv: TableGlobalAction; @@ -41,10 +41,7 @@ export function useSummaryReport(filters: Filter): SummaryReportState { const snackbar = useSnackbar(); const [sorting, setSorting] = useState>(); - const selectablePeriods = useMemo(() => { - const currentYear = new Date().getFullYear(); - return _.range(currentYear - 10, currentYear + 1).map(year => year.toString()); - }, []); + const selectablePeriods = useSelectablePeriods(startYear); const filterOptions = useMemo(() => getFilterOptions(selectablePeriods), [selectablePeriods]); const getRows = useMemo( @@ -104,3 +101,5 @@ function getFilterOptions(selectablePeriods: string[]): FilterOptions { periods: selectablePeriods, }; } + +const startYear = 2014; diff --git a/src/webapp/reports/csy-summary-patient/csy-summary-list/useSummaryReport.tsx b/src/webapp/reports/csy-summary-patient/csy-summary-list/useSummaryReport.tsx index 248c5d6e..74e88087 100644 --- a/src/webapp/reports/csy-summary-patient/csy-summary-list/useSummaryReport.tsx +++ b/src/webapp/reports/csy-summary-patient/csy-summary-list/useSummaryReport.tsx @@ -7,7 +7,7 @@ import { SummaryItem } from "../../../../domain/reports/csy-summary-patient/enti import { emptyPage, PaginatedObjects, Sorting } from "../../../../domain/common/entities/PaginatedObjects"; import { getSummaryViews, SummaryViewModel } from "../SummaryViewModel"; import StorageIcon from "@material-ui/icons/Storage"; -import _ from "lodash"; +import { useSelectablePeriods } from "../../../utils/selectablePeriods"; interface SummaryReportState { downloadCsv: TableGlobalAction; @@ -40,11 +40,7 @@ export function useSummaryReport(filters: Filter): SummaryReportState { const [reloadKey, _reload] = useReload(); const [sorting, setSorting] = useState>(); - - const selectablePeriods = useMemo(() => { - const currentYear = new Date().getFullYear(); - return _.range(currentYear - 10, currentYear + 1).map(n => n.toString()); - }, []); + const selectablePeriods = useSelectablePeriods(startYear); const getRows = useMemo( () => async (_search: string, paging: TablePagination, sorting: TableSorting) => { @@ -105,3 +101,5 @@ function getFilterOptions(selectablePeriods: string[]) { periods: selectablePeriods, }; } + +const startYear = 2014; diff --git a/src/webapp/utils/reportType.ts b/src/webapp/utils/reportType.ts index b1a80120..f0f50f3d 100644 --- a/src/webapp/utils/reportType.ts +++ b/src/webapp/utils/reportType.ts @@ -18,6 +18,8 @@ export function getReportType(): ReportType { return "summary-mortality"; case report === "csy-audit-emergency": return "auditEmergency"; + case report === "csy-audit-operative": + return "auditOperative"; case report === "csy-audit-trauma": return "auditTrauma"; case report === "authorities-monitoring": diff --git a/src/webapp/utils/selectablePeriods.ts b/src/webapp/utils/selectablePeriods.ts new file mode 100644 index 00000000..de33d329 --- /dev/null +++ b/src/webapp/utils/selectablePeriods.ts @@ -0,0 +1,11 @@ +import _ from "lodash"; +import React from "react"; + +export function useSelectablePeriods(startYear: number): string[] { + const selectablePeriods = React.useMemo(() => { + const currentYear = new Date().getFullYear(); + return _.range(startYear, currentYear + 1).map(year => year.toString()); + }, [startYear]); + + return selectablePeriods; +}