diff --git a/app/src/hooks/domain/usePermissions.ts b/app/src/hooks/domain/usePermissions.ts index 337edf8d54..85eb91d4da 100644 --- a/app/src/hooks/domain/usePermissions.ts +++ b/app/src/hooks/domain/usePermissions.ts @@ -49,6 +49,8 @@ function usePermissions() { && ((userMe?.is_admin_for_countries.length ?? 0) > 0 || (userMe?.is_admin_for_regions.length ?? 0) > 0); + const isGlobalValidator = !isGuestUser && !!userMe?.is_global_validator; + return { isDrefRegionalCoordinator, isRegionAdmin, @@ -60,6 +62,7 @@ function usePermissions() { isSuperUser, isGuestUser, isRegionalOrCountryAdmin, + isGlobalValidator, }; }, [userMe], diff --git a/app/src/utils/localUnits.ts b/app/src/utils/localUnits.ts new file mode 100644 index 0000000000..2d7a2f266b --- /dev/null +++ b/app/src/utils/localUnits.ts @@ -0,0 +1,89 @@ +import { + isNotDefined, + isObject, +} from '@togglecorp/fujs'; +import { removeNull } from '@togglecorp/toggle-form'; + +import { type PartialLocalUnits } from '#views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsFormModal/LocalUnitsForm/schema'; + +import { type GoApiResponse } from './restRequest'; + +type LocalUnitResponse = NonNullable>; + +export function getFormFields(value: LocalUnitResponse | PartialLocalUnits) { + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + created_at, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + created_by_details, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + modified_at, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + modified_by, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + modified_by_details, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + is_locked, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + validated, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + version_id, + health, + ...formValues + // Note: the cast is safe as we're only trying to + // remove fields if they exist + } = removeNull(value) as LocalUnitResponse; + + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + modified_by_details: healthModifiedByDetails, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + modified_at: healthModifiedAt, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + modified_by: healthModifiedby, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + created_at: healthCreatedAt, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + created_by_details: healthCreatedByDetails, + ...formHealthValues + } = health ?? {}; + + return { ...formValues, health: { ...formHealthValues } }; +} + +// FIXME: this should be gracefully handled +function isObjectWithStringKey(obj: unknown): obj is Record { + return isObject(obj); +} + +export default function hasDifferences(newValue: unknown, oldValue: unknown): boolean { + if (isNotDefined(newValue) && isNotDefined(oldValue)) { + return false; + } + + // FIXME: we might need to also consider the order for array + if (Array.isArray(newValue) && Array.isArray(oldValue)) { + if (newValue.length !== oldValue.length) { + return true; + } + + return newValue.some( + (_, i) => hasDifferences(newValue[i], oldValue[i]), + ); + } + + if (isObjectWithStringKey(newValue) && isObjectWithStringKey(oldValue)) { + const newValueKeys = Object.keys(removeNull(newValue)); + const oldValueKeys = Object.keys(removeNull(oldValue)); + + if (newValueKeys.length !== oldValueKeys.length) { + return true; + } + + return newValueKeys.some( + (key) => hasDifferences(newValue[key], oldValue[key]), + ); + } + + return newValue !== oldValue; +} diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json index 6f7729ef7b..cff5136ea1 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/i18n.json @@ -86,6 +86,7 @@ "localUnitViewConfirmChangesContentQuestion": "Are you sure you want to have these changes in this local unit?", "localUnitViewNewLocalUnitDescription": "New local unit", "localUnitViewLatitude": "Latitude", - "localUnitViewLongitude": "Longitude" + "localUnitViewLongitude": "Longitude", + "localUnitViewNoChanges": "No changes found" } } diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx index fb947666d0..61a93bd7bc 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitView/index.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Container, TextOutput, @@ -18,6 +19,7 @@ import MultiSelectOutput from '#components/MultiSelectOutput'; import SelectOutput from '#components/SelectOutput'; import useCountry from '#hooks/domain/useCountry'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; +import hasDifferences, { getFormFields } from '#utils/localUnits'; import { type GoApiResponse, useRequest, @@ -68,7 +70,6 @@ function LocalUnitView(props: Props) { const { response: localUnitPreviousResponse, pending: localUnitPreviousResponsePending, - // error: localUnitPreviousResponseError, } = useRequest({ skip: isDefined(locallyChangedValue) || isNotDefined(localUnitId), url: '/api/v2/local-units/{id}/latest-change-request/', @@ -82,14 +83,24 @@ function LocalUnitView(props: Props) { ? localUnitResponse : (localUnitPreviousResponse?.previous_data_details as unknown as LocalUnitResponse); - // FIXME: Handle case when there is no change. - // We need to display message to the user + const hasDifference = useMemo(() => { + if (!newValue || !oldValue) { + return false; + } + + const newFormFields = getFormFields(newValue); + const oldFormFields = getFormFields(oldValue); + + return hasDifferences(newFormFields, oldFormFields); + }, [newValue, oldValue]); return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (); const hasValidatePermission = isSuperUser + || isGlobalValidator || isCountryAdmin(Number(countryId)) || isRegionAdmin(Number(countryResponse?.region)); @@ -183,7 +187,6 @@ function LocalUnitsForm(props: Props) { validate, setError, setValue, - pristine, } = useForm( schema, { @@ -224,7 +227,9 @@ function LocalUnitsForm(props: Props) { }, }); - const { response: localUnitPreviousResponse } = useRequest({ + const { + response: localUnitPreviousResponse, + } = useRequest({ url: '/api/v2/local-units/{id}/latest-change-request/', pathVariables: isDefined(localUnitId) ? { id: localUnitId } : undefined, /* @@ -431,22 +436,39 @@ function LocalUnitsForm(props: Props) { const healthFormError = getErrorObject(error?.health); const revertChangesFormError = getErrorObject(revertChangesError); + const previousData = ( + localUnitPreviousResponse?.previous_data_details as unknown as LocalUnitResponse + ); + const isNewLocalUnit = useMemo(() => ( + isNotDefined(previousData) || Object.keys(previousData).length === 0 + ), [previousData]); + + const showChanges = !isNewLocalUnit && !!localUnitDetailsResponse?.is_locked; + + const hasDifference = useMemo(() => { + if (isNotDefined(value) || isNotDefined(previousData)) { + return false; + } + const newFormFields = getFormFields(value); + const oldFormFields = getFormFields(previousData); + + return hasDifferences(newFormFields, oldFormFields); + }, [value, previousData]); + const submitButton = readOnly ? null : ( ); - const previousData = ( - localUnitPreviousResponse?.previous_data_details as unknown as LocalUnitResponse - ); - const isNewLocalUnit = isNotDefined(previousData); - const showChanges = !isNewLocalUnit && !!localUnitDetailsResponse?.is_locked; - return (
{isDefined(localUnitDetailsResponse) @@ -560,7 +582,6 @@ function LocalUnitsForm(props: Props) { {hasValidatePermission && ( @@ -1361,6 +1382,7 @@ function LocalUnitsForm(props: Props) { ?.health?.number_of_isolation_rooms } enabled={showChanges} + diffContainerClassName={styles.diffContainer} > diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsTable/LocalUnitTableActions/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsTable/LocalUnitTableActions/index.tsx index efbff2d893..27ce10dc10 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsTable/LocalUnitTableActions/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/LocalUnitsTable/LocalUnitTableActions/index.tsx @@ -51,6 +51,7 @@ function LocalUnitsTableActions(props: Props) { const { isSuperUser, + isGlobalValidator, isRegionAdmin, isCountryAdmin, isGuestUser, @@ -59,6 +60,7 @@ function LocalUnitsTableActions(props: Props) { const { isAuthenticated } = useAuth(); const hasValidatePermission = isSuperUser + || isGlobalValidator || isCountryAdmin(Number(countryId)) || isRegionAdmin(Number(countryDetails?.region)); diff --git a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/index.tsx b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/index.tsx index 321bcfc252..5e5835537a 100644 --- a/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/index.tsx +++ b/app/src/views/CountryNsOverviewContextAndStructure/NationalSocietyLocalUnits/index.tsx @@ -52,7 +52,12 @@ function NationalSocietyLocalUnits(props: Props) { const [activeTab, setActiveTab] = useState<'map'| 'table'>('map'); const { isAuthenticated } = useAuth(); const { countryResponse } = useOutletContext(); - const { isSuperUser, isCountryAdmin, isGuestUser } = usePermissions(); + const { + isSuperUser, + isCountryAdmin, + isGuestUser, + isGlobalValidator, + } = usePermissions(); const containerRef = useRef(null); // NOTE: key is used to refresh the page when local unit data is updated @@ -111,7 +116,9 @@ function NationalSocietyLocalUnits(props: Props) { const strings = useTranslation(i18n); - const hasAddEditLocalUnitPermission = isCountryAdmin(countryResponse?.id) || isSuperUser; + const hasAddEditLocalUnitPermission = isCountryAdmin(countryResponse?.id) + || isSuperUser + || isGlobalValidator; useEffect(() => { document.addEventListener('fullscreenchange', handleFullScreenChange);