From e017c2c8eb29d80ce819b13e188a57f8938fabe4 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Thu, 6 Nov 2025 17:35:59 +0545 Subject: [PATCH 1/2] feat(local-unit): enhance local unit form, permissions, and imports - Add location search and new fields to the local unit form - Reorder and adjust field layout and orientation - Update map zoom level in the form - Fix add, edit, and delete permissions, including organization-based edit access - Update import modal file naming and descriptions --- .../domain/BaseMapPointInput/index.tsx | 90 +- .../BaseMapPointInput/styles.module.css | 14 +- .../domain/LocationSearchInput/index.tsx | 105 + app/src/hooks/domain/usePermissions.ts | 26 +- app/src/utils/constants.ts | 21 + app/src/utils/localUnits.ts | 4 +- app/src/utils/restRequest/go.ts | 12 +- app/src/utils/restRequest/index.ts | 7 + app/src/utils/restRequest/overrideTypes.ts | 7 + .../ConfirmationModal/index.tsx | 17 +- .../ConfigureLocalUnitsModal/index.tsx | 23 +- .../LocalUnitImportModal/i18n.json | 10 +- .../LocalUnitImportModal/index.tsx | 238 +- .../LocalUnitImportModal/styles.module.css | 50 +- .../LocalUnitValidateButton/i18n.json | 6 + .../LocalUnitValidateButton/index.tsx | 8 +- .../LocalUnitValidateModal/index.tsx | 1 - .../OtherProfilesDiffOutput/i18n.json | 7 + .../OtherProfilesDiffOutput/index.tsx | 58 + .../LocalUnitView/i18n.json | 4 +- .../LocalUnitView/index.tsx | 86 +- .../OtherProfilesInput/i18n.json | 8 + .../OtherProfilesInput/index.tsx | 130 + .../LocalUnitsForm/i18n.json | 5 +- .../LocalUnitsForm/index.tsx | 2847 +++++++++-------- .../LocalUnitsForm/schema.ts | 616 +++- .../LocalUnitsForm/styles.module.css | 32 +- .../LocalUnitsFormModal/index.tsx | 8 +- .../LocalUnitsMap/index.tsx | 4 - .../LocalUnitTableActions/index.tsx | 24 +- .../LocalUnitsTable/i18n.json | 5 +- .../LocalUnitsTable/index.tsx | 20 +- .../NationalSocietyLocalUnits/common.ts | 30 + .../NationalSocietyLocalUnits/index.tsx | 23 +- go-api | 2 +- .../000062-1765884779160.json | 133 + 36 files changed, 2928 insertions(+), 1753 deletions(-) create mode 100644 app/src/components/domain/LocationSearchInput/index.tsx create mode 100644 app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/i18n.json create mode 100644 app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/OtherProfilesDiffOutput/i18n.json create mode 100644 app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/OtherProfilesDiffOutput/index.tsx create mode 100644 app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/OtherProfilesInput/i18n.json create mode 100644 app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/OtherProfilesInput/index.tsx create mode 100644 translationMigrations/000062-1765884779160.json diff --git a/app/src/components/domain/BaseMapPointInput/index.tsx b/app/src/components/domain/BaseMapPointInput/index.tsx index 0a711b8407..b4d43f0db3 100644 --- a/app/src/components/domain/BaseMapPointInput/index.tsx +++ b/app/src/components/domain/BaseMapPointInput/index.tsx @@ -1,8 +1,12 @@ import { useCallback, useMemo, + useState, } from 'react'; -import { NumberInput } from '@ifrc-go/ui'; +import { + ListView, + NumberInput, +} from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; import { _cs, @@ -10,6 +14,7 @@ import { isNotDefined, } from '@togglecorp/fujs'; import { + MapCenter, MapContainer, MapLayer, MapSource, @@ -17,8 +22,11 @@ import { import { type ObjectError } from '@togglecorp/toggle-form'; import getBbox from '@turf/bbox'; import { + type AnySourceData, type CircleLayer, type FillLayer, + type FitBoundsOptions, + type FlyToOptions, type LngLat, type Map, type MapboxGeoJSONFeature, @@ -36,15 +44,34 @@ import { import { localUnitMapStyle } from '#utils/map'; import ActiveCountryBaseMapLayer from '../ActiveCountryBaseMapLayer'; +import LocationSearchInput, { type LocationSearchResult } from '../LocationSearchInput'; import i18n from './i18n.json'; import styles from './styles.module.css'; +const centerOptions = { + zoom: 16, + duration: 1000, +} satisfies FlyToOptions; + +const geoJsonSourceOptions = { + type: 'geojson', +} satisfies AnySourceData; + interface GeoPoint { lng: number; lat: number } +const fitBoundsOptions = { + padding: { + left: 20, + top: 20, + bottom: 50, + right: 20, + }, +} satisfies FitBoundsOptions; + type Value = Partial; interface Props extends BaseMapProps { @@ -90,17 +117,6 @@ function BaseMapPointInput(props: Props) { const countryDetails = useCountry({ id: country ?? -1 }); const strings = useTranslation(i18n); - const bounds = useMemo( - () => { - if (isNotDefined(countryDetails)) { - return undefined; - } - - return getBbox(countryDetails.bbox); - }, - [countryDetails], - ); - const pointGeoJson = useMemo( () => { if (isNotDefined(value) || isNotDefined(value.lng) || isNotDefined(value.lat)) { @@ -189,9 +205,33 @@ function BaseMapPointInput(props: Props) { [value, onChange, name], ); + const bounds = useMemo( + () => { + if (isNotDefined(countryDetails)) { + return undefined; + } + + return getBbox(countryDetails.bbox); + }, + [countryDetails], + ); + + const [searchResult, setSearchResult] = useState(); + + const center = useMemo(() => { + if (isDefined(value?.lng) && isDefined(value?.lat)) { + return [value.lng, value.lat] satisfies [number, number]; + } + if (isDefined(searchResult)) { + return [+searchResult.lon, +searchResult.lat] satisfies [number, number]; + } + + return undefined; + }, [searchResult, value?.lng, value?.lat]); + return (
-
+ (props: Props) { className={diffWrapperClassName} > (props: Props) { className={diffWrapperClassName} > (props: Props) { required={required} /> -
+ + {isDefined(countryDetails) && ( +
+ +
+ )} (props: Props) { (props: Props) { /> )} + {center && ( + + )} {children}
diff --git a/app/src/components/domain/BaseMapPointInput/styles.module.css b/app/src/components/domain/BaseMapPointInput/styles.module.css index c07f3b9f5e..fe698ac87f 100644 --- a/app/src/components/domain/BaseMapPointInput/styles.module.css +++ b/app/src/components/domain/BaseMapPointInput/styles.module.css @@ -1,11 +1,17 @@ .base-map-point-input { display: flex; + position: relative; flex-direction: column; gap: var(--go-ui-spacing-md); + isolation: isolate; - .location-inputs { - display: grid; - gap: var(--go-ui-spacing-sm); - grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + .location-search { + position: absolute; + right: var(--go-ui-spacing-sm); + bottom: var(--go-ui-spacing-sm); + z-index: 1; + border-radius: var(--go-ui-border-radius-lg); + background-color: var(--go-ui-color-foreground); + padding: var(--go-ui-spacing-sm); } } diff --git a/app/src/components/domain/LocationSearchInput/index.tsx b/app/src/components/domain/LocationSearchInput/index.tsx new file mode 100644 index 0000000000..306edb157f --- /dev/null +++ b/app/src/components/domain/LocationSearchInput/index.tsx @@ -0,0 +1,105 @@ +import { + useCallback, + useState, +} from 'react'; +import { SearchLineIcon } from '@ifrc-go/icons'; +import { SearchSelectInput } from '@ifrc-go/ui'; +import { useDebouncedValue } from '@ifrc-go/ui/hooks'; + +import { useExternalRequest } from '#utils/restRequest'; + +export interface LocationSearchResult { + addresstype: string; + boundingbox: string[]; + readOnly?: boolean; + class: string; + display_name: string; + importance: number; + lat: string; + licence: string; + lon: string; + name: string; + osm_id: number; + osm_type: string; + place_id: number; + place_rank: number; + type: string; +} + +function keySelector(result: LocationSearchResult) { + return String(result.osm_id); +} + +function labelSelector(result: LocationSearchResult) { + return result.name; +} + +function descriptionSelector(result: LocationSearchResult) { + return result.display_name; +} + +interface Props { + className?: string; + onResultSelect: (result: LocationSearchResult | undefined) => void; + countryIso: string; + readOnly?: boolean; +} + +function LocationSearchInput(props: Props) { + const { + className, onResultSelect, readOnly, countryIso, + } = props; + + const [opened, setOpened] = useState(false); + const [searchText, setSearchText] = useState(undefined); + + const debouncedSearchText = useDebouncedValue(searchText?.trim() ?? ''); + + const { pending, response: options } = useExternalRequest< + LocationSearchResult[] | undefined + >({ + skip: !opened || debouncedSearchText.length === 0, + url: 'https://nominatim.openstreetmap.org/search', + query: { + q: debouncedSearchText, + countrycodes: countryIso, + format: 'json', + }, + }); + + const handleOptionSelect = useCallback( + ( + _: string | undefined, + __: string, + option: LocationSearchResult | undefined, + ) => { + onResultSelect(option); + }, + [onResultSelect], + ); + + return ( + } + /> + ); +} + +export default LocationSearchInput; diff --git a/app/src/hooks/domain/usePermissions.ts b/app/src/hooks/domain/usePermissions.ts index feff71b43c..c1cd95fcda 100644 --- a/app/src/hooks/domain/usePermissions.ts +++ b/app/src/hooks/domain/usePermissions.ts @@ -1,8 +1,16 @@ import { useMemo } from 'react'; -import { isDefined } from '@togglecorp/fujs'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { type GlobalEnums } from '#contexts/domain'; import useUserMe from '#hooks/domain/useUserMe'; +type OrganizationType = NonNullable[number]['key']; + +const canEditLocalUnitOrganization: OrganizationType[] = ['NTLS', 'DLGN', 'SCRT']; + function usePermissions() { const userMe = useUserMe(); @@ -20,7 +28,7 @@ function usePermissions() { && isDefined(countryId) && !!userMe?.is_admin_for_countries?.includes(countryId) ); - const isRegionAdmin = (regionId: number | undefined) => ( + const isRegionAdmin = (regionId: number | null | undefined) => ( !isGuestUser && isDefined(regionId) && !!userMe?.is_admin_for_regions?.includes(regionId) @@ -96,6 +104,19 @@ function usePermissions() { ) ); + const canEditLocalUnit = ( + countryId: number | undefined, + ) => { + if (isGuestUser + || isNotDefined(countryId) + || isNotDefined(userMe?.profile.org_type)) return false; + + return ( + userMe?.profile.country?.id === countryId + && canEditLocalUnitOrganization.includes(userMe?.profile.org_type) + ); + }; + const isPerAdmin = !isGuestUser && ((userMe?.is_per_admin_for_countries.length ?? 0) > 0 || (userMe?.is_per_admin_for_regions.length ?? 0) > 0); @@ -126,6 +147,7 @@ function usePermissions() { isSuperUser, isGuestUser, isRegionalOrCountryAdmin, + canEditLocalUnit, }; }, [userMe], diff --git a/app/src/utils/constants.ts b/app/src/utils/constants.ts index 8d3ec82ce2..93b6b00878 100644 --- a/app/src/utils/constants.ts +++ b/app/src/utils/constants.ts @@ -2,6 +2,8 @@ import { listToMap } from '@togglecorp/fujs'; import { type components } from '#generated/types'; +import { type GoApiResponse } from './restRequest'; + export const defaultChartMargin = { top: 0, right: 0, @@ -199,3 +201,22 @@ export const multiMonthSelectDefaultValue = listToMap( export const ERU_READINESS_READY = 1; export const ERU_READINESS_CAN_CONTRIBUTE = 2; export const ERU_READINESS_NO_CAPACITY = 3; + +// LocalUnits +type LocalUnitHealthFacilityTypeOptions = NonNullable['health_facility_type']>[number]>['id'] + +export const AMBULANCE_TYPE = 1 satisfies LocalUnitHealthFacilityTypeOptions; +export const HOSPITAL_TYPE = 3 satisfies LocalUnitHealthFacilityTypeOptions; +export const PRIMARY_HEALTH_TYPE = 5 satisfies LocalUnitHealthFacilityTypeOptions; +export const RESIDENTIAL_TYPE = 6 satisfies LocalUnitHealthFacilityTypeOptions; +export const TRAINING_FACILITY_TYPE = 7 satisfies LocalUnitHealthFacilityTypeOptions; +export const SPECIALIZED_SERVICES_TYPE = 8 satisfies LocalUnitHealthFacilityTypeOptions; +export const OTHER_TYPE = 9 satisfies LocalUnitHealthFacilityTypeOptions; + +type LocalUnitTrainingFacilityTypeOptions = NonNullable['professional_training_facilities']>[number]>['id'] + +export const OTHER_TRAINING_FACILITIES = 9 satisfies LocalUnitTrainingFacilityTypeOptions; + +type LocalUnitAffiliationOptions = NonNullable['affiliation']>[number]>['id'] + +export const OTHER_AFFILIATION = 9 satisfies LocalUnitAffiliationOptions; diff --git a/app/src/utils/localUnits.ts b/app/src/utils/localUnits.ts index 79a0852632..6ae48b7cd9 100644 --- a/app/src/utils/localUnits.ts +++ b/app/src/utils/localUnits.ts @@ -10,7 +10,7 @@ import { type GoApiResponse } from './restRequest'; type LocalUnitResponse = NonNullable>; -export function getFormFields(value: LocalUnitResponse | PartialLocalUnits) { +export function getFormFields(value: LocalUnitResponse | PartialLocalUnits | undefined) { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars created_at, @@ -30,7 +30,7 @@ export function getFormFields(value: LocalUnitResponse | PartialLocalUnits) { ...formValues // Note: the cast is safe as we're only trying to // remove fields if they exist - } = removeNull(value) as LocalUnitResponse; + } = removeNull(value ?? {}) as LocalUnitResponse || undefined; const { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/app/src/utils/restRequest/go.ts b/app/src/utils/restRequest/go.ts index 477822b687..b9153939de 100644 --- a/app/src/utils/restRequest/go.ts +++ b/app/src/utils/restRequest/go.ts @@ -239,7 +239,17 @@ const isSuccessfulStatus = (status: number): boolean => status >= 200 && status const isContentTypeExcel = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_EXCEL; -const isContentTypeJson = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_JSON; +const isContentTypeJson = (res: Response): boolean => { + const contentTypeHeaders = res.headers.get('content-type'); + + if (isNotDefined(contentTypeHeaders)) { + return false; + } + + const mediaTypes = contentTypeHeaders.split('; '); + + return mediaTypes[0]?.toLowerCase() === CONTENT_TYPE_JSON; +}; const isLoginRedirect = (url: string): boolean => new URL(url).pathname.includes('login'); diff --git a/app/src/utils/restRequest/index.ts b/app/src/utils/restRequest/index.ts index ad31e30028..f8ebd92b38 100644 --- a/app/src/utils/restRequest/index.ts +++ b/app/src/utils/restRequest/index.ts @@ -15,6 +15,8 @@ import type { CustomLazyRequestReturn, CustomRequestOptions, CustomRequestReturn, + ExternalRequestOptions, + ExternalRequestReturn, VALID_METHOD, } from './overrideTypes'; @@ -70,8 +72,13 @@ const useRiskLazyRequest = useLazyRequest as < requestOptions: CustomLazyRequestOptions & { apiType: 'risk' } ) => CustomLazyRequestReturn; +const useExternalRequest = useRequest as ( + requestOptions: ExternalRequestOptions, +) => ExternalRequestReturn; + export { RequestContext, + useExternalRequest, useGoLazyRequest as useLazyRequest, useGoRequest as useRequest, useRiskLazyRequest, diff --git a/app/src/utils/restRequest/overrideTypes.ts b/app/src/utils/restRequest/overrideTypes.ts index 974ec91371..9911d6d96e 100644 --- a/app/src/utils/restRequest/overrideTypes.ts +++ b/app/src/utils/restRequest/overrideTypes.ts @@ -270,6 +270,11 @@ type LazyRequestOptionsBase = { OptionOmissions >; +export type ExternalRequestOptions = Pick< +RequestOptions, +'query' | 'url' | 'skip' +>; + export type CustomRequestOptions< SCHEMA extends object, PATH extends keyof SCHEMA, @@ -327,6 +332,8 @@ type LazyRequestReturn = ReturnType>; +export type ExternalRequestReturn = RequestReturn; + export type CustomRequestReturn< SCHEMA, PATH extends keyof SCHEMA, diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/ConfigureLocalUnitsModal/ConfirmationModal/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/ConfigureLocalUnitsModal/ConfirmationModal/index.tsx index ff26e57b89..c99fb50dda 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/ConfigureLocalUnitsModal/ConfirmationModal/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/ConfigureLocalUnitsModal/ConfirmationModal/index.tsx @@ -5,11 +5,13 @@ import { import { useOutletContext } from 'react-router-dom'; import { Button, + ListView, Modal, TextInput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; import { resolveToString } from '@ifrc-go/ui/utils'; +import { isDefined } from '@togglecorp/fujs'; import useAlert from '#hooks/useAlert'; import { type CountryOutletContext } from '#utils/outletContext'; @@ -18,7 +20,7 @@ import { useLazyRequest } from '#utils/restRequest'; import i18n from './i18n.json'; export type ManageLocalUnitsValues = { - id: number; + id: number | undefined; country: number; local_unit_type: number; enabled: boolean; @@ -91,7 +93,8 @@ function ConfirmationModal(props: Props) { url: '/api/v2/externally-managed-local-unit/{id}/', method: 'PUT', body: (values: ManageLocalUnitsValues) => values, - pathVariables: manageLocalUnitsValues && { id: manageLocalUnitsValues?.id }, + pathVariables: isDefined(manageLocalUnitsValues?.id) + ? { id: manageLocalUnitsValues.id } : undefined, onSuccess: () => { alert.show( manageLocalUnitsValues?.enabled @@ -109,9 +112,11 @@ function ConfirmationModal(props: Props) { const handleConfirmButtonChange = useCallback(() => { if (isNewManageLocalUnit) { addManageLocalUnit(manageLocalUnitsValues); + } else { + updateManageLocalUnit(manageLocalUnitsValues); } - updateManageLocalUnit(manageLocalUnitsValues); - }, [isNewManageLocalUnit, addManageLocalUnit, manageLocalUnitsValues, updateManageLocalUnit]); + }, [isNewManageLocalUnit, + addManageLocalUnit, manageLocalUnitsValues, updateManageLocalUnit]); return ( + - + )} > { setLocalUnitType(name); - if (isDefined(manageResponse) - && isDefined(manageResponse[name]) - && isDefined(countryResponse) - ) { + + if (isDefined(countryResponse)) { + const isNew = isNotDefined(manageResponse) || isNotDefined(manageResponse[name]); + const manageId = isNew ? undefined : manageResponse[name]?.externallyManagedId; + setManageLocalUnitsValues({ - id: manageResponse[name].externallyManagedId, + id: manageId, country: countryResponse.id, local_unit_type: name, enabled: value, @@ -84,11 +88,12 @@ function ConfigureLocalUnitsModal(props: Props) { ]); const isNewManageLocalUnit = useMemo(() => { - if (isDefined(manageResponse) && isDefined(manageLocalUnitsValues)) { - return !(manageLocalUnitsValues.local_unit_type in manageResponse); + if (isNotDefined(manageLocalUnitsValues) + || isNotDefined(manageLocalUnitsValues?.id)) { + return true; } return false; - }, [manageResponse, manageLocalUnitsValues]); + }, [manageLocalUnitsValues]); const { response: localUnitsOptions, diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/i18n.json b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/i18n.json index 1a0d5fa30b..756d0bca15 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/i18n.json +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/i18n.json @@ -2,16 +2,18 @@ "namespace": "localUnitImportModal", "strings": { "modalHeading": "Import Local Units for {countryName}", - "modalDescription": "Please select a local unit type and upload the CSV file", + "modalDescription": "Please select a local unit type and upload the xlsx file", "modalImportPendingDescription": "Your file is currently being processed. You can close this modal and check the result from the upload history", "closeButtonLabel": "Close", "localUnitTypeInputLabel": "Local unit type", - "uploadFileSectionTitle": "Upload CSV file", - "uploadFileSectionDescription": "Please make sure to select a supported file format (csv) with size less than 10MB", + "uploadFileSectionTitle": "Upload xlsx file", + "uploadFileSectionDescription": "Please make sure to select a supported file format (xlsx, xlsm) with size less than 10MB", "selectFileButtonLabel": "Select a file", "cancelUploadButtonLabel": "Cancel", "startUploadButtonLabel": "Upload", - "contentStructureDescription": "The contents in the CSV should follow the structure provided in {templateLink}.", + "contentStructureDescription": "The contents in the xlsx should follow the structure provided in {templateLink}.", + "contentStructureNoteLabel": "Note", + "contentStructureNote": "To enable multi-select functionality in certain fields, please click \"Enable Macros\" when prompted after opening the downloaded template file.", "templateLinkLabel": "this template", "noPermissionBothDescription": "You don't have permission and this unit is not externally managed.", "noPermissionErrorDescription": "You do not have the permission to upload local unit data", diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/index.tsx index 0eb1fc088a..10b75226d6 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/index.tsx @@ -11,9 +11,11 @@ import { import { Button, Container, + ListView, Modal, RawFileInput, SelectInput, + TextOutput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; import { @@ -49,7 +51,9 @@ import { import i18n from './i18n.json'; import styles from './styles.module.css'; -type BulkUploadEnumsResponse = NonNullable['local_units_bulk_upload_status']>[number]; +type BulkUploadEnumsResponse = NonNullable< + GoApiResponse<'/api/v2/global-enums/'>['local_units_bulk_upload_status'] +>[number]; type BulkStatusKey = BulkUploadEnumsResponse['key']; const BULK_UPLOAD_PENDING = 3 satisfies BulkStatusKey; @@ -88,7 +92,9 @@ function LocalUnitBulkUploadModal(props: Props) { || isLocalUnitCountryValidatorByType(countryResponse?.id, localUnitType) || isLocalUnitRegionValidatorByType(countryResponse?.region, localUnitType); - const { response: localUnitsOptions } = useRequest({ url: '/api/v2/local-units-options/' }); + const { response: localUnitsOptions } = useRequest({ + url: '/api/v2/local-units-options/', + }); const { response: bulkUploadHealthTemplate } = useRequest({ url: '/api/v2/bulk-upload-local-unit/get-bulk-upload-template/', @@ -115,15 +121,14 @@ function LocalUnitBulkUploadModal(props: Props) { }, }); - const { - response: importSummaryResponse, - pending: importSummaryPending, - } = useRequest({ + const { response: importSummaryResponse, pending: importSummaryPending } = useRequest({ url: '/api/v2/bulk-upload-local-unit/{id}/', skip: isNotDefined(bulkUploadResponse?.id), - pathVariables: isDefined(bulkUploadResponse) ? { - id: bulkUploadResponse?.id, - } : undefined, + pathVariables: isDefined(bulkUploadResponse) + ? { + id: bulkUploadResponse?.id, + } + : undefined, shouldPoll: (poll) => { if (poll?.errored || poll?.value?.status !== BULK_UPLOAD_PENDING) { return -1; @@ -133,7 +138,10 @@ function LocalUnitBulkUploadModal(props: Props) { }, }); - const error = transformObjectError(bulkUploadError?.value.formErrors, () => undefined); + const error = transformObjectError( + bulkUploadError?.value.formErrors, + () => undefined, + ); const handleStartUploadButtonClick = useCallback(() => { if ( @@ -176,125 +184,119 @@ function LocalUnitBulkUploadModal(props: Props) { // FIXME: update styling return ( + )} > - - {isDefined(localUnitType) && isDefined(permissionError) && ( + + + {isDefined(localUnitType) && isDefined(permissionError) && ( + + )} - )} - - {isNotDefined(importSummaryResponse) && ( - - {resolveToComponent( - strings.contentStructureDescription, - { - templateLink: ( - - {strings.templateLinkLabel} - - ), - }, - )} - - )} - > - } - /> - {isNotDefined(bulkUploadFile) && ( - } - > - {strings.selectFileButtonLabel} - - )} - {isDefined(bulkUploadFile) && ( -
-
+ {isNotDefined(importSummaryResponse) && ( + + + {resolveToComponent(strings.contentStructureDescription, { + templateLink: ( + + {strings.templateLinkLabel} + + ), + })} + + + + )} + > + } + /> + {isNotDefined(bulkUploadFile) && ( + } + > + {strings.selectFileButtonLabel} + + )} + {isDefined(bulkUploadFile) && ( + {bulkUploadFile.name} -
-
- - -
-
- )} -
- )} - {isDefined(importSummaryResponse) && ( - - )} + + + + +
+ )} + + )} + {isDefined(importSummaryResponse) && ( + + )} +
); } diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/styles.module.css b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/styles.module.css index 95e5357626..ba0074eb1d 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/styles.module.css +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitImportModal/styles.module.css @@ -1,53 +1,7 @@ -.bulk-upload-content { - .upload-summary { - border-radius: var(--go-ui-border-radius-lg); - background-color: var(--go-ui-color-background); - } - - .upload-section { - display: flex; - align-items: start; - border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator); - border-radius: var(--go-ui-border-radius-lg); - padding: var(--go-ui-spacing-sm); - gap: var(--go-ui-spacing-sm); - - .file-icon { - font-size: var(--go-ui-font-size-2xl); - } - - .selected-file-name { - display: flex; - align-items: start; - gap: var(--go-ui-spacing-sm); - flex-grow: 1; - overflow-wrap: anywhere; - } - - .actions { - display: flex; - flex-shrink: 0; - gap: var(--go-ui-spacing-xs); - } - } - - .format-details { - align-items: center; - - .format-description { - display: inline; - vertical-align: baseline; - line-height: 1; - } - } +.file-icon { + font-size: var(--go-ui-font-size-2xl); } .icon { font-size: var(--go-ui-height-icon-multiplier); } - -.header-description-content { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-sm); -} diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/i18n.json b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/i18n.json new file mode 100644 index 0000000000..bad0c650d0 --- /dev/null +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "countryNsOverviewContextAndStructure", + "strings": { + "localUnitReviewButtonLabel": "Review" + } +} diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/index.tsx index e581e9fcf9..8aa90d5509 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateButton/index.tsx @@ -1,8 +1,10 @@ import { Button } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; import { _cs } from '@togglecorp/fujs'; import { VALIDATED } from '../common'; +import i18n from './i18n.json'; import styles from './styles.module.css'; interface Props { @@ -13,11 +15,12 @@ interface Props { function LocalUnitValidateButton(props: Props) { const { status, - // statusDetails, onClick, hasValidatePermission, } = props; + const strings = useTranslation(i18n); + const isValidated = status === VALIDATED; if (isValidated || !hasValidatePermission) { @@ -36,9 +39,8 @@ function LocalUnitValidateButton(props: Props) { !hasValidatePermission || isValidated } - // FIXME: use translations > - Review + {strings.localUnitReviewButtonLabel} ); } diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateModal/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateModal/index.tsx index 16cd031adf..40e88e5b3f 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateModal/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitValidateModal/index.tsx @@ -157,7 +157,6 @@ function LocalUnitValidateModal(props: Props) { return ( ['other_profiles'] +>[number]; + +interface Props { + newValue: OtherProfile; + oldValue: OtherProfile | undefined; +} + +function OtherProfilesDiffOutput(props: Props) { + const { newValue, oldValue } = props; + + const strings = useTranslation(i18n); + + return ( + + + + + + + + + ); +} + +export default OtherProfilesDiffOutput; diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json index e29680e742..c170e04482 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json @@ -55,8 +55,10 @@ "localUnitViewDentist": "Dentist", "localUnitViewNursingAid": "Nursing aid", "localUnitViewMidwife": "Midwife", - "localUnitViewOtherMedicalHeal": "Other medical heal", + "localUnitViewPharmacists": "Pharmacists", "localUnitViewOtherProfiles": "Other profiles", + "localUnitViewRemovedOtherProfiles": "Removed Other profiles", + "localUnitViewOtherMedicalHeal": "Other medical heal", "localUnitViewCommentsNS": "Comments by the NS", "localUnitViewLatitude": "Latitude", "localUnitViewLongitude": "Longitude", diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx index 6a6cb77be8..c88ba1b749 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx @@ -1,4 +1,7 @@ -import { useMemo } from 'react'; +import { + useCallback, + useMemo, +} from 'react'; import { Container, ListView, @@ -6,6 +9,7 @@ import { } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; import { + injectClientId, numericIdSelector, stringNameSelector, stringValueSelector, @@ -28,10 +32,12 @@ import { useRequest, } from '#utils/restRequest'; +import { injectClientIdToResponse } from '../common'; import { type PartialLocalUnits, TYPE_HEALTH_CARE, } from '../LocalUnitsFormModal/LocalUnitsForm/schema'; +import OtherProfilesDiffOutput from './OtherProfilesDiffOutput'; import i18n from './i18n.json'; import styles from './styles.module.css'; @@ -80,15 +86,49 @@ function LocalUnitView(props: Props) { pathVariables: isDefined(localUnitId) ? { id: localUnitId } : undefined, }); + const previousValue = injectClientIdToResponse( + // eslint-disable-next-line max-len + localUnitPreviousResponse?.previous_data_details as unknown as (LocalUnitResponse | undefined), + ); + const newValue = isDefined(locallyChangedValue) ? locallyChangedValue - : localUnitResponse; + : injectClientIdToResponse(localUnitResponse); + const oldValue = isDefined(locallyChangedValue) - ? localUnitResponse - : (localUnitPreviousResponse?.previous_data_details as unknown as LocalUnitResponse); + ? injectClientIdToResponse(localUnitResponse) + : previousValue; + + const getPreviousProfileValue = useCallback((profileClientId: string) => ( + oldValue?.health?.other_profiles?.find( + (previousProfile) => injectClientId(previousProfile)?.client_id === profileClientId, + ) + ), [oldValue]); + + const removedOtherProfiles = useMemo(() => ( + oldValue?.health?.other_profiles?.filter(({ client_id: oldClientId }) => ( + newValue?.health?.other_profiles?.findIndex( + ({ client_id: newClientId }) => oldClientId === newClientId, + ) === -1 + )) + ), [oldValue, newValue]); + + const changedOtherProfiles = useMemo(() => { + const potentiallyChanged = newValue?.health?.other_profiles?.filter( + (newOne) => { + const oldOne = oldValue?.health?.other_profiles?.find( + ({ client_id: oldClientId }) => newOne.client_id === oldClientId, + ); + + return hasDifferences(newOne, oldOne); + }, + ); + + return potentiallyChanged; + }, [oldValue, newValue]); const hasDifference = useMemo(() => { - if (isNotDefined(newValue) || isNotDefined(oldValue)) { + if (isNotDefined(newValue) && isNotDefined(oldValue)) { return false; } @@ -858,16 +898,44 @@ function LocalUnitView(props: Props) { + {isDefined(changedOtherProfiles) && changedOtherProfiles.length > 0 && ( + <> + + {strings.localUnitViewOtherProfiles} + + {newValue?.health?.other_profiles?.map((profile) => ( + + ))} + + )} + {isDefined(removedOtherProfiles) && removedOtherProfiles.length > 0 && ( + <> + + {strings.localUnitViewRemovedOtherProfiles} + + {removedOtherProfiles?.map((profile) => ( + + ))} + + )} ['other_profiles'] +>[number]; +type PreviousValueResponse = NonNullable< + GoApiResponse<'/api/v2/local-units/{id}/'> +>; +type OtherProfilesResponseFields = NonNullable< + NonNullable['health']>['other_profiles'] +>[number]; + +const defaultOtherProfilesValue: OtherProfilesFormFields = { + client_id: '-1', +}; + +interface Props { + value: OtherProfilesFormFields; + previousValue?: OtherProfilesResponseFields; + showValueChanges: boolean; + showChanges: boolean; + error: ArrayError | undefined; + onChange: ( + value: SetValueArg, + index: number + ) => void; + onRemove: (index: number) => void; + index: number; + readOnly?: boolean; +} + +function OtherProfilesInput(props: Props) { + const strings = useTranslation(i18n); + + const { + value, + onChange, + index, + onRemove, + readOnly, + showValueChanges, + showChanges, + previousValue, + error: errorFromProps, + } = props; + + const onFieldChange = useFormObject( + index, + onChange, + defaultOtherProfilesValue, + ); + + const error = value && value.client_id && errorFromProps + ? getErrorObject(errorFromProps?.[value.client_id]) + : undefined; + + return ( + + + + + + + + + + + ); +} + +export default OtherProfilesInput; diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/i18n.json b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/i18n.json index cd3baabd48..6a8089556e 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/i18n.json +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/i18n.json @@ -19,6 +19,7 @@ "sourceEn": "Source (En)", "sourceLocal": "Source (Local)", "addressAndContactTitle": "Address and Contact", + "qualifiersTitle": "Qualifiers", "humanResourcesTitle": "Human Resources", "addressEn": "Address (En)", "addressLocal": "Address (Local)", @@ -47,6 +48,7 @@ "otherServices": "Other Services", "bloodServices": "Blood Services", "professionalTrainingFacilities": "Professional training facilities", + "otherTrainingFacilities": "Other training facilities", "generalMedicalServices": "General medical services", "specialist": "Specialist", "primaryHealthCareCenter": "Primary health care center", @@ -65,9 +67,10 @@ "dentist": "Dentist", "nursingAid": "Nursing aid", "midwife": "Midwife", + "pharmacists": "Pharmacists", "otherMedicalHeal": "Other medical heal", - "otherProfiles": "Other profiles", "commentsNS": "Comments by the NS", + "addOtherProfilesButtonLabel": "Add Other Profiles", "submitButtonLabel": "Submit", "editButtonLabel": "Edit", "localUnitDeleteButtonLabel": "Delete", diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/index.tsx index f167062270..9a974db0e0 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/index.tsx @@ -9,6 +9,7 @@ import { useOutletContext } from 'react-router-dom'; import { BooleanInput, Button, + Checklist, Container, DateInput, DateOutput, @@ -26,6 +27,7 @@ import { useTranslation, } from '@ifrc-go/ui/hooks'; import { + injectClientId, numericIdSelector, resolveToComponent, stringNameSelector, @@ -34,12 +36,14 @@ import { import { isDefined, isNotDefined, + listToMap, + randomString, } from '@togglecorp/fujs'; import { getErrorObject, getErrorString, - removeNull, useForm, + useFormArray, useFormObject, } from '@togglecorp/toggle-form'; @@ -58,7 +62,18 @@ import { getFirstTruthyString, hasChanged, } from '#utils/common'; -import { VISIBILITY_PUBLIC } from '#utils/constants'; +import { + AMBULANCE_TYPE, + HOSPITAL_TYPE, + OTHER_AFFILIATION, + OTHER_TRAINING_FACILITIES, + OTHER_TYPE, + PRIMARY_HEALTH_TYPE, + RESIDENTIAL_TYPE, + SPECIALIZED_SERVICES_TYPE, + TRAINING_FACILITY_TYPE, + VISIBILITY_PUBLIC, +} from '#utils/constants'; import { getUserName } from '#utils/domain/user'; import { type CountryOutletContext } from '#utils/outletContext'; import { @@ -70,7 +85,7 @@ import { transformObjectError } from '#utils/restRequest/error'; import { EXTERNALLY_MANAGED, - type ManageResponse, + injectClientIdToResponse, UNVALIDATED, VALIDATED, } from '../../common'; @@ -80,6 +95,7 @@ import LocalUnitStatus from '../../LocalUnitStatus'; import LocalUnitValidateButton from '../../LocalUnitValidateButton'; import LocalUnitValidateModal from '../../LocalUnitValidateModal'; import LocalUnitViewModal from '../../LocalUnitViewModal'; +import OtherProfilesInput from './OtherProfilesInput'; import schema, { type LocalUnitsRequestPostBody, type PartialLocalUnits, @@ -89,7 +105,8 @@ import schema, { import i18n from './i18n.json'; import styles from './styles.module.css'; -type HealthLocalUnitFormFields = PartialLocalUnits['health']; +type HealthLocalUnitFormFields = NonNullable; +type OtherProfilesFormFields = NonNullable[number]; type VisibilityOptions = NonNullable['api_visibility_choices']>[number] type LocalUnitResponse = NonNullable>; @@ -120,7 +137,6 @@ interface Props { actionsContainerRef: RefObject; headingDescriptionRef?: RefObject; headerDescriptionRef: RefObject; - manageResponse: ManageResponse; } function LocalUnitsForm(props: Props) { @@ -133,7 +149,6 @@ function LocalUnitsForm(props: Props) { headingDescriptionRef, headerDescriptionRef, onDeleteActionSuccess, - manageResponse, } = props; const { isAuthenticated } = useAuth(); @@ -141,9 +156,11 @@ function LocalUnitsForm(props: Props) { const { isSuperUser, isCountryAdmin, + isRegionAdmin, isLocalUnitGlobalValidatorByType, isLocalUnitRegionValidatorByType, isLocalUnitCountryValidatorByType, + canEditLocalUnit, } = usePermissions(); const { api_visibility_choices: visibilityOptions } = useGlobalEnums(); @@ -202,6 +219,14 @@ function LocalUnitsForm(props: Props) { defaultHealthValue, ); + const { + setValue: onOtherProfilesChanges, + removeValue: onOtherProfilesRemove, + } = useFormArray<'other_profiles', OtherProfilesFormFields>( + 'other_profiles', + onHealthFieldChange, + ); + const { response: localUnitDetailsResponse, pending: localUnitDetailsPending, @@ -211,31 +236,54 @@ function LocalUnitsForm(props: Props) { url: '/api/v2/local-units/{id}/', pathVariables: isDefined(localUnitId) ? { id: localUnitId } : undefined, onSuccess: (response) => { - setValue(removeNull(response)); + const responseWithClientId = injectClientIdToResponse(response); + + if (isDefined(responseWithClientId)) { + setValue(responseWithClientId); + } }, }); const { response: localUnitPreviousResponse, } = useRequest({ + skip: isNotDefined(localUnitId), url: '/api/v2/local-units/{id}/latest-change-request/', pathVariables: isDefined(localUnitId) ? { id: localUnitId } : undefined, }); - const isLocked = ( - isDefined(localUnitDetailsResponse?.status) - && !(localUnitDetailsResponse.status === VALIDATED) - ); + const { + response: externallyManagedLocalUnitsResponse, + } = useRequest({ + url: '/api/v2/externally-managed-local-unit/', + query: { + country__id: countryResponse?.id, + limit: 9999, + }, + }); - const isNewLocalUnit = localUnitDetailsResponse?.status === UNVALIDATED; + const externallyManagedByLocalUnitType = useMemo(() => { + if (isNotDefined(externallyManagedLocalUnitsResponse?.results)) { + return undefined; + } + + return listToMap( + externallyManagedLocalUnitsResponse?.results, + (res) => res.local_unit_type_details.id, + (res) => res.enabled, + ); + }, [externallyManagedLocalUnitsResponse]); - const isExternallyManaged = (localUnitDetailsResponse?.status === EXTERNALLY_MANAGED - || (isDefined(value.type) - && isDefined(manageResponse) - && !!manageResponse[value.type]?.enabled)); + const isEditable = localUnitDetailsResponse?.status === VALIDATED; - const readOnly = readOnlyFromProps - || isLocked || isExternallyManaged; + const isNewlyCreated = isNotDefined(localUnitDetailsResponse?.status) + || localUnitDetailsResponse?.status === UNVALIDATED; + const isExternallyManaged = localUnitDetailsResponse?.status === EXTERNALLY_MANAGED; + const isExternallyManagedType = isDefined(value.type) + ? !!(externallyManagedByLocalUnitType?.[value.type]) + : false; + + const readOnly = readOnlyFromProps || isExternallyManaged || isExternallyManagedType; const { response: localUnitsOptions, @@ -342,11 +390,13 @@ function LocalUnitsForm(props: Props) { && (isSuperUser || isLocalUnitGlobalValidatorByType(value.type) || isLocalUnitCountryValidatorByType(countryResponse?.id, value.type) - || isLocalUnitRegionValidatorByType(countryResponse?.region, value.type)); + || isLocalUnitRegionValidatorByType(countryResponse?.region, value.type) + ); - const hasUpdatePermission = (isCountryAdmin(countryResponse?.id) - || hasValidatePermission) - && !isExternallyManaged; + const hasUpdatePermission = isCountryAdmin(countryResponse?.id) + || isRegionAdmin(countryResponse?.region) + || hasValidatePermission + || canEditLocalUnit(countryResponse?.id); const handleFormSubmit = useCallback( () => { @@ -401,22 +451,48 @@ function LocalUnitsForm(props: Props) { localUnitPreviousResponse?.previous_data_details as unknown as LocalUnitResponse | undefined ); - const showChanges = !isNewLocalUnit - && isLocked - && showValueChanges - && !isExternallyManaged; + const diffViewEnabled = showValueChanges + || (!isNewlyCreated + && !isEditable + && !isExternallyManagedType + && !isExternallyManaged); - const showViewChanges = !isNewLocalUnit + const showViewChangesButton = !isNewlyCreated && isDefined(localUnitId) - && isLocked + && !isEditable && !isExternallyManaged; - const permissionError = useMemo(() => { + const isOtherTrainingFacility = useMemo(() => { + if (isNotDefined(value.health?.professional_training_facilities)) { + return false; + } + return value.health?.professional_training_facilities?.some( + (facility) => facility === OTHER_TRAINING_FACILITIES, + ); + }, [value.health?.professional_training_facilities]); + + const handleOtherProfilesAddButtonClick = useCallback( + () => { + const newOtherProfiles: OtherProfilesFormFields = { + client_id: randomString(), + }; + + onHealthFieldChange( + (oldValue: OtherProfilesFormFields[] | undefined) => ( + [...(oldValue ?? []), newOtherProfiles] + ), + 'other_profiles' as const, + ); + }, + [onHealthFieldChange], + ); + + const otherErrors = useMemo(() => { if (isExternallyManaged) { - if (isDefined(localUnitId)) { - return strings.noPermissionFormUpdateExternallyManaged; - } + return strings.noPermissionFormUpdateExternallyManaged; + } + if (isExternallyManagedType) { return strings.noPermissionFormExternallyManaged; } @@ -432,6 +508,7 @@ function LocalUnitsForm(props: Props) { }, [ localUnitId, isExternallyManaged, + isExternallyManagedType, hasUpdatePermission, strings.noPermissionFormUpdateExternallyManaged, strings.noLocalUnitAddPermission, @@ -439,6 +516,12 @@ function LocalUnitsForm(props: Props) { strings.noPermissionFormExternallyManaged, ]); + const getPreviousProfileValue = useCallback((profileClientId: string) => ( + previousData?.health?.other_profiles?.find( + (previousProfile) => injectClientId(previousProfile)?.client_id === profileClientId, + ) + ), [previousData]); + const submitButton = readOnly ? null : ( - )} {hasValidatePermission && ( - + <> + + + )} {readOnlyFromProps - && !isLocked + && isEditable && hasUpdatePermission && (