diff --git a/i18n/en.pot b/i18n/en.pot index 383565f7..836445da 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-11-07T10:23:53.316Z\n" -"PO-Revision-Date: 2024-11-07T10:23:53.316Z\n" +"POT-Creation-Date: 2024-11-08T16:32:23.115Z\n" +"PO-Revision-Date: 2024-11-08T16:32:23.115Z\n" msgid "Low" msgstr "" @@ -87,9 +87,6 @@ msgstr "" msgid "Edit Action Plan" msgstr "" -msgid "Event completed" -msgstr "" - msgid "Edit Details" msgstr "" @@ -177,6 +174,18 @@ msgstr "" msgid "Risks associated with this event have not yet been assessed." msgstr "" +msgid "Complete event" +msgstr "" + +msgid "Not now" +msgstr "" + +msgid "Complete" +msgstr "" + +msgid "Are you sure you want to complete this Event? This cannot be undone." +msgstr "" + msgid "N/A" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index d1247b77..a4a9ac52 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-10-15T13:56:24.806Z\n" +"POT-Creation-Date: 2024-11-08T16:32:23.115Z\n" "PO-Revision-Date: 2018-10-25T09:02:35.143Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -89,6 +89,9 @@ msgstr "" msgid "Edit Details" msgstr "" +msgid "Complete Event" +msgstr "" + msgid "Notes" msgstr "" @@ -98,6 +101,9 @@ msgstr "" msgid "Currently assigned:" msgstr "" +msgid "Error loading current Incident Management Team" +msgstr "" + msgid "Create Event" msgstr "" @@ -167,6 +173,18 @@ msgstr "" msgid "Risks associated with this event have not yet been assessed." msgstr "" +msgid "Complete event" +msgstr "" + +msgid "Not now" +msgstr "" + +msgid "Complete" +msgstr "" + +msgid "Are you sure you want to complete this Event? This cannot be undone." +msgstr "" + msgid "N/A" msgstr "" @@ -194,6 +212,9 @@ msgstr "" msgid "Incident Response Actions saved successfully" msgstr "" +msgid "Incident Management Team Member saved successfully" +msgstr "" + msgid "Create an incident action plan" msgstr "" @@ -203,16 +224,13 @@ msgstr "" msgid "Create IAP" msgstr "" -msgid "Incident Management Team Member saved successfully" -msgstr "" - msgid "Incident Action Plan" msgstr "" -msgid "Incident Management Team Builder" +msgid "Team" msgstr "" -msgid "Cholera in NW Province, June 2023" +msgid "Edit Team" msgstr "" msgid "Incident Management Team Builder" @@ -224,10 +242,16 @@ msgstr "" msgid "Assign Role" msgstr "" +msgid "Delete Roles" +msgstr "" + msgid "Delete Role" msgstr "" -msgid "Delete team role" +msgid "Confirm deletion" +msgstr "" + +msgid "Delete" msgstr "" msgid "Resources" diff --git a/src/data/repositories/IncidentActionD2Repository.ts b/src/data/repositories/IncidentActionD2Repository.ts index 92424d7e..c15469f3 100644 --- a/src/data/repositories/IncidentActionD2Repository.ts +++ b/src/data/repositories/IncidentActionD2Repository.ts @@ -67,7 +67,6 @@ export type IncidentResponseActionDataValues = { subPillar: Maybe; searchAssignRO: Maybe; dueDate: Maybe; - timeLine: Maybe; status: Maybe; verification: Maybe; }; diff --git a/src/data/repositories/TeamMemberD2Repository.ts b/src/data/repositories/TeamMemberD2Repository.ts index 8913f1e4..20f69a67 100644 --- a/src/data/repositories/TeamMemberD2Repository.ts +++ b/src/data/repositories/TeamMemberD2Repository.ts @@ -9,6 +9,7 @@ import { Future } from "../../domain/entities/generic/Future"; const RTSL_ZEBRA_INCIDENTMANAGER = "RTSL_ZEBRA_INCIDENTMANAGER"; const RTSL_ZEBRA_RISKASSESSOR = "RTSL_ZEBRA_RISKASSESSOR"; const RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_MEMBERS = "RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_MEMBERS"; +const RTSL_ZEBRA_INCIDENT_RESPONSE_OFFICERS = "RTSL_ZEBRA_INCIDENT_RESPONSE_OFFICERS"; export class TeamMemberD2Repository implements TeamMemberRepository { constructor(private api: D2Api) {} @@ -42,6 +43,10 @@ export class TeamMemberD2Repository implements TeamMemberRepository { return this.getTeamMembersByUserGroup(RTSL_ZEBRA_INCIDENT_MANAGEMENT_TEAM_MEMBERS); } + getIncidentResponseOfficers(): FutureData { + return this.getTeamMembersByUserGroup(RTSL_ZEBRA_INCIDENT_RESPONSE_OFFICERS); + } + private getTeamMembersByUserGroup(userGroupCode: string): FutureData { return apiToFuture( this.api.metadata.get({ diff --git a/src/data/repositories/consts/IncidentActionConstants.ts b/src/data/repositories/consts/IncidentActionConstants.ts index 8f7927b4..87c552b7 100644 --- a/src/data/repositories/consts/IncidentActionConstants.ts +++ b/src/data/repositories/consts/IncidentActionConstants.ts @@ -60,7 +60,6 @@ export const responseActionConstants = { subPillar: "RTSL_ZEB_DET_SUB_PILLAR", searchAssignRO: "RTSL_ZEB_DET_SEARCH_ASSIGN_RO", dueDate: "RTSL_ZEB_DET_DUE_DATE", - timeLine: "RTSL_ZEB_DET_TIMELINE", status: "RTSL_ZEB_DET_STATUS", verification: "RTSL_ZEB_DET_VERIFICATION", } as const; @@ -127,7 +126,6 @@ export function getValueFromIncidentResponseAction( RTSL_ZEB_DET_SUB_PILLAR: incidentResponseAction.subPillar || "", RTSL_ZEB_DET_SEARCH_ASSIGN_RO: incidentResponseAction.searchAssignRO?.username || "", RTSL_ZEB_DET_DUE_DATE: incidentResponseAction.dueDate.toISOString(), - RTSL_ZEB_DET_TIMELINE: incidentResponseAction.timeLine || "", RTSL_ZEB_DET_STATUS: incidentResponseAction.status, RTSL_ZEB_DET_VERIFICATION: incidentResponseAction.verification, }; diff --git a/src/data/repositories/test/TeamMemberTestRepository.ts b/src/data/repositories/test/TeamMemberTestRepository.ts index 38e86470..ae880719 100644 --- a/src/data/repositories/test/TeamMemberTestRepository.ts +++ b/src/data/repositories/test/TeamMemberTestRepository.ts @@ -52,6 +52,22 @@ export class TeamMemberTestRepository implements TeamMemberRepository { return Future.success([teamMember]); } + getIncidentResponseOfficers(): FutureData { + const teamMember: TeamMember = new TeamMember({ + id: "incidentResponseOfficer", + username: "incidentResponseOfficer", + name: `Team Member Name test`, + email: `email@email.com`, + phone: `121-1234`, + teamRoles: undefined, + status: "Available", + photo: new URL("https://www.example.com"), + workPosition: "workPosition", + }); + + return Future.success([teamMember]); + } + getAll(): FutureData { const teamMember: TeamMember = new TeamMember({ id: "test", diff --git a/src/data/repositories/utils/DateTimeHelper.ts b/src/data/repositories/utils/DateTimeHelper.ts index 90b28c0e..5715fe6a 100644 --- a/src/data/repositories/utils/DateTimeHelper.ts +++ b/src/data/repositories/utils/DateTimeHelper.ts @@ -48,3 +48,13 @@ export function getDateAsLocaleDateString(date: Date): string { export function getISODateAsLocaleDateString(date: string): Date { return moment.utc(date).local().toDate(); } + +const getQuarter = (month: number): number => Math.ceil((month + 1) / 3); + +export function formatQuarterString(date: Date): string { + const year = date.getFullYear(); + const month = date.toLocaleString("default", { month: "short" }); + const quarter = getQuarter(date.getMonth()); + + return `Qtr ${quarter}, ${month} ${year}`; +} diff --git a/src/data/repositories/utils/IncidentActionMapper.ts b/src/data/repositories/utils/IncidentActionMapper.ts index cc01fb9e..43d8e881 100644 --- a/src/data/repositories/utils/IncidentActionMapper.ts +++ b/src/data/repositories/utils/IncidentActionMapper.ts @@ -77,7 +77,6 @@ export function mapDataElementsToIncidentResponseActions( const subPillar = getValueById(dataValues, incidentResponseActionsIds.subPillar); const searchAssignRO = getValueById(dataValues, incidentResponseActionsIds.searchAssignRO); const dueDate = getValueById(dataValues, incidentResponseActionsIds.dueDate); - const timeLine = getValueById(dataValues, incidentResponseActionsIds.timeLine); const status = getValueById(dataValues, incidentResponseActionsIds.status) as Status; const verification = getValueById( dataValues, @@ -91,7 +90,6 @@ export function mapDataElementsToIncidentResponseActions( subPillar, searchAssignRO, dueDate, - timeLine, status, verification, }; @@ -173,19 +171,24 @@ export function mapIncidentResponseActionToDataElements( const dataElementValues: Record = getValueFromIncidentResponseAction(incidentResponseAction); - const dataValues: DataValue[] = programStageDataElementsMetadata.map(programStage => { - if (!isStringInIncidentResponseActionCodes(programStage.dataElement.code)) { - throw new Error( - `DataElement code ${programStage.dataElement.code} not found in Incident Action Plan Codes` + const dataValues: DataValue[] = programStageDataElementsMetadata + .filter( + programStageDataElement => + programStageDataElement.dataElement.id !== incidentResponseActionsIds.timeLine + ) + .map(programStage => { + if (!isStringInIncidentResponseActionCodes(programStage.dataElement.code)) { + throw new Error( + `DataElement code ${programStage.dataElement.code} not found in Incident Action Plan Codes` + ); + } + const typedCode: IncidentResponseActionKeyCode = programStage.dataElement.code; + + return getPopulatedDataElement( + programStage.dataElement.id, + dataElementValues[typedCode] ); - } - const typedCode: IncidentResponseActionKeyCode = programStage.dataElement.code; - - return getPopulatedDataElement( - programStage.dataElement.id, - dataElementValues[typedCode] - ); - }); + }); return getIncidentActionTrackerEvent( programStageId, diff --git a/src/domain/entities/AppConfigurations.ts b/src/domain/entities/AppConfigurations.ts index 3ba0a476..5e531355 100644 --- a/src/domain/entities/AppConfigurations.ts +++ b/src/domain/entities/AppConfigurations.ts @@ -48,5 +48,6 @@ export type Configurations = { all: TeamMember[]; riskAssessors: TeamMember[]; incidentManagers: TeamMember[]; + responseOfficers: TeamMember[]; }; }; diff --git a/src/domain/entities/incident-action-plan/ResponseAction.ts b/src/domain/entities/incident-action-plan/ResponseAction.ts index 2c32d2a8..54ad16dd 100644 --- a/src/domain/entities/incident-action-plan/ResponseAction.ts +++ b/src/domain/entities/incident-action-plan/ResponseAction.ts @@ -25,7 +25,6 @@ interface ResponseActionAttrs { subPillar: string; searchAssignRO: Maybe; dueDate: Date; - timeLine: string; status: Status; verification: Verification; } diff --git a/src/domain/repositories/TeamMemberRepository.ts b/src/domain/repositories/TeamMemberRepository.ts index 26f94769..68170c7b 100644 --- a/src/domain/repositories/TeamMemberRepository.ts +++ b/src/domain/repositories/TeamMemberRepository.ts @@ -8,4 +8,5 @@ export interface TeamMemberRepository { getIncidentManagers(): FutureData; getRiskAssessors(): FutureData; getForIncidentManagementTeamMembers(): FutureData; + getIncidentResponseOfficers(): FutureData; } diff --git a/src/domain/usecases/GetConfigurationsUseCase.ts b/src/domain/usecases/GetConfigurationsUseCase.ts index ca529279..7b6fd86f 100644 --- a/src/domain/usecases/GetConfigurationsUseCase.ts +++ b/src/domain/usecases/GetConfigurationsUseCase.ts @@ -12,39 +12,47 @@ export class GetConfigurationsUseCase { ) {} public execute(): FutureData { - return this.teamMemberRepository.getIncidentManagers().flatMap(managers => { - return this.teamMemberRepository.getRiskAssessors().flatMap(riskAssessors => { - return this.teamMemberRepository.getAll().flatMap(teamMembers => { - return this.configurationsRepository - .getSelectableOptions() - .flatMap(selectableOptionsResponse => { - const selectableOptions: SelectableOptions = - this.mapOptionsAndTeamMembersToSelectableOptions( - selectableOptionsResponse, - managers, - riskAssessors, - teamMembers - ); - const configurations: Configurations = { - selectableOptions: selectableOptions, - teamMembers: { - all: teamMembers, - riskAssessors: riskAssessors, - incidentManagers: managers, - }, - }; - return Future.success(configurations); - }); - }); - }); - }); + return Future.joinObj({ + allTeamMembers: this.teamMemberRepository.getAll(), + incidentResponseOfficers: this.teamMemberRepository.getIncidentResponseOfficers(), + managers: this.teamMemberRepository.getIncidentManagers(), + riskAssessors: this.teamMemberRepository.getRiskAssessors(), + selectableOptionsResponse: this.configurationsRepository.getSelectableOptions(), + }).flatMap( + ({ + allTeamMembers, + incidentResponseOfficers, + managers, + riskAssessors, + selectableOptionsResponse, + }) => { + const selectableOptions: SelectableOptions = + this.mapOptionsAndTeamMembersToSelectableOptions( + selectableOptionsResponse, + managers, + riskAssessors, + incidentResponseOfficers + ); + + const configurations: Configurations = { + selectableOptions: selectableOptions, + teamMembers: { + all: allTeamMembers, + riskAssessors: riskAssessors, + incidentManagers: managers, + responseOfficers: incidentResponseOfficers, + }, + }; + return Future.success(configurations); + } + ); } mapOptionsAndTeamMembersToSelectableOptions( selectableOptionsResponse: SelectableOptions, managers: TeamMember[], riskAssessors: TeamMember[], - teamMembers: TeamMember[] + incidentResponseOfficers: TeamMember[] ): SelectableOptions { const selectableOptions: SelectableOptions = { eventTrackerConfigurations: { @@ -66,7 +74,7 @@ export class GetConfigurationsUseCase { }, incidentResponseActionConfigurations: { ...selectableOptionsResponse.incidentResponseActionConfigurations, - searchAssignRO: teamMembers, + searchAssignRO: incidentResponseOfficers, }, }; diff --git a/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts b/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts index 278d0da4..7fd0702a 100644 --- a/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts +++ b/src/domain/usecases/utils/incident-action/GetIncidentActionById.ts @@ -65,7 +65,6 @@ export function getIncidentAction( dueDate: responseActionDataValue?.dueDate ? new Date(responseActionDataValue.dueDate) : new Date(), - timeLine: responseActionDataValue?.timeLine ?? "", status: status, verification: verification, }); diff --git a/src/utils/tests.tsx b/src/utils/tests.tsx index 2c175a19..e94989b2 100644 --- a/src/utils/tests.tsx +++ b/src/utils/tests.tsx @@ -65,6 +65,7 @@ export function getTestContext() { all: [], riskAssessors: [], incidentManagers: [], + responseOfficers: [], }, }, }; diff --git a/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx b/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx index 2b8f9a45..52bc5661 100644 --- a/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx +++ b/src/webapp/components/form/form-summary/EventTrackerFormSummary.tsx @@ -13,46 +13,34 @@ import { FormSummaryData } from "../../../pages/event-tracker/useDiseaseOutbreak import { Maybe } from "../../../../utils/ts-utils"; import { FormType } from "../../../pages/form-page/FormPage"; import { Id } from "../../../../domain/entities/Ref"; -import { useAppContext } from "../../../contexts/app-context"; +import { GlobalMessage } from "../../../pages/form-page/useForm"; export type EventTrackerFormSummaryProps = { id: Id; formType: FormType; formSummary: Maybe; - summaryError: Maybe; + globalMessage: Maybe; + onOpenModal: () => void; }; const ROW_COUNT = 3; export const EventTrackerFormSummary: React.FC = React.memo(props => { - const { compositionRoot } = useAppContext(); - const { id, formType, formSummary, summaryError } = props; + const { id, formType, formSummary, onOpenModal: onCompleteClick, globalMessage } = props; const { goTo } = useRoutes(); const snackbar = useSnackbar(); useEffect(() => { - if (!summaryError) return; + if (!globalMessage) return; - snackbar.error(summaryError); + snackbar[globalMessage.type](globalMessage.text); goTo(RouteName.DASHBOARD); - }, [summaryError, snackbar, goTo]); + }, [globalMessage, goTo, snackbar]); const onEditClick = useCallback(() => { goTo(RouteName.EDIT_FORM, { formType: formType, id: id }); }, [formType, goTo, id]); - const onCompleteClick = useCallback(() => { - compositionRoot.diseaseOutbreakEvent.complete.execute(id).run( - () => { - snackbar.success(i18n.t("Event completed")); - }, - err => { - snackbar.error(i18n.t(`Failed to complete event: ${err.message}`)); - console.error(err); - } - ); - }, [compositionRoot, id, snackbar]); - const editButton = ( + } + alignFooterButtons="end" + buttonDirection="row-reverse" + > + {i18n.t("Are you sure you want to complete this Event? This cannot be undone.")} + ); }); diff --git a/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts b/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts index 0433f5e2..ba003c21 100644 --- a/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts +++ b/src/webapp/pages/event-tracker/useDiseaseOutbreakEvent.ts @@ -15,6 +15,8 @@ import { User } from "../../components/user-selector/UserSelector"; import { TableRowType } from "../../components/table/BasicTable"; import { RiskAssessmentGrading } from "../../../domain/entities/risk-assessment/RiskAssessmentGrading"; import { mapTeamMemberToUser } from "../form-page/mapEntityToFormState"; +import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; +import { GlobalMessage } from "../form-page/useForm"; const EventTypeLabel = "Event type"; const DiseaseLabel = "Disease"; @@ -32,9 +34,12 @@ export type FormSummaryData = { export function useDiseaseOutbreakEvent(id: Id) { const { compositionRoot, configurations } = useAppContext(); const [formSummary, setFormSummary] = useState(); - const [summaryError, setSummaryError] = useState(); + const [globalMessage, setGlobalMessage] = useState>(); const [riskAssessmentRows, setRiskAssessmentRows] = useState([]); const [eventTrackerDetails, setEventTrackerDetails] = useState(); + const [openCompleteModal, setOpenCompleteModal] = useState(false); + const { changeExistingEventTrackerTypes, existingEventTrackerTypes } = + useExistingEventTrackerTypes(); useEffect(() => { compositionRoot.diseaseOutbreakEvent.get.execute(id, configurations).run( @@ -47,7 +52,10 @@ export function useDiseaseOutbreakEvent(id: Id) { }, err => { console.debug(err); - setSummaryError(`Event tracker with id: ${id} does not exist`); + setGlobalMessage({ + type: "error", + text: `Event tracker with id: ${id} does not exist`, + }); } ); }, [compositionRoot.diseaseOutbreakEvent.get, configurations, id]); @@ -141,6 +149,7 @@ export function useDiseaseOutbreakEvent(id: Id) { return []; } }; + const orderByRiskAssessmentDate = useCallback( (direction: "asc" | "desc") => { setRiskAssessmentRows(prevRows => { @@ -170,11 +179,60 @@ export function useDiseaseOutbreakEvent(id: Id) { [setRiskAssessmentRows] ); + const onCompleteClick = useCallback(() => { + compositionRoot.diseaseOutbreakEvent.complete.execute(id).run( + () => { + const eventTrackerName = + eventTrackerDetails?.hazardType ?? eventTrackerDetails?.suspectedDisease?.name; + + const updatedEventTrackerTypes = existingEventTrackerTypes.filter( + eventTrackerType => eventTrackerType !== eventTrackerName + ); + + if (eventTrackerName) { + changeExistingEventTrackerTypes(updatedEventTrackerTypes); + } + + setGlobalMessage({ + type: "success", + text: `Event tracker with id: ${id} has been completed`, + }); + }, + err => { + console.error(err); + setGlobalMessage({ + type: "error", + text: `Failed to complete event: : ${err.message}`, + }); + } + ); + }, [ + changeExistingEventTrackerTypes, + compositionRoot.diseaseOutbreakEvent.complete, + eventTrackerDetails?.hazardType, + eventTrackerDetails?.suspectedDisease?.name, + existingEventTrackerTypes, + id, + ]); + + const onOpenCompleteModal = useCallback( + () => setOpenCompleteModal(true), + [setOpenCompleteModal] + ); + const onCloseCompleteModal = useCallback( + () => setOpenCompleteModal(false), + [setOpenCompleteModal] + ); + return { formSummary, - summaryError, + globalMessage, riskAssessmentRows, eventTrackerDetails, + openCompleteModal, + onCloseCompleteModal, + onCompleteClick, + onOpenCompleteModal, orderByRiskAssessmentDate, }; } diff --git a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts index 8166e870..0a0f654a 100644 --- a/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts +++ b/src/webapp/pages/form-page/incident-action/mapIncidentActionToInitialFormState.ts @@ -349,17 +349,6 @@ function getResponseActionSection(options: { showIsRequired: true, disabled: false, }, - { - id: `${responseActionConstants.timeLine}_${index}`, - label: "Time line", - isVisible: true, - errors: [], - value: incidentResponseAction?.timeLine || "", - type: "text", - required: true, - showIsRequired: true, - disabled: false, - }, { id: `${responseActionConstants.status}_${index}`, label: "Status", diff --git a/src/webapp/pages/form-page/mapFormStateToEntityData.ts b/src/webapp/pages/form-page/mapFormStateToEntityData.ts index 25de3fd4..b738a802 100644 --- a/src/webapp/pages/form-page/mapFormStateToEntityData.ts +++ b/src/webapp/pages/form-page/mapFormStateToEntityData.ts @@ -559,9 +559,6 @@ function mapFormStateToIncidentResponseAction( const dueDate = allFields.find(field => field.id.includes(`${responseActionConstants.dueDate}_${index}`) )?.value as Date; - const timeLine = allFields.find(field => - field.id.includes(`${responseActionConstants.timeLine}_${index}`) - )?.value as string; const searchAssignROValue = allFields.find(field => field.id.includes(`${responseActionConstants.searchAssignRO}_${index}`) @@ -591,7 +588,6 @@ function mapFormStateToIncidentResponseAction( subActivities: subActivities, subPillar: subPillar, dueDate: dueDate, - timeLine: timeLine, searchAssignRO: searchAssignRO, status: status.id as Status, verification: verification.id as Verification, diff --git a/src/webapp/pages/form-page/useForm.ts b/src/webapp/pages/form-page/useForm.ts index f3b90e47..d237f8c7 100644 --- a/src/webapp/pages/form-page/useForm.ts +++ b/src/webapp/pages/form-page/useForm.ts @@ -23,6 +23,7 @@ import { import { useExistingEventTrackerTypes } from "../../contexts/existing-event-tracker-types-context"; import { useCheckWritePermission } from "../../hooks/useHasCurrentUserCaptureAccess"; import { useSnackbar } from "@eyeseetea/d2-ui-components"; +import { usePerformanceOverview } from "../dashboard/usePerformanceOverview"; export type GlobalMessage = { text: string; @@ -67,9 +68,18 @@ export function useForm(formType: FormType, id?: Id): State { const [isLoading, setIsLoading] = useState(false); const currentEventTracker = getCurrentEventTracker(); const { existingEventTrackerTypes } = useExistingEventTrackerTypes(); + const { dataPerformanceOverview } = usePerformanceOverview(); useCheckWritePermission(formType); const snackbar = useSnackbar(); + const allDataPerformanceEvents = dataPerformanceOverview?.map( + event => event.hazardType || event.suspectedDisease + ); + const existingEventTrackers = + existingEventTrackerTypes.length === 0 + ? allDataPerformanceEvents + : existingEventTrackerTypes; + useEffect(() => { compositionRoot.getConfigurableForm .execute(formType, currentEventTracker, configurations, id) @@ -79,7 +89,7 @@ export function useForm(formType: FormType, id?: Id): State { setFormLabels(formData.labels); setFormState({ kind: "loaded", - data: mapEntityToFormState(formData, !!id, existingEventTrackerTypes), + data: mapEntityToFormState(formData, !!id, existingEventTrackers), }); }, error => { @@ -99,7 +109,7 @@ export function useForm(formType: FormType, id?: Id): State { id, currentEventTracker, configurations, - existingEventTrackerTypes, + existingEventTrackers, snackbar, goTo, ]); @@ -300,7 +310,10 @@ export function useForm(formType: FormType, id?: Id): State { }); break; case "incident-response-action": - if (currentEventTracker?.id) goTo(RouteName.INCIDENT_ACTION_PLAN); + if (currentEventTracker?.id) + goTo(RouteName.INCIDENT_ACTION_PLAN, { + id: currentEventTracker?.id, + }); setGlobalMessage({ text: i18n.t(`Incident Response Actions saved successfully`), type: "success", diff --git a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx index beda3408..d2b165cd 100644 --- a/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx +++ b/src/webapp/pages/incident-action-plan/IncidentActionPlanPage.tsx @@ -24,6 +24,7 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { summaryError, incidentActionExists, responseActionColumns, + orderByDueDate, saveTableOption, } = useIncidentActionPlan(id); @@ -68,6 +69,7 @@ export const IncidentActionPlanPage: React.FC = React.memo(() => { responseActionColumns={responseActionColumns} responseActionRows={responseActionRows} onChange={saveTableOption} + onOrderBy={orderByDueDate} /> , rowIndex: number, column: TableColumn["value"]) => void; + onOrderBy: (direction: "asc" | "desc") => void; responseActionColumns: TableColumn[]; responseActionRows: { [key: TableColumn["value"]]: string; @@ -16,7 +17,7 @@ type ResponseActionTableProps = { }; export const ResponseActionTable: React.FC = React.memo( - ({ onChange, responseActionColumns, responseActionRows }) => { + ({ onChange, onOrderBy, responseActionColumns, responseActionRows }) => { const { goTo } = useRoutes(); const { icon: responseActionIcon, label: responseActionLabel } = @@ -48,6 +49,7 @@ export const ResponseActionTable: React.FC = React.mem onChange={onChange} columns={responseActionColumns} rows={responseActionRows} + onOrderBy={onOrderBy} /> diff --git a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts index 6575a35c..54753178 100644 --- a/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts +++ b/src/webapp/pages/incident-action-plan/useIncidentActionPlan.ts @@ -2,6 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Id } from "../../../domain/entities/Ref"; import { Maybe } from "../../../utils/ts-utils"; import { useAppContext } from "../../contexts/app-context"; +import { + formatQuarterString, + getISODateAsLocaleDateString, +} from "../../../data/repositories/utils/DateTimeHelper"; import { TableColumn, TableRowType } from "../../components/table/BasicTable"; import { getIAPTypeByCode, @@ -16,6 +20,7 @@ import { import { Option } from "../../components/utils/option"; import { useCurrentEventTracker } from "../../contexts/current-event-tracker-context"; import { DiseaseOutbreakEvent } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import _c from "../../../domain/entities/generic/Collection"; export type IncidentActionFormSummaryData = { subTitle: string; @@ -30,11 +35,13 @@ export type UIIncidentActionOptions = { export function useIncidentActionPlan(id: Id) { const { compositionRoot, configurations: appConfiguration } = useAppContext(); const { changeCurrentEventTracker, getCurrentEventTracker } = useCurrentEventTracker(); + const currentEventTracker = getCurrentEventTracker(); const [incidentAction, setIncidentAction] = useState(); const [actionPlanSummary, setActionPlanSummary] = useState(); const [responseActionRows, setResponseActionRows] = useState([]); const [globalMessage, setGlobalMessage] = useState(); + const [incidentActionPlan, setIncidentActionPlan] = useState(); const [incidentActionExists, setIncidentActionExists] = useState(false); const [incidentActionOptions, setIncidentActionOptions] = useState(); @@ -89,22 +96,8 @@ export function useIncidentActionPlan(id: Id) { incidentActionPlan => { const incidentActionExists = !!incidentActionPlan?.actionPlan?.id; const incidentActionOptions = incidentActionPlan?.incidentActionOptions; - const currentEventTracker = getCurrentEventTracker(); - if ( - incidentActionExists && - currentEventTracker && - (currentEventTracker.incidentActionPlan?.actionPlan?.lastUpdated?.getTime() !== - incidentActionPlan.actionPlan?.lastUpdated?.getTime() || - currentEventTracker.incidentActionPlan?.responseActions.length !== - incidentActionPlan.responseActions.length) - ) { - const updatedEventTracker = new DiseaseOutbreakEvent({ - ...currentEventTracker, - incidentActionPlan: incidentActionPlan, - }); - changeCurrentEventTracker(updatedEventTracker); - } + setIncidentActionPlan(incidentActionPlan); setIncidentActionExists(incidentActionExists); setIncidentActionOptions(mapIncidentActionOptionsToTable(incidentActionOptions)); setIncidentAction(getIncidentActionFormSummary(incidentActionPlan)); @@ -116,7 +109,58 @@ export function useIncidentActionPlan(id: Id) { setGlobalMessage(`Event tracker with id: ${id} does not exist`); } ); - }, [compositionRoot, id, changeCurrentEventTracker, getCurrentEventTracker, appConfiguration]); + }, [appConfiguration, compositionRoot.incidentActionPlan.get, id]); + + useEffect(() => { + if ( + incidentActionExists && + currentEventTracker && + (currentEventTracker.incidentActionPlan?.actionPlan?.lastUpdated !== + incidentActionPlan?.actionPlan?.lastUpdated || + currentEventTracker.incidentActionPlan?.responseActions.length !== + incidentActionPlan?.responseActions.length) + ) { + const updatedEventTracker = new DiseaseOutbreakEvent({ + ...currentEventTracker, + incidentActionPlan: incidentActionPlan, + }); + + changeCurrentEventTracker(updatedEventTracker); + } + }, [changeCurrentEventTracker, currentEventTracker, incidentActionExists, incidentActionPlan]); + + const orderByDueDate = useCallback( + (direction: "asc" | "desc") => { + setResponseActionRows(prevRows => { + if (direction === "asc") { + const sortedRows = prevRows.sort((a, b) => { + if (!a.dueDate) return -1; + if (!b.dueDate) return 1; + + const dateA = new Date(a.dueDate).toISOString(); + const dateB = new Date(b.dueDate).toISOString(); + + return dateA < dateB ? -1 : dateA > dateB ? 1 : 0; + }); + + return sortedRows; + } else { + const sortedRows = prevRows.sort((a, b) => { + if (!a.dueDate) return -1; + if (!b.dueDate) return -1; + + const dateA = new Date(a.dueDate).toISOString(); + const dateB = new Date(b.dueDate).toISOString(); + + return dateA < dateB ? 1 : dateA > dateB ? -1 : 0; + }); + + return sortedRows; + } + }); + }, + [setResponseActionRows] + ); return { incidentActionExists: incidentActionExists, @@ -126,6 +170,7 @@ export function useIncidentActionPlan(id: Id) { formSummary: incidentAction, responseActionRows: responseActionRows, summaryError: globalMessage, + orderByDueDate: orderByDueDate, }; } @@ -189,17 +234,26 @@ const mapIncidentResponseActionToTableRows = ( incidentActionPlan: Maybe ): TableRowType[] => { if (incidentActionPlan) { - return incidentActionPlan.responseActions.map(responseAction => ({ - id: responseAction.id, - mainTask: responseAction.mainTask, - subActivities: responseAction.subActivities, - subPillar: responseAction.subPillar, - searchAssignRO: responseAction.searchAssignRO?.username ?? "", - status: getStatusTypeByCode(responseAction.status) ?? "", - verification: getVerificationTypeByCode(responseAction.verification) ?? "", - timeLine: responseAction.timeLine, - dueDate: responseAction.dueDate.toISOString(), - })); + return ( + _c(incidentActionPlan.responseActions) + .map(responseAction => ({ + id: responseAction.id, + mainTask: responseAction.mainTask, + subActivities: responseAction.subActivities, + subPillar: responseAction.subPillar, + searchAssignRO: responseAction.searchAssignRO?.username ?? "", + status: getStatusTypeByCode(responseAction.status) ?? "", + verification: getVerificationTypeByCode(responseAction.verification) ?? "", + timeLine: formatQuarterString(responseAction.dueDate), + dueDate: getISODateAsLocaleDateString( + responseAction.dueDate.toISOString() + ).toDateString(), + })) + //DHIS returns events last updated first, zebra app needs it in order of creation, + //so we reverse the order + .reverse() + .value() + ); } else { return []; }