diff --git a/index.html b/index.html index 492d83db..1e5a8d80 100644 --- a/index.html +++ b/index.html @@ -4,15 +4,22 @@ - + - + - Vite + React + TS + Zebra diff --git a/package.json b/package.json index 321eefa8..d5f49e60 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "dhis2-app-skeleton", - "description": "DHIS2 Skeleton App", + "name": "zebra", + "description": "Zambia Emergency Bridge for Response Application", "version": "0.0.1", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", "repository": { "type": "git", - "url": "git+https://github.com/eyeseetea/dhis2-app-skeleton.git" + "url": "git+https://github.com/eyeseetea/zebra-dev.git" }, "dependencies": { "@dhis2/app-runtime": "2.8.0", @@ -108,8 +108,8 @@ "script-example": "npx ts-node src/scripts/example.ts" }, "manifest.webapp": { - "name": "DHIS2 Skeleton App", - "description": "DHIS2 Skeleton App", + "name": "zebra", + "description": "Zambia Emergency Bridge for Response Application", "icons": { "48": "icon.png" }, diff --git a/src/domain/entities/DiseaseOutbreak.ts b/src/domain/entities/DiseaseOutbreak.ts new file mode 100644 index 00000000..5e67512c --- /dev/null +++ b/src/domain/entities/DiseaseOutbreak.ts @@ -0,0 +1,56 @@ +//Note: DiseaseOutbreak represents Event in the Figma. +//Not using event as it is a keyword and can also be confused with dhis event +import { Struct } from "./generic/Struct"; +import { IncidentActionPlan } from "./incident-action-plan/IncidentActionPlan"; +import { IncidentManagementTeam } from "./incident-management-team/IncidentManagementTeam"; +import { TeamMember } from "./incident-management-team/TeamMember"; +import { OrgUnit } from "./OrgUnit"; +import { NamedRef, Option } from "./Ref"; +import { RiskAssessment } from "./risk-assessment/RiskAssessment"; + +type HazardType = + | "Biological:Human" + | "Biological:Animal" + | "Chemical" + | "Environmental" + | "Unknown"; + +type IncidentStatusType = "Watch" | "Alert" | "Respond" | "Closed" | "Discarded"; + +type DateWithNarrative = { + date: Date; + narrative: string; +}; + +interface DiseaseOutbreakAttrs extends NamedRef { + created: Date; + lastUpdated: Date; + createdBy: TeamMember; + hazardType: HazardType; + mainSyndrome: Option; + suspectedDisease: Option; + notificationSource: Option; + areasAffected: { + provinces: OrgUnit[]; + districts: OrgUnit[]; + }; + incidentStatus: IncidentStatusType; + dateEmerged: DateWithNarrative; + dateDetected: DateWithNarrative; + dateNotified: DateWithNarrative; + responseNarrative: string; + incidentManager: TeamMember; + notes: string; + //when should risk assessment, IAP,IMT be fetched? Only when the user clicks on the risk assessment tab? + //Can we async get only 1 property in a class? + riskAssessments: RiskAssessment[]; + //we need only response actions property from IncidentActionPlan. How can we map that? + IncidentActionPlan: IncidentActionPlan; + IncidentManagementTeam: IncidentManagementTeam; +} + +export class DiseaseOutbreak extends Struct() { + static validateEventName() { + //Ensure event name is unique on event creation. + } +} diff --git a/src/domain/entities/OrgUnit.ts b/src/domain/entities/OrgUnit.ts new file mode 100644 index 00000000..6c5c0875 --- /dev/null +++ b/src/domain/entities/OrgUnit.ts @@ -0,0 +1,7 @@ +import { CodedNamedRef } from "./Ref"; + +type OrgUnitLevelType = "Province" | "District"; + +export interface OrgUnit extends CodedNamedRef { + level: OrgUnitLevelType; +} diff --git a/src/domain/entities/Properties.ts b/src/domain/entities/Properties.ts new file mode 100644 index 00000000..492723f3 --- /dev/null +++ b/src/domain/entities/Properties.ts @@ -0,0 +1,32 @@ +//TO DO : Can there be a better name for a generic property? +import { CodedNamedRef } from "./Ref"; + +type PropertTypes = "string" | "date" | "number" | "boolean"; + +//TO DO : what other attributes of a generic domain property? +interface BaseProperty extends CodedNamedRef { + text: string; //or label or key? + type: PropertTypes; +} + +interface StringProperty extends BaseProperty { + type: "string"; + value: string; +} + +interface DateProperty extends BaseProperty { + type: "date"; + value: Date; +} + +interface NumberProperty extends BaseProperty { + type: "number"; + value: number; +} + +interface BooleanProperty extends BaseProperty { + type: "boolean"; + value: boolean; +} + +export type Property = StringProperty | DateProperty | NumberProperty | BooleanProperty; diff --git a/src/domain/entities/Ref.ts b/src/domain/entities/Ref.ts index 8b95ca2f..bdd6e18d 100644 --- a/src/domain/entities/Ref.ts +++ b/src/domain/entities/Ref.ts @@ -7,3 +7,9 @@ export interface Ref { export interface NamedRef extends Ref { name: string; } + +export interface CodedNamedRef extends NamedRef { + code: string; +} + +export type Option = CodedNamedRef; diff --git a/src/domain/entities/incident-action-plan/ActionPlan.ts b/src/domain/entities/incident-action-plan/ActionPlan.ts new file mode 100644 index 00000000..09336e77 --- /dev/null +++ b/src/domain/entities/incident-action-plan/ActionPlan.ts @@ -0,0 +1,8 @@ +import { Property } from "../Properties"; +import { Struct } from "../generic/Struct"; + +interface ActionPlanAttrs { + properties: Property[]; +} + +export class ActionPlan extends Struct() {} diff --git a/src/domain/entities/incident-action-plan/IncidentActionPlan.ts b/src/domain/entities/incident-action-plan/IncidentActionPlan.ts new file mode 100644 index 00000000..222a8df2 --- /dev/null +++ b/src/domain/entities/incident-action-plan/IncidentActionPlan.ts @@ -0,0 +1,12 @@ +import { ActionPlan } from "./ActionPlan"; +import { Ref } from "../Ref"; +import { Struct } from "../generic/Struct"; +import { ResponseAction } from "./ResponseAction"; + +interface IncidentActionPlanAttrs extends Ref { + lastUpdated: Date; + actionPlan: ActionPlan; + responseActions: ResponseAction[]; +} + +export class IncidentActionPlan extends Struct() {} diff --git a/src/domain/entities/incident-action-plan/ResponseAction.ts b/src/domain/entities/incident-action-plan/ResponseAction.ts new file mode 100644 index 00000000..efac906d --- /dev/null +++ b/src/domain/entities/incident-action-plan/ResponseAction.ts @@ -0,0 +1,20 @@ +import { Struct } from "../generic/Struct"; +import { TeamMember } from "../incident-management-team/TeamMember"; +import { Option } from "../Ref"; + +//TO DO : Should this be Option? +type ResponseActionStatusType = "Not done" | "Pending" | "In Progress" | "Complete"; +type ResponseActionVerificationType = "Verified" | "Unverified"; + +interface ResponseActionAttrs { + mainTask: Option; + subActivities: string; + subPillar: Option; + responsibleOfficer: TeamMember; + dueDate: Date; + timeLine: Option; + status: ResponseActionStatusType; + verification: ResponseActionVerificationType; +} + +export class ResponseAction extends Struct() {} diff --git a/src/domain/entities/incident-management-team/IncidentManagementTeam.ts b/src/domain/entities/incident-management-team/IncidentManagementTeam.ts new file mode 100644 index 00000000..89532595 --- /dev/null +++ b/src/domain/entities/incident-management-team/IncidentManagementTeam.ts @@ -0,0 +1,17 @@ +import { Struct } from "../generic/Struct"; +import { TeamMember } from "./TeamMember"; + +interface TeamRole { + role: string; + level: number; +} + +interface RoleTeamMemberMap { + role: TeamRole; + teamMember: TeamMember; +} +interface IncidentManagementTeamAttrs { + teamHeirarchy: RoleTeamMemberMap[]; //Is there a better way to represent heirarchy? Maybe a tree? +} + +export class IncidentManagementTeam extends Struct() {} diff --git a/src/domain/entities/incident-management-team/TeamMember.ts b/src/domain/entities/incident-management-team/TeamMember.ts new file mode 100644 index 00000000..ede7e5fd --- /dev/null +++ b/src/domain/entities/incident-management-team/TeamMember.ts @@ -0,0 +1,21 @@ +import { NamedRef } from "../Ref"; +import { Struct } from "../generic/Struct"; + +type PhoneNumber = string; +type Email = string; +type IncidentManagerStatus = "Available" | "Unavailable"; + +interface TeamMemberAttrs extends NamedRef { + position: string; + phone: PhoneNumber; + email: Email; + status: IncidentManagerStatus; + photo: string; //URL to photo +} + +export class TeamMember extends Struct() { + static validatePhAndEmail() { + //TO DO : any validations for phone number? + //TO DO : any validations for email? + } +} diff --git a/src/domain/entities/risk-assessment/RiskAssessment.ts b/src/domain/entities/risk-assessment/RiskAssessment.ts new file mode 100644 index 00000000..d2b5f63f --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessment.ts @@ -0,0 +1,12 @@ +import { Struct } from "../generic/Struct"; +import { RiskAssessmentGrading } from "./RiskAssessmentGrading"; +import { RiskAssessmentQuestionnaire } from "./RiskAssessmentQuestionnaire"; +import { RiskAssessmentSummary } from "./RiskAssessmentSummary"; + +interface RiskAssessmentAttrs { + riskAssessmentGrading: RiskAssessmentGrading[]; + riskAssessmentSummary: RiskAssessmentSummary; + riskAssessmentQuestionnaire: RiskAssessmentQuestionnaire[]; +} + +export class RiskAssessment extends Struct() {} diff --git a/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts new file mode 100644 index 00000000..a035c19a --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts @@ -0,0 +1,118 @@ +import { Ref } from "../Ref"; +import { Struct } from "../generic/Struct"; + +type WeightedOptions = { + label: "Low" | "Medium" | "High"; + weight: 1 | 2 | 3; +}; + +export const LowWeightedOption: WeightedOptions = { + label: "Low", + weight: 1, +}; +export const MediumWeightedOption: WeightedOptions = { + label: "Medium", + weight: 2, +}; +export const HighWeightedOption: WeightedOptions = { + label: "High", + weight: 3, +}; + +type PopulationWeightOptions = { + label: "Less than 0.1%" | "Between 0.1% to 0.25%" | "Above 0.25%"; + weight: 1 | 2 | 3; +}; + +export const LowPopulationAtRisk: PopulationWeightOptions = { + label: "Less than 0.1%", + weight: 1, +}; +export const MediumPopulationAtRisk: PopulationWeightOptions = { + label: "Between 0.1% to 0.25%", + weight: 2, +}; +export const HighPopulationAtRisk: PopulationWeightOptions = { + label: "Above 0.25%", + weight: 3, +}; + +type GeographicalSpreadOptions = { + label: + | "Within a district" + | "Within a province with more than one district affected" + | "More than one province affected with high threat of spread locally and internationally"; + weight: 1 | 2 | 3; +}; + +export const LowGeographicalSpread: GeographicalSpreadOptions = { + label: "Within a district", + weight: 1, +}; +export const MediumGeographicalSpread: GeographicalSpreadOptions = { + label: "Within a province with more than one district affected", + weight: 2, +}; +export const HighGeographicalSpread: GeographicalSpreadOptions = { + label: "More than one province affected with high threat of spread locally and internationally", + weight: 3, +}; + +type CapacityOptions = { + label: + | "Available within the district with support from provincial and national level " + | "Available within the province with minimal support from national level" + | " Available at national with support required from international"; + weight: 1 | 2 | 3; +}; + +export const LowCapacity: CapacityOptions = { + label: "Available within the district with support from provincial and national level ", + weight: 1, +}; +export const MediumCapacity: CapacityOptions = { + label: "Available within the province with minimal support from national level", + weight: 2, +}; +export const HighCapacity: CapacityOptions = { + label: " Available at national with support required from international", + weight: 3, +}; + +export type Grade = "Grade 1" | "Grade 2" | "Grade 3"; + +interface RiskAssessmentGradingAttrs extends Ref { + lastUpdated: Date; + populationAtRisk: PopulationWeightOptions; + attackRate: WeightedOptions; + geographicalSpread: GeographicalSpreadOptions; + complexity: WeightedOptions; + capacity: CapacityOptions; + reputationalRisk: WeightedOptions; + severity: WeightedOptions; + // capability: WeightedOptions; + grade?: Grade; +} + +export class RiskAssessmentGrading extends Struct() { + calculateAndSetGrade(): void { + const totalWeight = + this.populationAtRisk.weight + + this.attackRate.weight + + this.geographicalSpread.weight + + this.complexity.weight + + this.capacity.weight + + this.reputationalRisk.weight + + this.severity.weight; + // this.capability.weight; + + if (totalWeight > 21) throw new Error("Invalid grade"); + + this.grade = + totalWeight <= 7 + ? "Grade 1" + : totalWeight > 7 && totalWeight <= 14 + ? "Grade 2" + : "Grade 3"; + } +} diff --git a/src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts b/src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts new file mode 100644 index 00000000..4a77cab4 --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessmentQuestionnaire.ts @@ -0,0 +1,8 @@ +import { Property } from "../Properties"; +import { Struct } from "../generic/Struct"; + +interface RiskAssessmentQuestionnaireAttrs { + questions: Property[]; +} + +export class RiskAssessmentQuestionnaire extends Struct() {} diff --git a/src/domain/entities/risk-assessment/RiskAssessmentSummary.ts b/src/domain/entities/risk-assessment/RiskAssessmentSummary.ts new file mode 100644 index 00000000..21b26ce2 --- /dev/null +++ b/src/domain/entities/risk-assessment/RiskAssessmentSummary.ts @@ -0,0 +1,8 @@ +import { Property } from "../Properties"; +import { Struct } from "../generic/Struct"; + +interface RiskAssessmentSummaryAttrs { + properties: Property[]; +} + +export class RiskAssessmentSummary extends Struct() {} diff --git a/src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts b/src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts new file mode 100644 index 00000000..7631ff17 --- /dev/null +++ b/src/domain/entities/risk-assessment/__tests__/RiskAssessmentGrading.spec.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { + HighCapacity, + HighGeographicalSpread, + HighPopulationAtRisk, + HighWeightedOption, + LowCapacity, + LowGeographicalSpread, + LowPopulationAtRisk, + LowWeightedOption, + MediumCapacity, + MediumGeographicalSpread, + MediumPopulationAtRisk, + MediumWeightedOption, + RiskAssessmentGrading, +} from "../RiskAssessmentGrading"; + +describe("RiskAssessmentGrading", () => { + it("should be Grade1 if total weight is less than or equal to 7", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "1", + lastUpdated: new Date(), + populationAtRisk: LowPopulationAtRisk, + attackRate: LowWeightedOption, + geographicalSpread: LowGeographicalSpread, + complexity: LowWeightedOption, + capacity: LowCapacity, + reputationalRisk: LowWeightedOption, + severity: LowWeightedOption, + // capability: LowWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 1"); + }); + + it("should be Grade2 if total weight is greater than 7 and less than equal to 14", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "2", + lastUpdated: new Date(), + populationAtRisk: MediumPopulationAtRisk, + attackRate: MediumWeightedOption, + geographicalSpread: MediumGeographicalSpread, + complexity: MediumWeightedOption, + capacity: MediumCapacity, + reputationalRisk: MediumWeightedOption, + severity: MediumWeightedOption, + // capability: MediumWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 2"); + }); + + it("should be Grade3 if score is greater than 14", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "3", + lastUpdated: new Date(), + populationAtRisk: HighPopulationAtRisk, + attackRate: HighWeightedOption, + geographicalSpread: HighGeographicalSpread, + complexity: HighWeightedOption, + capacity: HighCapacity, + reputationalRisk: HighWeightedOption, + severity: HighWeightedOption, + // capability: MediumWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 3"); + }); + + it("should be Grade3 if score is greater than 14", () => { + const riskAssessmentGrading = RiskAssessmentGrading.create({ + id: "4", + lastUpdated: new Date(), + populationAtRisk: LowPopulationAtRisk, + attackRate: MediumWeightedOption, + geographicalSpread: HighGeographicalSpread, + complexity: MediumWeightedOption, + capacity: LowCapacity, + reputationalRisk: HighWeightedOption, + severity: HighWeightedOption, + // capability: MediumWeightedOption, + }); + + riskAssessmentGrading.calculateAndSetGrade(); + + expect(riskAssessmentGrading.grade).toBe("Grade 3"); + }); +}); diff --git a/src/webapp/pages/app/__tests__/App.spec.tsx b/src/webapp/pages/app/__tests__/App.spec.tsx deleted file mode 100644 index 8a974586..00000000 --- a/src/webapp/pages/app/__tests__/App.spec.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { fireEvent, render } from "@testing-library/react"; - -import App from "../App"; -import { getTestContext } from "../../../../utils/tests"; -import { Provider } from "@dhis2/app-runtime"; - -describe("App", () => { - it("renders the feedback component", async () => { - const view = getView(); - - expect(await view.findByText("Send feedback")).toBeInTheDocument(); - }); - - it("navigates to page", async () => { - const view = getView(); - - fireEvent.click(await view.findByText("John")); - - expect(await view.findByText("Hello John")).toBeInTheDocument(); - expect(view.asFragment()).toMatchSnapshot(); - }); -}); - -function getView() { - const { compositionRoot } = getTestContext(); - return render( - - - - ); -}