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(
-
-
-
- );
-}