diff --git a/hat/assets/js/apps/Iaso/domains/app/types.ts b/hat/assets/js/apps/Iaso/domains/app/types.ts index 5861e6367d..fcad575b7f 100644 --- a/hat/assets/js/apps/Iaso/domains/app/types.ts +++ b/hat/assets/js/apps/Iaso/domains/app/types.ts @@ -44,3 +44,13 @@ export type Plugin = { export type Plugins = { plugins: Plugin[]; }; + +export type PaginatedResponse = { + hasPrevious?: boolean; + hasNext?: boolean; + count?: number; + page?: number; + pages?: number; + limit?: number; + results?: T[]; +}; diff --git a/hat/assets/js/apps/Iaso/domains/instances/components/LinkToInstance.tsx b/hat/assets/js/apps/Iaso/domains/instances/components/LinkToInstance.tsx index 24879d3515..bda6449e5d 100644 --- a/hat/assets/js/apps/Iaso/domains/instances/components/LinkToInstance.tsx +++ b/hat/assets/js/apps/Iaso/domains/instances/components/LinkToInstance.tsx @@ -1,7 +1,6 @@ import React, { FunctionComponent } from 'react'; import { userHasOneOfPermissions } from '../../users/utils'; import { baseUrls } from '../../../constants/urls'; -import * as Permission from '../../../utils/permissions'; import { useCurrentUser } from '../../../utils/usersUtils'; import MESSAGES from '../../assignments/messages'; import { SUBMISSIONS, SUBMISSIONS_UPDATE } from '../../../utils/permissions'; @@ -42,5 +41,4 @@ export const LinkToInstance: FunctionComponent = ({ iconSize={iconSize} /> ); - color={color} }; diff --git a/hat/assets/js/apps/Iaso/hooks/useTabs.tsx b/hat/assets/js/apps/Iaso/hooks/useTabs.tsx index 2a46611c90..d5b0e12df5 100644 --- a/hat/assets/js/apps/Iaso/hooks/useTabs.tsx +++ b/hat/assets/js/apps/Iaso/hooks/useTabs.tsx @@ -9,9 +9,9 @@ import { useRedirectToReplace } from 'bluesquare-components'; import { Optional } from '../types/utils'; type UseTabsParams = { - params: Record>; + params?: Record>; defaultTab: T; - baseUrl: string; + baseUrl?: string; }; // T should be a union type of the possible string values for the Tabs @@ -32,11 +32,13 @@ export const useTabs = ({ const handleChangeTab = useCallback( (_event, newTab) => { - const newParams = { - ...params, - tab: newTab, - }; - redirectToReplace(baseUrl, newParams); + if (baseUrl && params) { + const newParams = { + ...params, + tab: newTab, + }; + redirectToReplace(baseUrl, newParams); + } setTab(newTab); }, [params, redirectToReplace, baseUrl], diff --git a/hat/assets/js/apps/Iaso/utils/table.ts b/hat/assets/js/apps/Iaso/utils/table.ts index 4119ba4a22..8fb663fc47 100644 --- a/hat/assets/js/apps/Iaso/utils/table.ts +++ b/hat/assets/js/apps/Iaso/utils/table.ts @@ -4,6 +4,8 @@ import { setTableSelection, } from 'bluesquare-components'; import { Selection } from '../domains/orgUnits/types/selection'; +import { useObjectState } from '../hooks/useObjectState'; +import { PaginationParams } from '../types/general'; type UseTableSelection = { selection: Selection; @@ -74,3 +76,29 @@ export const useTableSelection = (count?: number): UseTableSelection => { }; }, [handleSelectAll, handleTableSelection, handleUnselectAll, selection]); }; + +const defaultInitialState: PaginationParams = { + order: '-updated_at', + page: '1', + pageSize: '10', +}; + +type TableState = { + params: PaginationParams; + // eslint-disable-next-line no-unused-vars + onTableParamsChange: (newParams: PaginationParams) => void; +}; +export const useTableState = (initialState?: PaginationParams): TableState => { + const [tableState, setTableState] = useObjectState( + initialState ?? defaultInitialState, + ); + + const onTableParamsChange = useCallback( + (newParams: PaginationParams) => setTableState(newParams), + [setTableState], + ); + + return useMemo(() => { + return { params: tableState, onTableParamsChange }; + }, [onTableParamsChange, tableState]); +}; diff --git a/plugins/polio/js/src/constants/messages.js b/plugins/polio/js/src/constants/messages.js index 58590951d4..2409450378 100644 --- a/plugins/polio/js/src/constants/messages.js +++ b/plugins/polio/js/src/constants/messages.js @@ -2392,6 +2392,10 @@ const MESSAGES = defineMessages({ defaultMessage: 'Go to campaign', id: 'iaso.polio.lqas.goToCampaign', }, + subActivities: { + defaultMessage: 'Sub-activities', + id: 'iaso.polio.subActivities', + }, }); export default MESSAGES; diff --git a/plugins/polio/js/src/constants/translations/en.json b/plugins/polio/js/src/constants/translations/en.json index 8cbafc1185..ad51e78c8a 100644 --- a/plugins/polio/js/src/constants/translations/en.json +++ b/plugins/polio/js/src/constants/translations/en.json @@ -194,12 +194,16 @@ "iaso.polio.form.label.who_sent_budget": "WHO CO sent budget", "iaso.polio.form.pleaseCreateVrf": "Create VRF to unlock this tab", "iaso.polio.form.validator.error.endDateAfterNextStartDate": "End date can't be after or equal next round start date", + "iaso.polio.form.validator.error.endDateAfterRoundDate": "End date can't be after round end date", + "iaso.polio.form.validator.error.endDateBeforeRoundStart": "End date can't be before round start date", "iaso.polio.form.validator.error.endDateBeforeStartDate": "End date can't be before start date", "iaso.polio.form.validator.error.positiveInteger": "Please use a positive integer", "iaso.polio.form.validator.error.positiveNumber": "Please use a positive number", "iaso.polio.form.validator.error.positiveRangeInteger": "Please use a positive integer between 0 and 100", "iaso.polio.form.validator.error.startDateAfterEndDate": "Start date can't be after end date", + "iaso.polio.form.validator.error.startDateAfterRoundEnd": "Start date can't be after round end date", "iaso.polio.form.validator.error.startDateBeforePreviousEndDate": "Start date can't be before or equal previous round end date", + "iaso.polio.form.validator.error.startDateBeforeRoundDate": "Start date can't be before round start date", "iaso.polio.forms.deleteRound": "Delete round", "iaso.polio.forms.emptyEndDate": "Round {roundNumber} has no end date", "iaso.polio.forms.emptyStartDate": "Round {roundNumber} has no start date", @@ -222,6 +226,10 @@ "iaso.polio.label.addVaccine": "Add vaccine", "iaso.polio.label.addVar": "Add VAR", "iaso.polio.label.afroMapfilterInfo": "The latest campaign is the campaign with the round that started the most recently within the chosen time frame", + "iaso.polio.label.ageGroup": "Age group", + "iaso.polio.label.ageMax": "Age max", + "iaso.polio.label.ageMin": "Age min", + "iaso.polio.label.ageUnit": "Enter age in", "iaso.polio.label.all": "All", "iaso.polio.label.amount": "Amount", "iaso.polio.label.approval_ongoing": "Approval ongoing", @@ -283,6 +291,7 @@ "iaso.polio.label.countryPassing": "80% or more districts passed", "iaso.polio.label.create": "Create", "iaso.polio.label.createReasonForDelay": "Create reason for delay", + "iaso.polio.label.createSubActivity": "Create sub-activity", "iaso.polio.label.createVrf": "Create VRF", "iaso.polio.label.creation_email_send_at": "Email creation date", "iaso.polio.label.csv": "CSV", @@ -310,6 +319,7 @@ "iaso.polio.label.deleteIncidentWarning": "Are you sure you want to delete this incident report ?", "iaso.polio.label.deleteNopv2Auth": "Delete authorisation", "iaso.polio.label.deleteStockWarning": "Are you sure you want to delete this vaccine stock", + "iaso.polio.label.deleteSubActivity": "Delete sub-activity?", "iaso.polio.label.deleteVRF": "Delete VRF?", "iaso.polio.label.deleteVRFWarning": "This will also delete all attached pre-alerts and VARs", "iaso.polio.label.deleteWarning": "Are you sure you want to delete this campaign?", @@ -341,6 +351,7 @@ "iaso.polio.label.editGroupedCampaign": "Edit grouped campaign", "iaso.polio.label.editReasonForDelay": "Edit reason for delay", "iaso.polio.label.editRoundDates": "Edit round dates", + "iaso.polio.label.editSubActivity": "Edit sub-activity", "iaso.polio.label.emailListEmpty": "No email configured for this country", "iaso.polio.label.emailListLabel": "Configured emails :", "iaso.polio.label.emailListTooltip": "Change email list via country configuration", @@ -435,6 +446,7 @@ "iaso.polio.label.modifiedBy": "Modified by", "iaso.polio.label.moh": "MOH", "iaso.polio.label.MOH_DECISION": "Decision from MOH", + "iaso.polio.label.months": "Months", "iaso.polio.label.name": "Name", "iaso.polio.label.name_en": "English text", "iaso.polio.label.name_fr": "French text", @@ -619,6 +631,7 @@ "iaso.polio.label.who": "WHO", "iaso.polio.label.whoCompletedRecruitement": "WHO Completed Recruitment", "iaso.polio.label.whoToRecruit": "WHO To Recruit", + "iaso.polio.label.years": "Years", "iaso.polio.label.yes": "Yes", "iaso.polio.lqas.goToCampaign": "Go to campaign", "iaso.polio.lqasim.penultimate": "Penultimate", @@ -678,6 +691,7 @@ "iaso.polio.sendEmail.error": "Error sending notification email", "iaso.polio.sendEmail.success": "Notification email sent", "iaso.polio.showDeletedCampaigns": "Show deleted campaigns", + "iaso.polio.subActivities": "Sub-activities", "iaso.polio.table.label.actions": "Actions", "iaso.polio.table.label.country": "Country", "iaso.polio.table.label.language": "Language", diff --git a/plugins/polio/js/src/constants/translations/fr.json b/plugins/polio/js/src/constants/translations/fr.json index bbeb54ad0f..5f453f2470 100644 --- a/plugins/polio/js/src/constants/translations/fr.json +++ b/plugins/polio/js/src/constants/translations/fr.json @@ -193,12 +193,16 @@ "iaso.polio.form.label.who_sent_budget": "Budget envoyé par WHO CO", "iaso.polio.form.pleaseCreateVrf": "Veuillez créer un VRF pour débloquer ce tab", "iaso.polio.form.validator.error.endDateAfterNextStartDate": "La date de fin ne peut être après ou égal à la date de début du round suivant", + "iaso.polio.form.validator.error.endDateAfterRoundDate": "La date de fin ne peut être après celle du round", + "iaso.polio.form.validator.error.endDateBeforeRoundStart": "La date de fin ne peut être avant le début du round", "iaso.polio.form.validator.error.endDateBeforeStartDate": "La date de fin ne peut être avant la date de début", "iaso.polio.form.validator.error.positiveInteger": "Veuillez remplir avec un nombre entier positif", "iaso.polio.form.validator.error.positiveNumber": "Veuillez remplir avec un nombre positif", "iaso.polio.form.validator.error.positiveRangeInteger": "Veuillez remplir avec un nombre entier positif entre 0 et 100", "iaso.polio.form.validator.error.startDateAfterEndDate": "La date de début ne peut pas être après la date de fin", + "iaso.polio.form.validator.error.startDateAfterRoundEnd": "La date de début ne peut être après la fin du round", "iaso.polio.form.validator.error.startDateBeforePreviousEndDate": "La date de début ne peut être avant ou égal à la date de fin du round précédent", + "iaso.polio.form.validator.error.startDateBeforeRoundDate": "La date de début ne peut pas précéder celle du round", "iaso.polio.forms.deleteRound": "Effacer round", "iaso.polio.forms.emptyEndDate": "Le round {roundNumber} n'a pas de date de fin", "iaso.polio.forms.emptyStartDate": "Le round {roundNumber} n'a pas de date de début", @@ -221,6 +225,10 @@ "iaso.polio.label.addVaccine": "Ajouter un vaccin", "iaso.polio.label.addVar": "Ajouter VAR", "iaso.polio.label.afroMapfilterInfo": "La dernière campagne est celle ayant le round actif le plus récent pour la période sélectionnée", + "iaso.polio.label.ageGroup": "Groupe d'âge", + "iaso.polio.label.ageMax": "Âge max", + "iaso.polio.label.ageMin": "Âge min", + "iaso.polio.label.ageUnit": "Entrer l'âge en", "iaso.polio.label.all": "Tous", "iaso.polio.label.amount": "Montant", "iaso.polio.label.approval_ongoing": "Approbation en cours", @@ -282,6 +290,7 @@ "iaso.polio.label.countryPassing": "80% ou plus de districts ont réussi", "iaso.polio.label.create": "Créer", "iaso.polio.label.createReasonForDelay": "Ajouter un raison de retard", + "iaso.polio.label.createSubActivity": "Créer une sous-activité", "iaso.polio.label.createVrf": "Créer VRF", "iaso.polio.label.creation_email_send_at": "Date de création email", "iaso.polio.label.csv": "CSV", @@ -309,6 +318,7 @@ "iaso.polio.label.deleteIncidentWarning": "Etes-vous sûr(e) de vouloir effacer ce rapport d'incident ?", "iaso.polio.label.deleteNopv2Auth": "Effacer autorisation", "iaso.polio.label.deleteStockWarning": "Voulez-vous effacer ce stock de vaccins", + "iaso.polio.label.deleteSubActivity": "Effacer sous-activité?", "iaso.polio.label.deleteVRF": "Effacter VRF?", "iaso.polio.label.deleteVRFWarning": "Les pré-alertes et VARs liés seront aussi effacés", "iaso.polio.label.deleteWarning": "Etes-vous sûr(e) de vouloir effacer cette campagne?", @@ -340,6 +350,7 @@ "iaso.polio.label.editGroupedCampaign": "Editer campagne groupée", "iaso.polio.label.editReasonForDelay": "Modifier une raisond de retard", "iaso.polio.label.editRoundDates": "Modifier les dates de round", + "iaso.polio.label.editSubActivity": "Editer sous-activité", "iaso.polio.label.emailListEmpty": "Pas de destinataire configuré sur ce pays", "iaso.polio.label.emailListLabel": "Destinataires configurés:", "iaso.polio.label.emailListTooltip": "La liste d'email est configurable sur les pays", @@ -434,6 +445,7 @@ "iaso.polio.label.modifiedBy": "Modifié par", "iaso.polio.label.moh": "MOH", "iaso.polio.label.MOH_DECISION": "Décision du Ministère de la Santé", + "iaso.polio.label.months": "Mois", "iaso.polio.label.name": "Nom", "iaso.polio.label.name_en": "Texte anglais", "iaso.polio.label.name_fr": "Texte français", @@ -618,6 +630,7 @@ "iaso.polio.label.who": "WHO", "iaso.polio.label.whoCompletedRecruitement": "Recrutement WHO complet", "iaso.polio.label.whoToRecruit": "Recrutements à effecter WHO", + "iaso.polio.label.years": "Années", "iaso.polio.label.yes": "Oui", "iaso.polio.lqas.goToCampaign": "Voir la campagne", "iaso.polio.lqasim.penultimate": "Avant-dernier", @@ -677,6 +690,7 @@ "iaso.polio.sendEmail.error": "Erreur lors de l'envoi de l'email de notification", "iaso.polio.sendEmail.success": "Email de notification envoyé", "iaso.polio.showDeletedCampaigns": "Montrer les campagnes effacées", + "iaso.polio.subActivities": "Sous-activités", "iaso.polio.table.label.actions": "Actions", "iaso.polio.table.label.country": "Pays", "iaso.polio.table.label.language": "Langue", diff --git a/plugins/polio/js/src/constants/virus.ts b/plugins/polio/js/src/constants/virus.ts index 3927fd2bec..1ab24bbbc9 100644 --- a/plugins/polio/js/src/constants/virus.ts +++ b/plugins/polio/js/src/constants/virus.ts @@ -32,7 +32,7 @@ const polioViruses = [ }, ]; -type PolioVaccine = { +export type PolioVaccine = { value: Vaccine; label: string; color: string; diff --git a/plugins/polio/js/src/domains/Campaigns/Rounds/EvaluationForm.tsx b/plugins/polio/js/src/domains/Campaigns/Evaluations/EvaluationForm.tsx similarity index 100% rename from plugins/polio/js/src/domains/Campaigns/Rounds/EvaluationForm.tsx rename to plugins/polio/js/src/domains/Campaigns/Evaluations/EvaluationForm.tsx diff --git a/plugins/polio/js/src/domains/Campaigns/Evaluations/EvaluationsForms.tsx b/plugins/polio/js/src/domains/Campaigns/Evaluations/EvaluationsForms.tsx index 287a164fe4..2016584052 100644 --- a/plugins/polio/js/src/domains/Campaigns/Evaluations/EvaluationsForms.tsx +++ b/plugins/polio/js/src/domains/Campaigns/Evaluations/EvaluationsForms.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent, useState } from 'react'; import MESSAGES from '../../../constants/messages'; import { Campaign, Round } from '../../../constants/types'; -import { EvaluationForm } from '../Rounds/EvaluationForm'; +import { EvaluationForm } from './EvaluationForm'; export const scopeFormFields = ['separate_scopes_per_round', 'scopes']; diff --git a/plugins/polio/js/src/domains/Campaigns/Rounds/LqasDistrictsPassed/LqasDistrictsPassed.tsx b/plugins/polio/js/src/domains/Campaigns/Evaluations/LqasDistrictsPassed/LqasDistrictsPassed.tsx similarity index 100% rename from plugins/polio/js/src/domains/Campaigns/Rounds/LqasDistrictsPassed/LqasDistrictsPassed.tsx rename to plugins/polio/js/src/domains/Campaigns/Evaluations/LqasDistrictsPassed/LqasDistrictsPassed.tsx diff --git a/plugins/polio/js/src/domains/Campaigns/MainDialog/CreateEditDialog.tsx b/plugins/polio/js/src/domains/Campaigns/MainDialog/CreateEditDialog.tsx index 2a94bceaec..c874436bf3 100644 --- a/plugins/polio/js/src/domains/Campaigns/MainDialog/CreateEditDialog.tsx +++ b/plugins/polio/js/src/domains/Campaigns/MainDialog/CreateEditDialog.tsx @@ -21,7 +21,7 @@ import { merge } from 'lodash'; import { BackdropClickModal, - IconButton as IconButtonComponent, + IconButton, LoadingSpinner, useSafeIntl, } from 'bluesquare-components'; @@ -137,8 +137,11 @@ const CreateEditDialog: FunctionComponent = ({ const CurrentForm = tabs[selectedTab].form; // default to tab 0 when opening + // This seems necessary regardless of state default value, the cause should be investigated useEffect(() => { - setSelectedTab(0); + setSelectedTab(value => { + return value || 0; + }); }, [isOpen]); const isFormChanged = !isEqual(formik.values, formik.initialValues); @@ -191,7 +194,7 @@ const CreateEditDialog: FunctionComponent = ({ className={classes.historyLink} > - , @@ -41,13 +42,22 @@ export const usePolioDialogTabs = ( title: formatMessage(MESSAGES.rounds), form: RoundsForm, key: 'rounds', - diabled: !formik.values.initial_org_unit, + disabled: !formik.values.initial_org_unit, hasTabError: compareArraysValues( roundFormFields(selectedCampaign?.rounds ?? []), formik.errors, ) || compareArraysValues(scopeFormFields, formik.errors), }, + { + title: formatMessage(MESSAGES.subActivities), + form: SubActivitiesForm, + key: 'subActivities', + disabled: + !formik.values.initial_org_unit || + formik.values.rounds.length === 0, + hasTabError: false, + }, { title: formatMessage(MESSAGES.scope), form: ScopeForm, diff --git a/plugins/polio/js/src/domains/Campaigns/Rounds/RoundDates/RoundDates.tsx b/plugins/polio/js/src/domains/Campaigns/Rounds/RoundDates/RoundDates.tsx index 352d832853..acaa6442f4 100644 --- a/plugins/polio/js/src/domains/Campaigns/Rounds/RoundDates/RoundDates.tsx +++ b/plugins/polio/js/src/domains/Campaigns/Rounds/RoundDates/RoundDates.tsx @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { Box, Divider, Grid, Typography } from '@mui/material'; -import { useSafeIntl } from 'bluesquare-components'; +import { textPlaceholder, useSafeIntl } from 'bluesquare-components'; import { Field, FormikProvider, useFormik, useFormikContext } from 'formik'; import { isEqual } from 'lodash'; import moment from 'moment'; @@ -136,7 +136,7 @@ export const RoundDates: FunctionComponent = ({ ? moment( currentStartDate, ).format(dateFormat) - : '--' + : textPlaceholder }`} diff --git a/plugins/polio/js/src/domains/Campaigns/Scope/ScopeField.tsx b/plugins/polio/js/src/domains/Campaigns/Scope/ScopeField.tsx index ddfdddce74..f823b9de5d 100644 --- a/plugins/polio/js/src/domains/Campaigns/Scope/ScopeField.tsx +++ b/plugins/polio/js/src/domains/Campaigns/Scope/ScopeField.tsx @@ -4,6 +4,8 @@ import { ScopeSearch } from './Scopes/ScopeSearch'; import { ScopeInput } from './ScopeInput'; import { FilteredDistricts } from './Scopes/types'; import { OrgUnit } from '../../../../../../../hat/assets/js/apps/Iaso/domains/orgUnits/types/orgUnit'; +import { CampaignFormValues } from '../../../constants/types'; +import { PolioVaccine } from '../../../constants/virus'; type Props = { name: string; @@ -21,6 +23,9 @@ type Props = { page: number; // eslint-disable-next-line no-unused-vars setPage: (page: number) => void; + campaign: CampaignFormValues; // See ScopeField props for explanation + availableVaccines?: PolioVaccine[]; + searchInputWithMargin?: boolean; }; export const ScopeField: FunctionComponent = ({ @@ -36,6 +41,9 @@ export const ScopeField: FunctionComponent = ({ setSearch, page, setPage, + campaign, + availableVaccines, + searchInputWithMargin = true, }) => ( = ({ searchComponent={} page={page} setPage={setPage} + campaign={campaign} + availableVaccines={availableVaccines} + searchInputWithMargin={searchInputWithMargin} /> ); diff --git a/plugins/polio/js/src/domains/Campaigns/Scope/ScopeForm.tsx b/plugins/polio/js/src/domains/Campaigns/Scope/ScopeForm.tsx index c62e5bed25..f39eb817bb 100644 --- a/plugins/polio/js/src/domains/Campaigns/Scope/ScopeForm.tsx +++ b/plugins/polio/js/src/domains/Campaigns/Scope/ScopeForm.tsx @@ -1,6 +1,5 @@ /* eslint-disable camelcase */ import { Field, useFormikContext } from 'formik'; -import cloneDeep from 'lodash/cloneDeep'; import React, { FunctionComponent, useMemo, useState } from 'react'; import { useDebounce } from 'use-debounce'; // @ts-ignore @@ -14,7 +13,7 @@ import MESSAGES from '../../../constants/messages'; import { useStyles } from '../../../styles/theme'; import { ScopeField } from './ScopeField'; -import { findRegion, findScopeWithOrgUnit } from './Scopes/utils'; +import { useFilteredDistricts } from './Scopes/utils'; import { useGetGeoJson } from './hooks/useGetGeoJson'; import { useGetParentOrgUnit } from './hooks/useGetParentOrgUnit'; @@ -76,52 +75,15 @@ export const ScopeForm: FunctionComponent = () => { return []; }, [currentTab, rounds, scopePerRound, sortedRounds, values.scopes]); - const filteredDistricts: FilteredDistricts[] | undefined = useMemo(() => { - if (!districtShapes || !regionShapes) return undefined; - - const orgUnitIdToVaccine = new Map(); - if (isPolio && scopes) { - scopes.forEach(scope => { - scope.group.org_units.forEach(ouId => { - orgUnitIdToVaccine.set(ouId, scope.vaccine); - }); - }); - } - const filtered = districtShapes - .filter(district => { - const isInScope = scopes.some(sc => - sc.group.org_units.includes(district.id), - ); - return ( - (district.validation_status === 'VALID' || isInScope) && - (!searchScope || isInScope) && - (!debouncedSearch || - district.name - .toLowerCase() - .includes(debouncedSearch.toLowerCase())) - ); - }) - .map(district => { - const scope = findScopeWithOrgUnit(scopes, district.id); - const vaccineName = - orgUnitIdToVaccine.get(district.id) || undefined; - return { - ...cloneDeep(district), - region: findRegion(district, regionShapes), - scope, - vaccineName, - }; - }); - - return filtered; - }, [ - districtShapes, - regionShapes, - scopes, - debouncedSearch, - searchScope, - isPolio, - ]); + const filteredDistricts: FilteredDistricts[] | undefined = + useFilteredDistricts({ + isPolio, + scopes, + districtShapes, + regionShapes, + search: debouncedSearch, + searchScope, + }); useSkipEffectOnMount(() => { setPage(0); @@ -168,6 +130,7 @@ export const ScopeForm: FunctionComponent = () => { setSearch={setSearch} page={page} setPage={setPage} + campaign={values} /> )} {scopePerRound && @@ -196,6 +159,7 @@ export const ScopeForm: FunctionComponent = () => { setSearch={setSearch} page={page} setPage={setPage} + campaign={values} /> ))} diff --git a/plugins/polio/js/src/domains/Campaigns/Scope/ScopeInput.tsx b/plugins/polio/js/src/domains/Campaigns/Scope/ScopeInput.tsx index a021aed57b..34881cac7f 100644 --- a/plugins/polio/js/src/domains/Campaigns/Scope/ScopeInput.tsx +++ b/plugins/polio/js/src/domains/Campaigns/Scope/ScopeInput.tsx @@ -21,6 +21,7 @@ import { OrgUnit } from '../../../../../../../hat/assets/js/apps/Iaso/domains/or import { CampaignFormValues, Scope, Vaccine } from '../../../constants/types'; import { useIsPolioCampaign } from '../hooks/useIsPolioCampaignCheck'; import { FilteredDistricts } from './Scopes/types'; +import { PolioVaccine } from '../../../constants/virus'; type ExtraProps = { filteredDistricts: FilteredDistricts[]; @@ -35,13 +36,15 @@ type ExtraProps = { page: number; // eslint-disable-next-line no-unused-vars setPage: (page: number) => void; + campaign: CampaignFormValues; // Passing the campaign i.o getting it from formik context so we can re-use the component for subactivities + availableVaccines?: PolioVaccine[]; + searchInputWithMargin?: boolean; // needed to remove the margin on the serach component without breaking existing scope form }; type Props = FieldProps & ExtraProps; export const ScopeInput: FunctionComponent = ({ field, - form: { values }, filteredDistricts, searchScope, onChangeSearchScope, @@ -52,10 +55,13 @@ export const ScopeInput: FunctionComponent = ({ searchComponent, page, setPage, + campaign, + availableVaccines, + searchInputWithMargin = true, }) => { const [selectRegion, setSelectRegion] = useState(false); const [selectedVaccine, setSelectedVaccine] = useState('nOPV2'); - const isPolio = useIsPolioCampaign(values); + const isPolio = useIsPolioCampaign(campaign); const [, , helpers] = useField(field.name); const { formatMessage } = useSafeIntl(); const { value: scopes = [] } = field; @@ -193,7 +199,7 @@ export const ScopeInput: FunctionComponent = ({ - + {searchComponent} = ({ {isFetching && } void; isPolio?: boolean; + availableVaccines?: PolioVaccine[]; }; const getBackgroundLayerStyle = () => { @@ -58,6 +59,7 @@ export const MapScope: FunctionComponent = ({ selectedVaccine, setSelectedVaccine, isPolio, + availableVaccines = polioVaccines, }) => { const classes: Record = useStyles(); const { formatMessage } = useSafeIntl(); @@ -79,7 +81,7 @@ export const MapScope: FunctionComponent = ({ const scope = findScopeWithOrgUnit(scopes, shape.id); if (scope) { - const vaccine = polioVaccines.find( + const vaccine = availableVaccines.find( v => v.value === scope.vaccine, ); return { @@ -90,7 +92,12 @@ export const MapScope: FunctionComponent = ({ if (values.org_unit?.id === shape.id) return initialDistrict; return unselectedPathOptions; }, - [values.org_unit?.id, scopes, theme], + [ + scopes, + values.org_unit?.id, + availableVaccines, + theme.palette.primary.main, + ], ); const filterOrgUnits = useCallback( (orgUnits: OrgUnit[]) => @@ -136,7 +143,7 @@ export const MapScope: FunctionComponent = ({ content={ - {polioVaccines.map(vaccine => ( + {availableVaccines.map(vaccine => ( { + return useMemo(() => { + if (!districtShapes || !regionShapes) return undefined; + const orgUnitIdToVaccine = new Map(); + if (isPolio && scopes) { + scopes.forEach(scope => { + scope.group.org_units.forEach(ouId => { + orgUnitIdToVaccine.set(ouId, scope.vaccine); + }); + }); + } + const filtered = districtShapes + .filter(district => { + const isInScope = scopes.some(sc => + sc.group.org_units.includes(district.id), + ); + return ( + (district.validation_status === 'VALID' || isInScope) && + (!searchScope || isInScope) && + (!search || + district.name + .toLowerCase() + .includes(search.toLowerCase())) + ); + }) + .map(district => { + const scope = findScopeWithOrgUnit(scopes, district.id); + const vaccineName = + orgUnitIdToVaccine.get(district.id) || undefined; + return { + ...cloneDeep(district), + region: findRegion(district, regionShapes), + scope, + vaccineName, + }; + }); + + return filtered; + }, [districtShapes, regionShapes, isPolio, scopes, searchScope, search]); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/SubActivitiesForm.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/SubActivitiesForm.tsx new file mode 100644 index 0000000000..0636fd1b22 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/SubActivitiesForm.tsx @@ -0,0 +1,50 @@ +import React, { FunctionComponent, useMemo } from 'react'; +import { useFormikContext } from 'formik'; +import { useSafeIntl } from 'bluesquare-components'; +import { Box, Tab } from '@mui/material'; +import { TabContext, TabList } from '@mui/lab'; +import { useTabs } from '../../../../../../../hat/assets/js/apps/Iaso/hooks/useTabs'; +import { CampaignFormValues } from '../../../constants/types'; +import { SubActivityForm } from './SubActivityForm'; +import MESSAGES from './messages'; + +export const SubActivitiesForm: FunctionComponent = () => { + const { formatMessage } = useSafeIntl(); + const { + values: { rounds = [] }, + } = useFormikContext(); + const { tab, handleChangeTab } = useTabs({ + defaultTab: rounds[0] ? `${rounds[0].number}` : '1', + }); + const round = useMemo(() => { + return rounds.find(r => r.number === parseInt(tab, 10)); + }, [rounds, tab]); + return ( + + + + {rounds.map(rnd => ( + ({ + fontSize: 12, + minWidth: 0, + padding: '10px 12px', + [theme.breakpoints.up('sm')]: { + minWidth: 0, + }, + })} + key={rnd.number} + label={`${formatMessage(MESSAGES.round)} ${ + rnd.number + }`} + value={`${rnd.number}`} + /> + ))} + + + + + + + ); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/SubActivityForm.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/SubActivityForm.tsx new file mode 100644 index 0000000000..2eebb432ae --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/SubActivityForm.tsx @@ -0,0 +1,53 @@ +import React, { FunctionComponent } from 'react'; +import { Table } from 'bluesquare-components'; +import { Box, Grid } from '@mui/material'; +import { Round } from '../../../constants/types'; +import { useTableState } from '../../../../../../../hat/assets/js/apps/Iaso/utils/table'; +import { useGetSubActivities } from './hooks/api/useGetSubActivities'; +import { useSubActivitiesColumns } from './hooks/useSubActivitiesColumns'; +import { CreateSubActivity } from './components/Modal/CreateEditSubActivity'; +import { RoundDates } from './components/RoundDates'; + +type Props = { round?: Round }; + +// It's not really a form, but it's named so to keep in line with other polio tabs +export const SubActivityForm: FunctionComponent = ({ round }) => { + const { params, onTableParamsChange } = useTableState(); + const { data: subActivities, isFetching: loading } = useGetSubActivities({ + round, + params, + }); + const columns = useSubActivitiesColumns(round); + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/components/AgeRangeCell.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/AgeRangeCell.tsx new file mode 100644 index 0000000000..bc5653ec46 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/AgeRangeCell.tsx @@ -0,0 +1,18 @@ +import { textPlaceholder, useSafeIntl } from 'bluesquare-components'; +import React from 'react'; +import MESSAGES from '../messages'; + +export const AgeRangeCell = settings => { + const { formatMessage } = useSafeIntl(); + const { age_unit, age_min, age_max } = settings?.row?.original ?? {}; + if (!age_min && !age_max && !age_unit) { + return {textPlaceholder}; + } + return ( + + {`${age_min ?? textPlaceholder}-${age_max ?? textPlaceholder} (${ + age_unit ? formatMessage(MESSAGES[age_unit]) : textPlaceholder + })`} + + ); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/components/Modal/CreateEditSubActivity.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/Modal/CreateEditSubActivity.tsx new file mode 100644 index 0000000000..b93cf5adb1 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/Modal/CreateEditSubActivity.tsx @@ -0,0 +1,187 @@ +import React, { FunctionComponent, useCallback } from 'react'; +import { + AddButton, + ConfirmCancelModal, + LoadingSpinner, + makeFullModal, + useSafeIntl, +} from 'bluesquare-components'; +import { Field, FormikProvider, useFormik, useFormikContext } from 'formik'; +import { isEqual } from 'lodash'; +import { Box, Divider, Grid } from '@mui/material'; +import { + DateInput, + NumberInput, + TextInput, +} from '../../../../../components/Inputs'; +import { SingleSelect } from '../../../../../components/Inputs/SingleSelect'; +import MESSAGES from '../../messages'; +import { EditIconButton } from '../../../../../../../../../hat/assets/js/apps/Iaso/components/Buttons/EditIconButton'; +import { useSaveSubActivity } from '../../hooks/api/useSaveSubActivity'; +import { useAgeRangeOptions } from '../../hooks/useAgeRangeOptions'; +import { CampaignFormValues, Round } from '../../../../../constants/types'; +import { SubActivityScopeField } from '../SubActivityScopeField'; +import { useSubActivityValidation } from '../../hooks/useSubActivityValidation'; +import { SubActivityFormValues } from '../../types'; + +type Props = { + closeDialog: () => void; + isOpen: boolean; + subActivity?: any; + round?: Round; +}; + +export const CreateEditSubActivity: FunctionComponent = ({ + subActivity, + closeDialog, + isOpen, + round, +}) => { + const { formatMessage } = useSafeIntl(); + const { values: campaign } = useFormikContext(); + const onSuccess = useCallback(() => { + closeDialog(); + }, [closeDialog]); + const { mutateAsync: saveSubActivity, isLoading: isSaving } = + useSaveSubActivity(onSuccess); + const ageRangeOptions = useAgeRangeOptions(); + const validationSchema = useSubActivityValidation(round); + + const formik = useFormik({ + initialValues: { + round_number: round?.number, + campaign: campaign.obr_name, + id: subActivity?.id, + name: subActivity?.name, + start_date: subActivity?.start_date, + end_date: subActivity?.end_date, + age_unit: subActivity?.age_unit, + age_min: subActivity?.age_min, + age_max: subActivity?.age_max, + scopes: subActivity?.scopes ?? [], + }, + validationSchema, + onSubmit: values => saveSubActivity(values), + }); + const titleMessage = subActivity?.id + ? MESSAGES.editSubActivity + : MESSAGES.createSubActivity; + + const isScopeChanged = !isEqual( + formik.initialValues.scopes.map(scope => scope.group.org_units).flat(), + formik.values.scopes.map(scope => scope.group.org_units).flat(), + ); + // isEqual won't catch changes in scopes because of deep nesting, so we check it on its own + const isValuesChanged = + !isEqual(formik.initialValues, formik.values) || isScopeChanged; + const allowConfirm = + formik.isValid && + (!isEqual(formik.touched, {}) || isScopeChanged) && + !formik.isSubmitting && + isValuesChanged && + !isSaving; + isScopeChanged; + + return ( + + null} + closeDialog={closeDialog} + open={isOpen} + titleMessage={titleMessage} + onConfirm={formik.handleSubmit} + onCancel={() => { + closeDialog(); + }} + confirmMessage={MESSAGES.confirm} + cancelMessage={MESSAGES.cancel} + allowConfirm={allowConfirm} + maxWidth="xl" + closeOnConfirm={false} + > + + + + + {(isSaving || formik.isSubmitting) && } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const CreateSubActivity = makeFullModal( + CreateEditSubActivity, + AddButton, +); +export const EditSubActivity = makeFullModal( + CreateEditSubActivity, + EditIconButton, +); diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/components/RoundDates.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/RoundDates.tsx new file mode 100644 index 0000000000..01527e7012 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/RoundDates.tsx @@ -0,0 +1,52 @@ +import React, { FunctionComponent } from 'react'; +import { Grid, Typography } from '@mui/material'; +import { textPlaceholder, useSafeIntl } from 'bluesquare-components'; +import moment from 'moment'; +import { Round } from '../../../../constants/types'; +import MESSAGES from '../messages'; +import { dateFormat } from '../../../Calendar/campaignCalendar/constants'; + +type Props = { + round?: Round; +}; + +export const RoundDates: FunctionComponent = ({ round }) => { + const { formatMessage } = useSafeIntl(); + + return ( + + + + + {`${formatMessage(MESSAGES.startDate)}: `} + + + + + {`${ + round?.started_at + ? moment(round.started_at).format(dateFormat) + : textPlaceholder + }`} + + + + + + + {`${formatMessage(MESSAGES.endDate)}: `} + + + + + {`${ + round?.ended_at + ? moment(round.ended_at).format(dateFormat) + : textPlaceholder + }`} + + + + + ); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/components/SubActivityScopeField.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/SubActivityScopeField.tsx new file mode 100644 index 0000000000..10ab8bbffc --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/components/SubActivityScopeField.tsx @@ -0,0 +1,77 @@ +import React, { FunctionComponent, useMemo, useState } from 'react'; +import { useFormikContext } from 'formik'; +import { useDebounce } from 'use-debounce'; +import { useSkipEffectOnMount } from 'bluesquare-components'; +import { polioVaccines } from '../../../../constants/virus'; +import { ScopeField } from '../../Scope/ScopeField'; +import { CampaignFormValues, Round } from '../../../../constants/types'; +import { useIsPolioCampaign } from '../../hooks/useIsPolioCampaignCheck'; +import { FilteredDistricts } from '../../Scope/Scopes/types'; +import { useFilteredDistricts } from '../../Scope/Scopes/utils'; +import { useGetSubActivityShapes } from '../hooks/api/subActivityShapes'; +import { SubActivityFormValues } from '../types'; + +type Props = { campaign: CampaignFormValues; round?: Round }; + +export const SubActivityScopeField: FunctionComponent = ({ + campaign, + round, +}) => { + const { values: subActivity } = useFormikContext(); + const isPolio = useIsPolioCampaign(campaign); + const [searchScope, setSearchScope] = useState(true); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebounce(search, 500); + + const { + districtShapes, + regionShapes, + isFetchingDistricts, + isFetchingRegions, + } = useGetSubActivityShapes(campaign, round); + + const filteredDistricts: FilteredDistricts[] | undefined = + useFilteredDistricts({ + scopes: subActivity.scopes, + regionShapes, + districtShapes, + isPolio, + search: debouncedSearch, + searchScope, + }); + + const availableVaccines = useMemo(() => { + const subActivityVaccines = round?.scopes?.map(scope => scope.vaccine); + if (!subActivityVaccines) { + return undefined; + } + return polioVaccines.filter(vaccine => + subActivityVaccines.includes(vaccine.value), + ); + }, [round?.scopes]); + + useSkipEffectOnMount(() => { + setPage(0); + }, [filteredDistricts]); + + return ( + + ); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/subActivityShapes.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/subActivityShapes.tsx new file mode 100644 index 0000000000..aab9bd3685 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/subActivityShapes.tsx @@ -0,0 +1,54 @@ +import { useMemo } from 'react'; +import { CampaignFormValues, Round } from '../../../../../constants/types'; +import { useGetParentOrgUnit } from '../../../Scope/hooks/useGetParentOrgUnit'; +import { useGetGeoJson } from '../../../Scope/hooks/useGetGeoJson'; + +export const useGetSubActivityShapes = ( + campaign: CampaignFormValues, + round?: Round, +) => { + const roundScopes = useMemo(() => { + if (!campaign.separate_scopes_per_round) { + return campaign.scopes.map(scope => scope.group.org_units).flat(); + } + if (round?.scopes) { + return round.scopes.map(scope => scope.group.org_units).flat(); + } + return []; + }, [campaign.scopes, campaign.separate_scopes_per_round, round?.scopes]); + + const { data: country } = useGetParentOrgUnit(campaign.initial_org_unit); + + const parentCountryId = + country?.country_parent?.id || country?.root?.id || country?.id; + + const { data: districtShapes, isFetching: isFetchingDistricts } = + useGetGeoJson(parentCountryId, 'DISTRICT'); + const { data: regionShapes, isFetching: isFetchingRegions } = useGetGeoJson( + parentCountryId, + 'REGION', + ); + + const districtShapesForSubActivity = districtShapes?.filter(shape => + roundScopes.includes(shape.id), + ); + const regionShapesForSubActivity = regionShapes?.filter(shape => + (districtShapesForSubActivity ?? []).some( + district => district.parent_id === shape.id, + ), + ); + + return useMemo(() => { + return { + districtShapes: districtShapesForSubActivity, + regionShapes: regionShapesForSubActivity, + isFetchingRegions, + isFetchingDistricts, + }; + }, [ + districtShapesForSubActivity, + isFetchingDistricts, + isFetchingRegions, + regionShapesForSubActivity, + ]); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useDeleteSubActivity.ts b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useDeleteSubActivity.ts new file mode 100644 index 0000000000..49b0d5091f --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useDeleteSubActivity.ts @@ -0,0 +1,17 @@ +import { UseMutationResult } from 'react-query'; +import { deleteRequest } from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/Api'; +import { useSnackMutation } from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/apiHooks'; + +const apiUrl = '/api/polio/campaigns_subactivities/'; + +const deleteActivity = (id: string) => { + return deleteRequest(`${apiUrl}${id}`); +}; + +export const useDeleteSubActivity = (): UseMutationResult => { + return useSnackMutation({ + mutationFn: deleteActivity, + invalidateQueryKey: 'subActivities', + // TODO add success and error messages + }); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useGetSubActivities.ts b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useGetSubActivities.ts new file mode 100644 index 0000000000..90865474d1 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useGetSubActivities.ts @@ -0,0 +1,34 @@ +import { UseQueryResult } from 'react-query'; +import { PaginationParams } from '../../../../../../../../../hat/assets/js/apps/Iaso/types/general'; +import { useSnackQuery } from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/apiHooks'; +import { Round } from '../../../../../constants/types'; +import { getRequest } from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/Api'; +import { SubActivityFormValues } from '../../types'; +import { PaginatedResponse } from '../../../../../../../../../hat/assets/js/apps/Iaso/domains/app/types'; + +const apiUrl = '/api/polio/campaigns_subactivities'; +type Args = { + round?: Round; + params: PaginationParams; +}; + +export const useGetSubActivities = ({ + round, + params, +}: Args): UseQueryResult> => { + const queryString = new URLSearchParams({ + ...params, + round__id: `${round?.id}`, + }).toString(); + const url = `${apiUrl}/?${queryString}`; + return useSnackQuery({ + queryKey: ['subActivities', round?.id, queryString], + queryFn: () => getRequest(url), + options: { + keepPreviousData: true, + staleTime: 60000, + cacheTime: 60000, + enabled: Boolean(round?.id), + }, + }); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useSaveSubActivity.ts b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useSaveSubActivity.ts new file mode 100644 index 0000000000..915af5d65e --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/api/useSaveSubActivity.ts @@ -0,0 +1,27 @@ +import { UseMutationResult } from 'react-query'; +import { + postRequest, + putRequest, +} from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/Api'; +import { useSnackMutation } from '../../../../../../../../../hat/assets/js/apps/Iaso/libs/apiHooks'; + +const apiUrl = '/api/polio/campaigns_subactivities/'; + +const saveActivity = values => { + const { id, ...body } = values; + if (id) { + return putRequest(`${apiUrl}${id}/`, body); + } + return postRequest(apiUrl, body); +}; + +export const useSaveSubActivity = ( + onSuccess: () => void, +): UseMutationResult => { + return useSnackMutation({ + mutationFn: saveActivity, + invalidateQueryKey: ['subActivities'], + showSucessSnackBar: false, + options: { onSuccess }, + }); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useAgeRangeOptions.ts b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useAgeRangeOptions.ts new file mode 100644 index 0000000000..9ca9718e64 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useAgeRangeOptions.ts @@ -0,0 +1,18 @@ +import { useSafeIntl } from 'bluesquare-components'; +import { useMemo } from 'react'; +import MESSAGES from '../messages'; +import { DropdownOptions } from '../../../../../../../../hat/assets/js/apps/Iaso/types/utils'; + +const options = ['m', 'y']; + +export const useAgeRangeOptions = (): DropdownOptions<'m' | 'y'>[] => { + const { formatMessage } = useSafeIntl(); + return useMemo(() => { + return options.map((value: 'm' | 'y') => { + const label = MESSAGES[value] + ? formatMessage(MESSAGES[value]) + : value; + return { value, label }; + }); + }, [formatMessage]); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useSubActivitiesColumns.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useSubActivitiesColumns.tsx new file mode 100644 index 0000000000..0413e98a12 --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useSubActivitiesColumns.tsx @@ -0,0 +1,63 @@ +import React, { useMemo } from 'react'; +import { Column, useSafeIntl } from 'bluesquare-components'; +import DeleteDialog from '../../../../../../../../hat/assets/js/apps/Iaso/components/dialogs/DeleteDialogComponent'; +import { DateCell } from '../../../../../../../../hat/assets/js/apps/Iaso/components/Cells/DateTimeCell'; +import { AgeRangeCell } from '../components/AgeRangeCell'; +import MESSAGES from '../messages'; +import { EditSubActivity } from '../components/Modal/CreateEditSubActivity'; +import { useDeleteSubActivity } from './api/useDeleteSubActivity'; +import { Round } from '../../../../constants/types'; + +export const useSubActivitiesColumns = (round?: Round): Column[] => { + const { formatMessage } = useSafeIntl(); + const { mutateAsync: deleteSubActivity } = useDeleteSubActivity(); + return useMemo(() => { + return [ + { + Header: formatMessage(MESSAGES.name), + id: 'name', + accessor: 'name', + }, + { + Header: formatMessage(MESSAGES.ageGroup), + id: 'age_min', + accessor: 'age_min', + Cell: AgeRangeCell, + }, + { + Header: formatMessage(MESSAGES.startDate), + id: 'start_date', + accessor: 'start_date', + Cell: DateCell, + }, + { + Header: formatMessage(MESSAGES.endDate), + id: 'end_date', + accessor: 'end_date', + Cell: DateCell, + }, + { + Header: formatMessage(MESSAGES.actions), + id: 'scopes', + accessor: 'scopes', + Cell: settings => { + return ( + <> + + + deleteSubActivity(settings.row.original.id) + } + /> + + ); + }, + }, + ]; + }, [deleteSubActivity, formatMessage, round]); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useSubActivityValidation.tsx b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useSubActivityValidation.tsx new file mode 100644 index 0000000000..fb8e8f209a --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/hooks/useSubActivityValidation.tsx @@ -0,0 +1,122 @@ +import * as yup from 'yup'; +import { useSafeIntl } from 'bluesquare-components'; +import moment from 'moment'; +import { useMemo } from 'react'; +import { Round } from '../../../../constants/types'; +import { dateFormat } from '../../../Calendar/campaignCalendar/constants'; +import MESSAGES from '../messages'; + +yup.addMethod( + yup.date, + 'validateStartDate', + function validateStartDate(formatMessage, round) { + return this.test('validateStartDate', '', (value, context) => { + const { path, createError, parent } = context; + const newStartDate = moment(value); + const endDate = + parent.end_date && moment(parent.end_date, dateFormat); + + let errorMessage; + + if (endDate?.isSameOrBefore(newStartDate)) { + errorMessage = formatMessage(MESSAGES.startDateAfterEndDate); + } else { + const roundStartDate = moment(round?.started_at, dateFormat); + const roundEndDate = moment(round?.ended_at, dateFormat); + if (roundStartDate?.isAfter(newStartDate)) { + errorMessage = formatMessage( + MESSAGES.startDateBeforeRoundDate, + ); + } + if (roundEndDate?.isSameOrBefore(newStartDate)) { + errorMessage = formatMessage( + MESSAGES.startDateAfterRoundEnd, + ); + } + } + if (errorMessage) { + return createError({ + path, + message: errorMessage, + }); + } + return true; + }); + }, +); + +yup.addMethod( + yup.date, + 'validateEndDate', + function validateEndDate(formatMessage, round) { + return this.test('validateEndDate', '', (value, context) => { + const { path, createError, parent } = context; + const newEndDate = moment(value); + const startDate = + parent.start_date && moment(parent.start_date, dateFormat); + let errorMessage; + + if (startDate?.isSameOrAfter(newEndDate)) { + errorMessage = formatMessage(MESSAGES.endDateBeforeStartDate); + } else { + const roundStartDate = moment(round?.started_at, dateFormat); + const roundEndDate = moment(round?.ended_at, dateFormat); + if (roundEndDate?.isBefore(newEndDate)) { + errorMessage = formatMessage( + MESSAGES.endDateAfterRoundDate, + ); + } + if (roundStartDate.isSameOrAfter(newEndDate)) { + errorMessage = formatMessage( + MESSAGES.endDateBeforeRoundStart, + ); + } + } + + if (errorMessage) { + return createError({ + path, + message: errorMessage, + }); + } + return true; + }); + }, +); + +export const useSubActivityValidation = ( + round?: Round, +): yup.ObjectSchema => { + const { formatMessage } = useSafeIntl(); + return useMemo( + () => + yup.object().shape({ + name: yup + .string() + .nullable() + .required(formatMessage(MESSAGES.fieldRequired)), + start_date: yup + .date() + .typeError(formatMessage(MESSAGES.invalidDate)) + .nullable() + // start should be before end + // start should not be before round start + // @ts-ignore + .validateStartDate(formatMessage, round) + .required(formatMessage(MESSAGES.fieldRequired)), + end_date: yup + .date() + .typeError(formatMessage(MESSAGES.invalidDate)) + .nullable() + // end should be after start + // end should not be after round end + // @ts-ignore + .validateEndDate(formatMessage, round) + .required(formatMessage(MESSAGES.fieldRequired)), + age_unit: yup.string().nullable(), + age_min: yup.number().nullable(), + age_max: yup.number().nullable(), + }), + [formatMessage, round], + ); +}; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/messages.ts b/plugins/polio/js/src/domains/Campaigns/SubActivities/messages.ts new file mode 100644 index 0000000000..4117ea133b --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/messages.ts @@ -0,0 +1,110 @@ +import { defineMessages } from 'react-intl'; + +const MESSAGES = defineMessages({ + round: { + id: 'iaso.polio.label.round', + defaultMessage: 'Round', + }, + Months: { + id: 'iaso.polio.label.months', + defaultMessage: 'Months', + }, + Years: { + id: 'iaso.polio.label.years', + defaultMessage: 'Years', + }, + m: { + id: 'iaso.polio.label.months', + defaultMessage: 'Months', + }, + y: { + id: 'iaso.polio.label.years', + defaultMessage: 'Years', + }, + name: { + defaultMessage: 'Name', + id: 'iaso.label.name', + }, + startDate: { + id: 'iaso.label.dateFrom', + defaultMessage: 'Start date', + }, + endDate: { + id: 'iaso.label.dateTo', + defaultMessage: 'End date', + }, + ageGroup: { + id: 'iaso.polio.label.ageGroup', + defaultMessage: 'Age group', + }, + actions: { + defaultMessage: 'Action(s)', + id: 'iaso.label.actions', + }, + ageMax: { + defaultMessage: 'Age max', + id: 'iaso.polio.label.ageMax', + }, + ageMin: { + defaultMessage: 'Age min', + id: 'iaso.polio.label.ageMin', + }, + ageUnit: { + defaultMessage: 'Enter age in', + id: 'iaso.polio.label.ageUnit', + }, + cancel: { + id: 'iaso.label.cancel', + defaultMessage: 'Cancel', + }, + confirm: { + defaultMessage: 'Confirm', + id: 'iaso.label.confirm', + }, + createSubActivity: { + defaultMessage: 'Create sub-activity', + id: 'iaso.polio.label.createSubActivity', + }, + editSubActivity: { + defaultMessage: 'Edit sub-activity', + id: 'iaso.polio.label.editSubActivity', + }, + deleteSubActivity: { + defaultMessage: 'Delete sub-activity?', + id: 'iaso.polio.label.deleteSubActivity', + }, + fieldRequired: { + id: 'iaso.polio.form.fieldRequired', + defaultMessage: 'This field is required', + }, + invalidDate: { + id: 'iaso.polio.form.invalidDate', + defaultMessage: 'Date is invalid', + }, + endDateBeforeStartDate: { + id: 'iaso.polio.form.validator.error.endDateBeforeStartDate', + defaultMessage: "End date can't be before start date", + }, + startDateAfterEndDate: { + id: 'iaso.polio.form.validator.error.startDateAfterEndDate', + defaultMessage: "Start date can't be after end date", + }, + startDateBeforeRoundDate: { + id: 'iaso.polio.form.validator.error.startDateBeforeRoundDate', + defaultMessage: "Start date can't be before round start date", + }, + endDateAfterRoundDate: { + id: 'iaso.polio.form.validator.error.endDateAfterRoundDate', + defaultMessage: "End date can't be after round end date", + }, + endDateBeforeRoundStart: { + id: 'iaso.polio.form.validator.error.endDateBeforeRoundStart', + defaultMessage: "End date can't be before round start date", + }, + startDateAfterRoundEnd: { + id: 'iaso.polio.form.validator.error.startDateAfterRoundEnd', + defaultMessage: "Start date can't be after round end date", + }, +}); + +export default MESSAGES; diff --git a/plugins/polio/js/src/domains/Campaigns/SubActivities/types.ts b/plugins/polio/js/src/domains/Campaigns/SubActivities/types.ts new file mode 100644 index 0000000000..ec91b56f7b --- /dev/null +++ b/plugins/polio/js/src/domains/Campaigns/SubActivities/types.ts @@ -0,0 +1,14 @@ +import { Scope } from '../../../constants/types'; + +export type SubActivityFormValues = { + round_number?: number; + campaign?: string; // obr name + id?: number; + name?: string; + start_date: string; // date + end_date: string; // date + age_unit?: 'm' | 'y'; + age_min?: number; + age_max?: number; + scopes: Scope[]; +}; diff --git a/plugins/polio/js/src/domains/Campaigns/hooks/api/useSaveCampaign.js b/plugins/polio/js/src/domains/Campaigns/hooks/api/useSaveCampaign.js index b1ed4e99af..e560a6d7f0 100644 --- a/plugins/polio/js/src/domains/Campaigns/hooks/api/useSaveCampaign.js +++ b/plugins/polio/js/src/domains/Campaigns/hooks/api/useSaveCampaign.js @@ -22,6 +22,6 @@ export const useSaveCampaign = () => { ) : postRequest('/api/polio/campaigns/', hackedBody); }, - invalidateQueryKey: ['campaign'], + invalidateQueryKey: ['campaigns'], }); };