diff --git a/.github/workflows/build-images-from-branch.yml b/.github/workflows/build-images-from-branch.yml index 535f14771a3..182f5d54cfe 100644 --- a/.github/workflows/build-images-from-branch.yml +++ b/.github/workflows/build-images-from-branch.yml @@ -56,9 +56,9 @@ jobs: # another workflow is calling this one if [ "${{ github.event_name }}" == 'push' ]; then - BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} elif [ "${{ github.event_name }}" == 'pull_request' ]; then - BRANCH=${{ github.event.pull_request.head.ref }} + BRANCH=${{ github.event.pull_request.head.ref }} else BRANCH=${{ inputs.branch_name }} fi diff --git a/CHANGELOG.md b/CHANGELOG.md index bc17731e6dd..7b8f0fd7a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Breaking changes - **Dashboard:** Changes made to the dashboard configuration will reset after upgrading OpenCRVS. +- Removed unused searchBirthRegistrations and searchDeathRegistrations queries, as they are no longer used by the client. +- **Retrieve action deprecated:** Field agents & registration agents used to be able to retrieve records to view the audit history & PII. We are removing this in favor of audit capabilities that is planned for in a future release. ### New features diff --git a/docker-compose.yml b/docker-compose.yml index b498e5bb12a..e7a51d41803 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -186,6 +186,7 @@ services: - CONFIG_SMS_CODE_EXPIRY_SECONDS=600 - NOTIFICATION_SERVICE_URL=http://notification:2020/ - METRICS_URL=http://metrics:1050 + - COUNTRY_CONFIG_URL_INTERNAL=http://countryconfig:3040 user-mgnt: image: opencrvs/ocrvs-user-mgnt:${VERSION} #platform: linux/amd64 diff --git a/packages/auth/package.json b/packages/auth/package.json index 8de2522c6fc..f3d6ddea247 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -7,7 +7,7 @@ "scripts": { "start": "cross-env NODE_ENV=development NODE_OPTIONS=--dns-result-order=ipv4first nodemon --exec ts-node -r tsconfig-paths/register src/index.ts", "start:prod": "TS_NODE_BASEURL=./build/dist/src node -r tsconfig-paths/register build/dist/src/index.js", - "test": "jest --coverage --silent --noStackTrace && yarn test:compilation", + "test": "yarn test:compilation && jest --coverage --silent --noStackTrace", "test:watch": "jest --watch", "open:cov": "yarn test && opener coverage/index.html", "lint": "eslint -c .eslintrc.js --fix ./src --max-warnings=0", @@ -83,7 +83,8 @@ "" ], "moduleNameMapper": { - "@auth/(.*)": "/src/$1" + "@auth/(.*)": "/src/$1", + "@opencrvs/commons/(.*)": "/../commons/src/$1" }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", "setupFiles": [ diff --git a/packages/auth/resources/generate-test-token.ts b/packages/auth/resources/generate-test-token.ts index 40731f8e967..6f64fadfaab 100644 --- a/packages/auth/resources/generate-test-token.ts +++ b/packages/auth/resources/generate-test-token.ts @@ -8,8 +8,8 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import * as commandLineArgs from 'command-line-args' -import * as commandLineUsage from 'command-line-usage' +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' import { join } from 'path' const optionList = [ diff --git a/packages/auth/resources/request-token.ts b/packages/auth/resources/request-token.ts index e6421dbb6f4..14a3b87503a 100644 --- a/packages/auth/resources/request-token.ts +++ b/packages/auth/resources/request-token.ts @@ -8,8 +8,8 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import * as commandLineArgs from 'command-line-args' -import * as commandLineUsage from 'command-line-usage' +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' import fetch from 'node-fetch' import * as readline from 'readline' diff --git a/packages/auth/src/environment.ts b/packages/auth/src/environment.ts index 721dfe6f9d1..cdbff5b584d 100644 --- a/packages/auth/src/environment.ts +++ b/packages/auth/src/environment.ts @@ -19,7 +19,8 @@ export const env = cleanEnv(process.env, { METRICS_URL: url({ devDefault: 'http://localhost:1050' }), NOTIFICATION_SERVICE_URL: url({ devDefault: 'http://localhost:2020/' }), DOMAIN: str({ devDefault: '*' }), - COUNTRY_CONFIG_URL: url({ devDefault: 'http://localhost:3040/' }), + COUNTRY_CONFIG_URL: url({ devDefault: 'http://localhost:3040/' }), // used for external requests (CORS whitelist) + COUNTRY_CONFIG_URL_INTERNAL: url({ devDefault: 'http://localhost:3040/' }), // used for internal service-to-service communication LOGIN_URL: url({ devDefault: 'http://localhost:3020/' }), CLIENT_APP_URL: url({ devDefault: 'http://localhost:3000/' }), CERT_PRIVATE_KEY_PATH: str({ devDefault: '../../.secrets/private-key.pem' }), diff --git a/packages/auth/src/features/authenticate/handler.test.ts b/packages/auth/src/features/authenticate/handler.test.ts index d528d9b4218..abc7c483ed0 100644 --- a/packages/auth/src/features/authenticate/handler.test.ts +++ b/packages/auth/src/features/authenticate/handler.test.ts @@ -10,7 +10,8 @@ */ import * as fetchAny from 'jest-fetch-mock' import { createProductionEnvironmentServer } from '@auth/tests/util' -import { AuthServer, createServer } from '@auth/server' +import { createServer, AuthServer } from '@auth/server' +import { DEFAULT_ROLES_DEFINITION } from '@opencrvs/commons/authentication' const fetch = fetchAny as fetchAny.FetchMock describe('authenticate handler receives a request', () => { @@ -54,7 +55,7 @@ describe('authenticate handler receives a request', () => { it('returns 403', async () => { fetch.mockResponse( JSON.stringify({ - userId: '1', + id: '1', status: 'deactivated', scope: ['admin'] }) @@ -78,14 +79,18 @@ describe('authenticate handler receives a request', () => { jest.spyOn(reloadedCodeService, 'generateNonce').mockReturnValue('12345') - fetch.mockResponse( + fetch.mockResponseOnce( JSON.stringify({ - userId: '1', + id: '1', status: 'active', - scope: ['admin'], + role: 'NATIONAL_SYSTEM_ADMIN', mobile: `+345345343` }) ) + + fetch.mockResponse(JSON.stringify(DEFAULT_ROLES_DEFINITION), { + status: 200 + }) const spy = jest.spyOn(reloadedCodeService, 'sendVerificationCode') await server.server.inject({ @@ -109,14 +114,19 @@ describe('authenticate handler receives a request', () => { jest.spyOn(reloadedCodeService, 'generateNonce').mockReturnValue('12345') - fetch.mockResponse( + fetch.mockResponseOnce( JSON.stringify({ - userId: '1', + id: '1', status: 'pending', - scope: ['admin'], + role: 'NATIONAL_SYSTEM_ADMIN', mobile: `+345345343` }) ) + + fetch.mockResponse(JSON.stringify(DEFAULT_ROLES_DEFINITION), { + status: 200 + }) + const spy = jest.spyOn(reloadedCodeService, 'sendVerificationCode') await server.server.inject({ diff --git a/packages/auth/src/features/authenticate/handler.ts b/packages/auth/src/features/authenticate/handler.ts index ca503c6d594..cf8e83b5ef3 100644 --- a/packages/auth/src/features/authenticate/handler.ts +++ b/packages/auth/src/features/authenticate/handler.ts @@ -8,21 +8,22 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import * as Hapi from '@hapi/hapi' -import * as Joi from 'joi' +import { JWT_ISSUER, WEB_USER_JWT_AUDIENCES } from '@auth/constants' import { + IAuthentication, authenticate, - storeUserInformation, createToken, generateAndSendVerificationCode, - IAuthentication + storeUserInformation } from '@auth/features/authenticate/service' import { NotificationEvent, generateNonce } from '@auth/features/verifyCode/service' -import { unauthorized, forbidden } from '@hapi/boom' -import { WEB_USER_JWT_AUDIENCES, JWT_ISSUER } from '@auth/constants' +import { forbidden, unauthorized } from '@hapi/boom' +import * as Hapi from '@hapi/hapi' +import * as Joi from 'joi' +import { getUserRoleScopeMapping } from '@auth/features/scopes/service' interface IAuthPayload { username: string @@ -43,6 +44,7 @@ export default async function authenticateHandler( ): Promise { const payload = request.payload as IAuthPayload let result: IAuthentication + const { username, password } = payload try { result = await authenticate(username.trim(), password) @@ -63,10 +65,15 @@ export default async function authenticateHandler( const isPendingUser = response.status && response.status === 'pending' + const roleScopeMappings = await getUserRoleScopeMapping() + + const role = result.role as keyof typeof roleScopeMappings + const scopes = roleScopeMappings[role] + if (isPendingUser) { response.token = await createToken( result.userId, - result.scope, + scopes, WEB_USER_JWT_AUDIENCES, JWT_ISSUER ) @@ -75,7 +82,7 @@ export default async function authenticateHandler( nonce, result.name, result.userId, - result.scope, + scopes, result.mobile, result.email ) @@ -84,13 +91,14 @@ export default async function authenticateHandler( await generateAndSendVerificationCode( nonce, - result.scope, + scopes, notificationEvent, result.name, result.mobile, result.email ) } + return response } @@ -104,6 +112,7 @@ export const responseSchema = Joi.object({ mobile: Joi.string().optional(), email: Joi.string().optional(), status: Joi.string(), + role: Joi.string(), token: Joi.string().optional() }) diff --git a/packages/auth/src/features/authenticate/service.ts b/packages/auth/src/features/authenticate/service.ts index 8b170a6d563..b53cf827fa0 100644 --- a/packages/auth/src/features/authenticate/service.ts +++ b/packages/auth/src/features/authenticate/service.ts @@ -24,8 +24,10 @@ import { } from '@auth/features/verifyCode/service' import { logger, UUID } from '@opencrvs/commons' import { unauthorized } from '@hapi/boom' -import { chainW, tryCatch } from 'fp-ts/Either' -import { pipe } from 'fp-ts/function' +import * as F from 'fp-ts' +import { Scope } from '@opencrvs/commons/authentication' +const { chainW, tryCatch } = F.either +const { pipe } = F.function import { env } from '@auth/environment' const cert = readFileSync(env.CERT_PRIVATE_KEY_PATH) @@ -48,14 +50,14 @@ export interface IAuthentication { mobile?: string userId: string status: string - scope: string[] email?: string + role: string } export interface ISystemAuthentication { systemId: string status: string - scope: string[] + scope: Scope[] } export class UserInfoNotFoundError extends Error {} @@ -79,11 +81,13 @@ export async function authenticate( if (res.status !== 200) { throw Error(res.statusText) } + const body = await res.json() + return { name: body.name, userId: body.id, - scope: body.scope, + role: body.role, status: body.status, mobile: body.mobile, email: body.email @@ -121,9 +125,6 @@ export async function createToken( issuer: string, temporary?: boolean ): Promise { - if (typeof userId === undefined) { - throw new Error('Invalid userId found for token creation') - } return sign({ scope }, cert, { subject: userId, algorithm: 'RS256', @@ -201,12 +202,7 @@ export async function generateAndSendVerificationCode( email?: string ) { const isDemoUser = scope.indexOf('demo') > -1 || env.QA_ENV - logger.info( - `isDemoUser, - ${JSON.stringify({ - isDemoUser: isDemoUser - })}` - ) + logger.info(`Is demo user: ${isDemoUser}. Scopes: ${scope.join(', ')}`) let verificationCode if (isDemoUser) { verificationCode = '000000' diff --git a/packages/auth/src/features/authenticateSuperUser/handler.ts b/packages/auth/src/features/authenticateSuperUser/handler.ts index df10febf82a..ee8c599a203 100644 --- a/packages/auth/src/features/authenticateSuperUser/handler.ts +++ b/packages/auth/src/features/authenticateSuperUser/handler.ts @@ -17,6 +17,8 @@ import { } from '@auth/features/authenticate/service' import { unauthorized } from '@hapi/boom' import { WEB_USER_JWT_AUDIENCES, JWT_ISSUER } from '@auth/constants' +import { Scope, SCOPES } from '@opencrvs/commons/authentication' +import { logger } from '@opencrvs/commons' interface IAuthPayload { username: string @@ -36,9 +38,19 @@ export default async function authenticateSuperUserHandler( throw unauthorized() } + if (result.status === 'deactivated') { + logger.info('Login attempt with a deactivated super user account detected') + throw unauthorized() + } + + const SUPER_ADMIN_SCOPES = [ + SCOPES.BYPASSRATELIMIT, + SCOPES.USER_DATA_SEEDING + ] satisfies Scope[] + const token = await createToken( result.userId, - result.scope, + SUPER_ADMIN_SCOPES, WEB_USER_JWT_AUDIENCES, JWT_ISSUER ) diff --git a/packages/auth/src/features/oauthToken/client-credentials.ts b/packages/auth/src/features/oauthToken/client-credentials.ts index 434709f1e8c..3254e4677fe 100644 --- a/packages/auth/src/features/oauthToken/client-credentials.ts +++ b/packages/auth/src/features/oauthToken/client-credentials.ts @@ -20,6 +20,7 @@ import { NOTIFICATION_API_USER_AUDIENCE } from '@auth/constants' import * as oauthResponse from './responses' +import { SCOPES } from '@opencrvs/commons/authentication' export async function clientCredentialsHandler( request: Hapi.Request, @@ -43,7 +44,7 @@ export async function clientCredentialsHandler( return oauthResponse.invalidClient(h) } - const isNotificationAPIUser = result.scope.includes('notification-api') + const isNotificationAPIUser = result.scope.includes(SCOPES.NOTIFICATION_API) const token = await createToken( result.systemId, diff --git a/packages/auth/src/features/oauthToken/token-exchange.ts b/packages/auth/src/features/oauthToken/token-exchange.ts index 08224019e3c..a1a2b843409 100644 --- a/packages/auth/src/features/oauthToken/token-exchange.ts +++ b/packages/auth/src/features/oauthToken/token-exchange.ts @@ -17,8 +17,10 @@ import { import { pipe } from 'fp-ts/lib/function' import { UUID } from '@opencrvs/commons' -const SUBJECT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token' -const RECORD_TOKEN_TYPE = 'urn:opencrvs:oauth:token-type:single_record_token' +export const SUBJECT_TOKEN_TYPE = + 'urn:ietf:params:oauth:token-type:access_token' +export const RECORD_TOKEN_TYPE = + 'urn:opencrvs:oauth:token-type:single_record_token' /** * Allows creating record-specific tokens for when a 3rd party system needs to confirm a registration diff --git a/packages/auth/src/features/refresh/handler.test.ts b/packages/auth/src/features/refresh/handler.test.ts index 22e97fbfb22..c7630a50b6f 100644 --- a/packages/auth/src/features/refresh/handler.test.ts +++ b/packages/auth/src/features/refresh/handler.test.ts @@ -10,6 +10,12 @@ */ import { AuthServer } from '@auth/server' import { createProductionEnvironmentServer } from '@auth/tests/util' +import { + DEFAULT_ROLES_DEFINITION, + SCOPES +} from '@opencrvs/commons/authentication' +import * as fetchAny from 'jest-fetch-mock' +const fetch = fetchAny as fetchAny.FetchMock import { AuthenticateResponse } from '@auth/features/authenticate/handler' describe('authenticate handler receives a request', () => { @@ -21,15 +27,21 @@ describe('authenticate handler receives a request', () => { describe('refresh expiring token', () => { it('verifies a token and generates a new token', async () => { + fetch.mockResponseOnce(JSON.stringify(DEFAULT_ROLES_DEFINITION), { + status: 200 + }) // eslint-disable-next-line @typescript-eslint/no-var-requires const codeService = require('../verifyCode/service') // eslint-disable-next-line @typescript-eslint/no-var-requires const authService = require('../authenticate/service') const codeSpy = jest.spyOn(codeService, 'sendVerificationCode') + fetch.mockResponseOnce(JSON.stringify(DEFAULT_ROLES_DEFINITION), { + status: 200 + }) jest.spyOn(authService, 'authenticate').mockReturnValue({ userId: '1', - scope: ['admin'], + role: 'NATIONAL_SYSTEM_ADMIN', mobile: '+345345343' }) @@ -75,18 +87,32 @@ describe('authenticate handler receives a request', () => { const [, payload] = token.split('.') const body = JSON.parse(Buffer.from(payload, 'base64').toString()) - expect(body.scope).toEqual(['admin']) + expect(body.scope).toEqual([ + SCOPES.SYSADMIN, + SCOPES.NATLSYSADMIN, + SCOPES.USER_CREATE, + SCOPES.USER_READ, + SCOPES.USER_UPDATE, + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.PERFORMANCE_EXPORT_VITAL_STATISTICS + ]) expect(body.sub).toBe('1') }) it('refreshError returns a 401 to the client if the token is bad', async () => { + fetch.mockResponseOnce(JSON.stringify(DEFAULT_ROLES_DEFINITION), { + status: 200 + }) // eslint-disable-next-line const codeService = require('../verifyCode/service') // eslint-disable-next-line const authService = require('../authenticate/service') const codeSpy = jest.spyOn(codeService, 'sendVerificationCode') jest.spyOn(authService, 'authenticate').mockReturnValue({ - userId: '1', - scope: ['admin'], + id: '1', + role: 'NATIONAL_SYSTEM_ADMIN', + scope: ['natlsysadmin'], username: '+345345343' }) diff --git a/packages/auth/src/features/resend/handler.test.ts b/packages/auth/src/features/resend/handler.test.ts index c2409673ce9..ffe30b7309e 100644 --- a/packages/auth/src/features/resend/handler.test.ts +++ b/packages/auth/src/features/resend/handler.test.ts @@ -53,7 +53,7 @@ describe('resend handler receives a request', () => { // eslint-disable-next-line const authService = require('../authenticate/service') jest.spyOn(authService, 'getStoredUserInformation').mockReturnValue({ - userId: '1', + id: '1', scope: ['admin'], mobile: '+345345343' }) diff --git a/packages/auth/src/features/retrievalSteps/verifyNumber/handler.test.ts b/packages/auth/src/features/retrievalSteps/verifyNumber/handler.test.ts index 50846d66328..d9f2547fc1e 100644 --- a/packages/auth/src/features/retrievalSteps/verifyNumber/handler.test.ts +++ b/packages/auth/src/features/retrievalSteps/verifyNumber/handler.test.ts @@ -25,7 +25,7 @@ describe('verifyNumber handler receives a request', () => { jest.spyOn(codeService, 'generateNonce').mockReturnValue('12345') fetch.mockResponse( JSON.stringify({ - userId: '1', + id: '1', username: 'fake_user_name', status: 'active', scope: ['demo'], diff --git a/packages/auth/src/features/retrievalSteps/verifyUser/handler.test.ts b/packages/auth/src/features/retrievalSteps/verifyUser/handler.test.ts index 2f571e100a5..3729858a5a1 100644 --- a/packages/auth/src/features/retrievalSteps/verifyUser/handler.test.ts +++ b/packages/auth/src/features/retrievalSteps/verifyUser/handler.test.ts @@ -49,7 +49,7 @@ describe('verifyUser handler receives a request', () => { jest.spyOn(codeService, 'generateNonce').mockReturnValue('12345') fetch.mockResponse( JSON.stringify({ - userId: '1', + id: '1', username: 'fake_user_name', status: 'active', scope: ['demo'], @@ -75,7 +75,7 @@ describe('verifyUser handler receives a request', () => { fetch.mockResponse( JSON.stringify({ - userId: '1', + id: '1', username: 'fake_user_name', status: 'active', scope: ['admin'], diff --git a/packages/auth/src/features/scopes/service.ts b/packages/auth/src/features/scopes/service.ts new file mode 100644 index 00000000000..6688598bb12 --- /dev/null +++ b/packages/auth/src/features/scopes/service.ts @@ -0,0 +1,47 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import { env } from '@auth/environment' +import { fetchJSON, joinURL, logger, Roles } from '@opencrvs/commons' +import { + DEFAULT_ROLES_DEFINITION, + Scope +} from '@opencrvs/commons/authentication' + +export async function getUserRoleScopeMapping() { + const roles = await fetchJSON( + joinURL(env.COUNTRY_CONFIG_URL_INTERNAL, '/roles') + ) + + logger.info( + 'Country config implements the new /roles response format. Custom scopes apply' + ) + + const defaultRoleMappings = DEFAULT_ROLES_DEFINITION.reduce< + Record + >((acc, { id, scopes }) => { + acc[id] = scopes + return acc + }, {}) + + const userRoleMappings = roles.reduce>( + (acc, { id, scopes }) => { + acc[id] = scopes + return acc + }, + {} + ) + + return { + ...defaultRoleMappings, + ...userRoleMappings + } +} diff --git a/packages/auth/src/features/verifyCode/handler.test.ts b/packages/auth/src/features/verifyCode/handler.test.ts index b6c83127d9a..978600d3489 100644 --- a/packages/auth/src/features/verifyCode/handler.test.ts +++ b/packages/auth/src/features/verifyCode/handler.test.ts @@ -10,6 +10,13 @@ */ import { AuthServer } from '@auth/server' import { createProductionEnvironmentServer } from '@auth/tests/util' +import { + DEFAULT_ROLES_DEFINITION, + SCOPES +} from '@opencrvs/commons/authentication' +import * as fetchMock from 'jest-fetch-mock' + +const fetch: fetchMock.FetchMock = fetchMock as fetchMock.FetchMock import { AuthenticateResponse } from '@auth/features/authenticate/handler' describe('authenticate handler receives a request', () => { @@ -27,6 +34,10 @@ describe('authenticate handler receives a request', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const authService = require('../authenticate/service') const codeSpy = jest.spyOn(codeService, 'sendVerificationCode') + + fetch.mockResponseOnce(JSON.stringify(DEFAULT_ROLES_DEFINITION), { + status: 200 + }) jest.spyOn(authService, 'authenticate').mockReturnValue({ name: [ { @@ -36,7 +47,7 @@ describe('authenticate handler receives a request', () => { } ], userId: '1', - scope: ['admin'], + role: 'NATIONAL_SYSTEM_ADMIN', status: 'active', mobile: '+345345343', email: 'test@test.org' @@ -68,7 +79,17 @@ describe('authenticate handler receives a request', () => { expect(res.result!.token.split('.')).toHaveLength(3) const [, payload] = res.result!.token.split('.') const body = JSON.parse(Buffer.from(payload, 'base64').toString()) - expect(body.scope).toEqual(['admin']) + expect(body.scope).toEqual([ + SCOPES.SYSADMIN, + SCOPES.NATLSYSADMIN, + SCOPES.USER_CREATE, + SCOPES.USER_READ, + SCOPES.USER_UPDATE, + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.PERFORMANCE_EXPORT_VITAL_STATISTICS + ]) expect(body.sub).toBe('1') }) }) @@ -77,7 +98,7 @@ describe('authenticate handler receives a request', () => { // eslint-disable-next-line const authService = require('../authenticate/service') jest.spyOn(authService, 'authenticate').mockReturnValue({ - userId: '1', + id: '1', scope: ['admin'], status: 'active', mobile: '+345345343' diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 655efd18756..337b0e2bb71 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -65,6 +65,7 @@ import { getPublicKey } from '@auth/features/authenticate/service' import anonymousTokenHandler, { responseSchema } from './features/anonymousToken/handler' +import { Boom, badRequest } from '@hapi/boom' export type AuthServer = { server: Hapi.Server @@ -83,7 +84,22 @@ export async function createServer() { port: env.AUTH_PORT, routes: { cors: { origin: whitelist }, - payload: { maxBytes: 52428800, timeout: DEFAULT_TIMEOUT } + payload: { maxBytes: 52428800, timeout: DEFAULT_TIMEOUT }, + response: { + failAction: async (req, _2, err: Boom) => { + if (process.env.NODE_ENV === 'production') { + // In prod, log a limited error message and throw the default Bad Request error. + logger.error(`Response validationError: ${err.message}`) + throw badRequest(`Invalid response payload returned from handler`) + } else { + // During development, log and respond with the full error. + logger.error( + `${req.path} response has a validation error: ${err.message}` + ) + throw err + } + } + } } }) diff --git a/packages/auth/src/tests/util.ts b/packages/auth/src/tests/util.ts index c6c68d55016..fef9d44f24b 100644 --- a/packages/auth/src/tests/util.ts +++ b/packages/auth/src/tests/util.ts @@ -10,7 +10,7 @@ */ export function createServerWithEnvironment(env: Record) { jest.resetModules() - process.env = { ...process.env, ...env } + process.env = { ...process.env, ...env, LOG_LEVEL: 'error' } // eslint-disable-next-line @typescript-eslint/no-var-requires return require('../server').createServer() } @@ -22,6 +22,7 @@ export function createProductionEnvironmentServer() { AUTH_PORT: '4040', CLIENT_APP_URL: 'http://localhost:3000/', COUNTRY_CONFIG_URL: 'http://localhost:3040/', + COUNTRY_CONFIG_URL_INTERNAL: 'http://localhost:3040/', DOMAIN: '*', LOGIN_URL: 'http://localhost:3020/', METRICS_URL: 'http://localhost:1050', diff --git a/packages/auth/test/setupJest.ts b/packages/auth/test/setupJest.ts index 7c8bbd12709..c27c5742726 100644 --- a/packages/auth/test/setupJest.ts +++ b/packages/auth/test/setupJest.ts @@ -38,6 +38,7 @@ const mock: IDatabaseConnector = { } jest.setMock('src/database', mock) +jest.setMock('src/metrics', { postUserActionToMetrics: jest.fn() }) process.env.CERT_PRIVATE_KEY_PATH = join(__dirname, './cert.key') process.env.CERT_PUBLIC_KEY_PATH = join(__dirname, './cert.key.pub') diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 67be16ff585..bd253ddd6ff 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -5,12 +5,12 @@ "@auth/*": ["./*"] }, "target": "es6", - "module": "commonjs", + "module": "node16", "allowSyntheticDefaultImports": true, "skipLibCheck": true, "outDir": "build/dist", "sourceMap": true, - "moduleResolution": "node", + "moduleResolution": "node16", "rootDir": ".", "lib": ["esnext.asynciterable", "es6", "es2019"], "forceConsistentCasingInFileNames": true, diff --git a/packages/client/graphql.schema.json b/packages/client/graphql.schema.json index f18e9d1a158..6af6c7dab9d 100644 --- a/packages/client/graphql.schema.json +++ b/packages/client/graphql.schema.json @@ -925,6 +925,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "eventLocationLevel6", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "fatherDoB", "description": null, @@ -1713,6 +1725,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "eventLocationLevel6", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "fatherDoB", "description": null, @@ -3635,6 +3659,30 @@ "name": "Certificate", "description": null, "fields": [ + { + "name": "certificateTemplateId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "certifier", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "collector", "description": null, @@ -3674,18 +3722,6 @@ }, "isDeprecated": false, "deprecationReason": null - }, - { - "name": "templateConfig", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "CertificateConfigData", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null } ], "inputFields": null, @@ -3694,44 +3730,75 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "CertificateConfigData", + "kind": "INPUT_OBJECT", + "name": "CertificateInput", "description": null, - "fields": [ + "fields": null, + "inputFields": [ { - "name": "event", + "name": "certificateTemplateId", "description": null, - "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "fee", + "name": "collector", "description": null, - "args": [], "type": { - "kind": "NON_NULL", + "kind": "INPUT_OBJECT", + "name": "RelatedPersonInput", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasShowedVerifiedDocument", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "payments", + "description": null, + "type": { + "kind": "LIST", "name": null, "ofType": { - "kind": "OBJECT", - "name": "CertificateFee", + "kind": "INPUT_OBJECT", + "name": "PaymentInput", "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CertificationMetric", + "description": null, + "fields": [ { - "name": "id", + "name": "eventType", "description": null, "args": [], "type": { @@ -3747,55 +3814,93 @@ "deprecationReason": null }, { - "name": "label", + "name": "total", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "CertificateLabel", + "kind": "SCALAR", + "name": "Float", "ofType": null } }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CertifyActionInput", + "description": null, + "fields": null, + "inputFields": [ { - "name": "lateRegistrationTarget", + "name": "data", "description": null, - "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "FieldInput", + "ofType": null + } + } } }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Comment", + "description": null, + "fields": [ + { + "name": "comment", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "printInAdvance", + "name": "createdAt", "description": null, "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } + "kind": "SCALAR", + "name": "Date", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "registrationTarget", + "name": "id", "description": null, "args": [], "type": { @@ -3803,7 +3908,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "Int", + "name": "ID", "ofType": null } }, @@ -3811,17 +3916,13 @@ "deprecationReason": null }, { - "name": "svgUrl", + "name": "user", "description": null, "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "OBJECT", + "name": "User", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -3834,776 +3935,62 @@ }, { "kind": "INPUT_OBJECT", - "name": "CertificateConfigDataInput", + "name": "CommentInput", "description": null, "fields": null, "inputFields": [ { - "name": "event", + "name": "comment", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "fee", + "name": "createdAt", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CertificateFeeInput", - "ofType": null - } + "kind": "SCALAR", + "name": "Date", + "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "id", + "name": "user", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "INPUT_OBJECT", + "name": "UserInput", + "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ConfirmRegistrationInput", + "description": null, + "fields": null, + "inputFields": [ { - "name": "label", + "name": "identifiers", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "CertificateLabelInput", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lateRegistrationTarget", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "printInAdvance", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "registrationTarget", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "svgUrl", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CertificateFee", - "description": null, - "fields": [ - { - "name": "delayed", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "late", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onTime", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CertificateFeeInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "delayed", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "late", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "onTime", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CertificateInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "collector", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "RelatedPersonInput", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hasShowedVerifiedDocument", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "payments", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PaymentInput", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "templateConfig", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "CertificateConfigDataInput", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CertificateLabel", - "description": null, - "fields": [ - { - "name": "defaultMessage", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CertificateLabelInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "defaultMessage", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CertificationMetric", - "description": null, - "fields": [ - { - "name": "eventType", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "total", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Float", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CertifyActionInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "data", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "FieldInput", - "ofType": null - } - } - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Comment", - "description": null, - "fields": [ - { - "name": "comment", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "args": [], - "type": { - "kind": "SCALAR", - "name": "Date", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "CommentInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "comment", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "createdAt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Date", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "UserInput", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "ComparisonInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "eq", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "gte", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "in", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lt", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lte", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ne", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nin", - "description": null, - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "ConfirmRegistrationInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "error", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "identifiers", - "description": null, - "type": { - "kind": "LIST", + "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", @@ -6754,9 +6141,13 @@ "description": null, "args": [], "type": { - "kind": "OBJECT", - "name": "User", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null @@ -7618,6 +7009,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "certificateTemplateId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "certificates", "description": null, @@ -7942,18 +7345,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "templateConfig", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "CertificateConfigData", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "user", "description": null, @@ -8074,43 +7465,102 @@ "deprecationReason": null }, { - "name": "marriedLastName", + "name": "marriedLastName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "middleName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "use", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "I18nMessage", + "description": null, + "fields": [ + { + "name": "defaultMessage", "description": null, + "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "middleName", + "name": "description", "description": null, + "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "use", + "name": "id", "description": null, + "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null } ], - "interfaces": null, + "inputFields": null, + "interfaces": [], "enumValues": null, "possibleTypes": null }, @@ -8539,49 +7989,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "INPUT_OBJECT", - "name": "LabelInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "label", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lang", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "LocalRegistrar", @@ -8612,13 +8019,9 @@ "description": null, "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SystemRoleType", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -12876,16 +12279,52 @@ "deprecationReason": null }, { - "name": "updateRole", + "name": "upsertRegistrationIdentifier", "description": null, "args": [ { - "name": "systemRole", + "name": "id", "description": null, "type": { - "kind": "INPUT_OBJECT", - "name": "SystemRoleInput", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "identifierType", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "identifierValue", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, @@ -12896,8 +12335,8 @@ "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "Response", + "kind": "SCALAR", + "name": "ID", "ofType": null } }, @@ -14968,129 +14407,28 @@ "ofType": null } }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filterBy", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "locationId", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "size", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "skip", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timeEnd", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timeStart", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "UNION", - "name": "MixedTotalMetricsResult", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "getSystemRoles", - "description": null, - "args": [ + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { - "name": "active", + "name": "filterBy", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "role", + "name": "locationId", "description": null, "type": { "kind": "SCALAR", @@ -15102,48 +14440,64 @@ "deprecationReason": null }, { - "name": "sortBy", + "name": "size", "description": null, "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "sortOrder", + "name": "skip", "description": null, "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "title", + "name": "timeEnd", "description": null, "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "value", + "name": "timeStart", "description": null, "type": { - "kind": "INPUT_OBJECT", - "name": "ComparisonInput", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, @@ -15151,17 +14505,9 @@ } ], "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "SystemRole", - "ofType": null - } - } + "kind": "UNION", + "name": "MixedTotalMetricsResult", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -15634,6 +14980,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "getUserRoles", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UserRole", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "getVSExports", "description": null, @@ -15887,88 +15257,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "searchBirthRegistrations", - "description": null, - "args": [ - { - "name": "fromDate", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Date", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "toDate", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Date", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "BirthRegistration", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "searchDeathRegistrations", - "description": null, - "args": [ - { - "name": "fromDate", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Date", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "toDate", - "description": null, - "type": { - "kind": "SCALAR", - "name": "Date", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DeathRegistration", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "searchEvents", "description": null, @@ -16319,18 +15607,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "systemRole", - "description": null, - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "username", "description": null, @@ -18874,121 +18150,24 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "userId", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "RequestCorrectionActionInput", - "description": null, - "fields": null, - "inputFields": [ - { - "name": "data", - "description": null, - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "FieldInput", - "ofType": null - } - } - } - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Response", - "description": null, - "fields": [ - { - "name": "roleIdMap", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Map", + "name": "String", "ofType": null } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "RevokeActionInput", - "description": null, - "fields": null, - "inputFields": [ + }, { - "name": "data", + "name": "userId", "description": null, "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "FieldInput", - "ofType": null - } - } + "kind": "SCALAR", + "name": "String", + "ofType": null } }, "defaultValue": null, @@ -19002,7 +18181,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "RevokeCorrectionActionInput", + "name": "RequestCorrectionActionInput", "description": null, "fields": null, "inputFields": [ @@ -19036,30 +18215,14 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "Role", + "kind": "INPUT_OBJECT", + "name": "RevokeActionInput", "description": null, - "fields": [ - { - "name": "_id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, + "fields": null, + "inputFields": [ { - "name": "labels", + "name": "data", "description": null, - "args": [], "type": { "kind": "NON_NULL", "name": null, @@ -19070,42 +18233,30 @@ "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "RoleLabel", + "kind": "INPUT_OBJECT", + "name": "FieldInput", "ofType": null } } } }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null } ], - "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "INPUT_OBJECT", - "name": "RoleInput", + "name": "RevokeCorrectionActionInput", "description": null, "fields": null, "inputFields": [ { - "name": "_id", - "description": null, - "type": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "labels", + "name": "data", "description": null, "type": { "kind": "NON_NULL", @@ -19118,7 +18269,7 @@ "name": null, "ofType": { "kind": "INPUT_OBJECT", - "name": "LabelInput", + "name": "FieldInput", "ofType": null } } @@ -19133,49 +18284,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "RoleLabel", - "description": null, - "fields": [ - { - "name": "label", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lang", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "SearchFieldAgentResponse", @@ -19259,7 +18367,7 @@ "args": [], "type": { "kind": "OBJECT", - "name": "Role", + "name": "UserRole", "ofType": null }, "isDeprecated": false, @@ -19660,227 +18768,85 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "System", - "description": null, - "fields": [ - { - "name": "_id", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "clientId", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "integratingSystemType", - "description": null, - "args": [], - "type": { - "kind": "ENUM", - "name": "IntegratingSystemType", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "settings", - "description": null, - "args": [], - "type": { - "kind": "OBJECT", - "name": "SystemSettings", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "shaSecret", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "status", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SystemStatus", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SystemType", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "SystemInput", + { + "kind": "OBJECT", + "name": "System", "description": null, - "fields": null, - "inputFields": [ + "fields": [ { - "name": "integratingSystemType", + "name": "_id", "description": null, + "args": [], "type": { - "kind": "ENUM", - "name": "IntegratingSystemType", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "name", + "name": "clientId", "description": null, + "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "settings", + "name": "integratingSystemType", "description": null, + "args": [], "type": { - "kind": "INPUT_OBJECT", - "name": "SystemSettingsInput", + "kind": "ENUM", + "name": "IntegratingSystemType", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "type", + "name": "name", "description": null, + "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "SystemType", + "kind": "SCALAR", + "name": "String", "ofType": null } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "SystemRole", - "description": null, - "fields": [ + }, { - "name": "active", + "name": "settings", "description": null, "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null - } + "kind": "OBJECT", + "name": "SystemSettings", + "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "id", + "name": "shaSecret", "description": null, "args": [], "type": { @@ -19896,31 +18862,23 @@ "deprecationReason": null }, { - "name": "roles", + "name": "status", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Role", - "ofType": null - } - } + "kind": "ENUM", + "name": "SystemStatus", + "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { - "name": "value", + "name": "type", "description": null, "args": [], "type": { @@ -19928,7 +18886,7 @@ "name": null, "ofType": { "kind": "ENUM", - "name": "SystemRoleType", + "name": "SystemType", "ofType": null } }, @@ -19943,16 +18901,16 @@ }, { "kind": "INPUT_OBJECT", - "name": "SystemRoleInput", + "name": "SystemInput", "description": null, "fields": null, "inputFields": [ { - "name": "active", + "name": "integratingSystemType", "description": null, "type": { - "kind": "SCALAR", - "name": "Boolean", + "kind": "ENUM", + "name": "IntegratingSystemType", "ofType": null }, "defaultValue": null, @@ -19960,14 +18918,14 @@ "deprecationReason": null }, { - "name": "id", + "name": "name", "description": null, "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", - "name": "ID", + "name": "String", "ofType": null } }, @@ -19976,32 +18934,28 @@ "deprecationReason": null }, { - "name": "roles", + "name": "settings", "description": null, "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "RoleInput", - "ofType": null - } - } + "kind": "INPUT_OBJECT", + "name": "SystemSettingsInput", + "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "value", + "name": "type", "description": null, "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SystemType", + "ofType": null + } }, "defaultValue": null, "isDeprecated": false, @@ -20012,59 +18966,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "ENUM", - "name": "SystemRoleType", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "FIELD_AGENT", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "LOCAL_REGISTRAR", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "LOCAL_SYSTEM_ADMIN", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "NATIONAL_REGISTRAR", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "NATIONAL_SYSTEM_ADMIN", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "PERFORMANCE_MANAGEMENT", - "description": null, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "REGISTRATION_AGENT", - "description": null, - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, { "kind": "OBJECT", "name": "SystemSecret", @@ -20806,9 +19707,13 @@ "description": null, "args": [], "type": { - "kind": "OBJECT", - "name": "Location", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Location", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null @@ -20822,7 +19727,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "Role", + "name": "UserRole", "ofType": null } }, @@ -20877,22 +19782,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "systemRole", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "SystemRoleType", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "underInvestigation", "description": null, @@ -21423,35 +20312,86 @@ "deprecationReason": null }, { - "name": "systemRole", + "name": "username", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UserRole", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "label", "description": null, + "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "SystemRoleType", + "kind": "OBJECT", + "name": "I18nMessage", "ofType": null } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "username", + "name": "scopes", "description": null, + "args": [], "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null } ], - "interfaces": null, + "inputFields": null, + "interfaces": [], "enumValues": null, "possibleTypes": null }, diff --git a/packages/client/src/App.test.tsx b/packages/client/src/App.test.tsx index 5d4aead51f2..879de1cdcb5 100644 --- a/packages/client/src/App.test.tsx +++ b/packages/client/src/App.test.tsx @@ -8,22 +8,15 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import * as React from 'react' -import { - createTestApp, - getItem, - flushPromises, - createTestComponent -} from '@client/tests/util' +import { createTestApp, flushPromises, getItem } from '@client/tests/util' -import { createClient } from '@client/utils/apolloClient' import * as actions from '@client/notification/actions' +import { AppStore } from '@client/store' +import { createClient } from '@client/utils/apolloClient' import { referenceApi } from '@client/utils/referenceApi' -import { StyledErrorBoundary } from '@client/components/StyledErrorBoundary' -import { createStore, AppStore } from '@client/store' -import { waitFor } from './tests/wait-for-element' import { ReactWrapper } from 'enzyme' import { vi } from 'vitest' +import { waitFor } from './tests/wait-for-element' const validToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJpYXQiOjE1MzMxOTUyMjgsImV4cCI6MTU0MzE5NTIyNywiYXVkIjpbImdhdGV3YXkiXSwic3ViIjoiMSJ9.G4KzkaIsW8fTkkF-O8DI0qESKeBI332UFlTXRis3vJ6daisu06W5cZsgYhmxhx_n0Q27cBYt2OSOnjgR72KGA5IAAfMbAJifCul8ib57R4VJN8I90RWqtvA0qGjV-sPndnQdmXzCJx-RTumzvr_vKPgNDmHzLFNYpQxcmQHA-N8li-QHMTzBHU4s9y8_5JOCkudeoTMOd_1021EDAQbrhonji5V1EOSY2woV5nMHhmq166I1L0K_29ngmCqQZYi1t6QBonsIowlXJvKmjOH5vXHdCCJIFnmwHmII4BK-ivcXeiVOEM_ibfxMWkAeTRHDshOiErBFeEvqd6VWzKvbKAH0UY-Rvnbh4FbprmO4u4_6Yd2y2HnbweSo-v76dVNcvUS0GFLFdVBt0xTay-mIeDy8CKyzNDOWhmNUvtVi9mhbXYfzzEkwvi9cWwT1M8ZrsWsvsqqQbkRCyBmey_ysvVb5akuabenpPsTAjiR8-XU2mdceTKqJTwbMU5gz-8fgulbTB_9TNJXqQlH7tyYXMWHUY3uiVHWg2xgjRiGaXGTiDgZd01smYsxhVnPAddQOhqZYCrAgVcT1GBFVvhO7CC-rhtNlLl21YThNNZNpJHsCgg31WA9gMQ_2qAJmw2135fAyylO8q7ozRUvx46EezZiPzhCkPMeELzLhQMEIqjo' @@ -62,8 +55,7 @@ describe('when user has a valid token in url but an expired one in localStorage' }) it("doesn't redirect user to SSO", async () => { - await createTestApp() - + await createTestApp({ waitUntilOfflineCountryConfigLoaded: false }) expect(assign.mock.calls).toHaveLength(0) }) @@ -131,39 +123,3 @@ describe('when user has a valid token in local storage', () => { expect(loadLocations).toHaveBeenCalled() }) }) - -describe('it handles react errors', () => { - it('displays react error page', async () => { - const { store } = createStore() - function Problem(): JSX.Element { - throw new Error('Error thrown.') - } - const { component } = await createTestComponent( - - - , - { store } - ) - - expect(component.find('#GoToHomepage').hostNodes()).toHaveLength(1) - }) -}) - -describe('it handles react unauthorized errors', () => { - it('displays react error page', async () => { - const { store } = createStore() - function Problem(): JSX.Element { - throw new Error('401') - } - const { component } = await createTestComponent( - - - , - { store } - ) - - expect(component.find('#GoToHomepage').hostNodes()).toHaveLength(1) - - component.find('#GoToHomepage').hostNodes().simulate('click') - }) -}) diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 11a29a0afc9..e2d7c4b6ba0 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -8,17 +8,6 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { ErrorBoundary } from '@client/components/ErrorBoundary' -import { StyledErrorBoundary } from '@client/components/StyledErrorBoundary' -import { I18nContainer } from '@client/i18n/components/I18nContainer' -import { useApolloClient } from '@client/utils/apolloClient' -import { ApolloProvider } from '@client/utils/ApolloProvider' -import { getTheme } from '@opencrvs/components/lib/theme' -import { Provider } from 'react-redux' -import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom' -import styled, { createGlobalStyle, ThemeProvider } from 'styled-components' - -import * as React from 'react' import { NotificationComponent } from '@client/components/Notification' import { Page } from '@client/components/Page' @@ -27,8 +16,17 @@ import { ProtectedRoute } from '@client/components/ProtectedRoute' import ScrollToTop from '@client/components/ScrollToTop' import { SessionExpireConfirmation } from '@client/components/SessionExpireConfirmation' import * as routes from '@client/navigation/routes' +import { AdvancedSearchResult } from '@client/views/AdvancedSearch/AdvancedSearchResult' +import { IssueCertificate } from '@client/views/IssueCertificate/IssueCertificate' +import { IssuePayment } from '@client/views/IssueCertificate/IssueCollectorForm/IssuePayment' +import { Home } from '@client/views/OfficeHome/Home' import { OfficeHome } from '@client/views/OfficeHome/OfficeHome' +import { AdministrativeLevels } from '@client/views/Organisation/AdministrativeLevels' +import { PerformanceDashboard } from '@client/views/Performance/Dashboard' import { FieldAgentList } from '@client/views/Performance/FieldAgentList' +import { Leaderboards } from '@client/views/Performance/Leaderboards' +import { RegistrationList } from '@client/views/Performance/RegistrationsList' +import { PerformanceStatistics } from '@client/views/Performance/Statistics' import { CollectorForm } from '@client/views/PrintCertificate/collectorForm/CollectorForm' import { Payment } from '@client/views/PrintCertificate/Payment' import { VerifyCollector } from '@client/views/PrintCertificate/VerifyCollector' @@ -41,22 +39,23 @@ import { CompletenessRates } from '@client/views/SysAdmin/Performance/Completene import { PerformanceHome } from '@client/views/SysAdmin/Performance/PerformanceHome' import { WorkflowStatus } from '@client/views/SysAdmin/Performance/WorkflowStatus' import { CreateNewUser } from '@client/views/SysAdmin/Team/user/userCreation/CreateNewUser' - -import { SystemRoleType } from '@client/utils/gateway' -import { AdvancedSearchResult } from '@client/views/AdvancedSearch/AdvancedSearchResult' -import { IssueCertificate } from '@client/views/IssueCertificate/IssueCertificate' -import { IssuePayment } from '@client/views/IssueCertificate/IssueCollectorForm/IssuePayment' -import { Home } from '@client/views/OfficeHome/Home' -import { AdministrativeLevels } from '@client/views/Organisation/AdministrativeLevels' -import { PerformanceDashboard } from '@client/views/Performance/Dashboard' -import { Leaderboards } from '@client/views/Performance/Leaderboards' -import { RegistrationList } from '@client/views/Performance/RegistrationsList' -import { PerformanceStatistics } from '@client/views/Performance/Statistics' import { VerifyCertificatePage } from '@client/views/VerifyCertificate/VerifyCertificatePage' import { ViewRecord } from '@client/views/ViewRecord/ViewRecord' +import { SCOPES } from '@opencrvs/commons/client' +import { getTheme } from '@opencrvs/components' +import * as React from 'react' +import { Provider } from 'react-redux' +import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom' +import styled, { createGlobalStyle, ThemeProvider } from 'styled-components' +import { ErrorBoundary } from './components/ErrorBoundary' +import { StyledErrorBoundary } from './components/StyledErrorBoundary' +import { I18nContainer } from './i18n/components/I18nContainer' +import { useApolloClient } from './utils/apolloClient' +import { ApolloProvider } from './utils/ApolloProvider' import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { AppStore } from './store' +import { routesConfig as v2RoutesConfig } from './v2-events/routes/config' import { CorrectionForm, CorrectionReviewForm } from './views/CorrectionForm' import { VerifyCorrector } from './views/CorrectionForm/VerifyCorrector' import { ReloadModal } from './views/Modals/ReloadModal' @@ -70,7 +69,6 @@ import { SystemList } from './views/SysAdmin/Config/Systems/Systems' import { UserList } from './views/SysAdmin/Team/user/UserList' import VSExport from './views/SysAdmin/Vsexports/VSExport' import { UserAudit } from './views/UserAudit/UserAudit' -import { routesConfig as v2RoutesConfig } from './v2-events/routes/config' // Injecting global styles for the body tag - used only once // eslint-disable-line @@ -149,7 +147,7 @@ export const routesConfig = [ { path: routes.ALL_USER_EMAIL, element: ( - + ) @@ -158,10 +156,11 @@ export const routesConfig = [ path: routes.ADVANCED_SEARCH, element: ( @@ -172,10 +171,13 @@ export const routesConfig = [ path: routes.ADVANCED_SEARCH_RESULT, element: ( @@ -200,13 +202,10 @@ export const routesConfig = [ path: routes.TEAM_USER_LIST, element: ( @@ -216,7 +215,7 @@ export const routesConfig = [ { path: routes.SYSTEM_LIST, element: ( - + ) @@ -224,12 +223,7 @@ export const routesConfig = [ { path: routes.VS_EXPORTS, element: ( - + ) @@ -243,14 +237,7 @@ export const routesConfig = [ { path: routes.PERFORMANCE_STATISTICS, element: ( - + ) @@ -258,14 +245,7 @@ export const routesConfig = [ { path: routes.PERFORMANCE_LEADER_BOARDS, element: ( - + ) @@ -273,14 +253,7 @@ export const routesConfig = [ { path: routes.PERFORMANCE_DASHBOARD, element: ( - + ) @@ -289,13 +262,10 @@ export const routesConfig = [ path: routes.ORGANISATIONS_INDEX, element: ( @@ -313,16 +283,7 @@ export const routesConfig = [ { path: routes.PERFORMANCE_HOME, element: ( - + ) diff --git a/packages/client/src/ListSyncController.ts b/packages/client/src/ListSyncController.ts index c59d84d24f0..fbe1c35e9b7 100644 --- a/packages/client/src/ListSyncController.ts +++ b/packages/client/src/ListSyncController.ts @@ -9,65 +9,45 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import { client } from '@client/utils/apolloClient' -import { - REGISTRATION_HOME_QUERY, - FIELD_AGENT_HOME_QUERY -} from '@client/views/OfficeHome/queries' +import { REGISTRATION_HOME_QUERY } from '@client/views/OfficeHome/queries' export async function syncRegistrarWorkqueue( + userId: string, locationId: string, reviewStatuses: string[], pageSize: number, - isFieldAgent: boolean, inProgressSkip: number, healthSystemSkip: number, reviewSkip: number, rejectSkip: number, + sentForReviewSkip: number, approvalSkip: number, externalValidationSkip: number, printSkip: number, - issueSkip: number, - userId?: string + issueSkip: number ) { - if (isFieldAgent && userId) { - try { - const queryResult = await client.query({ - query: FIELD_AGENT_HOME_QUERY, - variables: { - userId: userId, - declarationLocationId: locationId, - pageSize, - reviewSkip: reviewSkip, - rejectSkip: rejectSkip - }, - fetchPolicy: 'no-cache' - }) - return queryResult.data - } catch (exception) { - return undefined - } - } else { - try { - const queryResult = await client.query({ - query: REGISTRATION_HOME_QUERY, - variables: { - declarationLocationId: locationId, - pageSize, - reviewStatuses: reviewStatuses, - inProgressSkip: inProgressSkip, - healthSystemSkip: healthSystemSkip, - reviewSkip: reviewSkip, - rejectSkip: rejectSkip, - approvalSkip: approvalSkip, - externalValidationSkip: externalValidationSkip, - printSkip: printSkip, - issueSkip: issueSkip - }, - fetchPolicy: 'no-cache' - }) - return queryResult.data - } catch (exception) { - return undefined - } + try { + const queryResult = await client.query({ + query: REGISTRATION_HOME_QUERY, + variables: { + userId, + declarationLocationId: locationId, + pageSize, + reviewStatuses: reviewStatuses, + inProgressSkip: inProgressSkip, + healthSystemSkip: healthSystemSkip, + reviewSkip: reviewSkip, + rejectSkip: rejectSkip, + sentForReviewSkip, + approvalSkip: approvalSkip, + externalValidationSkip: externalValidationSkip, + printSkip: printSkip, + issueSkip: issueSkip + }, + fetchPolicy: 'no-cache' + }) + return queryResult.data + } catch (exception) { + return undefined } } diff --git a/packages/client/src/SubmissionController.test.ts b/packages/client/src/SubmissionController.test.ts index 860adab9f24..721bae4cd43 100644 --- a/packages/client/src/SubmissionController.test.ts +++ b/packages/client/src/SubmissionController.test.ts @@ -132,7 +132,17 @@ describe('Submission Controller', () => { scope: ['declare'] } }, - offline: { userDetails: { systemRole: 'FIELD_AGENT' } }, + offline: { + userDetails: { + role: { + label: { + defaultMessage: 'Field Agent', + description: 'Name for user role Field Agent', + id: 'userRole.fieldAgent' + } + } + } + }, declarationsState: { declarations: [ { diff --git a/packages/client/src/components/Header/Hamburger.tsx b/packages/client/src/components/Header/Hamburger.tsx index 22f414f1c61..bd128fe3bec 100644 --- a/packages/client/src/components/Header/Hamburger.tsx +++ b/packages/client/src/components/Header/Hamburger.tsx @@ -14,17 +14,17 @@ import { getUserDetails } from '@client/profile/profileSelectors' import { getLanguage } from '@client/i18n/selectors' import { getIndividualNameObj } from '@client/utils/userUtils' import { Avatar } from '@client/components/Avatar' -import { ExpandingMenu } from '@opencrvs/components/lib/ExpandingMenu' import { FixedNavigation } from '@client/components/interface/Navigation' import { Button } from '@opencrvs/components/lib/Button' +import { ExpandingMenu } from '@opencrvs/components/lib/ExpandingMenu' import { Icon } from '@opencrvs/components/lib/Icon' -import { Role } from '@client/utils/gateway' -import { getUserRole } from '@client/utils' +import { useIntl } from 'react-intl' export function Hamburger() { const [showMenu, setShowMenu] = useState(false) const userDetails = useSelector(getUserDetails) const language = useSelector(getLanguage) + const intl = useIntl() const toggleMenu = () => { setShowMenu((prevState) => !prevState) } @@ -36,9 +36,8 @@ export function Hamburger() { : '' } - // let's remove this type assertion after #4458 merges in const role = - (userDetails?.role && getUserRole(language, userDetails.role as Role)) ?? '' + (userDetails?.role && intl.formatMessage(userDetails.role.label)) ?? '' const avatar = diff --git a/packages/client/src/components/Header/Header.tsx b/packages/client/src/components/Header/Header.tsx index af9764fdf24..eedd185700c 100644 --- a/packages/client/src/components/Header/Header.tsx +++ b/packages/client/src/components/Header/Header.tsx @@ -12,21 +12,10 @@ import { ProfileMenu } from '@client/components/ProfileMenu' import { constantsMessages } from '@client/i18n/messages' import { messages } from '@client/i18n/messages/views/header' import { Icon } from '@opencrvs/components/lib/Icon' -import { formatUrl } from '@client/navigation' -import { getUserDetails } from '@client/profile/profileSelectors' import { IStoreState } from '@client/store' import styled from 'styled-components' import { Hamburger } from './Hamburger' -import { - FIELD_AGENT_ROLES, - NATL_ADMIN_ROLES, - ADVANCED_SEARCH_TEXT, - SYS_ADMIN_ROLES, - PERFORMANCE_MANAGEMENT_ROLES -} from '@client/utils/constants' -import { UserDetails } from '@client/utils/userUtils' import { Button } from '@opencrvs/components/lib/Button' - import { AppHeader, IDomProps } from '@opencrvs/components/lib/AppHeader' import { SearchTool, @@ -36,7 +25,6 @@ import { import * as React from 'react' import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl' import { connect } from 'react-redux' - import { TEAM_USER_LIST } from '@client/navigation/routes' import { setAdvancedSearchParam } from '@client/search/advancedSearch/actions' import { advancedSearchInitialState } from '@client/search/advancedSearch/reducer' @@ -45,15 +33,21 @@ import { getRegisterForm } from '@client/forms/register/declaration-selectors' import { getOfflineData } from '@client/offline/selectors' import { IOfflineData } from '@client/offline/reducer' import { SearchCriteria } from '@client/utils/referenceApi' +import { ADVANCED_SEARCH_TEXT } from '@client/utils/constants' +import { + RECORD_DECLARE_SCOPES, + usePermissions +} from '@client/hooks/useAuthorization' +import ProtectedComponent from '@client/components/ProtectedComponent' import { RouteComponentProps, withRouter } from '@client/components/WithRouterProps' -import * as routes from '@client/navigation/routes' import { parse, stringify } from 'query-string' +import { formatUrl } from '@client/navigation' +import * as routes from '@client/navigation/routes' type IStateProps = { - userDetails: UserDetails | null fieldNames: string[] language: string offlineData: IOfflineData @@ -63,7 +57,7 @@ type IDispatchProps = { setAdvancedSearchParam: typeof setAdvancedSearchParam } -type IProps = { +interface IProps { activeMenuItem: ACTIVE_MENU_ITEM title?: string searchText?: string @@ -118,30 +112,31 @@ const HeaderRight = styled.div` background: ${({ theme }) => theme.colors.white}; ` -const USERS_WITHOUT_SEARCH = SYS_ADMIN_ROLES.concat( - NATL_ADMIN_ROLES, - PERFORMANCE_MANAGEMENT_ROLES -) - const HeaderComponent = (props: IFullProps) => { const { router, - userDetails, mobileSearchBar, offlineData, className, intl, activeMenuItem, - title, mobileRight, setAdvancedSearchParam, mapPerformanceClickHandler, changeTeamLocation } = props + const { + canCreateUser, + canSearchRecords, + canSearchBirthRecords, + canSearchDeathRecords + } = usePermissions() + + const canDoAdvanceSearch = canSearchBirthRecords || canSearchDeathRecords + const getMobileHeaderActionProps = (activeMenuItem: ACTIVE_MENU_ITEM) => { const locationId = parse(router.location.search).locationId as string - if (activeMenuItem === ACTIVE_MENU_ITEM.PERFORMANCE) { return { mobileLeft: [ @@ -153,80 +148,74 @@ const HeaderComponent = (props: IFullProps) => { mobileRight: [ { icon: () => , - handler: () => - mapPerformanceClickHandler && mapPerformanceClickHandler() + handler: () => mapPerformanceClickHandler?.() } ] } - } - if (activeMenuItem === ACTIVE_MENU_ITEM.USERS && changeTeamLocation) { - return { - mobileLeft: [ - { - icon: () => , - handler: () => {} - } - ], - mobileRight: [ - { - icon: () => ( - - ), - handler: () => changeTeamLocation && changeTeamLocation() - }, - { - icon: () => , - handler: () => { - if (locationId) { - router.navigate( - formatUrl(routes.CREATE_USER_ON_LOCATION, { locationId }) - ) + } else if (activeMenuItem === ACTIVE_MENU_ITEM.USERS) { + if (changeTeamLocation) { + return { + mobileLeft: [ + { + icon: () => , + handler: () => {} + } + ], + mobileRight: [ + { + icon: () => ( + + ), + handler: changeTeamLocation + }, + { + icon: () => ( + + ), + handler: () => { + if (locationId) { + router.navigate( + formatUrl(routes.CREATE_USER_ON_LOCATION, { locationId }) + ) + } } } - } - ] - } - } - if ( - activeMenuItem === ACTIVE_MENU_ITEM.USERS && - userDetails?.systemRole && - SYS_ADMIN_ROLES.includes(userDetails?.systemRole) - ) { - return { - mobileLeft: [ - { - icon: () => , - handler: () => {} - } - ], - mobileRight: [ - { - icon: () => , - handler: () => { - if (locationId) { - router.navigate( - formatUrl(routes.CREATE_USER_ON_LOCATION, { locationId }) - ) + ] + } + } else if (canCreateUser) { + return { + mobileLeft: [ + { + icon: () => , + handler: () => {} + } + ], + mobileRight: [ + { + icon: () => ( + + ), + handler: () => { + if (locationId) { + router.navigate( + formatUrl(routes.CREATE_USER_ON_LOCATION, { locationId }) + ) + } } } - } - ] - } - } - if (activeMenuItem === ACTIVE_MENU_ITEM.USERS) { - return { - mobileLeft: [ - { - icon: () => , - handler: () => {} - } - ] + ] + } + } else { + return { + mobileLeft: [ + { + icon: () => , + handler: () => {} + } + ] + } } - } - if ( - userDetails?.systemRole && - USERS_WITHOUT_SEARCH.includes(userDetails?.systemRole) - ) { + } else if (!canSearchRecords) { return { mobileLeft: [ { @@ -235,37 +224,39 @@ const HeaderComponent = (props: IFullProps) => { } ] } - } - if (mobileSearchBar) { - return { - mobileLeft: [ - { - icon: () => , - handler: () => {} - } - ], - mobileBody: renderSearchInput(props, true) - } - } - return { - mobileLeft: [ - { - icon: () => , - handler: () => {} + } else { + if (mobileSearchBar) { + return { + mobileLeft: [ + { + icon: () => , + handler: () => {} + } + ], + mobileBody: renderSearchInput(props, true) } - ], - mobileRight: [ - { - icon: () => ( - - ), - handler: () => router.navigate(routes.SEARCH) + } else { + return { + mobileLeft: [ + { + icon: () => , + handler: () => {} + } + ], + mobileRight: [ + { + icon: () => ( + + ), + handler: () => router.navigate(routes.SEARCH) + } + ] } - ] + } } } - const renderSearchInput = (props: IFullProps, isMobile?: boolean) => { + function renderSearchInput(props: IFullProps, isMobile?: boolean) { const { intl, searchText, selectedSearchType, language, fieldNames } = props const searchTypeList: ISearchType[] = [ @@ -319,7 +310,7 @@ const HeaderComponent = (props: IFullProps) => { }) } - const navigationList: INavigationType[] = [ + const advancedSearchNavigationList: INavigationType[] = [ { label: intl.formatMessage(messages.advancedSearch), id: ADVANCED_SEARCH_TEXT, @@ -339,13 +330,10 @@ const HeaderComponent = (props: IFullProps) => { selectedSearchType ?? offlineData.config.SEARCH_DEFAULT_CRITERIA } searchTypeList={searchTypeList} - navigationList={ - FIELD_AGENT_ROLES.includes(userDetails?.systemRole as string) - ? undefined - : navigationList - } + // @TODO: How to hide the navigation list from field agents? Ask JPF + navigationList={canDoAdvanceSearch ? advancedSearchNavigationList : []} searchHandler={(text, type) => - router.navigate( + props.router.navigate( { pathname: routes.SEARCH_RESULT, search: stringify({ @@ -362,8 +350,8 @@ const HeaderComponent = (props: IFullProps) => { ) } - const headerTitle = - title || + const title = + props.title || intl.formatMessage( activeMenuItem === ACTIVE_MENU_ITEM.PERFORMANCE ? constantsMessages.performanceTitle @@ -389,26 +377,20 @@ const HeaderComponent = (props: IFullProps) => { }, { element: ( - <> - {!( - userDetails?.systemRole && - USERS_WITHOUT_SEARCH.includes(userDetails?.systemRole) - ) && ( - - - - {renderSearchInput(props)} - - )} - + + + + + {canSearchRecords && renderSearchInput(props)} + ) }, { @@ -420,11 +402,7 @@ const HeaderComponent = (props: IFullProps) => { } ] - if ( - activeMenuItem !== ACTIVE_MENU_ITEM.DECLARATIONS && - (NATL_ADMIN_ROLES.includes(userDetails?.systemRole as string) || - SYS_ADMIN_ROLES.includes(userDetails?.systemRole as string)) - ) { + if (activeMenuItem !== ACTIVE_MENU_ITEM.DECLARATIONS && !canSearchRecords) { rightMenu = [ { element: @@ -447,7 +425,7 @@ const HeaderComponent = (props: IFullProps) => { id="register_app_header" desktopRightMenu={rightMenu} className={className} - title={headerTitle} + title={title} {...mobileHeaderActionPropsWithDefaults} /> ) @@ -474,7 +452,6 @@ export const Header = withRouter( ? ACTIVE_MENU_ITEM.VSEXPORTS : ACTIVE_MENU_ITEM.DECLARATIONS, language: store.i18n.language, - userDetails: getUserDetails(store), offlineData: getOfflineData(store), fieldNames: Object.values(getRegisterForm(store)) .flatMap((form) => form.sections) diff --git a/packages/client/src/components/Header/HistoryNavigator.tsx b/packages/client/src/components/Header/HistoryNavigator.tsx index 479de1463d2..16dd43823ee 100644 --- a/packages/client/src/components/Header/HistoryNavigator.tsx +++ b/packages/client/src/components/Header/HistoryNavigator.tsx @@ -10,50 +10,21 @@ */ import React from 'react' import { Button } from '@opencrvs/components/lib/Button' -import { useLocation, useNavigate, useNavigationType } from 'react-router-dom' -import { useSelector } from 'react-redux' -import { getUserDetails } from '@client/profile/profileSelectors' -import { - FIELD_AGENT_ROLES, - NATL_ADMIN_ROLES, - REGISTRAR_ROLES, - SYS_ADMIN_ROLES -} from '@client/utils/constants' -import { - HOME, - PERFORMANCE_HOME, - REGISTRAR_HOME -} from '@client/navigation/routes' +import { useNavigate, useNavigationType } from 'react-router-dom' + import { Icon } from '@opencrvs/components/lib/Icon' +import { useHomePage } from '@client/hooks/useHomePage' export function HistoryNavigator({ hideForward = false }: { hideForward?: boolean }) { - const userDetails = useSelector(getUserDetails) - const role = userDetails && userDetails.systemRole - const location = useLocation() - const pathname = location.pathname + const navigationType = useNavigationType() const navigate = useNavigate() - const navigationType = useNavigationType() + const { isCurrentPageHome } = useHomePage() - const isLandingPage = () => { - if ( - (FIELD_AGENT_ROLES.includes(role as string) && HOME.includes(pathname)) || - (NATL_ADMIN_ROLES.includes(role as string) && - PERFORMANCE_HOME.includes(pathname)) || - (SYS_ADMIN_ROLES.includes(role as string) && - PERFORMANCE_HOME.includes(pathname)) || - (REGISTRAR_ROLES.includes(role as string) && - REGISTRAR_HOME.includes(pathname)) - ) { - return true - } else { - return false - } - } return (
+ + + ) : declarationToBeValidated ? ( - + + + + ) : completeDeclaration ? ( + <> + + + + ) : ( - + <> + + + + )} - {rejectDeclarationAction && !alreadyRejectedDeclaration && ( - + + + )} diff --git a/packages/client/src/components/interface/DownloadButton.test.tsx b/packages/client/src/components/interface/DownloadButton.test.tsx index deeea7b7e2a..ade1739254f 100644 --- a/packages/client/src/components/interface/DownloadButton.test.tsx +++ b/packages/client/src/components/interface/DownloadButton.test.tsx @@ -8,84 +8,107 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { createTestComponent, createTestStore } from '@client/tests/util' +import { + createTestComponent, + createTestStore, + setScopes +} from '@client/tests/util' import { DownloadButton } from './DownloadButton' import { AppStore } from '@client/store' import * as React from 'react' import { DownloadAction } from '@client/forms' -import { ReactWrapper } from 'enzyme' import * as declarationReducer from '@client/declarations' import { ApolloClient } from '@apollo/client' import { createClient } from '@client/utils/apolloClient' +import { SCOPES } from '@opencrvs/commons/client' const { DOWNLOAD_STATUS } = declarationReducer -describe('download button tests', () => { +describe('download button', () => { let store: AppStore - - let testComponent: ReactWrapper<{}, {}> let client: ApolloClient<{}> - describe('for download status downloaded', () => { - describe('when assignment object is undefined in props', () => { - beforeEach(async () => { - const testStore = await createTestStore() - store = testStore.store + describe('when there is no assignment', () => { + beforeEach(async () => { + const testStore = await createTestStore() + store = testStore.store - client = createClient(store) - const { component } = await createTestComponent( - , - { store, apolloClient: client } - ) + client = createClient(store) + }) - testComponent = component - }) + it('if the record is actionable, download button should not be disabled', async () => { + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + , + { store, apolloClient: client } + ) - it('download button renders', () => { - expect(testComponent).toBeDefined() - }) + expect( + component.find('#download-icon').hostNodes().prop('disabled') + ).toBeFalsy() }) - describe('when assignment object is defined in props', () => { - beforeEach(async () => { - const testStore = await createTestStore() - store = testStore.store - client = createClient(store) - const { component } = await createTestComponent( - , - { store, apolloClient: client } - ) + it('if the record is not actionable, download button should be disabled', async () => { + setScopes([SCOPES.RECORD_SUBMIT_FOR_REVIEW], store) + const { component } = await createTestComponent( + , + { store, apolloClient: client } + ) + expect( + component.find('#download-icon').hostNodes().prop('disabled') + ).toBeTruthy() + }) + }) - testComponent = component - }) + describe('when there is assignment', () => { + beforeEach(async () => { + const testStore = await createTestStore() + store = testStore.store + client = createClient(store) + }) - it('download button renders', () => { - expect(testComponent).toBeDefined() - }) + it('if assigned to current user & not downloaded then should not show avatar', async () => { + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + , + { store, apolloClient: client } + ) + expect(component.find('img').length).toBe(0) }) }) }) diff --git a/packages/client/src/components/interface/DownloadButton.tsx b/packages/client/src/components/interface/DownloadButton.tsx index 6a6fdac853c..eb6efd99d98 100644 --- a/packages/client/src/components/interface/DownloadButton.tsx +++ b/packages/client/src/components/interface/DownloadButton.tsx @@ -8,51 +8,40 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { - ApolloClient, - InternalRefetchQueriesInclude, - useApolloClient -} from '@apollo/client' +import { useDispatch, useSelector } from 'react-redux' +import { InternalRefetchQueriesInclude, useApolloClient } from '@apollo/client' import { AvatarSmall } from '@client/components/Avatar' import { - deleteDeclaration as deleteDeclarationAction, - DOWNLOAD_STATUS, downloadDeclaration, - unassignDeclaration + DOWNLOAD_STATUS, + SUBMISSION_STATUS } from '@client/declarations' import { Action } from '@client/forms' import { buttonMessages, constantsMessages } from '@client/i18n/messages' import { conflictsMessages } from '@client/i18n/messages/views/conflicts' import { IStoreState } from '@client/store' import { useOnlineStatus } from '@client/utils' -import { - FIELD_AGENT_ROLES, - ROLE_REGISTRATION_AGENT -} from '@client/utils/constants' import type { AssignmentData } from '@client/utils/gateway' -import { EventType, SystemRoleType } from '@client/utils/gateway' +import { EventType } from '@client/utils/gateway' import { Button } from '@opencrvs/components/lib/Button' import { ResponsiveModal } from '@opencrvs/components/lib/ResponsiveModal' import { Spinner } from '@opencrvs/components/lib/Spinner' -import { IActionObject } from '@opencrvs/components/lib/Workqueue' import { Download } from '@opencrvs/components/lib/icons' import { ConnectionError } from '@opencrvs/components/lib/icons/ConnectionError' -import { Downloaded } from '@opencrvs/components/lib/icons/Downloaded' -import * as React from 'react' -import { IntlShape, MessageDescriptor, useIntl } from 'react-intl' -import { connect } from 'react-redux' +import React from 'react' +import { useIntl } from 'react-intl' import ReactTooltip from 'react-tooltip' -import { Dispatch } from 'redux' +import { useModal } from '@client/hooks/useModal' +import { usePermissions } from '@client/hooks/useAuthorization' import styled from 'styled-components' +import { useDeclaration } from '@client/declarations/selectors' -const { useState, useCallback, useMemo } = React interface IDownloadConfig { event: string compositionId: string action: Action assignment?: AssignmentData refetchQueries?: InternalRefetchQueriesInclude - declarationStatus?: string } interface DownloadButtonProps { @@ -60,20 +49,9 @@ interface DownloadButtonProps { className?: string downloadConfigs: IDownloadConfig status?: DOWNLOAD_STATUS + declarationStatus: SUBMISSION_STATUS } -interface IConnectProps { - userRole?: SystemRoleType - practitionerId?: string -} -interface IDispatchProps { - downloadDeclaration: typeof downloadDeclaration - unassignDeclaration: typeof unassignDeclaration - deleteDeclaration: typeof deleteDeclarationAction -} - -type HOCProps = IConnectProps & IDispatchProps - const StatusIndicator = styled.div<{ isLoading?: boolean }>` @@ -91,85 +69,6 @@ const DownloadAction = styled(Button)` padding: 0px 0px; } ` -interface IModalAction extends Omit { - type: 'success' | 'danger' | 'tertiary' - id: string - label: MessageDescriptor -} -interface AssignModalOptions { - title: MessageDescriptor - actions: IModalAction[] - content: MessageDescriptor - contentArgs?: Record -} - -function getAssignModalOptions( - assignment: AssignmentData | undefined, - callbacks: { - onAssign: () => void - onUnassign: () => void - onCancel: () => void - }, - userRole?: SystemRoleType, - isDownloadedBySelf?: boolean -): AssignModalOptions { - const assignAction: IModalAction = { - id: 'assign', - label: buttonMessages.assign, - type: 'success', - handler: callbacks.onAssign - } - const unassignAction: IModalAction = { - id: 'unassign', - label: buttonMessages.unassign, - type: 'danger', - handler: callbacks.onUnassign - } - const cancelAction: IModalAction = { - id: 'cancel', - label: buttonMessages.cancel, - type: 'tertiary', - handler: callbacks.onCancel - } - - if (isDownloadedBySelf) { - return { - title: conflictsMessages.unassignTitle, - content: conflictsMessages.selfUnassignDesc, - actions: [cancelAction, unassignAction] - } - } else if (assignment) { - if ( - userRole === SystemRoleType.LocalRegistrar || - userRole === SystemRoleType.NationalRegistrar - ) { - return { - title: conflictsMessages.unassignTitle, - content: conflictsMessages.regUnassignDesc, - contentArgs: { - name: [assignment.firstName, assignment.lastName].join(' '), - officeName: assignment.officeName || '' - }, - actions: [cancelAction, unassignAction] - } - } - return { - title: conflictsMessages.assignedTitle, - content: conflictsMessages.assignedDesc, - contentArgs: { - name: [assignment.firstName, assignment.lastName].join(' '), - officeName: assignment.officeName || '' - }, - actions: [] - } - } else { - return { - title: conflictsMessages.assignTitle, - content: conflictsMessages.assignDesc, - actions: [cancelAction, assignAction] - } - } -} const NoConnectionViewContainer = styled.div` height: 40px; width: 40px; @@ -183,129 +82,112 @@ const NoConnectionViewContainer = styled.div` } ` -function renderModalAction(action: IModalAction, intl: IntlShape): JSX.Element { - let buttonType: 'positive' | 'negative' | 'tertiary' - if (action.type === 'success') { - buttonType = 'positive' as const - } else if (action.type === 'danger') { - buttonType = 'negative' - } else { - buttonType = 'tertiary' - } +const LOADING_STATUSES = [ + DOWNLOAD_STATUS.READY_TO_DOWNLOAD, + DOWNLOAD_STATUS.DOWNLOADING, + DOWNLOAD_STATUS.READY_TO_UNASSIGN, + DOWNLOAD_STATUS.UNASSIGNING +] + +const AssignModal: React.FC<{ + close: (result: boolean) => void +}> = ({ close }) => { + const intl = useIntl() return ( - + close(true)} + > + {intl.formatMessage(buttonMessages.assign)} + , + + ]} + autoHeight + responsive={false} + preventClickOnParent + handleClose={() => close(false)} + > + {intl.formatMessage(conflictsMessages.assignDesc)} + ) } -function DownloadButtonComponent(props: DownloadButtonProps & HOCProps) { +export function DownloadButton({ + id, + status, + className, + declarationStatus, + downloadConfigs: { + assignment: declarationAssignment, + event, + compositionId, + action + } +}: DownloadButtonProps) { const intl = useIntl() const client = useApolloClient() const isOnline = useOnlineStatus() - const LOADING_STATUSES = useMemo(function () { - return [ - DOWNLOAD_STATUS.READY_TO_DOWNLOAD, - DOWNLOAD_STATUS.DOWNLOADING, - DOWNLOAD_STATUS.READY_TO_UNASSIGN, - DOWNLOAD_STATUS.UNASSIGNING - ] - }, []) - const { - id, - status, - className, - downloadConfigs, - downloadDeclaration, - userRole, - practitionerId, - unassignDeclaration - } = props - const { assignment, compositionId } = downloadConfigs - const [assignModal, setAssignModal] = useState( - null + const dispatch = useDispatch() + const practitionerId = useSelector( + (state) => { + const { userDetails } = state.profile + return userDetails?.practitionerId + } ) - const download = useCallback(() => { - const { event, compositionId, action } = downloadConfigs - downloadDeclaration( - event.toLowerCase() as unknown as EventType, - compositionId, - action, - client - ) - }, [downloadConfigs, client, downloadDeclaration]) - const hideModal = useCallback(() => setAssignModal(null), []) - const unassign = useCallback(async () => { - unassignDeclaration(compositionId, client) - }, [compositionId, client, unassignDeclaration]) + const [modal, openModal] = useModal() + const { isRecordActionable } = usePermissions() - const isFailed = useMemo( - () => - status === DOWNLOAD_STATUS.FAILED || - status === DOWNLOAD_STATUS.FAILED_NETWORK, - [status] - ) + const declaration = useDeclaration(compositionId) + const assignment = declarationAssignment ?? declaration?.assignmentStatus - // reg agent can only retrieve validated and correction requested declarations - const isRetrieveableDeclarationsOfRegAgent = - downloadConfigs.declarationStatus && - ['VALIDATED', 'CORRECTION_REQUESTED'].includes( - downloadConfigs.declarationStatus - ) && - userRole === ROLE_REGISTRATION_AGENT + const assignedToSomeoneElse = + assignment && assignment.practitionerId !== practitionerId + const assignedToMe = assignment?.practitionerId === practitionerId - // field agents can only retrieve declarations - const isNotFieldAgent = !FIELD_AGENT_ROLES.includes(String(userRole)) + const isFailed = + status === DOWNLOAD_STATUS.FAILED || + status === DOWNLOAD_STATUS.FAILED_NETWORK - const isDownloadable = - status !== DOWNLOAD_STATUS.DOWNLOADED && - (!assignment || assignment.practitionerId === practitionerId) + const download = () => + dispatch( + downloadDeclaration( + event.toLowerCase() as unknown as EventType, + compositionId, + action, + client + ) + ) - const onDownloadClick = useCallback( - (e: React.MouseEvent) => { - if ( - (assignment?.practitionerId !== practitionerId || - status === DOWNLOAD_STATUS.DOWNLOADED) && - !isRetrieveableDeclarationsOfRegAgent && - isNotFieldAgent - ) { - setAssignModal( - getAssignModalOptions( - assignment, - { - onAssign: () => { - download() - hideModal() - }, - onUnassign: () => { - unassign() - hideModal() - }, - onCancel: hideModal - }, - userRole, - status === DOWNLOAD_STATUS.DOWNLOADED - ) - ) - } else if (status !== DOWNLOAD_STATUS.DOWNLOADED) { - // retrieve declaration + const handleDownload = async ( + e: React.MouseEvent + ) => { + if (!assignment) { + const assign = await openModal((close) => ( + + )) + if (assign) { download() } - e.stopPropagation() - }, - [ - assignment, - practitionerId, - status, - isRetrieveableDeclarationsOfRegAgent, - isNotFieldAgent, - hideModal, - userRole, - download, - unassign - ] - ) + } else if (assignedToMe && status !== DOWNLOAD_STATUS.DOWNLOADED) { + download() + } + e.stopPropagation() + } if (status && LOADING_STATUSES.includes(status)) { return ( @@ -341,13 +223,14 @@ function DownloadButtonComponent(props: DownloadButtonProps & HOCProps) { - {status === DOWNLOAD_STATUS.DOWNLOADED ? ( - - ) : assignment && assignment.practitionerId !== practitionerId ? ( + {assignment && + (assignedToSomeoneElse || + (assignedToMe && status === DOWNLOAD_STATUS.DOWNLOADED)) ? ( )} - {assignModal !== null && ( - - renderModalAction(action, intl) - )} - autoHeight - responsive={false} - preventClickOnParent - handleClose={hideModal} - > - {intl.formatMessage(assignModal.content, assignModal.contentArgs)} - - )} + {modal} ) } - -const mapStateToProps = (state: IStoreState): IConnectProps => ({ - userRole: state.profile.userDetails?.systemRole, - practitionerId: state.profile.userDetails?.practitionerId -}) - -const mapDispatchToProps = ( - dispatch: Dispatch, - ownProps: DownloadButtonProps -): IDispatchProps => ({ - downloadDeclaration: ( - event: EventType, - compositionId: string, - action: Action, - client: ApolloClient - ) => dispatch(downloadDeclaration(event, compositionId, action, client)), - deleteDeclaration: (id: string, client: ApolloClient) => - dispatch(deleteDeclarationAction(id, client)), - unassignDeclaration: (id: string, client: ApolloClient) => - dispatch( - unassignDeclaration(id, client, ownProps.downloadConfigs.refetchQueries) - ) -}) - -export const DownloadButton = connect( - mapStateToProps, - mapDispatchToProps -)(DownloadButtonComponent) diff --git a/packages/client/src/components/interface/Navigation.test.tsx b/packages/client/src/components/interface/Navigation.test.tsx index f348c72690e..969d8c10c06 100644 --- a/packages/client/src/components/interface/Navigation.test.tsx +++ b/packages/client/src/components/interface/Navigation.test.tsx @@ -8,27 +8,31 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { checkAuth } from '@client/profile/profileActions' + +import { Navigation } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { queries } from '@client/profile/queries' import { storage } from '@client/storage' import { createStore } from '@client/store' import { createTestComponent, - mockUserResponse, flushPromises, - natlSysAdminToken, - registerScopeToken + mockUserResponse, + REGISTRATION_AGENT_DEFAULT_SCOPES, + setScopes, + SYSTEM_ADMIN_DEFAULT_SCOPES } from '@client/tests/util' import { createClient } from '@client/utils/apolloClient' import { OfficeHome } from '@client/views/OfficeHome/OfficeHome' +import { ReactWrapper } from 'enzyme' import { merge } from 'lodash' import * as React from 'react' -import { Navigation } from '@client/components/interface/Navigation' -import { ReactWrapper } from 'enzyme' -import { Mock, vi } from 'vitest' +import { scopes as allScopes, Scope, SCOPES } from '@opencrvs/commons/client' +import { vi } from 'vitest' import { createMemoryRouter } from 'react-router-dom' +import { formatUrl } from '@client/navigation' +import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' -const getItem = window.localStorage.getItem as Mock const mockFetchUserDetails = vi.fn() const nameObj = { @@ -42,8 +46,7 @@ const nameObj = { __typename: 'HumanName' }, { use: 'bn', firstNames: '', familyName: '', __typename: 'HumanName' } - ], - systemRole: 'REGISTRATION_AGENT' + ] } } } @@ -59,8 +62,7 @@ const nameObjNatlSysAdmin = { __typename: 'HumanName' }, { use: 'bn', firstNames: '', familyName: '', __typename: 'HumanName' } - ], - systemRole: 'NATIONAL_SYSTEM_ADMIN' + ] } } } @@ -80,8 +82,8 @@ describe('Navigation for national system admin related tests', () => { queries.fetchUserDetails = mockFetchUserDetails ;({ store } = createStore()) client = createClient(store) - getItem.mockReturnValue(natlSysAdminToken) - await store.dispatch(checkAuth()) + + setScopes(SYSTEM_ADMIN_DEFAULT_SCOPES, store) await flushPromises() const { component } = await createTestComponent(, { store }) @@ -115,8 +117,9 @@ describe('Navigation for Registration agent related tests', () => { queries.fetchUserDetails = mockFetchUserDetails ;({ store } = createStore()) client = createClient(store) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + + setScopes(REGISTRATION_AGENT_DEFAULT_SCOPES, store) + await flushPromises() const { component, router: testRouter } = await createTestComponent( @@ -142,7 +145,7 @@ describe('Navigation for Registration agent related tests', () => { expect(testComponent.exists('#navigation_readyForReview')).toBeTruthy() expect(testComponent.exists('#navigation_requiresUpdate')).toBeTruthy() expect(testComponent.exists('#navigation_print')).toBeTruthy() - expect(testComponent.exists('#navigation_waitingValidation')).toBeTruthy() + expect(testComponent.exists('#navigation_waitingValidation')).toBeFalsy() expect(testComponent.exists('#navigation_approvals')).toBeTruthy() }) @@ -177,9 +180,6 @@ describe('Navigation for District Registrar related tests', () => { queries.fetchUserDetails = mockFetchUserDetails ;({ store } = createStore()) client = createClient(store) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) - await flushPromises() const { component } = await createTestComponent( {}} />, @@ -193,3 +193,571 @@ describe('Navigation for District Registrar related tests', () => { expect(testComponent.exists('#navigation_logout')).toBeTruthy() }) }) + +describe('Given a user with scopes views Navigation', () => { + let testComponent: ReactWrapper<{}, {}> + let build: () => Promise> + + beforeEach(async () => { + ;({ store } = createStore()) + client = createClient(store) + + build = async () => + ( + await createTestComponent(, { + store, + initialEntries: [ + formatUrl(REGISTRAR_HOME_TAB, { + tabId: WORKQUEUE_TABS.inProgress + }) + ], + path: REGISTRAR_HOME_TAB + }) + )?.component + }) + describe('My drafts', async () => { + const id = `#navigation_${WORKQUEUE_TABS.myDrafts}` + + const requiredScopes = [ + SCOPES.RECORD_DECLARE_BIRTH, + SCOPES.RECORD_DECLARE_BIRTH_MY_JURISDICTION, + SCOPES.RECORD_DECLARE_DEATH, + SCOPES.RECORD_DECLARE_DEATH_MY_JURISDICTION, + SCOPES.RECORD_DECLARE_MARRIAGE, + SCOPES.RECORD_DECLARE_MARRIAGE_MY_JURISDICTION + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [[requiredScopes[0]], true], + [[requiredScopes[1]], true], + [[requiredScopes[2]], true], + [[requiredScopes[3]], true], + [[requiredScopes[4]], true], + [[requiredScopes[5]], true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('In progress', async () => { + const id = `#navigation_${WORKQUEUE_TABS.inProgress}` + + const requiredScopes = [ + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES, + SCOPES.RECORD_REGISTER + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [[requiredScopes[0]], true], + [[requiredScopes[1]], true], + [[requiredScopes[2]], true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Sent for review', async () => { + const id = `#navigation_${WORKQUEUE_TABS.sentForReview}` + + const requiredScopes = [SCOPES.RECORD_SUBMIT_FOR_REVIEW] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Sent for approval', async () => { + const id = `#navigation_${WORKQUEUE_TABS.sentForApproval}` + + const requiredScopes = [ + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Requires update', async () => { + const id = `#navigation_${WORKQUEUE_TABS.requiresUpdate}` + + const requiredScopes = [SCOPES.RECORD_SUBMIT_FOR_UPDATES] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Ready for review', async () => { + const id = `#navigation_${WORKQUEUE_TABS.readyForReview}` + + const requiredScopes = [ + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES, + SCOPES.RECORD_REGISTER + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Ready to print', async () => { + const id = `#navigation_${WORKQUEUE_TABS.readyToPrint}` + + const requiredScopes = [ + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('External validation', async () => { + const id = `#navigation_${WORKQUEUE_TABS.externalValidation}` + + const requiredScopes = [SCOPES.RECORD_REGISTER] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes} EXTERNAL_VALIDATION_WORKQUEUE is true in config`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(window.config.FEATURES.EXTERNAL_VALIDATION_WORKQUEUE).toBe(true) + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Ready to issue', async () => { + const id = `#navigation_${WORKQUEUE_TABS.readyToIssue}` + + const requiredScopes = [ + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes} and PRINT_IN_ADVANCE is true in config`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect( + store.getState().offline.offlineData.config?.BIRTH.PRINT_IN_ADVANCE + ).toBeTruthy() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Outbox', async () => { + const id = `#navigation_${WORKQUEUE_TABS.outbox}` + + const requiredScopes = [ + SCOPES.RECORD_SUBMIT_INCOMPLETE, + SCOPES.RECORD_SUBMIT_FOR_REVIEW, + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES, + SCOPES.RECORD_REVIEW_DUPLICATES, + SCOPES.RECORD_REGISTER, + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES, + SCOPES.RECORD_REGISTRATION_CORRECT, + SCOPES.RECORD_DECLARATION_ARCHIVE, + SCOPES.RECORD_DECLARATION_REINSTATE + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Organisation', async () => { + const id = `#navigation_${WORKQUEUE_TABS.organisation}` + + const requiredScopes = [ + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.ORGANISATION_READ_LOCATIONS_MY_OFFICE, + SCOPES.ORGANISATION_READ_LOCATIONS_MY_JURISDICTION + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Team', async () => { + const id = `#navigation_${WORKQUEUE_TABS.team}` + + const requiredScopes = [ + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.ORGANISATION_READ_LOCATIONS_MY_OFFICE, + SCOPES.ORGANISATION_READ_LOCATIONS_MY_JURISDICTION + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Config', async () => { + const id = `#navigation_${WORKQUEUE_TABS.config}_main` + + const requiredScopes = [SCOPES.CONFIG_UPDATE_ALL] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Systems', async () => { + const id = `#navigation_${WORKQUEUE_TABS.systems}` + + const requiredScopes = [SCOPES.CONFIG_UPDATE_ALL] as Scope[] + + const tests = [[requiredScopes, true]] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes} and clicks config expander`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + testComponent + .find(`#navigation_${WORKQUEUE_TABS.config}_main`) + .hostNodes() + .simulate('click') + + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Communications', async () => { + const id = `#navigation_${WORKQUEUE_TABS.communications}_main` + + const requiredScopes = [SCOPES.CONFIG_UPDATE_ALL] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Email all users', async () => { + const id = `#navigation_${WORKQUEUE_TABS.emailAllUsers}` + + const requiredScopes = [SCOPES.CONFIG_UPDATE_ALL] as Scope[] + + const tests = [[requiredScopes, true]] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes} and clicks communciation expander`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + testComponent + .find(`#navigation_${WORKQUEUE_TABS.communications}_main`) + .hostNodes() + .simulate('click') + + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Dashboard', async () => { + const id = `#navigation_${WORKQUEUE_TABS.dashboard}` + + const requiredScopes = [SCOPES.PERFORMANCE_READ_DASHBOARDS] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Performance', async () => { + const id = `#navigation_${WORKQUEUE_TABS.performance}` + + const requiredScopes = [SCOPES.PERFORMANCE_READ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Statistics', async () => { + const id = `#navigation_${WORKQUEUE_TABS.statistics}` + + const requiredScopes = [SCOPES.PERFORMANCE_READ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Statistics', async () => { + const id = `#navigation_${WORKQUEUE_TABS.statistics}` + + const requiredScopes = [SCOPES.PERFORMANCE_READ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Statistics', async () => { + const id = `#navigation_${WORKQUEUE_TABS.leaderboards}` + + const requiredScopes = [SCOPES.PERFORMANCE_READ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) + + describe('Exports', async () => { + const id = `#navigation_${WORKQUEUE_TABS.vsexports}` + + const requiredScopes = [ + SCOPES.PERFORMANCE_EXPORT_VITAL_STATISTICS + ] as Scope[] + + const allOtherScopes = allScopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + + const tests = [ + [requiredScopes, true], + [allOtherScopes, false] + ] + + tests.forEach(async ([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + testComponent = await build() + expect(testComponent.exists(id)).toBe(exists) + }) + }) + }) +}) diff --git a/packages/client/src/components/interface/Navigation.tsx b/packages/client/src/components/interface/Navigation.tsx index e0aefd52d3d..259da6a5fc6 100644 --- a/packages/client/src/components/interface/Navigation.tsx +++ b/packages/client/src/components/interface/Navigation.tsx @@ -19,8 +19,7 @@ import { navigationMessages } from '@client/i18n/messages/views/navigation' import { formatUrl, generateGoToHomeTabUrl, - generatePerformanceHomeUrl, - getDefaultPerformanceLocationId + generatePerformanceHomeUrl } from '@client/navigation' import { ADVANCED_SEARCH_RESULT } from '@client/navigation/routes' import { IOfflineData } from '@client/offline/reducer' @@ -31,11 +30,11 @@ import { setAdvancedSearchParam } from '@client/search/advancedSearch/actions' import { getAdvancedSearchParamsState } from '@client/search/advancedSearch/advancedSearchSelectors' import { IAdvancedSearchParamState } from '@client/search/advancedSearch/reducer' import { storage } from '@client/storage' -import styled from 'styled-components' import { ALLOWED_STATUS_FOR_RETRY, INPROGRESS_STATUS } from '@client/SubmissionController' +import { IS_PROD_ENVIRONMENT } from '@client/utils/constants' import { isDeclarationInReadyToReviewStatus } from '@client/utils/draftUtils' import { EventType } from '@client/utils/gateway' import { UserDetails } from '@client/utils/userUtils' @@ -54,7 +53,12 @@ import { omit } from 'lodash' import * as React from 'react' import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl' import { connect } from 'react-redux' -import { IS_PROD_ENVIRONMENT } from '@client/utils/constants' +import styled from 'styled-components' +import { useNavigation } from '@client/hooks/useNavigation' +import { + TAB_GROUPS, + WORKQUEUE_TABS +} from '@client/components/interface/WorkQueueTabs' import { RouteComponentProps, withRouter @@ -64,128 +68,6 @@ import { stringify } from 'query-string' const SCREEN_LOCK = 'screenLock' -type Keys = keyof typeof WORKQUEUE_TABS -export type IWORKQUEUE_TABS = (typeof WORKQUEUE_TABS)[Keys] - -export const WORKQUEUE_TABS = { - inProgress: 'progress', - inProgressFieldAgent: 'progress/field-agents', - sentForReview: 'sentForReview', - readyForReview: 'readyForReview', - requiresUpdate: 'requiresUpdate', - sentForApproval: 'approvals', - readyToPrint: 'print', - outbox: 'outbox', - externalValidation: 'waitingValidation', - performance: 'performance', - vsexports: 'vsexports', - team: 'team', - config: 'config', - organisation: 'organisation', - application: 'application', - certificate: 'certificate', - systems: 'integration', - userRoles: 'userroles', - settings: 'settings', - logout: 'logout', - communications: 'communications', - informantNotification: 'informantnotification', - emailAllUsers: 'emailAllUsers', - readyToIssue: 'readyToIssue' -} as const - -const GROUP_ID = { - declarationGroup: 'declarationGroup', - analytics: 'analytics', - menuGroup: 'menuGroup' -} - -interface IUSER_SCOPE { - [key: string]: string[] -} - -const USER_SCOPE: IUSER_SCOPE = { - FIELD_AGENT: [ - WORKQUEUE_TABS.inProgress, - WORKQUEUE_TABS.sentForReview, - WORKQUEUE_TABS.requiresUpdate, - WORKQUEUE_TABS.outbox, - GROUP_ID.declarationGroup - ], - REGISTRATION_AGENT: [ - WORKQUEUE_TABS.inProgress, - WORKQUEUE_TABS.readyForReview, - WORKQUEUE_TABS.requiresUpdate, - WORKQUEUE_TABS.sentForApproval, - WORKQUEUE_TABS.readyToPrint, - WORKQUEUE_TABS.performance, - WORKQUEUE_TABS.organisation, - WORKQUEUE_TABS.team, - WORKQUEUE_TABS.outbox, - WORKQUEUE_TABS.readyToIssue, - GROUP_ID.declarationGroup, - GROUP_ID.menuGroup - ], - DISTRICT_REGISTRAR: [ - WORKQUEUE_TABS.inProgress, - WORKQUEUE_TABS.readyForReview, - WORKQUEUE_TABS.requiresUpdate, - WORKQUEUE_TABS.readyToPrint, - WORKQUEUE_TABS.performance, - WORKQUEUE_TABS.organisation, - WORKQUEUE_TABS.team, - WORKQUEUE_TABS.outbox, - WORKQUEUE_TABS.readyToIssue, - GROUP_ID.declarationGroup, - GROUP_ID.menuGroup - ], - LOCAL_REGISTRAR: [ - WORKQUEUE_TABS.inProgress, - WORKQUEUE_TABS.readyForReview, - WORKQUEUE_TABS.requiresUpdate, - WORKQUEUE_TABS.readyToPrint, - WORKQUEUE_TABS.performance, - WORKQUEUE_TABS.organisation, - WORKQUEUE_TABS.team, - WORKQUEUE_TABS.outbox, - WORKQUEUE_TABS.readyToIssue, - GROUP_ID.declarationGroup, - GROUP_ID.menuGroup - ], - NATIONAL_REGISTRAR: [ - WORKQUEUE_TABS.inProgress, - WORKQUEUE_TABS.readyForReview, - WORKQUEUE_TABS.requiresUpdate, - WORKQUEUE_TABS.readyToPrint, - WORKQUEUE_TABS.organisation, - WORKQUEUE_TABS.vsexports, - WORKQUEUE_TABS.team, - WORKQUEUE_TABS.outbox, - WORKQUEUE_TABS.readyToIssue, - GROUP_ID.declarationGroup, - GROUP_ID.menuGroup, - GROUP_ID.analytics - ], - LOCAL_SYSTEM_ADMIN: [ - WORKQUEUE_TABS.organisation, - WORKQUEUE_TABS.team, - WORKQUEUE_TABS.performance, - GROUP_ID.menuGroup - ], - NATIONAL_SYSTEM_ADMIN: [ - WORKQUEUE_TABS.team, - WORKQUEUE_TABS.config, - WORKQUEUE_TABS.organisation, - WORKQUEUE_TABS.vsexports, - WORKQUEUE_TABS.communications, - WORKQUEUE_TABS.userRoles, - WORKQUEUE_TABS.informantNotification, - GROUP_ID.menuGroup, - GROUP_ID.analytics - ], - PERFORMANCE_MANAGEMENT: [GROUP_ID.menuGroup, GROUP_ID.analytics] -} - interface ICount { inProgress?: number readyForReview?: number @@ -332,20 +214,18 @@ const NavigationView = (props: IFullProps) => { } updateRegistrarWorkqueue( userDetails.practitionerId, - 10, // Page size shouldn't matter here as we're only interested in totals - userDetails.systemRole === 'FIELD_AGENT' + 10 // Page size shouldn't matter here as we're only interested in totals ) }, [userDetails, updateRegistrarWorkqueue, loadWorkqueueStatuses]) const declarationCount = { + myDrafts: draftDeclarations.filter( + (draft) => + draft.submissionStatus === SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] + ).length, inProgress: !initialSyncDone ? 0 - : draftDeclarations.filter( - (draft) => - draft.submissionStatus === - SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] - ).length + - (filteredData.inProgressTab?.totalItems || 0) + + : (filteredData.inProgressTab?.totalItems || 0) + (filteredData.notificationTab?.totalItems || 0), readyForReview: !initialSyncDone ? 0 @@ -356,6 +236,9 @@ const NavigationView = (props: IFullProps) => { sentForApproval: !initialSyncDone ? 0 : filteredData.approvalTab?.totalItems || 0, + sentForReview: !initialSyncDone + ? 0 + : filteredData.sentForReviewTab?.totalItems || 0, externalValidation: window.config.FEATURES.EXTERNAL_VALIDATION_WORKQUEUE && !initialSyncDone ? 0 @@ -373,6 +256,12 @@ const NavigationView = (props: IFullProps) => { ).length } + const { routes: scopedRoutes } = useNavigation() + const hasAccess = (name: string): boolean => + scopedRoutes.some( + (x) => x.name === name || x.tabs.some((t) => t.name === name) + ) + return ( { avatar={() => userInfo && userInfo.avatar} className={className} > - {userDetails?.systemRole === 'FIELD_AGENT' ? ( - <> - + {hasAccess(TAB_GROUPS.declarations) && ( + + {hasAccess(WORKQUEUE_TABS.myDrafts) && ( + } + id={`navigation_${WORKQUEUE_TABS.myDrafts}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.myDrafts] + )} + count={declarationCount.myDrafts} + isSelected={tabId === WORKQUEUE_TABS.myDrafts} + onClick={() => { + props.router.navigate( + generateGoToHomeTabUrl({ + tabId: WORKQUEUE_TABS.myDrafts + }) + ) + + menuCollapse && menuCollapse() + }} + /> + )} + {hasAccess(WORKQUEUE_TABS.inProgress) && ( } id={`navigation_${WORKQUEUE_TABS.inProgress}`} label={intl.formatMessage( navigationMessages[WORKQUEUE_TABS.inProgress] )} - count={props.draftDeclarations.length} + count={declarationCount.inProgress} isSelected={tabId === WORKQUEUE_TABS.inProgress} onClick={() => { props.router.navigate( @@ -404,13 +313,15 @@ const NavigationView = (props: IFullProps) => { menuCollapse && menuCollapse() }} /> + )} + {hasAccess(WORKQUEUE_TABS.sentForReview) && ( } id={`navigation_${WORKQUEUE_TABS.sentForReview}`} label={intl.formatMessage( navigationMessages[WORKQUEUE_TABS.sentForReview] )} - count={declarationCount.readyForReview} + count={declarationCount.sentForReview} isSelected={tabId === WORKQUEUE_TABS.sentForReview} onClick={() => { props.router.navigate( @@ -422,6 +333,28 @@ const NavigationView = (props: IFullProps) => { menuCollapse && menuCollapse() }} /> + )} + {hasAccess(WORKQUEUE_TABS.readyForReview) && ( + } + id={`navigation_${WORKQUEUE_TABS.readyForReview}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.readyForReview] + )} + count={declarationCount.readyForReview} + isSelected={tabId === WORKQUEUE_TABS.readyForReview} + onClick={() => { + props.router.navigate( + generateGoToHomeTabUrl({ + tabId: WORKQUEUE_TABS.readyForReview + }) + ) + + menuCollapse && menuCollapse() + }} + /> + )} + {hasAccess(WORKQUEUE_TABS.requiresUpdate) && ( } id={`navigation_${WORKQUEUE_TABS.requiresUpdate}`} @@ -440,6 +373,88 @@ const NavigationView = (props: IFullProps) => { menuCollapse && menuCollapse() }} /> + )} + {hasAccess(WORKQUEUE_TABS.sentForApproval) && ( + } + id={`navigation_${WORKQUEUE_TABS.sentForApproval}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.sentForApproval] + )} + count={declarationCount.sentForApproval} + isSelected={tabId === WORKQUEUE_TABS.sentForApproval} + onClick={() => { + props.router.navigate( + generateGoToHomeTabUrl({ + tabId: WORKQUEUE_TABS.sentForApproval + }) + ) + + menuCollapse && menuCollapse() + }} + /> + )} + {window.config.FEATURES.EXTERNAL_VALIDATION_WORKQUEUE && + hasAccess(WORKQUEUE_TABS.externalValidation) && ( + } + id={`navigation_${WORKQUEUE_TABS.externalValidation}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.externalValidation] + )} + count={declarationCount.externalValidation} + isSelected={tabId === WORKQUEUE_TABS.externalValidation} + onClick={() => { + props.router.navigate( + generateGoToHomeTabUrl({ + tabId: WORKQUEUE_TABS.externalValidation + }) + ) + menuCollapse && menuCollapse() + }} + /> + )} + {hasAccess(WORKQUEUE_TABS.readyToPrint) && ( + } + id={`navigation_${WORKQUEUE_TABS.readyToPrint}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.readyToPrint] + )} + count={declarationCount.readyToPrint} + isSelected={tabId === WORKQUEUE_TABS.readyToPrint} + onClick={() => { + props.router.navigate( + generateGoToHomeTabUrl({ + tabId: WORKQUEUE_TABS.readyToPrint + }) + ) + + menuCollapse && menuCollapse() + }} + /> + )} + {isOnePrintInAdvanceOn && hasAccess(WORKQUEUE_TABS.readyToIssue) && ( + } + id={`navigation_${WORKQUEUE_TABS.readyToIssue}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.readyToIssue] + )} + count={declarationCount.readyToIssue} + isSelected={tabId === WORKQUEUE_TABS.readyToIssue} + onClick={() => { + props.router.navigate( + generateGoToHomeTabUrl({ + tabId: WORKQUEUE_TABS.readyToIssue + }) + ) + + menuCollapse && menuCollapse() + }} + /> + )} + {hasAccess(WORKQUEUE_TABS.outbox) && ( } id={`navigation_${WORKQUEUE_TABS.outbox}`} @@ -458,478 +473,220 @@ const NavigationView = (props: IFullProps) => { menuCollapse && menuCollapse() }} /> - - - ) : ( - <> - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - GROUP_ID.declarationGroup - ) && ( - - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.inProgress - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.inProgress}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.inProgress] - )} - count={declarationCount.inProgress} - isSelected={tabId === WORKQUEUE_TABS.inProgress} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.inProgress - }) - ) - - menuCollapse && menuCollapse() - }} - /> - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.readyForReview - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.readyForReview}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.readyForReview] - )} - count={declarationCount.readyForReview} - isSelected={tabId === WORKQUEUE_TABS.readyForReview} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.readyForReview - }) - ) - - menuCollapse && menuCollapse() - }} - /> - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.requiresUpdate - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.requiresUpdate}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.requiresUpdate] - )} - count={declarationCount.requiresUpdate} - isSelected={tabId === WORKQUEUE_TABS.requiresUpdate} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.requiresUpdate - }) - ) - - menuCollapse && menuCollapse() - }} - /> + )} + + )} + {hasAccess(TAB_GROUPS.organisations) && ( + + {userDetails && ( + <> + {hasAccess(WORKQUEUE_TABS.organisation) && ( + } + id={`navigation_${WORKQUEUE_TABS.organisation}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.organisation] )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.sentForApproval - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.sentForApproval}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.sentForApproval] - )} - count={declarationCount.sentForApproval} - isSelected={tabId === WORKQUEUE_TABS.sentForApproval} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.sentForApproval - }) - ) + onClick={() => + router.navigate( + formatUrl(routes.ORGANISATIONS_INDEX, { + locationId: '' // NOTE: Empty string is required + }) + ) + } + isSelected={ + enableMenuSelection && + activeMenuItem === WORKQUEUE_TABS.organisation + } + /> + )} - menuCollapse && menuCollapse() - }} - /> + {hasAccess(WORKQUEUE_TABS.team) && ( + } + id={`navigation_${WORKQUEUE_TABS.team}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.team] )} - {window.config.FEATURES.EXTERNAL_VALIDATION_WORKQUEUE && ( - } - id={`navigation_${WORKQUEUE_TABS.externalValidation}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.externalValidation] - )} - count={declarationCount.externalValidation} - isSelected={tabId === WORKQUEUE_TABS.externalValidation} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.externalValidation + onClick={() => { + if (userDetails && userDetails.primaryOffice) { + props.router.navigate({ + pathname: routes.TEAM_USER_LIST, + search: stringify({ + locationId: userDetails.primaryOffice.id }) - ) + }) + } + }} + isSelected={ + enableMenuSelection && + activeMenuItem === WORKQUEUE_TABS.team + } + /> + )} + + )} - menuCollapse && menuCollapse() - }} - /> + {hasAccess(WORKQUEUE_TABS.config) && ( + <> + } + id={`navigation_${WORKQUEUE_TABS.config}_main`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.config] )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.readyToPrint - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.readyToPrint}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.readyToPrint] - )} - count={declarationCount.readyToPrint} - isSelected={tabId === WORKQUEUE_TABS.readyToPrint} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.readyToPrint - }) - ) - - menuCollapse && menuCollapse() - }} - /> - )} - - {isOnePrintInAdvanceOn && - userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.readyToIssue - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.readyToIssue}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.readyToIssue] - )} - count={declarationCount.readyToIssue} - isSelected={tabId === WORKQUEUE_TABS.readyToIssue} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.readyToIssue - }) - ) - - menuCollapse && menuCollapse() - }} - /> - )} - - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.outbox - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.outbox}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.outbox] - )} - count={declarationCount.outbox} - isSelected={tabId === WORKQUEUE_TABS.outbox} - onClick={() => { - props.router.navigate( - generateGoToHomeTabUrl({ - tabId: WORKQUEUE_TABS.outbox - }) - ) - - menuCollapse && menuCollapse() - }} - /> - )} - - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes(GROUP_ID.menuGroup) && ( - - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.performance - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.performance}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.performance] - )} - onClick={() => { - props.router.navigate( - generatePerformanceHomeUrl({ - locationId: - getDefaultPerformanceLocationId(userDetails) - }) - ) - }} - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.performance - } - /> - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.organisation - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.organisation}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.organisation] - )} - onClick={() => - router.navigate( - formatUrl(routes.ORGANISATIONS_INDEX, { - locationId: '' // NOTE: Empty string is required - }) - ) - } - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.organisation - } - /> - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.team - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.team}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.team] - )} - onClick={() => { - if ( - userDetails && - userDetails.systemRole && - userDetails.primaryOffice - ) { - props.router.navigate({ - pathname: routes.TEAM_USER_LIST, - search: stringify({ - locationId: userDetails.primaryOffice.id - }) - }) - } - }} - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.team - } - /> - )} - - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.config - ) && ( - <> - } - id={`navigation_${WORKQUEUE_TABS.config}_main`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.config] - )} - onClick={() => setIsConfigExpanded(!isConfigExpanded)} - isSelected={ - enableMenuSelection && - configTab.includes(activeMenuItem) - } - expandableIcon={() => - isConfigExpanded || - configTab.includes(activeMenuItem) ? ( - - ) : ( - - ) - } - /> - {(isConfigExpanded || - configTab.includes(activeMenuItem)) && ( - <> - router.navigate(routes.SYSTEM_LIST)} - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.systems - } - /> - - )} - + onClick={() => setIsConfigExpanded(!isConfigExpanded)} + isSelected={ + enableMenuSelection && configTab.includes(activeMenuItem) + } + expandableIcon={() => + isConfigExpanded || configTab.includes(activeMenuItem) ? ( + + ) : ( + + ) + } + /> + {(isConfigExpanded || configTab.includes(activeMenuItem)) && ( + router.navigate(routes.SYSTEM_LIST)} + isSelected={ + enableMenuSelection && + activeMenuItem === WORKQUEUE_TABS.systems + } + /> + )} + + )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.communications - ) && ( - <> - } - id={`navigation_${WORKQUEUE_TABS.communications}_main`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.communications] - )} - onClick={() => - setIsCommunationExpanded(!isCommunationExpanded) - } - isSelected={ - enableMenuSelection && - conmmunicationTab.includes(activeMenuItem) - } - expandableIcon={() => - isCommunationExpanded || - conmmunicationTab.includes(activeMenuItem) ? ( - - ) : ( - - ) - } - /> - {(isCommunationExpanded || - conmmunicationTab.includes(activeMenuItem)) && ( - <> - - router.navigate(routes.ALL_USER_EMAIL) - } - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.emailAllUsers - } - /> - - )} - - )} - - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes(GROUP_ID.analytics) && ( - - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - GROUP_ID.analytics - ) && ( - <> - {showRegDashboard && ( - } - label={intl.formatMessage( - navigationMessages['dashboard'] - )} - onClick={() => - router.navigate(routes.PERFORMANCE_DASHBOARD, { - state: { isNavigatedInsideApp: true } - }) - } - id="navigation_dashboard" - isSelected={ - enableMenuSelection && - activeMenuItem === 'dashboard' - } - /> - )} - {showStatistics && ( - } - label={intl.formatMessage( - navigationMessages['statistics'] - )} - onClick={() => - router.navigate(routes.PERFORMANCE_STATISTICS, { - state: { isNavigatedInsideApp: true } - }) - } - id="navigation_statistics" - isSelected={ - enableMenuSelection && - activeMenuItem === 'statistics' - } - /> - )} - {showLeaderboard && ( - } - label={intl.formatMessage( - navigationMessages['leaderboards'] - )} - onClick={() => - router.navigate(routes.PERFORMANCE_LEADER_BOARDS, { - state: { isNavigatedInsideApp: true } - }) - } - id="navigation_leaderboards" - isSelected={ - enableMenuSelection && - activeMenuItem === 'leaderboards' - } - /> - )} - } - label={intl.formatMessage( - navigationMessages['performance'] - )} - onClick={() => { - props.router.navigate( - generatePerformanceHomeUrl({ - locationId: - getDefaultPerformanceLocationId(userDetails) - }) - ) - }} - id="navigation_report" - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.performance - } - /> - - )} - {userDetails?.systemRole && - USER_SCOPE[userDetails.systemRole].includes( - WORKQUEUE_TABS.vsexports - ) && ( - } - id={`navigation_${WORKQUEUE_TABS.vsexports}`} - label={intl.formatMessage( - navigationMessages[WORKQUEUE_TABS.vsexports] - )} - onClick={() => router.navigate(routes.VS_EXPORTS)} - isSelected={ - enableMenuSelection && - activeMenuItem === WORKQUEUE_TABS.vsexports - } - /> + {hasAccess(WORKQUEUE_TABS.config) && ( + <> + } + id={`navigation_${WORKQUEUE_TABS.communications}_main`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.communications] + )} + onClick={() => setIsCommunationExpanded(!isCommunationExpanded)} + isSelected={ + enableMenuSelection && + conmmunicationTab.includes(activeMenuItem) + } + expandableIcon={() => + isCommunationExpanded || + conmmunicationTab.includes(activeMenuItem) ? ( + + ) : ( + + ) + } + /> + {(isCommunationExpanded || + conmmunicationTab.includes(activeMenuItem)) && ( + - )} - + id={`navigation_${WORKQUEUE_TABS.emailAllUsers}`} + onClick={() => router.navigate(routes.ALL_USER_EMAIL)} + isSelected={ + enableMenuSelection && + activeMenuItem === WORKQUEUE_TABS.emailAllUsers + } + /> + )} + + )} + + )} + {hasAccess(TAB_GROUPS.performance) && ( + + { + <> + {showRegDashboard && hasAccess(WORKQUEUE_TABS.dashboard) && ( + } + label={intl.formatMessage(navigationMessages['dashboard'])} + onClick={() => + router.navigate(routes.PERFORMANCE_DASHBOARD, { + state: { isNavigatedInsideApp: true } + }) + } + id={`navigation_${WORKQUEUE_TABS.dashboard}`} + isSelected={ + enableMenuSelection && activeMenuItem === 'dashboard' + } + /> + )} + {showStatistics && hasAccess(WORKQUEUE_TABS.statistics) && ( + } + label={intl.formatMessage(navigationMessages['statistics'])} + onClick={() => + router.navigate(routes.PERFORMANCE_STATISTICS, { + state: { isNavigatedInsideApp: true } + }) + } + id={`navigation_${WORKQUEUE_TABS.statistics}`} + isSelected={ + enableMenuSelection && activeMenuItem === 'statistics' + } + /> + )} + {showLeaderboard && hasAccess(WORKQUEUE_TABS.leaderboards) && ( + } + label={intl.formatMessage(navigationMessages['leaderboards'])} + onClick={() => + router.navigate(routes.PERFORMANCE_LEADER_BOARDS, { + state: { isNavigatedInsideApp: true } + }) + } + id={`navigation_${WORKQUEUE_TABS.leaderboards}`} + isSelected={ + enableMenuSelection && activeMenuItem === 'leaderboards' + } + /> + )} + {userDetails && hasAccess(WORKQUEUE_TABS.performance) && ( + } + label={intl.formatMessage(navigationMessages['performance'])} + onClick={() => { + props.router.navigate( + generatePerformanceHomeUrl({ + locationId: '' + }) + ) + }} + id={`navigation_${WORKQUEUE_TABS.performance}`} + isSelected={ + enableMenuSelection && + activeMenuItem === WORKQUEUE_TABS.performance + } + /> + )} + + } + {hasAccess(WORKQUEUE_TABS.vsexports) && ( + } + id={`navigation_${WORKQUEUE_TABS.vsexports}`} + label={intl.formatMessage( + navigationMessages[WORKQUEUE_TABS.vsexports] + )} + onClick={() => router.navigate(routes.VS_EXPORTS)} + isSelected={ + enableMenuSelection && + activeMenuItem === WORKQUEUE_TABS.vsexports + } + /> + )} + )} diff --git a/packages/client/src/components/interface/WorkQueueTabs.tsx b/packages/client/src/components/interface/WorkQueueTabs.tsx new file mode 100644 index 00000000000..b9ce94f9bad --- /dev/null +++ b/packages/client/src/components/interface/WorkQueueTabs.tsx @@ -0,0 +1,50 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +type Keys = keyof typeof WORKQUEUE_TABS +export type IWORKQUEUE_TABS = (typeof WORKQUEUE_TABS)[Keys] + +export const WORKQUEUE_TABS = { + myDrafts: 'my-drafts', + inProgress: 'progress', + inProgressFieldAgent: 'progress/field-agents', + sentForReview: 'sentForReview', + readyForReview: 'readyForReview', + requiresUpdate: 'requiresUpdate', + sentForApproval: 'approvals', + readyToPrint: 'print', + outbox: 'outbox', + externalValidation: 'waitingValidation', + performance: 'performance', + vsexports: 'vsexports', + team: 'team', + config: 'config', + organisation: 'organisation', + application: 'application', + certificate: 'certificate', + systems: 'integration', + userRoles: 'userroles', + settings: 'settings', + logout: 'logout', + communications: 'communications', + informantNotification: 'informantnotification', + emailAllUsers: 'emailAllUsers', + readyToIssue: 'readyToIssue', + dashboard: 'dashboard', + statistics: 'statistics', + leaderboards: 'leaderboards' +} as const + +export const TAB_GROUPS = { + declarations: 'declarationsGroup', + organisations: 'organisationsGroup', + performance: 'performanceGroup' +} diff --git a/packages/client/src/components/review/RejectRegistrationForm.tsx b/packages/client/src/components/review/RejectRegistrationForm.tsx index c0a86ed6503..3c8f71796ae 100644 --- a/packages/client/src/components/review/RejectRegistrationForm.tsx +++ b/packages/client/src/components/review/RejectRegistrationForm.tsx @@ -41,6 +41,8 @@ import { } from '@client/components/WithRouterProps' import * as routes from '@client/navigation/routes' import styled from 'styled-components' +import ProtectedComponent from '@client/components/ProtectedComponent' +import { SCOPES } from '@opencrvs/commons/client' const Instruction = styled.div` margin-bottom: 28px; @@ -169,24 +171,28 @@ class RejectRegistrationView extends React.Component { > {intl.formatMessage(buttonMessages.cancel)} , - , + + , @@ -390,7 +391,9 @@ class CorrectionSummaryComponent extends React.Component { id="withoutCorrectionForApprovalPrompt" isOpen={showPrompt} title={intl.formatMessage( - this.props.userDetails?.systemRole === ROLE_REGISTRATION_AGENT + this.props.scopes?.includes( + SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION + ) ? messages.correctionForApprovalDialogTitle : messages.correctRecordDialogTitle )} @@ -411,7 +414,7 @@ class CorrectionSummaryComponent extends React.Component { id="send" key="continue" onClick={() => { - this.makeCorrection(this.props.userDetails?.systemRole) + this.makeCorrection() this.togglePrompt() }} > @@ -422,7 +425,9 @@ class CorrectionSummaryComponent extends React.Component {

{intl.formatMessage( - this.props.userDetails?.systemRole === ROLE_REGISTRATION_AGENT + this.props.scopes?.includes( + SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION + ) ? messages.correctionForApprovalDialogDescription : messages.correctRecordDialogDescription )} @@ -1020,9 +1025,11 @@ class CorrectionSummaryComponent extends React.Component { ) } - makeCorrection = (userRole: SystemRoleType | undefined) => { + makeCorrection = () => { const declaration = this.props.declaration - if (userRole === ROLE_REGISTRATION_AGENT) { + if ( + this.props.scopes?.includes(SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION) + ) { declaration.action = SubmissionAction.REQUEST_CORRECTION declaration.submissionStatus = SUBMISSION_STATUS.READY_TO_REQUEST_CORRECTION @@ -1045,7 +1052,9 @@ class CorrectionSummaryComponent extends React.Component { this.props.writeDeclaration(declaration) - if (userRole === ROLE_REGISTRATION_AGENT) { + if ( + this.props.scopes?.includes(SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION) + ) { this.props.router.navigate( generateGoToHomeTabUrl({ tabId: WORKQUEUE_TABS.sentForApproval @@ -1079,6 +1088,8 @@ export const CorrectionSummary = withRouter( registerForm: getRegisterForm(state), offlineResources: getOfflineData(state), language: getLanguage(state), + userPrimaryOffice: getUserDetails(state)?.primaryOffice, + scopes: getScope(state), userDetails: getUserDetails(state) }), { diff --git a/packages/client/src/views/CorrectionForm/CorrectorForm.test.tsx b/packages/client/src/views/CorrectionForm/CorrectorForm.test.tsx index 27db28bdb32..c9394dfa599 100644 --- a/packages/client/src/views/CorrectionForm/CorrectorForm.test.tsx +++ b/packages/client/src/views/CorrectionForm/CorrectorForm.test.tsx @@ -24,7 +24,7 @@ import { IDeclaration, storeDeclaration } from '@client/declarations' import { CorrectionForm } from './CorrectionForm' import { formatUrl } from '@client/navigation' import { CERTIFICATE_CORRECTION } from '@client/navigation/routes' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' let wrapper: TestComponentWithRouteMock diff --git a/packages/client/src/views/CorrectionForm/CorrectorForm.tsx b/packages/client/src/views/CorrectionForm/CorrectorForm.tsx index 1e4a0e0c644..4c80c459ffe 100644 --- a/packages/client/src/views/CorrectionForm/CorrectorForm.tsx +++ b/packages/client/src/views/CorrectionForm/CorrectorForm.tsx @@ -35,7 +35,7 @@ import { messages } from '@client/i18n/messages/views/correction' import { Content, ContentSize } from '@opencrvs/components/lib/Content' import { groupHasError } from './utils' import { CERTIFICATE_CORRECTION_REVIEW } from '@client/navigation/routes' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { replaceInitialValues } from '@client/views/RegisterForm/RegisterForm' import { getOfflineData } from '@client/offline/selectors' import { getUserDetails } from '@client/profile/profileSelectors' diff --git a/packages/client/src/views/CorrectionForm/ReviewForm.test.tsx b/packages/client/src/views/CorrectionForm/ReviewForm.test.tsx index 8ff4457afca..624b29754c5 100644 --- a/packages/client/src/views/CorrectionForm/ReviewForm.test.tsx +++ b/packages/client/src/views/CorrectionForm/ReviewForm.test.tsx @@ -12,10 +12,13 @@ import { mockDeclarationData, createTestApp, flushPromises, + setScopes, + waitForReady, TestComponentWithRouteMock } from '@client/tests/util' import { ReactWrapper } from 'enzyme' import { ReviewSection } from '@client/forms' +import { SCOPES } from '@opencrvs/commons/client' import { EventType, RegStatus } from '@client/utils/gateway' import { IDeclaration, @@ -26,10 +29,10 @@ import { import { formatUrl } from '@client/navigation' import { CERTIFICATE_CORRECTION_REVIEW } from '@client/navigation/routes' import { Store } from 'redux' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { waitForElement } from '@client/tests/wait-for-element' -let wrapper: ReactWrapper<{}, {}> +let wrapper: ReactWrapper let store: Store let router: TestComponentWithRouteMock['router'] @@ -73,6 +76,8 @@ describe('Review form for an declaration', () => { store = appBundle.store router = appBundle.router + setScopes([SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION], store) + await waitForReady(wrapper) store.dispatch(storeDeclaration(declaration)) await flushPromises() diff --git a/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.test.tsx b/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.test.tsx index 35fe0ac0e92..f42a50ebb6c 100644 --- a/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.test.tsx +++ b/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.test.tsx @@ -22,7 +22,7 @@ import { IDeclaration, storeDeclaration } from '@client/declarations' import { formatUrl } from '@client/navigation' import { CERTIFICATE_CORRECTION } from '@client/navigation/routes' import { CorrectionForm } from './CorrectionForm' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' let wrapper: TestComponentWithRouteMock diff --git a/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.tsx b/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.tsx index 425b049edd2..95057826e2e 100644 --- a/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.tsx +++ b/packages/client/src/views/CorrectionForm/SupportingDocumentsForm.tsx @@ -34,9 +34,9 @@ import { connect, useSelector } from 'react-redux' import { supportingDocumentsSection } from '@client/forms/correction/supportDocument' import { Content, ContentSize } from '@opencrvs/components/lib/Content' import { replaceInitialValues } from '@client/views/RegisterForm/RegisterForm' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' import { getOfflineData } from '@client/offline/selectors' import { getUserDetails } from '@client/profile/profileSelectors' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { useNavigate } from 'react-router-dom' type IProps = { diff --git a/packages/client/src/views/CorrectionForm/VerifyCorrector.test.tsx b/packages/client/src/views/CorrectionForm/VerifyCorrector.test.tsx index dff0f8d16bd..9021301563d 100644 --- a/packages/client/src/views/CorrectionForm/VerifyCorrector.test.tsx +++ b/packages/client/src/views/CorrectionForm/VerifyCorrector.test.tsx @@ -20,7 +20,7 @@ import { import { VerifyCorrector } from './VerifyCorrector' import { storeDeclaration } from '@client/declarations' import { EventType } from '@client/utils/gateway' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { vi } from 'vitest' import { formatUrl } from '@client/navigation' import { VERIFY_CORRECTOR } from '@client/navigation/routes' diff --git a/packages/client/src/views/CorrectionForm/VerifyCorrector.tsx b/packages/client/src/views/CorrectionForm/VerifyCorrector.tsx index afef6adbb6b..3cec61d24d4 100644 --- a/packages/client/src/views/CorrectionForm/VerifyCorrector.tsx +++ b/packages/client/src/views/CorrectionForm/VerifyCorrector.tsx @@ -41,7 +41,7 @@ import { } from '@client/navigation/routes' import { getVerifyCorrectorDefinition } from '@client/forms/correction/verifyCorrector' import { TimeMounted } from '@client/components/TimeMounted' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { draftToGqlTransformer } from '@client/transformer' import { getEventRegisterForm } from '@client/forms/register/declaration-selectors' import { getOfflineData } from '@client/offline/selectors' diff --git a/packages/client/src/views/CorrectionForm/utils.ts b/packages/client/src/views/CorrectionForm/utils.ts index ff093047ecb..67ba923459c 100644 --- a/packages/client/src/views/CorrectionForm/utils.ts +++ b/packages/client/src/views/CorrectionForm/utils.ts @@ -44,7 +44,6 @@ import { } from '@client/forms' import { getConditionalActionsForField, - getListOfLocations, getVisibleSectionGroupsBasedOnConditions } from '@client/forms/utils' import { getValidationErrorsForForm } from '@client/forms/validation' @@ -69,6 +68,7 @@ import { } from '@client/utils/gateway' import { generateLocations } from '@client/utils/locationUtils' import { UserDetails } from '@client/utils/userUtils' +import { getListOfLocations } from '@client/utils/validate' import { camelCase, clone, flattenDeep, get, isArray, isEqual } from 'lodash' import { IntlShape, MessageDescriptor } from 'react-intl' diff --git a/packages/client/src/views/DataProvider/birth/queries.ts b/packages/client/src/views/DataProvider/birth/queries.ts index 205b59422a1..8c09484a755 100644 --- a/packages/client/src/views/DataProvider/birth/queries.ts +++ b/packages/client/src/views/DataProvider/birth/queries.ts @@ -149,6 +149,13 @@ const GET_BIRTH_REGISTRATION_FOR_REVIEW = gql` contactRelationship contactPhoneNumber contactEmail + assignment { + practitionerId + firstName + lastName + officeName + avatarURL + } certificates { hasShowedVerifiedDocument certificateTemplateId @@ -268,13 +275,16 @@ const GET_BIRTH_REGISTRATION_FOR_REVIEW = gql` user { id role { - _id - labels { - lang - label + id + label { + id + defaultMessage + description } } - systemRole + primaryOffice { + id + } name { firstNames familyName @@ -328,6 +338,21 @@ const GET_BIRTH_REGISTRATION_FOR_REVIEW = gql` use } } + certifier { + name { + use + firstNames + familyName + } + role { + id + label { + id + defaultMessage + description + } + } + } } duplicateOf potentialDuplicates @@ -544,13 +569,13 @@ export const GET_BIRTH_REGISTRATION_FOR_CERTIFICATE = gql` user { id role { - _id - labels { - lang - label + id + label { + id + defaultMessage + description } } - systemRole name { firstNames familyName @@ -604,6 +629,21 @@ export const GET_BIRTH_REGISTRATION_FOR_CERTIFICATE = gql` use } } + certifier { + name { + use + firstNames + familyName + } + role { + id + label { + id + defaultMessage + description + } + } + } } duplicateOf potentialDuplicates diff --git a/packages/client/src/views/DataProvider/death/queries.ts b/packages/client/src/views/DataProvider/death/queries.ts index 42bcbcdd5c1..dbf5737b5f8 100644 --- a/packages/client/src/views/DataProvider/death/queries.ts +++ b/packages/client/src/views/DataProvider/death/queries.ts @@ -213,6 +213,13 @@ const GET_DEATH_REGISTRATION_FOR_REVIEW = gql` contactRelationship contactPhoneNumber contactEmail + assignment { + practitionerId + firstName + lastName + officeName + avatarURL + } certificates { hasShowedVerifiedDocument certificateTemplateId @@ -337,13 +344,16 @@ const GET_DEATH_REGISTRATION_FOR_REVIEW = gql` user { id role { - _id - labels { - lang - label + id + label { + id + defaultMessage + description } } - systemRole + primaryOffice { + id + } name { firstNames familyName @@ -397,6 +407,21 @@ const GET_DEATH_REGISTRATION_FOR_REVIEW = gql` use } } + certifier { + name { + use + firstNames + familyName + } + role { + id + label { + id + defaultMessage + description + } + } + } } duplicateOf potentialDuplicates @@ -603,13 +628,13 @@ export const GET_DEATH_REGISTRATION_FOR_CERTIFICATION = gql` user { id role { - _id - labels { - lang - label + id + label { + id + defaultMessage + description } } - systemRole name { firstNames familyName @@ -663,6 +688,21 @@ export const GET_DEATH_REGISTRATION_FOR_CERTIFICATION = gql` use } } + certifier { + name { + use + firstNames + familyName + } + role { + id + label { + id + defaultMessage + description + } + } + } } duplicateOf potentialDuplicates diff --git a/packages/client/src/views/DataProvider/marriage/queries.ts b/packages/client/src/views/DataProvider/marriage/queries.ts index ba215418f0d..b7c8a64d973 100644 --- a/packages/client/src/views/DataProvider/marriage/queries.ts +++ b/packages/client/src/views/DataProvider/marriage/queries.ts @@ -158,6 +158,13 @@ const GET_MARRIAGE_REGISTRATION_FOR_REVIEW = gql` contactRelationship contactPhoneNumber contactEmail + assignment { + practitionerId + firstName + lastName + officeName + avatarURL + } certificates { hasShowedVerifiedDocument certificateTemplateId @@ -271,13 +278,16 @@ const GET_MARRIAGE_REGISTRATION_FOR_REVIEW = gql` user { id role { - _id - labels { - lang - label + id + label { + id + defaultMessage + description } } - systemRole + primaryOffice { + id + } name { firstNames familyName @@ -330,6 +340,21 @@ const GET_MARRIAGE_REGISTRATION_FOR_REVIEW = gql` use } } + certifier { + name { + use + firstNames + familyName + } + role { + id + label { + id + defaultMessage + description + } + } + } } } } @@ -562,13 +587,13 @@ const GET_MARRIAGE_REGISTRATION_FOR_CERTIFICATE = gql` user { id role { - _id - labels { - lang - label + id + label { + id + defaultMessage + description } } - systemRole name { firstNames familyName @@ -610,7 +635,6 @@ const GET_MARRIAGE_REGISTRATION_FOR_CERTIFICATE = gql` collector { relationship otherRelationship - name { use firstNames @@ -622,6 +646,21 @@ const GET_MARRIAGE_REGISTRATION_FOR_CERTIFICATE = gql` use } } + certifier { + name { + use + firstNames + familyName + } + role { + id + label { + id + defaultMessage + description + } + } + } } } } diff --git a/packages/client/src/views/IssueCertificate/IssueCertificate.tsx b/packages/client/src/views/IssueCertificate/IssueCertificate.tsx index c3a4fd2fc7c..d540b53acd8 100644 --- a/packages/client/src/views/IssueCertificate/IssueCertificate.tsx +++ b/packages/client/src/views/IssueCertificate/IssueCertificate.tsx @@ -21,7 +21,7 @@ import { IssueCollectorForm } from './IssueCollectorForm/IssueCollectorForm' import { formatUrl, generateGoToHomeTabUrl } from '@client/navigation' import { IssueCollectorFormForOthers } from './IssueCollectorForm/IssueFormForOthers' import { issueMessages } from '@client/i18n/messages/issueCertificate' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' export function IssueCertificate() { diff --git a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueCollectorForm.tsx b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueCollectorForm.tsx index b8d8f5041a8..db3f6f89916 100644 --- a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueCollectorForm.tsx +++ b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueCollectorForm.tsx @@ -31,7 +31,7 @@ import { issueMessages } from '@client/i18n/messages/issueCertificate' import { getIssueCertCollectorGroupForEvent } from '@client/forms/certificate/fieldDefinitions/collectorSection' import { Navigate, useNavigate } from 'react-router-dom' import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { getOfflineData } from '@client/offline/selectors' import { getUserDetails } from '@client/profile/profileSelectors' diff --git a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueFormForOthers.tsx b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueFormForOthers.tsx index 41d62e12346..337b2c0ff4b 100644 --- a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueFormForOthers.tsx +++ b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssueFormForOthers.tsx @@ -36,7 +36,7 @@ import { import { Navigate, useNavigate } from 'react-router-dom' import { EventType } from '@client/utils/gateway' import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { getOfflineData } from '@client/offline/selectors' import { getUserDetails } from '@client/profile/profileSelectors' diff --git a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.test.tsx b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.test.tsx index e04b04aef2f..dc491f899c4 100644 --- a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.test.tsx +++ b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.test.tsx @@ -8,7 +8,7 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { formatUrl } from '@client/navigation' import { ISSUE_CERTIFICATE_PAYMENT, diff --git a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.tsx b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.tsx index a2d4bdd2595..06b826c1ab9 100644 --- a/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.tsx +++ b/packages/client/src/views/IssueCertificate/IssueCollectorForm/IssuePayment.tsx @@ -22,7 +22,7 @@ import { useIntl } from 'react-intl' import * as React from 'react' import { Navigate, useNavigate, useParams } from 'react-router-dom' import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { calculatePrice, getEventDate, diff --git a/packages/client/src/views/OfficeHome/Home.tsx b/packages/client/src/views/OfficeHome/Home.tsx index 2806d59b4d8..654e6b2167d 100644 --- a/packages/client/src/views/OfficeHome/Home.tsx +++ b/packages/client/src/views/OfficeHome/Home.tsx @@ -8,44 +8,11 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { getUserDetails } from '@client/profile/profileSelectors' -import * as React from 'react' -import { useSelector } from 'react-redux' -import { PERFORMANCE_HOME, REGISTRAR_HOME } from '@client/navigation/routes' -import { Navigate } from 'react-router-dom' -import { getDefaultPerformanceLocationId } from '@client/navigation' -import { UserDetails } from '@client/utils/userUtils' -import { - NATIONAL_REGISTRAR_ROLES, - NATL_ADMIN_ROLES, - PERFORMANCE_MANAGEMENT_ROLES, - SYS_ADMIN_ROLES -} from '@client/utils/constants' +import { useHomePage } from '@client/hooks/useHomePage' +import React from 'react' +import { Navigate } from 'react-router' export function Home() { - const userDetails = useSelector(getUserDetails) - const role = userDetails && userDetails.systemRole - const isRoleAdmins = - role && - [ - ...NATL_ADMIN_ROLES, - ...PERFORMANCE_MANAGEMENT_ROLES, - ...NATIONAL_REGISTRAR_ROLES - ].includes(role) - const roleIsLocalSysAdmin = role && SYS_ADMIN_ROLES.includes(role) - - if (isRoleAdmins) return - if (roleIsLocalSysAdmin) - return ( - - ) - - return + const { path } = useHomePage() + return } diff --git a/packages/client/src/views/OfficeHome/OfficeHome.test.tsx b/packages/client/src/views/OfficeHome/OfficeHome.test.tsx index d82ab98fd1b..2a330076666 100644 --- a/packages/client/src/views/OfficeHome/OfficeHome.test.tsx +++ b/packages/client/src/views/OfficeHome/OfficeHome.test.tsx @@ -8,14 +8,16 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { checkAuth } from '@client/profile/profileActions' + import { queries } from '@client/profile/queries' import { storage } from '@client/storage' import { createStore } from '@client/store' import { createTestComponent, mockUserResponse, - flushPromises + flushPromises, + setScopes, + REGISTRAR_DEFAULT_SCOPES } from '@client/tests/util' import { createClient } from '@client/utils/apolloClient' import { OfficeHome } from '@client/views/OfficeHome/OfficeHome' @@ -25,14 +27,12 @@ import * as React from 'react' import { waitFor, waitForElement } from '@client/tests/wait-for-element' import { SELECTOR_ID } from './inProgress/InProgress' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' -import { vi, Mock } from 'vitest' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' +import { Scope, SCOPES, scopes } from '@opencrvs/commons/client' +import { vi } from 'vitest' import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' import { formatUrl } from '@client/navigation' -const registerScopeToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsImNlcnRpZnkiLCJkZW1vIl0sImlhdCI6MTU0MjY4ODc3MCwiZXhwIjoxNTQzMjkzNTcwLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI1YmVhYWY2MDg0ZmRjNDc5MTA3ZjI5OGMifQ.ElQd99Lu7WFX3L_0RecU_Q7-WZClztdNpepo7deNHqzro-Cog4WLN7RW3ZS5PuQtMaiOq1tCb-Fm3h7t4l4KDJgvC11OyT7jD6R2s2OleoRVm3Mcw5LPYuUVHt64lR_moex0x_bCqS72iZmjrjS-fNlnWK5zHfYAjF2PWKceMTGk6wnI9N49f6VwwkinJcwJi6ylsjVkylNbutQZO0qTc7HRP-cBfAzNcKD37FqTRNpVSvHdzQSNcs7oiv3kInDN5aNa2536XSd3H-RiKR9hm9eID9bSIJgFIGzkWRd5jnoYxT70G0t03_mTVnDnqPXDtyI-lmerx24Ost0rQLUNIg' -const getItem = window.localStorage.getItem as Mock const mockFetchUserDetails = vi.fn() const mockListSyncController = vi.fn() @@ -72,8 +72,7 @@ let client = createClient(store) beforeEach(async () => { ;({ store } = createStore()) client = createClient(store) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) }) describe('OfficeHome related tests', () => { @@ -445,4 +444,50 @@ describe('OfficeHome related tests', () => { await waitForElement(testComponent, '#search-result-error-text-count') }) }) + + describe('new event button should be visible when the user has the correct scopes', () => { + const build = async () => + await createTestComponent(, { + store, + apolloClient: client, + path: REGISTRAR_HOME_TAB, + initialEntries: [ + formatUrl(REGISTRAR_HOME_TAB, { + tabId: WORKQUEUE_TABS.inProgress + }) + ] + }) + + const requiredScopes = [ + SCOPES.RECORD_DECLARE_BIRTH, + SCOPES.RECORD_DECLARE_BIRTH_MY_JURISDICTION, + SCOPES.RECORD_DECLARE_DEATH, + SCOPES.RECORD_DECLARE_DEATH_MY_JURISDICTION, + SCOPES.RECORD_DECLARE_MARRIAGE, + SCOPES.RECORD_DECLARE_MARRIAGE_MY_JURISDICTION + ] as Scope[] + + const allOtherScopes = scopes.filter( + (scope) => !requiredScopes.includes(scope) + ) + const tests = [ + [[requiredScopes[0]], true], + [[requiredScopes[1]], true], + [[requiredScopes[2]], true], + [[requiredScopes[3]], true], + [[requiredScopes[4]], true], + [[requiredScopes[5]], true], + [allOtherScopes, false] + ] + tests.forEach(([scopes, visible]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + const testComponent = await build() + + expect(testComponent.component.exists('#new_event_declaration')).toBe( + visible + ) + }) + }) + }) }) diff --git a/packages/client/src/views/OfficeHome/OfficeHome.tsx b/packages/client/src/views/OfficeHome/OfficeHome.tsx index b1276d38283..a2277d2cc05 100644 --- a/packages/client/src/views/OfficeHome/OfficeHome.tsx +++ b/packages/client/src/views/OfficeHome/OfficeHome.tsx @@ -27,7 +27,7 @@ import styled from 'styled-components' import { getUserLocation } from '@client/utils/userUtils' import { FloatingActionButton } from '@opencrvs/components/lib/buttons' import { PlusTransparentWhite } from '@opencrvs/components/lib/icons' -import { SYNC_WORKQUEUE_TIME, FIELD_AGENT_ROLES } from '@client/utils/constants' +import { SYNC_WORKQUEUE_TIME } from '@client/utils/constants' import { Toast } from '@opencrvs/components/lib/Toast' import * as React from 'react' import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl' @@ -39,10 +39,8 @@ import { ReadyToPrint } from './readyToPrint/ReadyToPrint' import { RequiresUpdate } from './requiresUpdate/RequiresUpdate' import { ReadyForReview } from './readyForReview/ReadyForReview' import { InExternalValidationTab } from './inExternalValidation/InExternalValidationTab' -import { - Navigation, - WORKQUEUE_TABS -} from '@client/components/interface/Navigation' +import { Navigation } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { isDeclarationInReadyToReviewStatus } from '@client/utils/draftUtils' import { navigationMessages } from '@client/i18n/messages/views/navigation' import { Frame } from '@opencrvs/components/lib/Frame' @@ -52,12 +50,15 @@ import { ArrayElement } from '@client/SubmissionController' import { ReadyToIssue } from './readyToIssue/ReadyToIssue' import { getOfflineData } from '@client/offline/selectors' import { IOfflineData } from '@client/offline/reducer' +import { SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' +import ProtectedComponent from '@client/components/ProtectedComponent' import { SELECT_VITAL_EVENT } from '@client/navigation/routes' import { RouteComponentProps, withRouter } from '@client/components/WithRouterProps' +import { MyDrafts } from './myDrafts/MyDrafts' const FABContainer = styled.div` position: fixed; @@ -77,7 +78,6 @@ interface IDispatchProps { type IBaseOfficeHomeStateProps = ReturnType interface IOfficeHomeState { - draftCurrentPage: number showCertificateToast: boolean offlineResources: IOfflineData } @@ -88,6 +88,7 @@ type IOfficeHomeProps = IntlShapeProps & RouteComponentProps const DECLARATION_WORKQUEUE_TABS = [ + WORKQUEUE_TABS.myDrafts, WORKQUEUE_TABS.inProgress, WORKQUEUE_TABS.sentForApproval, WORKQUEUE_TABS.sentForReview, @@ -124,17 +125,10 @@ class OfficeHomeView extends React.Component< pageSize = 10 showPaginated = false interval: NodeJS.Timeout | undefined = undefined - role = this.props.userDetails && this.props.userDetails.systemRole - isFieldAgent = this.role - ? FIELD_AGENT_ROLES.includes(this.role) - ? true - : false - : false constructor(props: IOfficeHomeProps) { super(props) this.state = { - draftCurrentPage: 1, showCertificateToast: Boolean( this.props.declarations.filter( (item) => item.submissionStatus === SUBMISSION_STATUS.READY_TO_CERTIFY @@ -147,8 +141,7 @@ class OfficeHomeView extends React.Component< updateWorkqueue() { this.props.updateRegistrarWorkqueue( this.props.userDetails?.practitionerId, - this.pageSize, - this.isFieldAgent + this.pageSize ) } @@ -182,13 +175,11 @@ class OfficeHomeView extends React.Component< notificationTab: pageId }) this.updateWorkqueue() - } else if ( - selectorId === SELECTOR_ID.ownDrafts && - pageId !== this.state.draftCurrentPage - ) { - this.setState({ draftCurrentPage: pageId }) } - } else if (pageId !== this.props[WORKQUEUE_TABS_PAGINATION[tabId]]) { + } else if ( + tabId !== WORKQUEUE_TABS.myDrafts && + pageId !== this.props[WORKQUEUE_TABS_PAGINATION[tabId]] + ) { this.props.updateWorkqueuePagination({ [WORKQUEUE_TABS_PAGINATION[tabId]]: pageId }) @@ -213,14 +204,6 @@ class OfficeHomeView extends React.Component< } } - userHasRegisterScope() { - return this.props.scope && this.props.scope.includes('register') - } - - userHasValidateScope() { - return this.props.scope && this.props.scope.includes('validate') - } - subtractDeclarationsWithStatus(count: number, status: string[]) { const outboxCount = this.props.storedDeclarations.filter( (app) => app.submissionStatus && status.includes(app.submissionStatus) @@ -238,10 +221,11 @@ class OfficeHomeView extends React.Component< tabId: WORKQUEUE_TABS.inProgress, selectorId: Object.values(SELECTOR_ID).includes(selectorId) ? selectorId - : SELECTOR_ID.ownDrafts, + : SELECTOR_ID.fieldAgentDrafts, pageId: newPageNumber }) ) + return } this.props.router.navigate( @@ -258,6 +242,7 @@ class OfficeHomeView extends React.Component< healthSystemCurrentPage: number, progressCurrentPage: number, reviewCurrentPage: number, + sentForReviewCurrentPage: number, approvalCurrentPage: number, printCurrentPage: number, issueCurrentPage: number, @@ -267,7 +252,6 @@ class OfficeHomeView extends React.Component< const { workqueue, tabId, - drafts, selectorId, storedDeclarations, offlineResources @@ -287,17 +271,21 @@ class OfficeHomeView extends React.Component< return ( <> + {tabId === WORKQUEUE_TABS.myDrafts && ( + + )} {tabId === WORKQUEUE_TABS.inProgress && ( )} - {!this.isFieldAgent ? ( - <> - {tabId === WORKQUEUE_TABS.readyForReview && ( - - )} - {tabId === WORKQUEUE_TABS.requiresUpdate && ( - - )} - - {tabId === WORKQUEUE_TABS.externalValidation && - window.config.FEATURES.EXTERNAL_VALIDATION_WORKQUEUE && ( - - )} - {tabId === WORKQUEUE_TABS.sentForApproval && ( - - )} - {tabId === WORKQUEUE_TABS.readyToPrint && ( - - )} - {isOnePrintInAdvanceOn && tabId === WORKQUEUE_TABS.readyToIssue && ( - - )} - - {tabId === WORKQUEUE_TABS.outbox && } - - ) : ( - <> - {tabId === WORKQUEUE_TABS.sentForReview && ( - - )} - {tabId === WORKQUEUE_TABS.requiresUpdate && ( - - )} - {tabId === WORKQUEUE_TABS.outbox && } - + {tabId === WORKQUEUE_TABS.readyForReview && ( + + )} + {tabId === WORKQUEUE_TABS.externalValidation && + window.config.FEATURES.EXTERNAL_VALIDATION_WORKQUEUE && ( + + )} + {tabId === WORKQUEUE_TABS.sentForApproval && ( + )} + {tabId === WORKQUEUE_TABS.readyToPrint && ( + + )} + {isOnePrintInAdvanceOn && tabId === WORKQUEUE_TABS.readyToIssue && ( + + )} + {tabId === WORKQUEUE_TABS.sentForReview && ( + + )} + {tabId === WORKQUEUE_TABS.requiresUpdate && ( + + )} + {tabId === WORKQUEUE_TABS.outbox && } ) } render() { const { intl } = this.props - const { draftCurrentPage } = this.state const { + pageId, notificationTab, inProgressTab, reviewTab, + sentForReviewTab, approvalTab, printTab, issueTab, @@ -447,10 +414,11 @@ class OfficeHomeView extends React.Component< navigation={} > {this.getData( - draftCurrentPage, + pageId, notificationTab, inProgressTab, reviewTab, + sentForReviewTab, approvalTab, printTab, issueTab, @@ -458,14 +426,25 @@ class OfficeHomeView extends React.Component< rejectTab )} - - - } - /> - - + + + + } + /> + + + {this.state.showCertificateToast && ( { - getItem.mockReturnValue(registerScopeToken) - store.dispatch(checkAuth()) -}) describe('In Progress tab', () => { it('redirects to different route upon selection', async () => { - const localDrafts = [ - { - id: '1', - event: EventType.Birth, - data: {} - }, - { - id: '2', - event: EventType.Birth, - data: {} - } - ] - const { component: testComponent, router } = await createTestComponent( + const { component: app, router } = await createTestComponent( {}} + onPageChange={(_pageId: number) => {}} loading={false} error={false} />, { store } ) - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() - const app = testComponent - - app.find(`#tab_${SELECTOR_ID.ownDrafts}`).hostNodes().simulate('click') - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) + app.find(`#tab_${SELECTOR_ID.hospitalDrafts}`).hostNodes().simulate('click') + app.update() expect(router.state.location.pathname).toContain( formatUrl(REGISTRAR_HOME_TAB, { tabId: WORKQUEUE_TABS.inProgress, - selectorId: SELECTOR_ID.ownDrafts + selectorId: SELECTOR_ID.hospitalDrafts }) ) app .find(`#tab_${SELECTOR_ID.fieldAgentDrafts}`) .hostNodes() .simulate('click') - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) + app.update() expect(router.state.location.pathname).toContain( formatUrl(REGISTRAR_HOME_TAB, { tabId: WORKQUEUE_TABS.inProgress, @@ -151,312 +79,29 @@ describe('In Progress tab', () => { }) it('renders two selectors with count for each', async () => { - const localDrafts = [ - { - id: '1', - event: EventType.Birth, - data: {} - }, - { - id: '2', - event: EventType.Birth, - data: {} - } - ] - - const { component: testComponent } = await createTestComponent( + const { component: app } = await createTestComponent( {}} + onPageChange={(_pageId: number) => {}} />, { store } ) - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() - const app = testComponent - - expect(app.find('#tab_you').hostNodes().text()).toContain('Yours (2)') expect(app.find('#tab_field-agents').hostNodes().text()).toContain( 'Field agents (5)' ) - }) - - describe('When the local drafts selector is selected', () => { - it('renders all items returned from local storage in inProgress tab', async () => { - const { store } = createStore() - const TIME_STAMP = 1562912635549 - const drafts: IDeclaration[] = [ - { - id: 'e302f7c5-ad87-4117-91c1-35eaf2ea7be8', - data: { - registration: { - informantType: 'MOTHER', - informant: 'MOTHER_ONLY', - registrationPhone: '01722222222', - whoseContactDetails: 'MOTHER' - }, - child: { - firstNamesEng: 'Anik', - familyNameEng: 'Hoque' - } - }, - event: EventType.Birth, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - modifiedOn: TIME_STAMP - }, - { - id: 'e6605607-92e0-4625-87d8-c168205bdde7', - event: EventType.Birth, - modifiedOn: TIME_STAMP, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - data: { - registration: { - informantType: 'MOTHER', - informant: 'MOTHER_ONLY', - registrationPhone: '01722222222', - whoseContactDetails: 'MOTHER' - }, - child: { - firstNamesEng: 'Anik', - familyNameEng: 'Hoque' - } - } - }, - { - id: 'cc66d69c-7f0a-4047-9283-f066571830f1', - data: { - deceased: { - firstNamesEng: 'Anik', - familyNameEng: 'Hoque' - } - }, - event: EventType.Death, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - modifiedOn: TIME_STAMP - }, - - { - id: '607afa75-4fb0-4785-9388-724911d62809', - data: { - deceased: { - firstNamesEng: 'Anik', - familyNameEng: 'Hoque' - } - }, - event: EventType.Death, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - modifiedOn: TIME_STAMP - } - ] - // @ts-ignore - const { component: testComponent } = await createTestComponent( - {}} - />, - { store } - ) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.update() - const data = testComponent - .find(Workqueue) - .prop>>('content') - const EXPECTED_DATE_OF_REJECTION = formattedDuration(TIME_STAMP) - - expect(data[0].id).toBe('e302f7c5-ad87-4117-91c1-35eaf2ea7be8') - expect(data[0].name).toBe('anik hoque') - expect(data[0].lastUpdated).toBe(EXPECTED_DATE_OF_REJECTION) - expect(data[0].event).toBe('Birth') - expect(data[0].actions).toBeDefined() - }) - - it('Should render pagination in progress tab if data is more than 10', async () => { - vi.clearAllMocks() - const drafts: IDeclaration[] = [] - for (let i = 0; i < 12; i++) { - drafts.push(createDeclaration(EventType.Birth)) - } - const { component: testComponent } = await createTestComponent( - {}} - />, - { store } - ) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() - const pagiBtn = testComponent.find('#pagination_container') - - expect(pagiBtn.hostNodes()).toHaveLength(1) - testComponent - .find('#pagination button') - .last() - .hostNodes() - .simulate('click') - }) - - it('redirects user to detail page on update click', async () => { - const TIME_STAMP = 1562912635549 - const drafts: IDeclaration[] = [ - { - id: 'e302f7c5-ad87-4117-91c1-35eaf2ea7be8', - event: EventType.Birth, - modifiedOn: TIME_STAMP, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - data: { - registration: { - contactPoint: { - value: 'MOTHER', - nestedFields: { - registrationPhone: '01722222222' - } - } - }, - child: { - firstNamesEng: 'Anik', - firstNames: 'অনিক', - familyNameEng: 'Hoque', - familyName: 'অনিক' - } - } - }, - { - id: 'bd22s7c5-ad87-4117-91c1-35eaf2ese32bw', - event: EventType.Birth, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - modifiedOn: TIME_STAMP, - data: { - child: { - familyNameEng: 'Hoque' - } - } - }, - { - id: 'cc66d69c-7f0a-4047-9283-f066571830f1', - event: EventType.Death, - modifiedOn: TIME_STAMP, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - data: { - deceased: { - firstNamesEng: 'Anik', - familyNameEng: 'Hoque' - } - } - }, - { - id: 'cc66d69c-7f0a-4047-9283-f066571830f2', - event: EventType.Death, - modifiedOn: TIME_STAMP, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - data: { - deceased: { - familyNameEng: 'Hoque' - } - } - }, - { - id: 'cc66d69c-7f0a-4047-9283-f066571830f4', - event: EventType.Death, - modifiedOn: TIME_STAMP + 1, - submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], - data: { - '': {} - } - } - ] - // @ts-ignore - store.dispatch(storeDeclaration(drafts)) - const { component: testComponent, router } = await createTestComponent( - {}} - />, - { store } - ) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.update() - expect( - testComponent.find('#ListItemAction-0-Update').hostNodes() - ).toHaveLength(1) - testComponent - .find('#ListItemAction-0-Update') - .hostNodes() - .simulate('click') - - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.update() - expect(router.state.location.pathname).toContain( - '/drafts/cc66d69c-7f0a-4047-9283-f066571830f4' - ) - }) + expect(app.find('#tab_hospitals').hostNodes().text()).toContain( + 'Hospitals (3)' + ) }) describe('When the remote drafts selector is selected', () => { @@ -466,7 +111,6 @@ describe('In Progress tab', () => { drafts.push(createDeclaration(EventType.Birth)) const { component: testComponent } = await createTestComponent( { }, notificationData: {} }} - isFieldAgent={false} paginationId={{ - draftId: 1, fieldAgentId: 1, healthSystemId: 1 }} pageSize={10} - onPageChange={(pageId: number) => {}} + onPageChange={(_pageId: number) => {}} />, { store } ) - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.update() const data = testComponent .find(Workqueue) .prop>>('content') - const EXPECTED_DATE_OF_REJECTION = formattedDuration(Number(TIME_STAMP)) + const EXPECTED_NOTIFICATION_SENT_DATE = formattedDuration( + Number(TIME_STAMP) + ) expect(data[0].id).toBe('956281c9-1f47-4c26-948a-970dd23c4094') expect(data[0].name).toBe('k m abdullah al amin khan') - expect(data[0].notificationSent).toBe(EXPECTED_DATE_OF_REJECTION) + expect(data[0].notificationSent).toBe(EXPECTED_NOTIFICATION_SENT_DATE) expect(data[0].event).toBe('Death') }) it('Should render pagination in progress tab if data is more than 10', async () => { - vi.clearAllMocks() const drafts: IDeclaration[] = [] drafts.push(createDeclaration(EventType.Birth)) const { component: testComponent } = await createTestComponent( {}} + onPageChange={(_pageId: number) => {}} />, { store } ) - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() const pagiBtn = testComponent.find('#pagination_container') expect(pagiBtn.hostNodes()).toHaveLength(1) @@ -594,14 +223,11 @@ describe('In Progress tab', () => { }) it('redirects to recordAudit page when item is clicked', async () => { - vi.clearAllMocks() const TIME_STAMP = '1562912635549' const drafts: IDeclaration[] = [] drafts.push(createDeclaration(EventType.Birth)) - // @ts-ignore const { component: testComponent, router } = await createTestComponent( { ] } }} - isFieldAgent={false} paginationId={{ - draftId: 1, fieldAgentId: 1, healthSystemId: 1 }} pageSize={10} - onPageChange={(pageId: number) => {}} + onPageChange={(_pageId: number) => {}} />, { store } ) - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.update() testComponent.find('#name_0').hostNodes().simulate('click') - await flushPromises() testComponent.update() expect(router.state.location.pathname).toContain( @@ -694,10 +312,11 @@ describe('In Progress tab', () => { }) describe('handles download status', () => { + let store: AppStore + const TIME_STAMP = '1562912635549' const declarationId = 'e302f7c5-ad87-4117-91c1-35eaf2ea7be8' const inprogressProps = { - drafts: [], selectorId: SELECTOR_ID.fieldAgentDrafts, registrarLocationId: '0627c48a-c721-4ff9-bc6e-1fba59a2332a', queryData: { @@ -709,7 +328,8 @@ describe('In Progress tab', () => { type: EventType.Birth, registration: { trackingId: 'BQ2IDOP', - modifiedAt: TIME_STAMP + modifiedAt: TIME_STAMP, + status: 'IN_PROGRESS' }, childName: [ { @@ -725,13 +345,18 @@ describe('In Progress tab', () => { }, isFieldAgent: false, paginationId: { - draftId: 1, fieldAgentId: 1, healthSystemId: 1 }, pageSize: 10, - onPageChange: (pageId: number) => {} + onPageChange: (_pageId: number) => {} } + + beforeEach(() => { + const newStore = createStore() + store = newStore.store + }) + it('renders download button when not downloaded', async () => { const downloadableDeclaration = makeDeclarationReadyToDownload( EventType.Birth, @@ -739,7 +364,7 @@ describe('In Progress tab', () => { DownloadAction.LOAD_REVIEW_DECLARATION ) downloadableDeclaration.downloadStatus = undefined - store.dispatch(modifyDeclaration(downloadableDeclaration)) + store.dispatch(storeDeclaration(downloadableDeclaration)) const { component: testComponent } = await createTestComponent( , { store } @@ -756,7 +381,7 @@ describe('In Progress tab', () => { DownloadAction.LOAD_REVIEW_DECLARATION ) downloadableDeclaration.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING - store.dispatch(modifyDeclaration(downloadableDeclaration)) + store.dispatch(storeDeclaration(downloadableDeclaration)) const { component: testComponent } = await createTestComponent( , { store } @@ -773,7 +398,7 @@ describe('In Progress tab', () => { DownloadAction.LOAD_REVIEW_DECLARATION ) downloadableDeclaration.downloadStatus = DOWNLOAD_STATUS.DOWNLOADED - store.dispatch(modifyDeclaration(downloadableDeclaration)) + store.dispatch(storeDeclaration(downloadableDeclaration)) const { component: testComponent, router } = await createTestComponent( , { store } @@ -788,9 +413,6 @@ describe('In Progress tab', () => { .hostNodes() .simulate('click') - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) testComponent.update() expect(router.state.location.pathname).toContain( @@ -808,7 +430,7 @@ describe('In Progress tab', () => { DownloadAction.LOAD_REVIEW_DECLARATION ) downloadableDeclaration.downloadStatus = DOWNLOAD_STATUS.FAILED - store.dispatch(modifyDeclaration(downloadableDeclaration)) + store.dispatch(storeDeclaration(downloadableDeclaration)) const { component: testComponent } = await createTestComponent( , { store } @@ -824,11 +446,8 @@ describe('In Progress tab', () => { it('Should render all items returned from graphQL', async () => { const TIME_STAMP = '1562912635549' const birthNotificationSentDateStr = '2019-10-20T11:03:20.660Z' - const drafts: IDeclaration[] = [] - drafts.push(createDeclaration(EventType.Birth)) const { component: testComponent } = await createTestComponent( { }, inProgressData: {} }} - isFieldAgent={false} paginationId={{ - draftId: 1, fieldAgentId: 1, healthSystemId: 1 }} pageSize={10} - onPageChange={(pageId: number) => {}} + onPageChange={(_pageId: number) => {}} />, { store } ) - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - testComponent.update() - const data = testComponent - .find(Workqueue) - .prop>>('content') - const EXPECTED_DATE_OF_REJECTION = formattedDuration( - new Date(birthNotificationSentDateStr) - ) + const data = testComponent + .find(Workqueue) + .prop>>('content') + const EXPECTED_NOTIFICATION_SENT_DATE = formattedDuration( + new Date(birthNotificationSentDateStr) + ) - expect(data[0].id).toBe('f0a1ca2c-6a14-4b9e-a627-c3e2e110587e') - expect(data[0].name).toBe('anik hoque') - expect(data[0].notificationSent).toBe(EXPECTED_DATE_OF_REJECTION) - expect(data[0].event).toBe('Birth') - }) + expect(data[0].id).toBe('f0a1ca2c-6a14-4b9e-a627-c3e2e110587e') + expect(data[0].name).toBe('anik hoque') + expect(data[0].notificationSent).toBe(EXPECTED_NOTIFICATION_SENT_DATE) + expect(data[0].event).toBe('Birth') }) }) @@ -924,26 +536,19 @@ describe('In Progress tab', () => { const { store } = createStore() beforeAll(async () => { - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) resizeWindow(800, 1280) }) - afterEach(() => { + afterAll(() => { resizeWindow(1024, 768) }) it('redirects to recordAudit page if item is clicked', async () => { - vi.clearAllMocks() const TIME_STAMP = '1562912635549' - const drafts: IDeclaration[] = [] - drafts.push(createDeclaration(EventType.Birth)) const declarationId = 'e302f7c5-ad87-4117-91c1-35eaf2ea7be8' - // @ts-ignore const { component: testComponent, router } = await createTestComponent( { }, notificationData: {} }} - isFieldAgent={false} paginationId={{ - draftId: 1, fieldAgentId: 1, healthSystemId: 1 }} pageSize={10} - onPageChange={(pageId: number) => {}} + onPageChange={(_pageId: number) => {}} />, { store } ) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.update() testComponent.find('#name_0').hostNodes().simulate('click') - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) testComponent.update() expect(router.state.location.pathname).toContain( diff --git a/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx b/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx index 864d4510e93..9749ee95a8a 100644 --- a/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx +++ b/packages/client/src/views/OfficeHome/inProgress/InProgress.tsx @@ -8,68 +8,56 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ - -import { - Workqueue, - ColumnContentAlignment, - COLUMNS, - SORT_ORDER -} from '@opencrvs/components/lib/Workqueue' -import type { - GQLHumanName, - GQLEventSearchResultSet -} from '@client/utils/gateway-deprecated-do-not-use' +import { DownloadButton } from '@client/components/interface/DownloadButton' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { - IDeclaration, DOWNLOAD_STATUS, - SUBMISSION_STATUS, - ITaskHistory + IDeclaration, + ITaskHistory, + SUBMISSION_STATUS } from '@client/declarations' -import { - formatUrl, - generateGoToHomeTabUrl, - generateGoToPageUrl -} from '@client/navigation' -import { - DRAFT_BIRTH_PARENT_FORM_PAGE, - DRAFT_DEATH_FORM_PAGE, - DRAFT_MARRIAGE_FORM_PAGE, - REVIEW_EVENT_PARENT_FORM_PAGE -} from '@client/navigation/routes' -import { withTheme } from 'styled-components' -import { ITheme } from '@opencrvs/components/lib/theme' -import { EMPTY_STRING, LANG_EN } from '@client/utils/constants' -import { createNamesMap } from '@client/utils/data-formatting' -import * as React from 'react' -import { WrappedComponentProps as IntlShapeProps, injectIntl } from 'react-intl' -import { connect } from 'react-redux' +import { DownloadAction } from '@client/forms' import { buttonMessages, constantsMessages, dynamicConstantsMessages, wqMessages } from '@client/i18n/messages' +import { navigationMessages } from '@client/i18n/messages/views/navigation' import { messages } from '@client/i18n/messages/views/registrarHome' +import { + formatUrl, + generateGoToHomeTabUrl, + generateGoToPageUrl +} from '@client/navigation' +import * as routes from '@client/navigation/routes' +import { REVIEW_EVENT_PARENT_FORM_PAGE } from '@client/navigation/routes' import { IOfflineData } from '@client/offline/reducer' import { getOfflineData } from '@client/offline/selectors' +import { getScope } from '@client/profile/profileSelectors' +import { + isBirthEvent, + isDeathEvent, + isMarriageEvent +} from '@client/search/transformer' import { IStoreState } from '@client/store' -import { DownloadAction } from '@client/forms' -import { EventType, RegStatus } from '@client/utils/gateway' -import { DownloadButton } from '@client/components/interface/DownloadButton' -import { getDeclarationFullName } from '@client/utils/draftUtils' +import { EMPTY_STRING, LANG_EN } from '@client/utils/constants' +import { createNamesMap } from '@client/utils/data-formatting' import { formattedDuration, isValidPlainDate, plainDateToLocalDate } from '@client/utils/date-formatting' -import { navigationMessages } from '@client/i18n/messages/views/navigation' -import { FormTabs } from '@opencrvs/components/lib/FormTabs' -import { IAction } from '@opencrvs/components/lib/common-types' +import { RegStatus } from '@client/utils/gateway' +import type { + GQLEventSearchResultSet, + GQLHumanName +} from '@client/utils/gateway-deprecated-do-not-use' import { IconWithName, IconWithNameEvent, - NoNameContainer, - NameContainer + NameContainer, + NoNameContainer } from '@client/views/OfficeHome/components' import { changeSortedColumn, @@ -77,17 +65,23 @@ import { getSortedItems } from '@client/views/OfficeHome/utils' import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' -import { Downloaded } from '@opencrvs/components/lib/icons/Downloaded' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { Scope } from '@opencrvs/commons/client' +import { IAction } from '@opencrvs/components/lib/common-types' +import { FormTabs } from '@opencrvs/components/lib/FormTabs' +import { useWindowSize } from '@opencrvs/components/lib/hooks' +import { ITheme } from '@opencrvs/components/lib/theme' import { - isMarriageEvent, - isBirthEvent, - isDeathEvent -} from '@client/search/transformer' + ColumnContentAlignment, + COLUMNS, + SORT_ORDER, + Workqueue +} from '@opencrvs/components/lib/Workqueue' +import * as React from 'react' import { useState } from 'react' -import { useWindowSize } from '@opencrvs/components/lib/hooks' +import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl' +import { connect } from 'react-redux' import { useNavigate } from 'react-router-dom' -import * as routes from '@client/navigation/routes' +import { withTheme } from 'styled-components' interface IQueryData { inProgressData: GQLEventSearchResultSet @@ -97,17 +91,15 @@ interface IQueryData { interface IBaseRegistrarHomeProps { theme: ITheme selectorId: string - drafts: IDeclaration[] outboxDeclarations: IDeclaration[] queryData: IQueryData - isFieldAgent: boolean onPageChange: (newPageNumber: number) => void paginationId: { - draftId: number fieldAgentId: number healthSystemId: number } pageSize: number + scopes: Scope[] | null } interface IProps { @@ -119,7 +111,6 @@ interface IProps { type IRegistrarHomeProps = IntlShapeProps & IBaseRegistrarHomeProps & IProps export const SELECTOR_ID = { - ownDrafts: 'you', fieldAgentDrafts: 'field-agents', hospitalDrafts: 'hospitals' } @@ -128,11 +119,7 @@ function InProgressComponent(props: IRegistrarHomeProps) { const navigate = useNavigate() const { width } = useWindowSize() - const [sortedCol, setSortedCol] = useState( - props.selectorId && props.selectorId !== SELECTOR_ID.ownDrafts - ? COLUMNS.NOTIFICATION_SENT - : COLUMNS.LAST_UPDATED - ) + const [sortedCol, setSortedCol] = useState(COLUMNS.NOTIFICATION_SENT) const [sortOrder, setSortOrder] = useState(SORT_ORDER.DESCENDING) const onColumnClick = (columnName: string) => { @@ -227,20 +214,23 @@ function InProgressComponent(props: IRegistrarHomeProps) { disabled: downloadStatus !== DOWNLOAD_STATUS.DOWNLOADED }) } - actions.push({ - actionComponent: ( - - ) - }) + if (reg.registration?.status) { + actions.push({ + actionComponent: ( + + ) + }) + } const NameComponent = name ? ( { - return drafts.slice((pageId - 1) * props.pageSize, pageId * props.pageSize) - } - - const transformDraftContent = () => { - const { intl } = props - const { locale } = intl - if (!props.drafts || props.drafts.length <= 0) { - return [] - } - const paginatedDrafts = getDraftsPaginatedData( - props.drafts, - props.paginationId.draftId - ) - const items = paginatedDrafts.map((draft: IDeclaration, index) => { - let pageRoute: string - if (draft.event && draft.event.toString() === 'birth') { - pageRoute = DRAFT_BIRTH_PARENT_FORM_PAGE - } else if (draft.event && draft.event.toString() === 'death') { - pageRoute = DRAFT_DEATH_FORM_PAGE - } else if (draft.event && draft.event.toString() === 'marriage') { - pageRoute = DRAFT_MARRIAGE_FORM_PAGE - } - const name = getDeclarationFullName(draft, locale) - const lastModificationDate = draft.modifiedOn || draft.savedOn - const actions: IAction[] = [] - - if (width > props.theme.grid.breakpoints.lg) { - actions.push({ - label: props.intl.formatMessage(buttonMessages.update), - handler: ( - e: React.MouseEvent | undefined - ) => { - if (e) { - e.stopPropagation() - } - - navigate( - generateGoToPageUrl({ - pageRoute, - declarationId: draft.id, - pageId: 'review', - event: (draft.event && draft.event.toString()) || '' - }) - ) - } - }) - } - actions.push({ - actionComponent: - }) - const event = - (draft.event && - intl.formatMessage( - dynamicConstantsMessages[draft.event.toLowerCase()] - )) || - '' - - const eventTime = - draft.event === EventType.Birth - ? draft.data.child?.childBirthDate || '' - : draft.event === EventType.Death - ? draft.data.deathEvent?.deathDate || '' - : draft.data.marriageEvent?.marriageDate || '' - - const dateOfEvent = isValidPlainDate(eventTime) - ? plainDateToLocalDate(eventTime) - : '' - const NameComponent = name ? ( - - navigate( - formatUrl(routes.DECLARATION_RECORD_AUDIT, { - tab: 'inProgressTab', - declarationId: draft.id - }) - ) - } - > - {name} - - ) : ( - - navigate( - formatUrl(routes.DECLARATION_RECORD_AUDIT, { - tab: 'inProgressTab', - declarationId: draft.id - }) - ) - } - > - {intl.formatMessage(constantsMessages.noNameProvided)} - - ) - return { - id: draft.id, - event, - name: (name && name.toString().toLowerCase()) || '', - iconWithName: ( - - ), - iconWithNameEvent: ( - - ), - lastUpdated: lastModificationDate || '', - dateOfEvent, - actions - } - }) - const sortedItems = getSortedItems(items, sortedCol, sortOrder) - - return sortedItems.map((item) => { - return { - ...item, - dateOfEvent: - item.dateOfEvent && formattedDuration(item.dateOfEvent as Date), - lastUpdated: - item.lastUpdated && formattedDuration(item.lastUpdated as number) - } - }) - } - const getColumns = () => { if (width > props.theme.grid.breakpoints.lg) { return [ @@ -479,19 +333,10 @@ function InProgressComponent(props: IRegistrarHomeProps) { sortFunction: onColumnClick }, { - label: - props.selectorId && props.selectorId !== SELECTOR_ID.ownDrafts - ? props.intl.formatMessage(constantsMessages.notificationSent) - : props.intl.formatMessage(constantsMessages.lastUpdated), + label: props.intl.formatMessage(constantsMessages.notificationSent), width: 18, - key: - props.selectorId && props.selectorId !== SELECTOR_ID.ownDrafts - ? COLUMNS.NOTIFICATION_SENT - : COLUMNS.LAST_UPDATED, - isSorted: - props.selectorId && props.selectorId !== SELECTOR_ID.ownDrafts - ? sortedCol === COLUMNS.NOTIFICATION_SENT - : sortedCol === COLUMNS.LAST_UPDATED, + key: COLUMNS.NOTIFICATION_SENT, + isSorted: sortedCol === COLUMNS.NOTIFICATION_SENT, sortFunction: onColumnClick }, { @@ -520,15 +365,11 @@ function InProgressComponent(props: IRegistrarHomeProps) { const getTabs = ( selectorId: string, - drafts: IDeclaration[], fieldAgentCount: number, hospitalCount: number ) => { - if (props.isFieldAgent) { - return undefined - } const tabs = { - activeTabId: selectorId || SELECTOR_ID.ownDrafts, + activeTabId: selectorId || SELECTOR_ID.fieldAgentDrafts, onTabClick: (selectorId: string) => { navigate( generateGoToHomeTabUrl({ @@ -538,13 +379,6 @@ function InProgressComponent(props: IRegistrarHomeProps) { ) }, sections: [ - { - id: SELECTOR_ID.ownDrafts, - title: `${props.intl.formatMessage(messages.inProgressOwnDrafts)} (${ - drafts && drafts.length - })`, - disabled: false - }, { id: SELECTOR_ID.fieldAgentDrafts, title: `${props.intl.formatMessage( @@ -601,15 +435,10 @@ function InProgressComponent(props: IRegistrarHomeProps) { ) } - const { intl, selectorId, drafts, queryData, onPageChange, isFieldAgent } = - props + const { intl, selectorId, queryData, onPageChange } = props const isShowPagination = - !props.selectorId || props.selectorId === SELECTOR_ID.ownDrafts - ? props.drafts.length > props.pageSize - ? true - : false - : props.selectorId === SELECTOR_ID.fieldAgentDrafts + !props.selectorId || props.selectorId === SELECTOR_ID.fieldAgentDrafts ? props.queryData.inProgressData && props.queryData.inProgressData.totalItems && props.queryData.inProgressData.totalItems > props.pageSize @@ -623,16 +452,12 @@ function InProgressComponent(props: IRegistrarHomeProps) { const { inProgressData, notificationData } = queryData const paginationId = - !selectorId || selectorId === SELECTOR_ID.ownDrafts - ? props.paginationId.draftId - : selectorId === SELECTOR_ID.fieldAgentDrafts + selectorId === SELECTOR_ID.fieldAgentDrafts ? props.paginationId.fieldAgentId : props.paginationId.healthSystemId const totalPages = - !selectorId || selectorId === SELECTOR_ID.ownDrafts - ? Math.ceil(props.drafts.length / props.pageSize) - : selectorId === SELECTOR_ID.fieldAgentDrafts + !selectorId || selectorId === SELECTOR_ID.fieldAgentDrafts ? props.queryData.inProgressData && props.queryData.inProgressData.totalItems && Math.ceil(props.queryData.inProgressData.totalItems / props.pageSize) @@ -641,59 +466,38 @@ function InProgressComponent(props: IRegistrarHomeProps) { Math.ceil(props.queryData.notificationData.totalItems / props.pageSize) const noContent = - !selectorId || selectorId === SELECTOR_ID.ownDrafts - ? transformDraftContent().length <= 0 - : selectorId === SELECTOR_ID.fieldAgentDrafts + !selectorId || selectorId === SELECTOR_ID.fieldAgentDrafts ? transformRemoteDraftsContent(inProgressData).length <= 0 : transformRemoteDraftsContent(notificationData).length <= 0 const noResultMessage = - !selectorId || selectorId === SELECTOR_ID.ownDrafts - ? intl.formatMessage(wqMessages.noRecordsDraft) - : selectorId === SELECTOR_ID.fieldAgentDrafts + !selectorId || selectorId === SELECTOR_ID.fieldAgentDrafts ? intl.formatMessage(wqMessages.noRecordsFieldAgents) : intl.formatMessage(wqMessages.noRecordsHealthSystem) + const tabs = getTabs( + selectorId, + (inProgressData && inProgressData.totalItems) || 0, + (notificationData && notificationData.totalItems) || 0 + ) + return ( - {(!selectorId || selectorId === SELECTOR_ID.ownDrafts) && ( - - )} - {selectorId === SELECTOR_ID.fieldAgentDrafts && - !isFieldAgent && + {(!selectorId || selectorId === SELECTOR_ID.fieldAgentDrafts) && renderFieldAgentTable(inProgressData, isShowPagination)} {selectorId === SELECTOR_ID.hospitalDrafts && - !isFieldAgent && renderHospitalTable(notificationData, isShowPagination)} ) @@ -702,7 +506,8 @@ function InProgressComponent(props: IRegistrarHomeProps) { function mapStateToProps(state: IStoreState) { return { outboxDeclarations: state.declarationsState.declarations, - offlineCountryConfig: getOfflineData(state) + offlineCountryConfig: getOfflineData(state), + scopes: getScope(state) } } diff --git a/packages/client/src/views/OfficeHome/myDrafts/MyDrafts.test.tsx b/packages/client/src/views/OfficeHome/myDrafts/MyDrafts.test.tsx new file mode 100644 index 00000000000..6a89aae5f86 --- /dev/null +++ b/packages/client/src/views/OfficeHome/myDrafts/MyDrafts.test.tsx @@ -0,0 +1,243 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import { + createDeclaration, + IDeclaration, + storeDeclaration, + SUBMISSION_STATUS +} from '@client/declarations' +import { createStore } from '@client/store' +import { createTestComponent } from '@client/tests/util' +import { formattedDuration } from '@client/utils/date-formatting' +import { EventType } from '@client/utils/gateway' +import { Workqueue } from '@opencrvs/components/lib/Workqueue' +import * as React from 'react' +import { MyDrafts } from './MyDrafts' + +describe('My drafts tab', () => { + it('renders all items returned from local storage', async () => { + const { store } = createStore() + const TIME_STAMP = 1562912635549 + const drafts: IDeclaration[] = [ + { + id: 'e302f7c5-ad87-4117-91c1-35eaf2ea7be8', + data: { + registration: { + informantType: 'MOTHER', + informant: 'MOTHER_ONLY', + registrationPhone: '01722222222', + whoseContactDetails: 'MOTHER' + }, + child: { + firstNamesEng: 'Anik', + familyNameEng: 'Hoque' + } + }, + event: EventType.Birth, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + modifiedOn: TIME_STAMP + }, + { + id: 'e6605607-92e0-4625-87d8-c168205bdde7', + event: EventType.Birth, + modifiedOn: TIME_STAMP, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + data: { + registration: { + informantType: 'MOTHER', + informant: 'MOTHER_ONLY', + registrationPhone: '01722222222', + whoseContactDetails: 'MOTHER' + }, + child: { + firstNamesEng: 'Anik', + familyNameEng: 'Hoque' + } + } + }, + { + id: 'cc66d69c-7f0a-4047-9283-f066571830f1', + data: { + deceased: { + firstNamesEng: 'Anik', + familyNameEng: 'Hoque' + } + }, + event: EventType.Death, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + modifiedOn: TIME_STAMP + }, + + { + id: '607afa75-4fb0-4785-9388-724911d62809', + data: { + deceased: { + firstNamesEng: 'Anik', + familyNameEng: 'Hoque' + } + }, + event: EventType.Death, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + modifiedOn: TIME_STAMP + } + ] + const { component: testComponent } = await createTestComponent( + {}} + />, + { store } + ) + + for (const draft of drafts) { + store.dispatch(storeDeclaration(draft)) + } + + testComponent.update() + + const data = testComponent + .find(Workqueue) + .prop>>('content') + const EXPECTED_LAST_UPDATE = formattedDuration(TIME_STAMP) + + expect(data.length).toBe(drafts.length) + expect(data[0].id).toBe('e302f7c5-ad87-4117-91c1-35eaf2ea7be8') + expect(data[0].name).toBe('anik hoque') + expect(data[0].lastUpdated).toBe(EXPECTED_LAST_UPDATE) + expect(data[0].event).toBe('Birth') + expect(data[0].actions).toBeDefined() + }) + + it('Should render pagination in drafts tab if data is more than 10', async () => { + const { store } = createStore() + const { component: testComponent } = await createTestComponent( + {}} + />, + { store } + ) + + for (let i = 0; i < 12; i++) { + store.dispatch(storeDeclaration(createDeclaration(EventType.Birth))) + } + + testComponent.update() + const pagiBtn = testComponent.find('#pagination_container') + + expect(pagiBtn.hostNodes()).toHaveLength(1) + testComponent + .find('#pagination button') + .last() + .hostNodes() + .simulate('click') + }) + + it('redirects user to detail page on update click', async () => { + const { store } = createStore() + const TIME_STAMP = 1562912635549 + const drafts: IDeclaration[] = [ + { + id: 'e302f7c5-ad87-4117-91c1-35eaf2ea7be8', + event: EventType.Birth, + modifiedOn: TIME_STAMP, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + data: { + registration: { + contactPoint: { + value: 'MOTHER', + nestedFields: { + registrationPhone: '01722222222' + } + } + }, + child: { + firstNamesEng: 'Anik', + firstNames: 'অনিক', + familyNameEng: 'Hoque', + familyName: 'অনিক' + } + } + }, + { + id: 'bd22s7c5-ad87-4117-91c1-35eaf2ese32bw', + event: EventType.Birth, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + modifiedOn: TIME_STAMP, + data: { + child: { + familyNameEng: 'Hoque' + } + } + }, + { + id: 'cc66d69c-7f0a-4047-9283-f066571830f1', + event: EventType.Death, + modifiedOn: TIME_STAMP, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + data: { + deceased: { + firstNamesEng: 'Anik', + familyNameEng: 'Hoque' + } + } + }, + { + id: 'cc66d69c-7f0a-4047-9283-f066571830f2', + event: EventType.Death, + modifiedOn: TIME_STAMP, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + data: { + deceased: { + familyNameEng: 'Hoque' + } + } + }, + { + id: 'cc66d69c-7f0a-4047-9283-f066571830f4', + event: EventType.Death, + modifiedOn: TIME_STAMP + 1, + submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT], + data: { + '': {} + } + } + ] + const { component: testComponent, router } = await createTestComponent( + {}} + />, + { store } + ) + + for (const draft of drafts) { + store.dispatch(storeDeclaration(draft)) + } + + testComponent.update() + + expect( + testComponent.find('#ListItemAction-0-Update').hostNodes() + ).toHaveLength(1) + + testComponent.find('#ListItemAction-0-Update').hostNodes().simulate('click') + + testComponent.update() + + expect(router.state.location.pathname).toContain( + '/drafts/cc66d69c-7f0a-4047-9283-f066571830f4' + ) + }) +}) diff --git a/packages/client/src/views/OfficeHome/myDrafts/MyDrafts.tsx b/packages/client/src/views/OfficeHome/myDrafts/MyDrafts.tsx new file mode 100644 index 00000000000..dd0e0347f97 --- /dev/null +++ b/packages/client/src/views/OfficeHome/myDrafts/MyDrafts.tsx @@ -0,0 +1,304 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import { IDeclaration, SUBMISSION_STATUS } from '@client/declarations' +import { + buttonMessages, + constantsMessages, + dynamicConstantsMessages, + wqMessages +} from '@client/i18n/messages' +import { navigationMessages } from '@client/i18n/messages/views/navigation' +import { formatUrl, generateGoToPageUrl } from '@client/navigation' +import * as routes from '@client/navigation/routes' +import { + DRAFT_BIRTH_PARENT_FORM_PAGE, + DRAFT_DEATH_FORM_PAGE, + DRAFT_MARRIAGE_FORM_PAGE +} from '@client/navigation/routes' +import { IStoreState } from '@client/store' +import { + formattedDuration, + isValidPlainDate, + plainDateToLocalDate +} from '@client/utils/date-formatting' +import { getDeclarationFullName } from '@client/utils/draftUtils' +import { EventType } from '@client/utils/gateway' +import { + IconWithName, + IconWithNameEvent, + NameContainer, + NoNameContainer +} from '@client/views/OfficeHome/components' +import { + changeSortedColumn, + getSortedItems +} from '@client/views/OfficeHome/utils' +import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' +import { IAction } from '@opencrvs/components/lib/common-types' +import { useWindowSize } from '@opencrvs/components/lib/hooks' +import { Downloaded } from '@opencrvs/components/lib/icons/Downloaded' +import { + ColumnContentAlignment, + COLUMNS, + SORT_ORDER, + Workqueue +} from '@opencrvs/components/lib/Workqueue' +import * as React from 'react' +import { useState } from 'react' +import { useIntl } from 'react-intl' +import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { useTheme } from 'styled-components' + +export const MyDrafts: React.FC<{ + pageSize: number + currentPage: number + onPageChange: (newPageNumber: number) => void +}> = (props) => { + const navigate = useNavigate() + const { width } = useWindowSize() + const intl = useIntl() + const theme = useTheme() + const drafts = useSelector((store: IStoreState) => + store.declarationsState.declarations.filter( + ({ submissionStatus }) => submissionStatus === SUBMISSION_STATUS.DRAFT + ) + ) + + const [sortedCol, setSortedCol] = useState(COLUMNS.LAST_UPDATED) + const [sortOrder, setSortOrder] = useState(SORT_ORDER.DESCENDING) + + const onColumnClick = (columnName: string) => { + const { newSortedCol, newSortOrder } = changeSortedColumn( + columnName, + sortedCol, + sortOrder + ) + setSortOrder(newSortOrder) + setSortedCol(newSortedCol) + } + + const getDraftsPaginatedData = (drafts: IDeclaration[], pageId: number) => { + return drafts.slice((pageId - 1) * props.pageSize, pageId * props.pageSize) + } + + const transformDraftContent = () => { + const { locale } = intl + if (drafts.length <= 0) { + return [] + } + const paginatedDrafts = getDraftsPaginatedData(drafts, props.currentPage) + const items = paginatedDrafts.map((draft: IDeclaration, index) => { + let pageRoute: string + if (draft.event && draft.event.toString() === 'birth') { + pageRoute = DRAFT_BIRTH_PARENT_FORM_PAGE + } else if (draft.event && draft.event.toString() === 'death') { + pageRoute = DRAFT_DEATH_FORM_PAGE + } else if (draft.event && draft.event.toString() === 'marriage') { + pageRoute = DRAFT_MARRIAGE_FORM_PAGE + } + const name = getDeclarationFullName(draft, locale) + const lastModificationDate = draft.modifiedOn || draft.savedOn + const actions: IAction[] = [] + + if (width > theme.grid.breakpoints.lg) { + actions.push({ + label: intl.formatMessage(buttonMessages.update), + handler: ( + e: React.MouseEvent | undefined + ) => { + if (e) { + e.stopPropagation() + } + + navigate( + generateGoToPageUrl({ + pageRoute, + declarationId: draft.id, + pageId: 'preview', + event: (draft.event && draft.event.toString()) || '' + }) + ) + } + }) + } + actions.push({ + actionComponent: + }) + const event = + (draft.event && + intl.formatMessage( + dynamicConstantsMessages[draft.event.toLowerCase()] + )) || + '' + + const eventTime = + draft.event === EventType.Birth + ? draft.data.child?.childBirthDate || '' + : draft.event === EventType.Death + ? draft.data.deathEvent?.deathDate || '' + : draft.data.marriageEvent?.marriageDate || '' + + const dateOfEvent = isValidPlainDate(eventTime) + ? plainDateToLocalDate(eventTime) + : '' + const NameComponent = name ? ( + + navigate( + formatUrl(routes.DECLARATION_RECORD_AUDIT, { + tab: 'myDraftsTab', + declarationId: draft.id + }) + ) + } + > + {name} + + ) : ( + + navigate( + formatUrl(routes.DECLARATION_RECORD_AUDIT, { + tab: 'myDraftsTab', + declarationId: draft.id + }) + ) + } + > + {intl.formatMessage(constantsMessages.noNameProvided)} + + ) + return { + id: draft.id, + event, + name: (name && name.toString().toLowerCase()) || '', + iconWithName: ( + + ), + iconWithNameEvent: ( + + ), + lastUpdated: lastModificationDate || '', + dateOfEvent, + actions + } + }) + const sortedItems = getSortedItems(items, sortedCol, sortOrder) + + return sortedItems.map((item) => { + return { + ...item, + dateOfEvent: + item.dateOfEvent && formattedDuration(item.dateOfEvent as Date), + lastUpdated: + item.lastUpdated && formattedDuration(item.lastUpdated as number) + } + }) + } + + const getColumns = () => { + if (width > theme.grid.breakpoints.lg) { + return [ + { + label: intl.formatMessage(constantsMessages.name), + width: 30, + key: COLUMNS.ICON_WITH_NAME, + isSorted: sortedCol === COLUMNS.NAME, + sortFunction: onColumnClick + }, + { + label: intl.formatMessage(constantsMessages.event), + width: 16, + key: COLUMNS.EVENT, + isSorted: sortedCol === COLUMNS.EVENT, + sortFunction: onColumnClick + }, + { + label: intl.formatMessage(constantsMessages.eventDate), + width: 18, + key: COLUMNS.DATE_OF_EVENT, + isSorted: sortedCol === COLUMNS.DATE_OF_EVENT, + sortFunction: onColumnClick + }, + { + label: intl.formatMessage(constantsMessages.lastUpdated), + width: 18, + key: COLUMNS.LAST_UPDATED, + isSorted: sortedCol === COLUMNS.LAST_UPDATED, + sortFunction: onColumnClick + }, + { + width: 18, + key: COLUMNS.ACTIONS, + isActionColumn: true, + alignment: ColumnContentAlignment.RIGHT + } + ] + } else { + return [ + { + label: intl.formatMessage(constantsMessages.name), + width: 70, + key: COLUMNS.ICON_WITH_NAME_EVENT + }, + { + width: 30, + key: COLUMNS.ACTIONS, + isActionColumn: true, + alignment: ColumnContentAlignment.RIGHT + } + ] + } + } + const transformedDraftContent = transformDraftContent() + + const showPagination = drafts.length > props.pageSize ? true : false + + const totalPages = Math.ceil(drafts.length / props.pageSize) + + const noContent = transformedDraftContent.length <= 0 + + const noResultMessage = intl.formatMessage(wqMessages.noRecordsDraft) + + return ( + + + + ) +} diff --git a/packages/client/src/views/OfficeHome/queries.ts b/packages/client/src/views/OfficeHome/queries.ts index 04f8702108c..95af146f558 100644 --- a/packages/client/src/views/OfficeHome/queries.ts +++ b/packages/client/src/views/OfficeHome/queries.ts @@ -76,6 +76,7 @@ const EVENT_SEARCH_RESULT_FIELDS = gql` export const REGISTRATION_HOME_QUERY = gql` ${EVENT_SEARCH_RESULT_FIELDS} query registrationHome( + $userId: String! $declarationLocationId: String! $pageSize: Int $inProgressSkip: Int @@ -83,6 +84,7 @@ export const REGISTRATION_HOME_QUERY = gql` $reviewStatuses: [String] $reviewSkip: Int $rejectSkip: Int + $sentForReviewSkip: Int $approvalSkip: Int $externalValidationSkip: Int $printSkip: Int @@ -152,6 +154,26 @@ export const REGISTRATION_HOME_QUERY = gql` ...EventSearchFields } } + sentForReviewTab: searchEvents( + userId: $userId + advancedSearchParameters: { + declarationLocationId: $declarationLocationId + registrationStatuses: [ + "DECLARED" + "IN_PROGRESS" + "VALIDATED" + "WAITING_VALIDATION" + "REGISTERED" + ] + } + count: $pageSize + skip: $sentForReviewSkip + ) { + totalItems + results { + ...EventSearchFields + } + } approvalTab: searchEvents( advancedSearchParameters: { declarationLocationId: $declarationLocationId @@ -206,51 +228,3 @@ export const REGISTRATION_HOME_QUERY = gql` } } ` - -export const FIELD_AGENT_HOME_QUERY = gql` - ${EVENT_SEARCH_RESULT_FIELDS} - query fieldAgentHome( - $userId: String - $declarationLocationId: String! - $pageSize: Int - $reviewSkip: Int - $rejectSkip: Int - ) { - reviewTab: searchEvents( - userId: $userId - advancedSearchParameters: { - declarationLocationId: $declarationLocationId - registrationStatuses: [ - "DECLARED" - "IN_PROGRESS" - "VALIDATED" - "WAITING_VALIDATION" - "REGISTERED" - ] - } - count: $pageSize - skip: $reviewSkip - ) { - totalItems - results { - ...EventSearchFields - } - } - rejectTab: searchEvents( - userId: $userId - advancedSearchParameters: { - declarationLocationId: $declarationLocationId - registrationStatuses: ["REJECTED"] - } - count: $pageSize - skip: $rejectSkip - sortColumn: "createdAt.keyword" - sort: "asc" - ) { - totalItems - results { - ...EventSearchFields - } - } - } -` diff --git a/packages/client/src/views/OfficeHome/readyForReview/ReadyForReview.tsx b/packages/client/src/views/OfficeHome/readyForReview/ReadyForReview.tsx index f2d69160843..90fde2394bb 100644 --- a/packages/client/src/views/OfficeHome/readyForReview/ReadyForReview.tsx +++ b/packages/client/src/views/OfficeHome/readyForReview/ReadyForReview.tsx @@ -17,7 +17,6 @@ import { getScope } from '@client/profile/profileSelectors' import { transformData } from '@client/search/transformer' import { IStoreState } from '@client/store' import { ITheme } from '@opencrvs/components/lib/theme' -import { Scope, hasRegisterScope } from '@client/utils/authUtils' import { ColumnContentAlignment, Workqueue, @@ -61,8 +60,10 @@ import { getSortedItems } from '@client/views/OfficeHome/utils' import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' +import { SCOPES } from '@opencrvs/commons/client' import { RegStatus } from '@client/utils/gateway' import { useWindowSize } from '@opencrvs/components/lib/hooks' +import { usePermissions } from '@client/hooks/useAuthorization' import * as routes from '@client/navigation/routes' import { useNavigate } from 'react-router-dom' @@ -71,7 +72,6 @@ const ToolTipContainer = styled.span` ` interface IBaseReviewTabProps { theme: ITheme - scope: Scope | null outboxDeclarations: IDeclaration[] queryData: { data: GQLEventSearchResultSet @@ -87,7 +87,6 @@ type IReviewTabProps = IntlShapeProps & IBaseReviewTabProps const ReadyForReviewComponent = ({ theme, - scope, outboxDeclarations, queryData, paginationId, @@ -101,10 +100,7 @@ const ReadyForReviewComponent = ({ const { width } = useWindowSize() const [sortedCol, setSortedCol] = useState(COLUMNS.SENT_FOR_REVIEW) const [sortOrder, setSortOrder] = useState(SORT_ORDER.DESCENDING) - - const userHasRegisterScope = () => { - return scope && hasRegisterScope(scope) - } + const { hasScope } = usePermissions() const onColumnClick = (columnName: string) => { const { newSortedCol, newSortOrder } = changeSortedColumn( @@ -174,6 +170,7 @@ const ReadyForReviewComponent = ({ }} key={`DownloadButton-${index}`} status={downloadStatus as DOWNLOAD_STATUS} + declarationStatus={reg.declarationStatus as SUBMISSION_STATUS} /> ) }) @@ -186,9 +183,7 @@ const ReadyForReviewComponent = ({ '' const isValidatedOnReview = reg.declarationStatus === SUBMISSION_STATUS.VALIDATED && - userHasRegisterScope() - ? true - : false + hasScope(SCOPES.RECORD_REGISTER) const dateOfEvent = (reg.dateOfEvent && reg.dateOfEvent.length > 0 && diff --git a/packages/client/src/views/OfficeHome/readyForReview/readyForReview.test.tsx b/packages/client/src/views/OfficeHome/readyForReview/readyForReview.test.tsx index e5609ad317d..4eb7d779a5a 100644 --- a/packages/client/src/views/OfficeHome/readyForReview/readyForReview.test.tsx +++ b/packages/client/src/views/OfficeHome/readyForReview/readyForReview.test.tsx @@ -16,13 +16,14 @@ import { } from '@client/declarations' import { DownloadAction } from '@client/forms' import { EventType } from '@client/utils/gateway' -import { checkAuth } from '@client/profile/profileActions' import { queries } from '@client/profile/queries' import { createStore } from '@client/store' import { createTestComponent, mockUserResponse, + REGISTRAR_DEFAULT_SCOPES, resizeWindow, + setScopes, TestComponentWithRouteMock } from '@client/tests/util' import { waitForElement, waitFor } from '@client/tests/wait-for-element' @@ -43,10 +44,6 @@ import { formattedDuration } from '@client/utils/date-formatting' import { birthDeclarationForReview } from '@client/tests/mock-graphql-responses' import { vi, Mock } from 'vitest' -const registerScopeToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsImNlcnRpZnkiLCJkZW1vIl0sImlhdCI6MTU0MjY4ODc3MCwiZXhwIjoxNTQzMjkzNTcwLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI1YmVhYWY2MDg0ZmRjNDc5MTA3ZjI5OGMifQ.ElQd99Lu7WFX3L_0RecU_Q7-WZClztdNpepo7deNHqzro-Cog4WLN7RW3ZS5PuQtMaiOq1tCb-Fm3h7t4l4KDJgvC11OyT7jD6R2s2OleoRVm3Mcw5LPYuUVHt64lR_moex0x_bCqS72iZmjrjS-fNlnWK5zHfYAjF2PWKceMTGk6wnI9N49f6VwwkinJcwJi6ylsjVkylNbutQZO0qTc7HRP-cBfAzNcKD37FqTRNpVSvHdzQSNcs7oiv3kInDN5aNa2536XSd3H-RiKR9hm9eID9bSIJgFIGzkWRd5jnoYxT70G0t03_mTVnDnqPXDtyI-lmerx24Ost0rQLUNIg' -const getItem = window.localStorage.getItem as Mock - const nameObj = { data: { getUser: { @@ -237,8 +234,7 @@ describe('OfficeHome sent for review tab related tests', () => { apolloClient = createClient(store) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) }) it('should show pagination bar if items more than 11 in ReviewTab', async () => { @@ -651,8 +647,7 @@ describe('OfficeHome sent for review tab related tests', () => { { store, graphqlMocks } ) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) testComponent = createdTestComponent }) @@ -809,8 +804,7 @@ describe('Tablet tests', () => { { store } ) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) const row = await waitForElement(testComponent, '#name_0') row.hostNodes().simulate('click') diff --git a/packages/client/src/views/OfficeHome/readyToIssue/ReadyToIssue.tsx b/packages/client/src/views/OfficeHome/readyToIssue/ReadyToIssue.tsx index 59ce780018d..0c67a2d4886 100644 --- a/packages/client/src/views/OfficeHome/readyToIssue/ReadyToIssue.tsx +++ b/packages/client/src/views/OfficeHome/readyToIssue/ReadyToIssue.tsx @@ -30,7 +30,8 @@ import { import { IStoreState } from '@client/store' import { DOWNLOAD_STATUS, - clearCorrectionAndPrintChanges + clearCorrectionAndPrintChanges, + SUBMISSION_STATUS } from '@client/declarations' import { DownloadAction } from '@client/forms' import { DownloadButton } from '@client/components/interface/DownloadButton' @@ -198,6 +199,7 @@ export const ReadyToIssue = ({ }} key={`DownloadButton-${index}`} status={downloadStatus} + declarationStatus={reg.declarationStatus as SUBMISSION_STATUS} /> ) }) diff --git a/packages/client/src/views/OfficeHome/readyToPrint/ReadyToPrint.tsx b/packages/client/src/views/OfficeHome/readyToPrint/ReadyToPrint.tsx index 4d5bbcfa9ef..81f414606a2 100644 --- a/packages/client/src/views/OfficeHome/readyToPrint/ReadyToPrint.tsx +++ b/packages/client/src/views/OfficeHome/readyToPrint/ReadyToPrint.tsx @@ -34,7 +34,8 @@ import { IStoreState } from '@client/store' import { IDeclaration, DOWNLOAD_STATUS, - clearCorrectionAndPrintChanges + clearCorrectionAndPrintChanges, + SUBMISSION_STATUS } from '@client/declarations' import { DownloadAction } from '@client/forms' import { DownloadButton } from '@client/components/interface/DownloadButton' @@ -195,6 +196,7 @@ function ReadyToPrintComponent(props: IPrintTabProps) { }} key={`DownloadButton-${index}`} status={downloadStatus} + declarationStatus={reg.declarationStatus as SUBMISSION_STATUS} /> ) }) diff --git a/packages/client/src/views/OfficeHome/readyToPrint/readyToPrint.test.tsx b/packages/client/src/views/OfficeHome/readyToPrint/readyToPrint.test.tsx index 2229cc78e31..1ea2e402203 100644 --- a/packages/client/src/views/OfficeHome/readyToPrint/readyToPrint.test.tsx +++ b/packages/client/src/views/OfficeHome/readyToPrint/readyToPrint.test.tsx @@ -15,14 +15,15 @@ import { } from '@client/declarations' import { DownloadAction } from '@client/forms' import { EventType } from '@client/utils/gateway' -import { checkAuth } from '@client/profile/profileActions' import { queries } from '@client/profile/queries' import { storage } from '@client/storage' import { createStore } from '@client/store' import { createTestComponent, mockUserResponse, + REGISTRAR_DEFAULT_SCOPES, resizeWindow, + setScopes, TestComponentWithRouteMock } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' @@ -37,14 +38,10 @@ import type { GQLDeathEventSearchSet } from '@client/utils/gateway-deprecated-do-not-use' import { formattedDuration } from '@client/utils/date-formatting' -import { vi, Mock } from 'vitest' +import { vi } from 'vitest' import { formatUrl } from '@client/navigation' import { REGISTRAR_HOME_TAB } from '@client/navigation/routes' -const registerScopeToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsImNlcnRpZnkiLCJkZW1vIl0sImlhdCI6MTU0MjY4ODc3MCwiZXhwIjoxNTQzMjkzNTcwLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI1YmVhYWY2MDg0ZmRjNDc5MTA3ZjI5OGMifQ.ElQd99Lu7WFX3L_0RecU_Q7-WZClztdNpepo7deNHqzro-Cog4WLN7RW3ZS5PuQtMaiOq1tCb-Fm3h7t4l4KDJgvC11OyT7jD6R2s2OleoRVm3Mcw5LPYuUVHt64lR_moex0x_bCqS72iZmjrjS-fNlnWK5zHfYAjF2PWKceMTGk6wnI9N49f6VwwkinJcwJi6ylsjVkylNbutQZO0qTc7HRP-cBfAzNcKD37FqTRNpVSvHdzQSNcs7oiv3kInDN5aNa2536XSd3H-RiKR9hm9eID9bSIJgFIGzkWRd5jnoYxT70G0t03_mTVnDnqPXDtyI-lmerx24Ost0rQLUNIg' -const getItem = window.localStorage.getItem as Mock - const mockFetchUserDetails = vi.fn() const mockListSyncController = vi.fn() @@ -243,8 +240,7 @@ describe('RegistrarHome ready to print tab related tests', () => { const client = createClient(store) beforeAll(async () => { - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) }) it('renders all items returned from graphql query in ready for print', async () => { @@ -730,26 +726,18 @@ describe('RegistrarHome ready to print tab related tests', () => { ).toHaveLength(1) testComponent.component.find('#assign').hostNodes().simulate('click') - expect( - testComponent.component - .find('#action-loading-ListItemAction-0') - .hostNodes() - ).toHaveLength(1) - - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.component.update() const action = await waitForElement( testComponent.component, '#ListItemAction-0-Print' ) + action.hostNodes().simulate('click') await new Promise((resolve) => { setTimeout(resolve, 100) }) + testComponent.component.update() expect(testComponent.router.state.location.pathname).toBe( '/cert/collector/956281c9-1f47-4c26-948a-970dd23c4094/death/certCollector' @@ -780,8 +768,7 @@ describe('Tablet tests', () => { const { store } = createStore() beforeAll(async () => { - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) resizeWindow(800, 1280) }) diff --git a/packages/client/src/views/OfficeHome/requiresUpdate/RequiresUpdate.tsx b/packages/client/src/views/OfficeHome/requiresUpdate/RequiresUpdate.tsx index 3ad0cf06bf1..3bb5e168cf1 100644 --- a/packages/client/src/views/OfficeHome/requiresUpdate/RequiresUpdate.tsx +++ b/packages/client/src/views/OfficeHome/requiresUpdate/RequiresUpdate.tsx @@ -14,7 +14,7 @@ import { getScope } from '@client/profile/profileSelectors' import { transformData } from '@client/search/transformer' import { IStoreState } from '@client/store' import { ITheme } from '@opencrvs/components/lib/theme' -import { Scope } from '@client/utils/authUtils' + import { ColumnContentAlignment, Workqueue, @@ -33,7 +33,11 @@ import { dynamicConstantsMessages, wqMessages } from '@client/i18n/messages' -import { IDeclaration, DOWNLOAD_STATUS } from '@client/declarations' +import { + IDeclaration, + DOWNLOAD_STATUS, + SUBMISSION_STATUS +} from '@client/declarations' import { DownloadAction } from '@client/forms' import { DownloadButton } from '@client/components/interface/DownloadButton' import { @@ -53,6 +57,7 @@ import { NameContainer } from '@client/views/OfficeHome/components' import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { RegStatus } from '@client/utils/gateway' import { useState } from 'react' import { useWindowSize } from '@opencrvs/components/lib/hooks' @@ -61,11 +66,11 @@ import { useNavigate } from 'react-router-dom' interface IBaseRejectTabProps { theme: ITheme - scope: Scope | null outboxDeclarations: IDeclaration[] queryData: { data: GQLEventSearchResultSet } + scope: Scope[] | null paginationId: number pageSize: number onPageChange: (newPageNumber: number) => void @@ -152,7 +157,15 @@ function RequiresUpdateComponent(props: IRejectTabProps) { if (!data || !data.results) { return [] } - const isFieldAgent = props.scope?.includes('declare') ? true : false + + const validateScopes = [ + SCOPES.RECORD_REGISTER, + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES + ] as Scope[] + + const isReviewer = props.scope?.some((x) => validateScopes.includes(x)) + const transformedData = transformData(data, props.intl) const items = transformedData.map((reg, index) => { const actions = [] as IAction[] @@ -163,7 +176,7 @@ function RequiresUpdateComponent(props: IRejectTabProps) { const isDuplicate = reg.duplicates && reg.duplicates.length > 0 if (downloadStatus !== DOWNLOAD_STATUS.DOWNLOADED) { - if (width > props.theme.grid.breakpoints.lg && !isFieldAgent) { + if (width > props.theme.grid.breakpoints.lg && isReviewer) { actions.push({ label: props.intl.formatMessage(buttonMessages.update), handler: () => {}, @@ -171,7 +184,7 @@ function RequiresUpdateComponent(props: IRejectTabProps) { }) } } else { - if (width > props.theme.grid.breakpoints.lg && !isFieldAgent) { + if (width > props.theme.grid.breakpoints.lg && isReviewer) { actions.push({ label: props.intl.formatMessage(buttonMessages.update), handler: ( @@ -204,6 +217,7 @@ function RequiresUpdateComponent(props: IRejectTabProps) { }} key={`DownloadButton-${index}`} status={downloadStatus as DOWNLOAD_STATUS} + declarationStatus={reg.declarationStatus as SUBMISSION_STATUS} /> ) }) diff --git a/packages/client/src/views/OfficeHome/requiresUpdate/requiresUpdate.test.tsx b/packages/client/src/views/OfficeHome/requiresUpdate/requiresUpdate.test.tsx index a50c419f603..60cb571978c 100644 --- a/packages/client/src/views/OfficeHome/requiresUpdate/requiresUpdate.test.tsx +++ b/packages/client/src/views/OfficeHome/requiresUpdate/requiresUpdate.test.tsx @@ -15,7 +15,6 @@ import { } from '@client/declarations' import { DownloadAction } from '@client/forms' import { EventType } from '@client/utils/gateway' -import { checkAuth } from '@client/profile/profileActions' import { queries } from '@client/profile/queries' import { storage } from '@client/storage' import { createStore } from '@client/store' @@ -23,8 +22,11 @@ import { createTestComponent, mockUserResponse, resizeWindow, - registrationClerkScopeToken, - TestComponentWithRouteMock + REGISTRATION_AGENT_DEFAULT_SCOPES, + setScopes, + REGISTRAR_DEFAULT_SCOPES, + TestComponentWithRouteMock, + flushPromises } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' import { createClient } from '@client/utils/apolloClient' @@ -38,16 +40,12 @@ import type { GQLDeathEventSearchSet } from '@client/utils/gateway-deprecated-do-not-use' import { formattedDuration } from '@client/utils/date-formatting' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { birthDeclarationForReview } from '@client/tests/mock-graphql-responses' -import { vi, Mock } from 'vitest' +import { vi } from 'vitest' import { formatUrl } from '@client/navigation' import { REGISTRAR_HOME_TAB_PAGE } from '@client/navigation/routes' -const registerScopeToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsImNlcnRpZnkiLCJkZW1vIl0sImlhdCI6MTU0MjY4ODc3MCwiZXhwIjoxNTQzMjkzNTcwLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI1YmVhYWY2MDg0ZmRjNDc5MTA3ZjI5OGMifQ.ElQd99Lu7WFX3L_0RecU_Q7-WZClztdNpepo7deNHqzro-Cog4WLN7RW3ZS5PuQtMaiOq1tCb-Fm3h7t4l4KDJgvC11OyT7jD6R2s2OleoRVm3Mcw5LPYuUVHt64lR_moex0x_bCqS72iZmjrjS-fNlnWK5zHfYAjF2PWKceMTGk6wnI9N49f6VwwkinJcwJi6ylsjVkylNbutQZO0qTc7HRP-cBfAzNcKD37FqTRNpVSvHdzQSNcs7oiv3kInDN5aNa2536XSd3H-RiKR9hm9eID9bSIJgFIGzkWRd5jnoYxT70G0t03_mTVnDnqPXDtyI-lmerx24Ost0rQLUNIg' -const getItem = window.localStorage.getItem as Mock - const mockFetchUserDetails = vi.fn() const mockListSyncController = vi.fn() @@ -165,8 +163,7 @@ describe('OfficeHome sent for update tab related tests', () => { const client = createClient(store) beforeAll(async () => { - getItem.mockReturnValue(registrationClerkScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRATION_AGENT_DEFAULT_SCOPES, store) }) it('renders all items returned from graphql query in sent for update tab', async () => { @@ -429,8 +426,7 @@ describe('OfficeHome sent for update tab related tests', () => { } ) testComponent = createdTestComponent - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRATION_AGENT_DEFAULT_SCOPES, store) }) it('downloads the declaration after clicking download button', async () => { @@ -452,12 +448,7 @@ describe('OfficeHome sent for update tab related tests', () => { testComponent.component .find('#action-loading-ListItemAction-0') .hostNodes() - ).toHaveLength(1) - - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - testComponent.component.update() + ) const action = await waitForElement( testComponent.component, @@ -469,6 +460,7 @@ describe('OfficeHome sent for update tab related tests', () => { setTimeout(resolve, 100) }) testComponent.component.update() + expect(testComponent.router.state.location.pathname).toBe( '/reviews/9a55d213-ad9f-4dcd-9418-340f3a7f6269/events/birth/parent/review' ) @@ -496,8 +488,7 @@ describe('Tablet tests', () => { const { store } = createStore() beforeAll(async () => { - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) resizeWindow(800, 1280) }) diff --git a/packages/client/src/views/OfficeHome/sentForReview/SentForReview.test.tsx b/packages/client/src/views/OfficeHome/sentForReview/SentForReview.test.tsx index be1b97d449f..86d557e8861 100644 --- a/packages/client/src/views/OfficeHome/sentForReview/SentForReview.test.tsx +++ b/packages/client/src/views/OfficeHome/sentForReview/SentForReview.test.tsx @@ -8,17 +8,18 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { checkAuth } from '@client/profile/profileActions' + import { AppStore, createStore } from '@client/store' import { createTestComponent, mockUserResponse, - resizeWindow + REGISTRATION_AGENT_DEFAULT_SCOPES, + resizeWindow, + setScopes } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' import { Workqueue } from '@opencrvs/components/lib/Workqueue' -import { readFileSync } from 'fs' -import * as jwt from 'jsonwebtoken' + import { merge } from 'lodash' import * as React from 'react' import { SentForReview } from './SentForReview' @@ -27,19 +28,9 @@ import type { GQLDeathEventSearchSet } from '@client/utils/gateway-deprecated-do-not-use' import { formattedDuration } from '@client/utils/date-formatting' -import { vi, Mock } from 'vitest' +import { vi } from 'vitest' import { EventType } from '@client/utils/gateway' -const validateScopeToken = jwt.sign( - { scope: ['validate'] }, - readFileSync('./test/cert.key'), - { - algorithm: 'RS256', - issuer: 'opencrvs:auth-service', - audience: 'opencrvs:gateway-user' - } -) - const nameObj = { data: { getUser: { @@ -134,15 +125,12 @@ for (let i = 0; i < 14; i++) { } merge(mockUserResponse, nameObj) -const getItem = window.localStorage.getItem as Mock - describe('RegistrationHome sent for approval tab related tests', () => { let store: AppStore beforeEach(async () => { ;({ store } = createStore()) - getItem.mockReturnValue(validateScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRATION_AGENT_DEFAULT_SCOPES, store) }) it('renders all items returned from graphql query in sent for approval', async () => { @@ -461,8 +449,7 @@ describe('Tablet tests', () => { const { store } = createStore() beforeAll(async () => { - getItem.mockReturnValue(validateScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRATION_AGENT_DEFAULT_SCOPES, store) resizeWindow(800, 1280) }) diff --git a/packages/client/src/views/OfficeHome/sentForReview/SentForReview.tsx b/packages/client/src/views/OfficeHome/sentForReview/SentForReview.tsx index 902330bad6d..16d5c7f1782 100644 --- a/packages/client/src/views/OfficeHome/sentForReview/SentForReview.tsx +++ b/packages/client/src/views/OfficeHome/sentForReview/SentForReview.tsx @@ -8,7 +8,11 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { IDeclaration, DOWNLOAD_STATUS } from '@client/declarations' +import { + IDeclaration, + DOWNLOAD_STATUS, + SUBMISSION_STATUS +} from '@client/declarations' import { constantsMessages, dynamicConstantsMessages, @@ -50,10 +54,9 @@ import { NameContainer } from '@client/views/OfficeHome/components' import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' -import { Scope } from '@client/utils/authUtils' import { DownloadButton } from '@client/components/interface/DownloadButton' import { DownloadAction } from '@client/forms' -import { Downloaded } from '@opencrvs/components/lib/icons/Downloaded' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { RegStatus } from '@client/utils/gateway' import { useState } from 'react' import { useWindowSize } from '@opencrvs/components/src/hooks' @@ -66,7 +69,7 @@ const ToolTipContainer = styled.span` interface IBaseApprovalTabProps { theme: ITheme outboxDeclarations: IDeclaration[] - scope: Scope | null + scope: Scope[] | null queryData: { data: GQLEventSearchResultSet } @@ -85,7 +88,9 @@ function SentForReviewComponent(props: IApprovalTabProps) { const [sortedCol, setSortedCol] = useState(COLUMNS.SENT_FOR_APPROVAL) const [sortOrder, setSortOrder] = useState(SORT_ORDER.DESCENDING) - const isFieldAgent = props.scope?.includes('declare') + const canSendForApproval = props.scope?.includes( + SCOPES.RECORD_SUBMIT_FOR_APPROVAL + ) const onColumnClick = (columnName: string) => { const { newSortedCol, newSortOrder } = changeSortedColumn( @@ -122,9 +127,9 @@ function SentForReviewComponent(props: IApprovalTabProps) { sortFunction: onColumnClick }, { - label: isFieldAgent - ? props.intl.formatMessage(navigationMessages.sentForReview) - : props.intl.formatMessage(navigationMessages.approvals), + label: canSendForApproval + ? props.intl.formatMessage(navigationMessages.approvals) + : props.intl.formatMessage(navigationMessages.sentForReview), width: 18, key: COLUMNS.SENT_FOR_APPROVAL, isSorted: sortedCol === COLUMNS.SENT_FOR_APPROVAL, @@ -168,27 +173,22 @@ function SentForReviewComponent(props: IApprovalTabProps) { const downloadStatus = (foundDeclaration && foundDeclaration.downloadStatus) || undefined - if (downloadStatus !== DOWNLOAD_STATUS.DOWNLOADED) { - actions.push({ - actionComponent: ( - - ) - }) - } else { - actions.push({ - actionComponent: - }) - } + actions.push({ + actionComponent: ( + + ) + }) + const event = (reg.event && intl.formatMessage( @@ -197,7 +197,7 @@ function SentForReviewComponent(props: IApprovalTabProps) { '' let sentForApproval - if (isFieldAgent) { + if (!canSendForApproval) { sentForApproval = getPreviousOperationDateByOperationType( reg.operationHistories, @@ -235,7 +235,7 @@ function SentForReviewComponent(props: IApprovalTabProps) { onClick={() => navigate( formatUrl(routes.DECLARATION_RECORD_AUDIT, { - tab: isFieldAgent ? 'reviewTab' : 'approvalTab', + tab: canSendForApproval ? 'approvalTab' : 'reviewTab', declarationId: reg.id }) ) @@ -249,7 +249,7 @@ function SentForReviewComponent(props: IApprovalTabProps) { onClick={() => navigate( formatUrl(routes.DECLARATION_RECORD_AUDIT, { - tab: isFieldAgent ? 'reviewTab' : 'approvalTab', + tab: canSendForApproval ? 'approvalTab' : 'reviewTab', declarationId: reg.id }) ) @@ -313,12 +313,12 @@ function SentForReviewComponent(props: IApprovalTabProps) { props.queryData.data.totalItems > pageSize ? true : false - const noResultText = isFieldAgent - ? intl.formatMessage(wqMessages.noRecordsSentForReview) - : intl.formatMessage(wqMessages.noRecordsSentForApproval) - const title = isFieldAgent - ? intl.formatMessage(navigationMessages.sentForReview) - : intl.formatMessage(navigationMessages.approvals) + const noResultText = canSendForApproval + ? intl.formatMessage(wqMessages.noRecordsSentForApproval) + : intl.formatMessage(wqMessages.noRecordsSentForReview) + const title = canSendForApproval + ? intl.formatMessage(navigationMessages.approvals) + : intl.formatMessage(navigationMessages.sentForReview) return ( { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.ORGANISATION_READ_LOCATIONS_MY_JURISDICTION], store) + }) + + it('link should be enabled if office is under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + + const { component } = await createTestComponent(, { + store, + path: ORGANISATIONS_INDEX, + initialEntries: [ + formatUrl(ORGANISATIONS_INDEX, { + locationId: '7a18cb4c-38f3-449f-b3dc-508473d485f3' + }) + ] + }) + + store.dispatch( + setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect( + component + .find({ children: 'Moktarpur Union Parishad' }) + .hostNodes() + .first() + .prop('disabled') + ).toBe(false) + }) + + it('link should be disabled if office is not under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + + const { component } = await createTestComponent(, { + store, + path: ORGANISATIONS_INDEX, + initialEntries: [ + formatUrl(ORGANISATIONS_INDEX, { + locationId: '6e1f3bce-7bcb-4bf6-8e35-0d9facdf158b' + }) + ] + }) + store.dispatch( + setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect( + component + .find({ children: 'Dhaka Union Parishad' }) + .hostNodes() + .first() + .prop('disabled') + ).toBe(true) + }) +}) + +describe('for user with read organisation scope', () => { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.ORGANISATION_READ_LOCATIONS], store) + }) + + it('link should be enabled if office is under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + + const { component } = await createTestComponent(, { + store, + path: ORGANISATIONS_INDEX, + initialEntries: [ + formatUrl(ORGANISATIONS_INDEX, { + locationId: '7a18cb4c-38f3-449f-b3dc-508473d485f3' + }) + ] + }) + + store.dispatch( + setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect( + component + .find({ children: 'Moktarpur Union Parishad' }) + .hostNodes() + .first() + .prop('disabled') + ).toBe(false) + }) + + it('link should be enabled even if office is not under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + + const { component } = await createTestComponent(, { + store, + path: ORGANISATIONS_INDEX, + initialEntries: [ + formatUrl(ORGANISATIONS_INDEX, { + locationId: '6e1f3bce-7bcb-4bf6-8e35-0d9facdf158b' + }) + ] + }) + + store.dispatch( + setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect( + component + .find({ children: 'Dhaka Union Parishad' }) + .hostNodes() + .first() + .prop('disabled') + ).toBe(false) + }) +}) + +describe('for user with read organisation my office scope', () => { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.ORGANISATION_READ_LOCATIONS_MY_OFFICE], store) + }) + + it("link should be enabled if user's office", async () => { + const userOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' + + const { component } = await createTestComponent(, { + store, + path: ORGANISATIONS_INDEX, + initialEntries: [ + formatUrl(ORGANISATIONS_INDEX, { + locationId: '7a18cb4c-38f3-449f-b3dc-508473d485f3' + }) + ] + }) + + store.dispatch( + setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect( + component + .find({ children: 'Moktarpur Union Parishad' }) + .hostNodes() + .first() + .prop('disabled') + ).toBe(false) + }) + + it('link should be disabled for other offices', async () => { + const userOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' + + const { component } = await createTestComponent(, { + store, + path: ORGANISATIONS_INDEX, + initialEntries: [ + formatUrl(ORGANISATIONS_INDEX, { + locationId: '5926982b-845c-4463-80aa-cbfb86762e0a' + }) + ] + }) + store.dispatch( + setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + + expect( + component + .find({ children: 'Comilla Union Parishad' }) + .hostNodes() + .first() + .prop('disabled') + ).toBe(true) + }) +}) diff --git a/packages/client/src/views/Organisation/AdministrativeLevels.tsx b/packages/client/src/views/Organisation/AdministrativeLevels.tsx index 5c29c206fb1..b05b7a8ef17 100644 --- a/packages/client/src/views/Organisation/AdministrativeLevels.tsx +++ b/packages/client/src/views/Organisation/AdministrativeLevels.tsx @@ -36,6 +36,7 @@ import startOfMonth from 'date-fns/startOfMonth' import subMonths from 'date-fns/subMonths' import styled from 'styled-components' import { getLocalizedLocationName } from '@client/utils/locationUtils' +import { usePermissions } from '@client/hooks/useAuthorization' import * as routes from '@client/navigation/routes' import { stringify } from 'querystring' @@ -61,6 +62,7 @@ const NoRecord = styled.div<{ isFullPage?: boolean }>` export function AdministrativeLevels() { const intl = useIntl() const { locationId } = useParams() + const { canAccessOffice } = usePermissions() const navigate = useNavigate() const getNewLevel = @@ -74,15 +76,13 @@ export function AdministrativeLevels() { [key: string]: ILocation } - let childLocations = Object.values(locations).filter( - (s) => s.partOf === `Location/${location}` - ) - - if (!childLocations.length) { - childLocations = Object.values(offices).filter( - (s) => s.partOf === `Location/${location}` + const childLocations = Object.values(locations) + .filter((s) => s.partOf === `Location/${location}`) + .concat( + Object.values(offices).filter( + (s) => s.partOf === `Location/${location}` + ) ) - } let dataOfBreadCrumb: IBreadCrumbData[] = [ { @@ -172,15 +172,19 @@ export function AdministrativeLevels() { { - if (level.type === 'ADMIN_STRUCTURE') { + level.type === 'ADMIN_STRUCTURE' ? ( + { setCurrentPageNumber(1) changeLevelAction(e, level.id) - } - - if (level.type === 'CRVS_OFFICE') { + }} + > + {getLocalizedLocationName(intl, level)} + + ) : level.type === 'CRVS_OFFICE' ? ( + navigate({ pathname: routes.TEAM_USER_LIST, search: stringify({ @@ -188,10 +192,10 @@ export function AdministrativeLevels() { }) }) } - }} - > - {getLocalizedLocationName(intl, level)} - + > + {getLocalizedLocationName(intl, level)} + + ) : null } actions={ - ) - } - return ( - - ) - } - return <> -} - -export const ShowReviewButton = ({ - declaration, - intl, - userDetails, - draft -}: CMethodParams) => { - const navigate = useNavigate() - const { id, type } = declaration || {} - - const isDownloaded = draft?.downloadStatus === DOWNLOAD_STATUS.DOWNLOADED - const systemRole = userDetails ? userDetails.systemRole : '' - const showActionButton = systemRole - ? FIELD_AGENT_ROLES.includes(systemRole) - ? false - : true - : false - - const reviewButtonRoleStatusMap: { [key: string]: string[] } = { - FIELD_AGENT: [], - REGISTRATION_AGENT: [EVENT_STATUS.DECLARED], - DISTRICT_REGISTRAR: [ - EVENT_STATUS.VALIDATED, - EVENT_STATUS.DECLARED, - EVENT_STATUS.CORRECTION_REQUESTED - ], - LOCAL_REGISTRAR: [ - EVENT_STATUS.VALIDATED, - EVENT_STATUS.DECLARED, - EVENT_STATUS.CORRECTION_REQUESTED - ], - NATIONAL_REGISTRAR: [ - EVENT_STATUS.VALIDATED, - EVENT_STATUS.DECLARED, - EVENT_STATUS.CORRECTION_REQUESTED - ] - } - - if ( - systemRole && - type && - systemRole in reviewButtonRoleStatusMap && - reviewButtonRoleStatusMap[systemRole].includes( - declaration?.status as string - ) && - showActionButton - ) { - if (!isDownloaded) { - return ( - - {intl.formatMessage(constantsMessages.review)} - - ) - } - return ( - { - navigate( - generateGoToPageUrl({ - pageRoute: - declaration.status === EVENT_STATUS.CORRECTION_REQUESTED - ? REVIEW_CORRECTION - : REVIEW_EVENT_PARENT_FORM_PAGE, - declarationId: id, - pageId: 'review', - event: type - }) - ) - }} - > - {intl.formatMessage(constantsMessages.review)} - - ) - } - return <> -} diff --git a/packages/client/src/views/RecordAudit/ActionDetailsModal.tsx b/packages/client/src/views/RecordAudit/ActionDetailsModal.tsx index b2e8cd407f2..e6fb42702db 100644 --- a/packages/client/src/views/RecordAudit/ActionDetailsModal.tsx +++ b/packages/client/src/views/RecordAudit/ActionDetailsModal.tsx @@ -45,6 +45,8 @@ import { recordAuditMessages } from '@client/i18n/messages/views/recordAudit' import { formatLongDate } from '@client/utils/date-formatting' import { EMPTY_STRING } from '@client/utils/constants' import { IReviewFormState } from '@client/forms/register/reviewReducer' +import { useSelector } from 'react-redux' +import { getScope } from '@client/profile/profileSelectors' interface IActionDetailsModalListTable { actionDetailsData: History @@ -406,7 +408,12 @@ const ActionDetailsModalListTable = ({ return {} } - const name = certificate.collector?.name + const name = certificate.certifier?.name + ? getIndividualNameObj( + certificate.certifier.name, + window.config.LANGUAGES + ) + : certificate.collector?.name ? getIndividualNameObj( certificate.collector.name, window.config.LANGUAGES @@ -421,9 +428,15 @@ const ActionDetailsModalListTable = ({ }` if (relation) return `${collectorName} (${intl.formatMessage(relation.label)})` - if (certificate.collector?.relationship === 'PRINT_IN_ADVANCE') { - return `${collectorName} (${certificate.collector?.otherRelationship})` - } + + if (certificate.certifier?.role) + return `${collectorName} (${intl.formatMessage( + certificate.certifier.role.label + )})` + + if (certificate.collector?.relationship === 'PRINT_IN_ADVANCE') + return `${collectorName} (${certificate.collector.otherRelationship})` + return collectorName } @@ -446,6 +459,7 @@ const ActionDetailsModalListTable = ({ { key: 'collector', label: + !collectorData.relationship || // relationship should not be available if certifier is found for certificate collectorData.relationship === 'PRINT_IN_ADVANCE' ? intl.formatMessage(certificateMessages.printedOnAdvance) : intl.formatMessage(certificateMessages.printedOnCollection), @@ -684,6 +698,7 @@ export const ActionDetailsModal = ({ offlineData: Partial draft: IDeclaration | null }) => { + const scopes = useSelector(getScope) if (isEmpty(actionDetailsData)) return <> const title = getStatusLabel( @@ -691,7 +706,8 @@ export const ActionDetailsModal = ({ actionDetailsData.regStatus, intl, actionDetailsData.user, - userDetails + userDetails, + scopes ) let userName = '' diff --git a/packages/client/src/views/RecordAudit/ActionMenu.test.tsx b/packages/client/src/views/RecordAudit/ActionMenu.test.tsx index bacb3a9cfe8..7f708dcd6d8 100644 --- a/packages/client/src/views/RecordAudit/ActionMenu.test.tsx +++ b/packages/client/src/views/RecordAudit/ActionMenu.test.tsx @@ -10,7 +10,11 @@ */ import * as React from 'react' -import { createTestComponent, flushPromises } from '@client/tests/util' +import { + createTestComponent, + flushPromises, + setScopes +} from '@client/tests/util' import { createStore } from '@client/store' import { ReactWrapper } from 'enzyme' import { @@ -19,7 +23,7 @@ import { SUBMISSION_STATUS } from '@client/declarations' import { ActionMenu } from './ActionMenu' -import { Scope } from '@sentry/react' +import { SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' import { vi } from 'vitest' @@ -74,13 +78,6 @@ const draftDeathNotDownloaded = { downloadStatus: DOWNLOAD_STATUS.READY_TO_DOWNLOAD } as unknown as IDeclaration -const SCOPES = { - FA: ['declare'] as any as Scope, - RA: ['validate'] as any as Scope, - REGISTRAR: ['register'] as any as Scope, - NONE: [] as any as Scope -} - enum ACTION_STATUS { HIDDEN = 'Hidden', ENABLED = 'Enabled', @@ -122,30 +119,24 @@ const actionStatus = ( } describe('View action', () => { - const VIEW_SCOPES = SCOPES.NONE it('Draft', async () => { const { store } = createStore() - const { component, router } = await createTestComponent( + setScopes([], store) + const { component } = await createTestComponent( {}} />, { store } ) - const { status, node } = actionStatus(component, [ACTION.VIEW_DECLARATION]) - expect(status).toBe(ACTION_STATUS.ENABLED) - - node?.simulate('click') + const { status } = actionStatus(component, [ACTION.VIEW_DECLARATION]) - expect(router.state.location.pathname).toContain( - defaultDeclaration.id + '/viewRecord' - ) + expect(status).toBe(ACTION_STATUS.HIDDEN) }) it('In progress', async () => { @@ -156,7 +147,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.IN_PROGRESS }} - scope={VIEW_SCOPES} draft={draftDeathDownloaded} toggleDisplayDialog={() => {}} />, @@ -181,7 +171,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.DECLARED }} - scope={VIEW_SCOPES} draft={draftBirthNotDownloaded} toggleDisplayDialog={() => {}} />, @@ -206,7 +195,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.DECLARED }} - scope={VIEW_SCOPES} draft={draftDeathNotDownloaded} duplicates={['duplicate1']} toggleDisplayDialog={() => {}} @@ -232,7 +220,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.REJECTED }} - scope={VIEW_SCOPES} draft={draftBirthDownloaded} toggleDisplayDialog={() => {}} />, @@ -257,7 +244,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.VALIDATED }} - scope={VIEW_SCOPES} draft={draftBirthDownloaded} toggleDisplayDialog={() => {}} />, @@ -282,7 +268,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.ARCHIVED }} - scope={VIEW_SCOPES} draft={draftBirthDownloaded} toggleDisplayDialog={() => {}} />, @@ -307,7 +292,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.REGISTERED }} - scope={VIEW_SCOPES} draft={draftBirthDownloaded} toggleDisplayDialog={() => {}} />, @@ -332,7 +316,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.CERTIFIED }} - scope={VIEW_SCOPES} draft={draftBirthDownloaded} toggleDisplayDialog={() => {}} />, @@ -357,7 +340,6 @@ describe('View action', () => { ...defaultDeclaration, status: SUBMISSION_STATUS.CORRECTION_REQUESTED }} - scope={VIEW_SCOPES} draft={draftBirthDownloaded} toggleDisplayDialog={() => {}} />, @@ -376,62 +358,58 @@ describe('View action', () => { }) describe('Review action', () => { - const REVIEW_SCOPES = SCOPES.RA it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, { store } ) - const { status } = actionStatus(component, [ - ACTION.REVIEW_DECLARATION, - ACTION.REVIEW_CORRECTION_REQUEST, - ACTION.REVIEW_POTENTIAL_DUPLICATE - ]) + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) it('In progress', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( {}} />, { store } ) - const { status } = actionStatus(component, [ - ACTION.REVIEW_DECLARATION, - ACTION.REVIEW_CORRECTION_REQUEST, - ACTION.REVIEW_POTENTIAL_DUPLICATE - ]) + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('In review - Downloaded', async () => { + it('In review - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component, router } = await createTestComponent( {}} />, @@ -454,13 +432,14 @@ describe('Review action', () => { it('In review - Not downloaded - Has Scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( {}} />, @@ -473,13 +452,199 @@ describe('Review action', () => { it('In review - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Potential duplicate', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Requires update', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Validated - Assigned', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component, router } = await createTestComponent( + {}} + />, + { store } + ) + + const { status, node } = actionStatus(component, [ + ACTION.REVIEW_DECLARATION + ]) + expect(status).toBe(ACTION_STATUS.ENABLED) + + node?.simulate('click') + + await flushPromises() + + expect(router.state.location.pathname).toContain( + 'reviews/' + defaultDeclaration.id + ) + }) + + it('Validated - Not downloaded - Has scope', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.DISABLED) + }) + + it('Validated - Does not have scope', async () => { + const { store } = createStore() + setScopes([], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Archived', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Registered', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Registered + Printed in advance', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Pending correction', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) + const { component } = await createTestComponent( + {}} />, @@ -489,16 +654,81 @@ describe('Review action', () => { const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) +}) + +describe('Review potential duplicate action', () => { + it('Draft', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('In progress', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('In review', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) it('Potential duplicate - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} @@ -514,13 +744,13 @@ describe('Review action', () => { it('Potential duplicate - Not downloaded - Has Scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) const { component } = await createTestComponent( {}} @@ -534,15 +764,19 @@ describe('Review action', () => { expect(status).toBe(ACTION_STATUS.DISABLED) }) - it('Potential duplicate - Downloaded', async () => { + it('Potential duplicate - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) const { component, router } = await createTestComponent( {}} @@ -566,13 +800,13 @@ describe('Review action', () => { it('Requires update', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) const { component } = await createTestComponent( {}} />, @@ -583,82 +817,287 @@ describe('Review action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Validated - Downloaded', async () => { + it('Validated - Does not have scope', async () => { const { store } = createStore() - const { component, router } = await createTestComponent( + const { component } = await createTestComponent( {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Archived', async () => { + const { store } = createStore() + const { component } = await createTestComponent( + {}} />, { store } ) - const { status, node } = actionStatus(component, [ - ACTION.REVIEW_DECLARATION + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE ]) - expect(status).toBe(ACTION_STATUS.ENABLED) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) - node?.simulate('click') + it('Validated', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) - await flushPromises() + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) - expect(router.state.location.pathname).toContain( - 'reviews/' + defaultDeclaration.id + it('Archived', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} + />, + { store } ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Validated - Not downloaded - Has scope', async () => { + it('Registered', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) const { component } = await createTestComponent( {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Registered + Printed in advance', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Pending correction', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REVIEW_DUPLICATES], store) + const { component } = await createTestComponent( + {}} />, { store } ) - const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) - expect(status).toBe(ACTION_STATUS.DISABLED) + const { status } = actionStatus(component, [ + ACTION.REVIEW_POTENTIAL_DUPLICATE + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) }) +}) - it('Validated - Does not have scope', async () => { +describe('Review correction action', () => { + it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_CORRECTION_REQUEST + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('In progress', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_CORRECTION_REQUEST + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('In review', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_CORRECTION_REQUEST + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Potential duplicate', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_CORRECTION_REQUEST + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Requires update', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) + const { component } = await createTestComponent( + {}} + />, + { store } + ) + + const { status } = actionStatus(component, [ + ACTION.REVIEW_CORRECTION_REQUEST + ]) + expect(status).toBe(ACTION_STATUS.HIDDEN) + }) + + it('Validated', async () => { + const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, { store } ) - const { status } = actionStatus(component, [ACTION.REVIEW_DECLARATION]) + const { status } = actionStatus(component, [ + ACTION.REVIEW_CORRECTION_REQUEST + ]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) it('Archived', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -666,22 +1105,20 @@ describe('Review action', () => { ) const { status } = actionStatus(component, [ - ACTION.REVIEW_DECLARATION, - ACTION.REVIEW_CORRECTION_REQUEST, - ACTION.REVIEW_POTENTIAL_DUPLICATE + ACTION.REVIEW_CORRECTION_REQUEST ]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) it('Registered', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -689,22 +1126,20 @@ describe('Review action', () => { ) const { status } = actionStatus(component, [ - ACTION.REVIEW_DECLARATION, - ACTION.REVIEW_CORRECTION_REQUEST, - ACTION.REVIEW_POTENTIAL_DUPLICATE + ACTION.REVIEW_CORRECTION_REQUEST ]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) it('Registered + Printed in advance', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -712,22 +1147,24 @@ describe('Review action', () => { ) const { status } = actionStatus(component, [ - ACTION.REVIEW_DECLARATION, - ACTION.REVIEW_CORRECTION_REQUEST, - ACTION.REVIEW_POTENTIAL_DUPLICATE + ACTION.REVIEW_CORRECTION_REQUEST ]) expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Pending correction - Downloaded', async () => { + it('Pending correction - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component, router } = await createTestComponent( {}} />, @@ -749,13 +1186,13 @@ describe('Review action', () => { it('Pending correction - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -770,13 +1207,13 @@ describe('Review action', () => { it('Pending correction - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -791,16 +1228,19 @@ describe('Review action', () => { }) describe('Update action', () => { - const UPDATE_SCOPES = SCOPES.RA it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component, router } = await createTestComponent( {}} />, @@ -820,15 +1260,19 @@ describe('Update action', () => { ) }) - it('In progress - Downloaded', async () => { + it('In progress - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component, router } = await createTestComponent( {}} />, @@ -850,13 +1294,13 @@ describe('Update action', () => { it('In progress - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -869,13 +1313,13 @@ describe('Update action', () => { it('In progress - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -888,13 +1332,13 @@ describe('Update action', () => { it('In review', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -907,13 +1351,13 @@ describe('Update action', () => { it('Potential duplicate', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} @@ -925,15 +1369,19 @@ describe('Update action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Requires update - Downloaded', async () => { + it('Requires update - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component, router } = await createTestComponent( {}} />, @@ -956,13 +1404,13 @@ describe('Update action', () => { it('Requires update - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -975,13 +1423,13 @@ describe('Update action', () => { it('Requires update - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -994,13 +1442,13 @@ describe('Update action', () => { it('Validated', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -1013,13 +1461,13 @@ describe('Update action', () => { it('Archived', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -1032,13 +1480,13 @@ describe('Update action', () => { it('Registered', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -1051,13 +1499,13 @@ describe('Update action', () => { it('Registered + Printed in advance', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -1070,13 +1518,13 @@ describe('Update action', () => { it('Pending correction - Downloaded', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTER], store) const { component } = await createTestComponent( {}} />, @@ -1089,16 +1537,15 @@ describe('Update action', () => { }) describe('Archive action', () => { - const ARCHIVE_SCOPES = ['validate', 'register'] as any as Scope it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1109,16 +1556,20 @@ describe('Archive action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('In progress - Downloaded', async () => { + it('In progress - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const toggleDisplayDialogMock = vi.fn() const { component } = await createTestComponent( , @@ -1134,13 +1585,13 @@ describe('Archive action', () => { it('In progress - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1153,13 +1604,13 @@ describe('Archive action', () => { it('In progress - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1170,16 +1621,20 @@ describe('Archive action', () => { expect(status).toBe(ACTION_STATUS.DISABLED) }) - it('In review - Downloaded', async () => { + it('In review - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const toggleDisplayDialogMock = vi.fn() const { component } = await createTestComponent( , @@ -1195,13 +1650,13 @@ describe('Archive action', () => { it('In review - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1214,13 +1669,13 @@ describe('Archive action', () => { it('In review - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1231,16 +1686,20 @@ describe('Archive action', () => { expect(status).toBe(ACTION_STATUS.DISABLED) }) - it('Requires update - Downloaded', async () => { + it('Requires update - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const toggleDisplayDialogMock = vi.fn() const { component } = await createTestComponent( , @@ -1256,13 +1715,13 @@ describe('Archive action', () => { it('Requires update - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1275,13 +1734,13 @@ describe('Archive action', () => { it('Requires update - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1292,16 +1751,20 @@ describe('Archive action', () => { expect(status).toBe(ACTION_STATUS.DISABLED) }) - it('Validated - Downloaded', async () => { + it('Validated - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const toggleDisplayDialogMock = vi.fn() const { component } = await createTestComponent( , @@ -1317,13 +1780,13 @@ describe('Archive action', () => { it('Validated - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1336,13 +1799,13 @@ describe('Archive action', () => { it('Validated - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1355,13 +1818,13 @@ describe('Archive action', () => { it('Archived', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1374,13 +1837,13 @@ describe('Archive action', () => { it('Registered', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1393,13 +1856,13 @@ describe('Archive action', () => { it('Registered + Printed in advance', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1412,13 +1875,13 @@ describe('Archive action', () => { it('Pending correction', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_ARCHIVE], store) const { component } = await createTestComponent( {}} />, @@ -1431,16 +1894,15 @@ describe('Archive action', () => { }) describe('Reinstate action', () => { - const REINSTATE_SCOPES = SCOPES.RA it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_REINSTATE], store) const { component } = await createTestComponent( {}} />, @@ -1451,16 +1913,20 @@ describe('Reinstate action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Archived - Downloaded', async () => { + it('Archived - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_REINSTATE], store) const toggleDisplayDialogMock = vi.fn() const { component } = await createTestComponent( , @@ -1475,13 +1941,13 @@ describe('Reinstate action', () => { it('Archived - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1494,13 +1960,13 @@ describe('Reinstate action', () => { it('Archived - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_REINSTATE], store) const { component } = await createTestComponent( {}} />, @@ -1513,13 +1979,13 @@ describe('Reinstate action', () => { it('Registered', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_DECLARATION_REINSTATE], store) const { component } = await createTestComponent( {}} />, @@ -1532,16 +1998,15 @@ describe('Reinstate action', () => { }) describe('Print action', () => { - const PRINT_SCOPES = SCOPES.RA it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1554,13 +2019,13 @@ describe('Print action', () => { it('In progress', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1573,13 +2038,13 @@ describe('Print action', () => { it('In review', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1592,13 +2057,13 @@ describe('Print action', () => { it('Potential duplicate', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} @@ -1612,13 +2077,13 @@ describe('Print action', () => { it('Requires update', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1631,13 +2096,13 @@ describe('Print action', () => { it('Validated', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1650,13 +2115,13 @@ describe('Print action', () => { it('Archived', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1667,15 +2132,19 @@ describe('Print action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Registered - Downloaded', async () => { + it('Registered - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component, router } = await createTestComponent( {}} />, @@ -1695,13 +2164,13 @@ describe('Print action', () => { it('Registered - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1714,13 +2183,13 @@ describe('Print action', () => { it('Registered - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1733,13 +2202,13 @@ describe('Print action', () => { it('Registered + Printed in advance', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1752,13 +2221,13 @@ describe('Print action', () => { it('Pending correction', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1771,16 +2240,15 @@ describe('Print action', () => { }) describe('Issue action', () => { - const ISSUE_SCOPES = SCOPES.RA it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1793,13 +2261,13 @@ describe('Issue action', () => { it('In progress', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1812,13 +2280,13 @@ describe('Issue action', () => { it('In review', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1831,13 +2299,13 @@ describe('Issue action', () => { it('Potential duplicate', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} @@ -1851,13 +2319,13 @@ describe('Issue action', () => { it('Requires update', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1870,13 +2338,13 @@ describe('Issue action', () => { it('Validated', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1889,13 +2357,13 @@ describe('Issue action', () => { it('Archived', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1908,13 +2376,13 @@ describe('Issue action', () => { it('Registered', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1925,15 +2393,19 @@ describe('Issue action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Registered + Printed in advance - Downloaded', async () => { + it('Registered + Printed in advance - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component, router } = await createTestComponent( {}} />, @@ -1954,13 +2426,13 @@ describe('Issue action', () => { it('Registered + Printed in advance - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -1973,13 +2445,13 @@ describe('Issue action', () => { it('Registered + Printed in advance - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -1992,13 +2464,13 @@ describe('Issue action', () => { it('Pending correction', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], store) const { component } = await createTestComponent( {}} />, @@ -2011,16 +2483,15 @@ describe('Issue action', () => { }) describe('Correct action', () => { - const CORRECTION_SCOPES = SCOPES.RA it('Draft', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2033,13 +2504,13 @@ describe('Correct action', () => { it('In progress', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2052,13 +2523,13 @@ describe('Correct action', () => { it('In review', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2071,13 +2542,13 @@ describe('Correct action', () => { it('Potential duplicate', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} @@ -2091,13 +2562,13 @@ describe('Correct action', () => { it('Requires update', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2110,13 +2581,13 @@ describe('Correct action', () => { it('Validated', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2129,13 +2600,13 @@ describe('Correct action', () => { it('Archived', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2146,15 +2617,19 @@ describe('Correct action', () => { expect(status).toBe(ACTION_STATUS.HIDDEN) }) - it('Registered - Downloaded', async () => { + it('Registered - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component, router } = await createTestComponent( {}} />, @@ -2174,13 +2649,13 @@ describe('Correct action', () => { it('Registered - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2193,13 +2668,13 @@ describe('Correct action', () => { it('Registered - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2212,13 +2687,13 @@ describe('Correct action', () => { it('Registered + Printed in advance - Does not have scope', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2231,13 +2706,13 @@ describe('Correct action', () => { it('Registered + Printed in advance - Not downloaded - Has scope', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2248,15 +2723,19 @@ describe('Correct action', () => { expect(status).toBe(ACTION_STATUS.DISABLED) }) - it('Registered + Printed in advance - Downloaded', async () => { + it('Registered + Printed in advance - Assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component, router } = await createTestComponent( {}} />, @@ -2276,13 +2755,13 @@ describe('Correct action', () => { it('Pending correction', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_REGISTRATION_CORRECT], store) const { component } = await createTestComponent( {}} />, @@ -2297,13 +2776,13 @@ describe('Correct action', () => { describe('Delete declaration action', () => { it('Draft', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2321,13 +2800,13 @@ describe('Delete declaration action', () => { it('In progress', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2340,13 +2819,13 @@ describe('Delete declaration action', () => { it('In review', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2359,17 +2838,16 @@ describe('Delete declaration action', () => { }) describe('Unassign action', () => { - const UNASSIGN_SCOPES = SCOPES.REGISTRAR const Assignment = 'Assigned to Kennedy Mweene at Ibombo District Office' it('Has scope - assigned to someone else', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_UNASSIGN_OTHERS], store) const { component } = await createTestComponent( {}} />, @@ -2388,13 +2866,13 @@ describe('Unassign action', () => { it('Does not have scope - assigned to someone else', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2410,13 +2888,17 @@ describe('Unassign action', () => { it('Assigned to self', async () => { const { store } = createStore() + setScopes([], store) const { component } = await createTestComponent( {}} />, @@ -2435,6 +2917,7 @@ describe('Unassign action', () => { it('Not assigned', async () => { const { store } = createStore() + setScopes([SCOPES.RECORD_UNASSIGN_OTHERS], store) const { component } = await createTestComponent( { status: SUBMISSION_STATUS.DECLARED, assignment: undefined }} - scope={UNASSIGN_SCOPES} draft={draftBirthNotDownloaded} toggleDisplayDialog={() => {}} />, diff --git a/packages/client/src/views/RecordAudit/ActionMenu.tsx b/packages/client/src/views/RecordAudit/ActionMenu.tsx index 1074486b719..baf984a7e19 100644 --- a/packages/client/src/views/RecordAudit/ActionMenu.tsx +++ b/packages/client/src/views/RecordAudit/ActionMenu.tsx @@ -17,7 +17,7 @@ import { TertiaryButton } from '@opencrvs/components/lib/buttons' import { CaretDown } from '@opencrvs/components/lib/Icon/all-icons' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { Icon, ResponsiveModal } from '@opencrvs/components' import { formatUrl, @@ -26,9 +26,6 @@ import { generateIssueCertificateUrl, generatePrintCertificateUrl } from '@client/navigation' -import { useIntl } from 'react-intl' -import { Scope } from '@sentry/react' -import { IDeclarationData } from './utils' import { clearCorrectionAndPrintChanges, deleteDeclaration, @@ -40,14 +37,21 @@ import { import { canBeCorrected, isArchivable, - isArchived, - isCertified, + canBeReinstated, + isIssuable, isPendingCorrection, isPrintable, isRecordOrDeclaration, isReviewableDeclaration, - isUpdatableDeclaration + isUpdatableDeclaration, + isViewable } from '@client/declarations/utils' +import ProtectedComponent from '@client/components/ProtectedComponent' +import { + RECORD_ALLOWED_SCOPES, + usePermissions +} from '@client/hooks/useAuthorization' +import { getUserDetails } from '@client/profile/profileSelectors' import { CorrectionSection } from '@client/forms' import { useModal } from '@client/hooks/useModal' import { buttonMessages } from '@client/i18n/messages' @@ -61,20 +65,24 @@ import { REVIEW_EVENT_PARENT_FORM_PAGE } from '@client/navigation/routes' import { client } from '@client/utils/apolloClient' +import { SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' -import { GQLAssignmentData } from '@client/utils/gateway-deprecated-do-not-use' import { DeleteModal } from '@client/views/RegisterForm/RegisterForm' +import { useIntl } from 'react-intl' +import { IDeclarationData } from './utils' import { useNavigate } from 'react-router-dom' import * as routes from '@client/navigation/routes' +import { useDeclaration } from '@client/declarations/selectors' +import { FETCH_DECLARATION_SHORT_INFO } from './queries' export const ActionMenu: React.FC<{ declaration: IDeclarationData - scope: Scope draft: IDeclaration | null duplicates?: string[] toggleDisplayDialog: () => void -}> = ({ declaration, scope, draft, toggleDisplayDialog, duplicates }) => { +}> = ({ declaration, draft, toggleDisplayDialog, duplicates }) => { const dispatch = useDispatch() + const userDetails = useSelector(getUserDetails) const navigate = useNavigate() const [modal, openModal] = useModal() @@ -82,9 +90,16 @@ export const ActionMenu: React.FC<{ const { id, type, assignment, status } = declaration - const isDownloaded = - draft?.downloadStatus === DOWNLOAD_STATUS.DOWNLOADED || - draft?.submissionStatus === SUBMISSION_STATUS.DRAFT + const assignedToSelf = + assignment?.practitionerId === userDetails?.practitionerId + const assignedToOther = !!( + assignment && assignment?.practitionerId !== userDetails?.practitionerId + ) + + const isDownloaded = draft?.downloadStatus === DOWNLOAD_STATUS.DOWNLOADED + const isDraft = draft?.submissionStatus === SUBMISSION_STATUS.DRAFT + + const isActionable = isDownloaded && assignedToSelf const isDuplicate = (duplicates ?? []).length > 0 @@ -98,6 +113,7 @@ export const ActionMenu: React.FC<{ } return } + const handleUnassign = async () => { const { firstName, lastName, officeName } = assignment || {} const unassignConfirm = await openModal( @@ -105,14 +121,14 @@ export const ActionMenu: React.FC<{ assignment && ( + /> ) ) if (unassignConfirm) { - dispatch(unassignDeclaration(id, client)) + dispatch(unassignDeclaration(id, client, [FETCH_DECLARATION_SHORT_INFO])) } return } @@ -126,7 +142,7 @@ export const ActionMenu: React.FC<{ - {!isDownloaded && assignment && ( + {assignment && assignedToOther && ( <> {intl.formatMessage(messages.assignedTo, { @@ -138,62 +154,86 @@ export const ActionMenu: React.FC<{ )} - - - - - - - + + + + + + + {isDraft ? ( + + ) : ( + + + + )} + + + + + + + + + + + + + + + @@ -203,8 +243,7 @@ export const ActionMenu: React.FC<{ } interface IActionItemCommonProps { - isDownloaded: boolean - scope: Scope + isActionable: boolean declarationStatus?: SUBMISSION_STATUS } @@ -220,6 +259,8 @@ const ViewAction: React.FC<{ const navigate = useNavigate() const intl = useIntl() + if (!isViewable(declarationStatus)) return null + return ( { @@ -240,7 +281,7 @@ const ViewAction: React.FC<{ const CorrectRecordAction: React.FC< IActionItemCommonProps & IDeclarationProps -> = ({ declarationId, declarationStatus, type, isDownloaded, scope }) => { +> = ({ declarationId, declarationStatus, type, isActionable }) => { const navigate = useNavigate() const dispatch = useDispatch() const intl = useIntl() @@ -248,17 +289,7 @@ const CorrectRecordAction: React.FC< const isBirthOrDeathEvent = type && [EventType.Birth, EventType.Death].includes(type as EventType) - // @ToDo use: `record.registration-correct` after configurable role pr is merged - const userHasRegisterScope = - scope && - ((scope as any as string[]).includes('register') || - (scope as any as string[]).includes('validate')) - - if ( - !isBirthOrDeathEvent || - !canBeCorrected(declarationStatus) || - !userHasRegisterScope - ) { + if (!isBirthOrDeathEvent || !canBeCorrected(declarationStatus)) { return null } @@ -274,7 +305,7 @@ const CorrectRecordAction: React.FC< }) ) }} - disabled={!isDownloaded} + disabled={!isActionable} > {intl.formatMessage(messages.correctRecord)} @@ -284,22 +315,12 @@ const CorrectRecordAction: React.FC< const ArchiveAction: React.FC< IActionItemCommonProps & { toggleDisplayDialog?: () => void } -> = ({ toggleDisplayDialog, isDownloaded, declarationStatus, scope }) => { +> = ({ toggleDisplayDialog, isActionable, declarationStatus }) => { const intl = useIntl() - - // @ToDo use: `record.registration-archive` after configurable role pr is merged - // @Question: If user has archive scope but not register scope, - // can he archive validated record? - const userHasArchiveScope = - scope && - ((scope as any as string[]).includes('register') || - ((scope as any as string[]).includes('validate') && - declarationStatus !== SUBMISSION_STATUS.VALIDATED)) - - if (!isArchivable(declarationStatus) || !userHasArchiveScope) return null + if (!isArchivable(declarationStatus)) return null return ( - + {intl.formatMessage(messages.archiveRecord)} @@ -308,21 +329,12 @@ const ArchiveAction: React.FC< const ReinstateAction: React.FC< IActionItemCommonProps & { toggleDisplayDialog?: () => void } -> = ({ toggleDisplayDialog, isDownloaded, declarationStatus, scope }) => { +> = ({ toggleDisplayDialog, isActionable, declarationStatus }) => { const intl = useIntl() - - // @ToDo use: `record.registration-reinstate` after configurable role pr is merged - // @Question: If user has reinstate scope but not register scope, - // can he reinstate validated record? - const userHasReinstateScope = - scope && - ((scope as any as string[]).includes('register') || - (scope as any as string[]).includes('validate')) - - if (!isArchived(declarationStatus) || !userHasReinstateScope) return null + if (!canBeReinstated(declarationStatus)) return null return ( - + {intl.formatMessage(messages.reinstateRecord)} @@ -331,25 +343,45 @@ const ReinstateAction: React.FC< const ReviewAction: React.FC< IActionItemCommonProps & IDeclarationProps & { isDuplicate: boolean } -> = ({ - declarationId, - declarationStatus, - type, - scope, - isDownloaded, - isDuplicate -}) => { - const navigate = useNavigate() +> = ({ declarationId, declarationStatus, type, isActionable, isDuplicate }) => { const intl = useIntl() + const navigate = useNavigate() + + if (!isReviewableDeclaration(declarationStatus)) { + return null + } + + return ( + { + navigate( + generateGoToPageUrl({ + pageRoute: REVIEW_EVENT_PARENT_FORM_PAGE, + declarationId, + pageId: 'review', + event: type as string + }) + ) + }} + disabled={!isActionable} + > + + {intl.formatMessage(messages.reviewDeclaration, { isDuplicate })} + + ) +} - const userHasReviewScope = - scope && - ((scope as any as string[]).includes('register') || - (scope as any as string[]).includes('validate')) +const ReviewCorrectionAction: React.FC< + IActionItemCommonProps & IDeclarationProps +> = ({ declarationId, declarationStatus, type, isActionable }) => { + const intl = useIntl() + const navigate = useNavigate() - if (!userHasReviewScope) return null + if (!isPendingCorrection(declarationStatus)) { + return null + } - return isPendingCorrection(declarationStatus) ? ( + return ( { if (type) { @@ -363,48 +395,24 @@ const ReviewAction: React.FC< ) } }} - disabled={!isDownloaded} + disabled={!isActionable} > {intl.formatMessage(messages.reviewCorrection)} - ) : isReviewableDeclaration(declarationStatus) ? ( - { - navigate( - generateGoToPageUrl({ - pageRoute: REVIEW_EVENT_PARENT_FORM_PAGE, - declarationId, - pageId: 'review', - event: type as string - }) - ) - }} - disabled={!isDownloaded} - > - - {intl.formatMessage(messages.reviewDeclaration, { isDuplicate })} - - ) : null + ) } const UpdateAction: React.FC = ({ declarationId, declarationStatus, type, - scope, - isDownloaded + isActionable }) => { const intl = useIntl() const navigate = useNavigate() - - // @ToDo use: appropriate scope after configurable role pr is merged - const userHasUpdateScope = - scope && - ((scope as any as string[]).includes('register') || - (scope as any as string[]).includes('validate') || - ((scope as any as string[]).includes('validate') && - declarationStatus === SUBMISSION_STATUS.DRAFT)) + const declaration = useDeclaration(declarationId) + const isDraft = declaration?.submissionStatus === SUBMISSION_STATUS.DRAFT let pageRoute: string, pageId: 'preview' | 'review' @@ -422,8 +430,7 @@ const UpdateAction: React.FC = ({ pageId = 'review' } - if (!isUpdatableDeclaration(declarationStatus) || !userHasUpdateScope) - return null + if (!isUpdatableDeclaration(declarationStatus) && !isDraft) return null return ( = ({ }) ) }} - disabled={!isDownloaded} + disabled={!isActionable} > {intl.formatMessage(messages.updateDeclaration)} @@ -448,21 +455,14 @@ const UpdateAction: React.FC = ({ const PrintAction: React.FC = ({ declarationId, declarationStatus, - scope, type, - isDownloaded + isActionable }) => { const intl = useIntl() const dispatch = useDispatch() const navigate = useNavigate() - // @ToDo use: `record.print-records` or other appropriate scope after configurable role pr is merged - const userHasPrintScope = - scope && - ((scope as any as string[]).includes('register') || - (scope as any as string[]).includes('validate')) - - if (!isPrintable(declarationStatus) || !userHasPrintScope) return null + if (!isPrintable(declarationStatus)) return null return ( = ({ }) ) }} - disabled={!isDownloaded} + disabled={!isActionable} > {intl.formatMessage(messages.printDeclaration)} @@ -486,21 +486,14 @@ const PrintAction: React.FC = ({ const IssueAction: React.FC = ({ declarationId, - isDownloaded, - declarationStatus, - scope + isActionable, + declarationStatus }) => { const intl = useIntl() const dispatch = useDispatch() const navigate = useNavigate() - // @ToDo use: `record.print-issue-certified-copies` or other appropriate scope after configurable role pr is merged - const userHasIssueScope = - scope && - ((scope as any as string[]).includes('register') || - (scope as any as string[]).includes('validate')) - - if (!isCertified(declarationStatus) || !userHasIssueScope) return null + if (!isIssuable(declarationStatus)) return null return ( = ({ }) ) }} - disabled={!isDownloaded} + disabled={!isActionable} > {intl.formatMessage(messages.issueCertificate)} @@ -535,18 +528,18 @@ const DeleteAction: React.FC<{ } const UnassignAction: React.FC<{ handleUnassign: () => void - isDownloaded: boolean - assignment?: GQLAssignmentData - scope: Scope -}> = ({ handleUnassign, isDownloaded, assignment, scope }) => { + assignedOther: boolean + assignedSelf: boolean + declarationStatus?: SUBMISSION_STATUS +}> = ({ handleUnassign, assignedOther, assignedSelf, declarationStatus }) => { + const { hasScope } = usePermissions() const intl = useIntl() - const isAssignedToSomeoneElse = !isDownloaded && assignment - - // @ToDo use: appropriate scope after configurable role pr is merged - const userHasUnassignScope = - scope && (scope as any as string[]).includes('register') - if (!isDownloaded && (!isAssignedToSomeoneElse || !userHasUnassignScope)) + if ( + declarationStatus === SUBMISSION_STATUS.DRAFT || + (!assignedSelf && + (!assignedOther || !hasScope(SCOPES.RECORD_UNASSIGN_OTHERS))) + ) return null return ( @@ -559,10 +552,10 @@ const UnassignAction: React.FC<{ const UnassignModal: React.FC<{ close: (result: boolean | null) => void - isDownloaded: boolean + assignedSelf: boolean name: string - officeName?: string -}> = ({ close, isDownloaded, name, officeName }) => { + officeName?: string | null +}> = ({ close, assignedSelf, name, officeName }) => { const intl = useIntl() return ( close(null)} > - {isDownloaded + {assignedSelf ? intl.formatMessage(conflictsMessages.selfUnassignDesc) : intl.formatMessage(conflictsMessages.regUnassignDesc, { name, diff --git a/packages/client/src/views/RecordAudit/History.tsx b/packages/client/src/views/RecordAudit/History.tsx index 4f587b6e98c..e8e7c8d2364 100644 --- a/packages/client/src/views/RecordAudit/History.tsx +++ b/packages/client/src/views/RecordAudit/History.tsx @@ -8,13 +8,29 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import React from 'react' +import { AvatarSmall } from '@client/components/Avatar' +import { DOWNLOAD_STATUS, SUBMISSION_STATUS } from '@client/declarations' +import { usePermissions } from '@client/hooks/useAuthorization' +import { constantsMessages, userMessages } from '@client/i18n/messages' +import { integrationMessages } from '@client/i18n/messages/views/integrations' +import { ILocation } from '@client/offline/reducer' +import { formatLongDate } from '@client/utils/date-formatting' +import { Avatar, History, RegStatus, SystemType } from '@client/utils/gateway' +import type { GQLHumanName } from '@client/utils/gateway-deprecated-do-not-use' +import { getLocalizedLocationName } from '@client/utils/locationUtils' +import { getIndividualNameObj } from '@client/utils/userUtils' +import { Link } from '@opencrvs/components' +import { ColumnContentAlignment } from '@opencrvs/components/lib/common-types' +import { Divider } from '@opencrvs/components/lib/Divider' +import { Box } from '@opencrvs/components/lib/icons/Box' +import { Pagination } from '@opencrvs/components/lib/Pagination' import { Table } from '@opencrvs/components/lib/Table' import { Text } from '@opencrvs/components/lib/Text' -import { Divider } from '@opencrvs/components/lib/Divider' +import React from 'react' +import { useIntl } from 'react-intl' import styled from 'styled-components' -import { ColumnContentAlignment } from '@opencrvs/components/lib/common-types' -import { constantsMessages, userMessages } from '@client/i18n/messages' +import { v4 as uuid } from 'uuid' +import { CMethodParams } from './ActionButtons' import { getPageItems, getStatusLabel, @@ -22,25 +38,8 @@ import { isSystemInitiated, isVerifiedAction } from './utils' -import { Pagination } from '@opencrvs/components/lib/Pagination' -import { CMethodParams } from './ActionButtons' -import type { GQLHumanName } from '@client/utils/gateway-deprecated-do-not-use' -import { getIndividualNameObj } from '@client/utils/userUtils' -import { AvatarSmall } from '@client/components/Avatar' -import { FIELD_AGENT_ROLES } from '@client/utils/constants' -import { DOWNLOAD_STATUS, SUBMISSION_STATUS } from '@client/declarations' -import { useIntl } from 'react-intl' -import { Box } from '@opencrvs/components/lib/icons/Box' -import { v4 as uuid } from 'uuid' -import { History, Avatar, RegStatus, SystemType } from '@client/utils/gateway' -import { Link } from '@opencrvs/components' -import { integrationMessages } from '@client/i18n/messages/views/integrations' -import { getLanguage } from '@client/i18n/selectors' import { useSelector } from 'react-redux' -import { formatLongDate } from '@client/utils/date-formatting' -import { getLocalizedLocationName } from '@client/utils/locationUtils' -import { ILocation } from '@client/offline/reducer' -import { getUserRole } from '@client/utils' +import { getScope } from '@client/profile/profileSelectors' import { useNavigate } from 'react-router-dom' import { formatUrl } from '@client/navigation' import * as routes from '@client/navigation/routes' @@ -168,13 +167,9 @@ export const GetHistory = ({ const navigate = useNavigate() const [currentPageNumber, setCurrentPageNumber] = React.useState(1) - const isFieldAgent = - userDetails?.systemRole && - FIELD_AGENT_ROLES.includes(userDetails.systemRole) - ? true - : false const DEFAULT_HISTORY_RECORD_PAGE_SIZE = 10 - const currentLanguage = useSelector(getLanguage) + const { canReadUser, canAccessOffice } = usePermissions() + const scopes = useSelector(getScope) const onPageChange = (currentPageNumber: number) => setCurrentPageNumber(currentPageNumber) @@ -202,8 +197,8 @@ export const GetHistory = ({ id: userDetails.userMgntUserID, name: userDetails.name, avatar: userDetails.avatar, - systemRole: userDetails.systemRole, - role: userDetails.role + role: userDetails.role, + primaryOffice: userDetails.primaryOffice }, office: userDetails.primaryOffice, comments: [], @@ -253,7 +248,8 @@ export const GetHistory = ({ item.regStatus, intl, item.user, - userDetails + userDetails, + scopes )} ), @@ -265,7 +261,7 @@ export const GetHistory = ({

) : isSystemInitiated(item) ? ( - ) : isFieldAgent ? ( + ) : !canReadUser(item.user!) ? ( ) : isVerifiedAction(item) ? (
- ) : isSystemInitiated(item) || !item.user?.systemRole ? ( + ) : isSystemInitiated(item) ? ( intl.formatMessage(getSystemType(item.system?.type || '')) ) : ( - getUserRole(currentLanguage, item.user?.role) + item.user && intl.formatMessage(item.user.role.label) ), location: @@ -309,9 +305,7 @@ export const GetHistory = ({ isVerifiedAction(item) || isSystemInitiated(item) ? (
- ) : isFieldAgent ? ( - <>{item.office?.name} - ) : ( + ) : item.office && canAccessOffice(item.office) ? ( { @@ -330,6 +324,8 @@ export const GetHistory = ({ ) : ''} + ) : ( + <>{item.office?.name} ) })) diff --git a/packages/client/src/views/RecordAudit/RecordAudit.test.tsx b/packages/client/src/views/RecordAudit/RecordAudit.test.tsx index 8d902d0d8de..7afb932087c 100644 --- a/packages/client/src/views/RecordAudit/RecordAudit.test.tsx +++ b/packages/client/src/views/RecordAudit/RecordAudit.test.tsx @@ -54,7 +54,6 @@ declaration.data.history = [ user: { id: userDetails.userMgntUserID, name: userDetails.name, - systemRole: userDetails.systemRole, role: userDetails.role }, office: userDetails.primaryOffice, diff --git a/packages/client/src/views/RecordAudit/RecordAudit.tsx b/packages/client/src/views/RecordAudit/RecordAudit.tsx index 4b607aef182..fe8f0512951 100644 --- a/packages/client/src/views/RecordAudit/RecordAudit.tsx +++ b/packages/client/src/views/RecordAudit/RecordAudit.tsx @@ -11,10 +11,8 @@ import React from 'react' import { Header } from '@client/components/Header/Header' import { Content, ContentSize } from '@opencrvs/components/lib/Content' -import { - Navigation, - WORKQUEUE_TABS -} from '@client/components/interface/Navigation' +import { Navigation } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import styled from 'styled-components' import { DeclarationIcon, @@ -47,7 +45,6 @@ import { Toast } from '@opencrvs/components/lib/Toast' import { ResponsiveModal } from '@opencrvs/components/lib/ResponsiveModal' import { Loader } from '@opencrvs/components/lib/Loader' import { getScope } from '@client/profile/profileSelectors' -import { Scope, hasRegisterScope } from '@client/utils/authUtils' import { PrimaryButton, TertiaryButton, @@ -63,6 +60,7 @@ import { recordAuditMessages } from '@client/i18n/messages/views/recordAudit' import { IForm } from '@client/forms' import { buttonMessages, constantsMessages } from '@client/i18n/messages' import { getLanguage } from '@client/i18n/selectors' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { MarkEventAsReinstatedMutation, MarkEventAsReinstatedMutationVariables, @@ -96,6 +94,7 @@ import { AppBar, IAppBarProps } from '@opencrvs/components/lib/AppBar' import { UserDetails } from '@client/utils/userUtils' import { client } from '@client/utils/apolloClient' +import { usePermissions } from '@client/hooks/useAuthorization' import { IReviewFormState } from '@client/forms/register/reviewReducer' import { ActionMenu } from './ActionMenu' import { @@ -138,7 +137,7 @@ interface IStateProps { userDetails: UserDetails | null language: string resources: IOfflineData - scope: Scope | null + scope: Scope[] declarationId?: string draft: IDeclaration | null tab?: IRecordAuditTabs @@ -247,7 +246,6 @@ function RecordAuditBody({ draft, duplicates, intl, - scope, refetchDeclarationInfo, userDetails, registerForm, @@ -258,7 +256,7 @@ function RecordAuditBody({ draft: IDeclaration | null duplicates?: string[] intl: IntlShape - scope: Scope | null + scope: Scope[] userDetails: UserDetails | null registerForm: IRegisterFormState offlineData: Partial @@ -273,6 +271,8 @@ function RecordAuditBody({ const [actionDetailsData, setActionDetailsData] = React.useState() + const { hasScope } = usePermissions() + if (!registerForm.registerForm || !declaration.type) return <> const toggleActionDetails = (actionItem: History | null, itemIndex = -1) => { @@ -294,7 +294,6 @@ function RecordAuditBody({ @@ -360,9 +359,7 @@ function RecordAuditBody({ const isValidatedOnReview = declaration.status === SUBMISSION_STATUS.VALIDATED && - hasRegisterScope(scope) - ? true - : false + hasScope(SCOPES.RECORD_REGISTER) const hasDuplicates = !!( duplicates && @@ -544,10 +541,13 @@ const BodyContent = ({ ...declaration, status: data.fetchRegistration?.registration?.status[0] .type as SUBMISSION_STATUS, - assignment: data.fetchRegistration?.registration?.assignment + assignment: draft?.assignmentStatus } } else { declaration = getGQLDeclaration(data.fetchRegistration, language) + /* draft might not be in store for unassigned record, + in that case use the one from the short declaration info query */ + declaration.assignment ??= draft?.assignmentStatus } return ( @@ -579,7 +579,7 @@ const BodyContent = ({ draft.submissionStatus === SUBMISSION_STATUS.DRAFT) ? { ...getDraftDeclarationData(draft, resources, intl, trackingId), - assignment: workqueueDeclaration?.registration?.assignment + assignment: draft?.assignmentStatus } : getWQDeclarationData( workqueueDeclaration as NonNullable, @@ -645,7 +645,7 @@ function mapStateToProps(state: IStoreState, props: RouteProps): IStateProps { ) || null, language: getLanguage(state), resources: getOfflineData(state), - scope: getScope(state), + scope: getScope(state)!, tab: tab as IRecordAuditTabs, userDetails: state.profile.userDetails, registerForm: state.registerForm, diff --git a/packages/client/src/views/RecordAudit/utils.ts b/packages/client/src/views/RecordAudit/utils.ts index bcb09097f0e..ee1164bea72 100644 --- a/packages/client/src/views/RecordAudit/utils.ts +++ b/packages/client/src/views/RecordAudit/utils.ts @@ -18,14 +18,11 @@ import { } from '@client/i18n/messages/views/recordAudit' import { IDynamicValues } from '@client/navigation' import { IOfflineData } from '@client/offline/reducer' -import { - EMPTY_STRING, - FIELD_AGENT_ROLES, - LANG_EN -} from '@client/utils/constants' +import { EMPTY_STRING, LANG_EN } from '@client/utils/constants' import { createNamesMap } from '@client/utils/data-formatting' import { getDeclarationFullName } from '@client/utils/draftUtils' import { + AssignmentData, EventType, History, HumanName, @@ -35,7 +32,6 @@ import { User } from '@client/utils/gateway' import type { - GQLAssignmentData, GQLBirthEventSearchSet, GQLDeathEventSearchSet, GQLEventSearchSet, @@ -47,6 +43,7 @@ import { generateLocationName } from '@client/utils/locationUtils' import { UserDetails } from '@client/utils/userUtils' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { get, has, PropertyPath } from 'lodash' import { IntlShape } from 'react-intl' @@ -65,7 +62,7 @@ export interface IDeclarationData { informant?: IInformantInfo registrationNo?: string nid?: string - assignment?: GQLAssignmentData + assignment?: AssignmentData } interface IInformantInfo { @@ -83,7 +80,7 @@ interface IGQLDeclaration { trackingId: string type: string status: { type: string }[] - assignment?: GQLAssignmentData + assignment?: AssignmentData } } @@ -466,7 +463,8 @@ export function getStatusLabel( regStatus: Maybe | undefined, intl: IntlShape, performedBy: Maybe | undefined, - loggedInUser: UserDetails | null + loggedInUser: UserDetails | null, + scopes: Scope[] | null ) { if (action) { return intl.formatMessage(regActionMessages[action], { @@ -476,8 +474,7 @@ export function getStatusLabel( if ( regStatus === RegStatus.Declared && performedBy?.id === loggedInUser?.userMgntUserID && - loggedInUser?.systemRole && - FIELD_AGENT_ROLES.includes(loggedInUser.systemRole) + scopes?.includes(SCOPES.RECORD_SUBMIT_INCOMPLETE) ) { return intl.formatMessage(recordAuditMessages.sentNotification) } diff --git a/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx b/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx index a6cc77a1b51..fa516e2eab5 100644 --- a/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx +++ b/packages/client/src/views/RegisterForm/DeclarationForm.test.tsx @@ -27,15 +27,19 @@ import { goToMotherSection, goToSection, selectOption, + setScopes, + REGISTRATION_AGENT_DEFAULT_SCOPES, + waitForReady, setPageVisibility, userDetails, validImageB64String } from '@client/tests/util' -import { waitForElement } from '@client/tests/wait-for-element' -import { EventType } from '@client/utils/gateway' import { ReactWrapper } from 'enzyme' -import { createMemoryRouter } from 'react-router-dom' import { Store } from 'redux' +import { SCOPES } from '@opencrvs/commons/client' +import { EventType } from '@client/utils/gateway' +import { waitForElement } from '@client/tests/wait-for-element' +import { createMemoryRouter } from 'react-router-dom' import { Mock, vi } from 'vitest' describe('when user starts a new declaration', () => { @@ -76,6 +80,9 @@ describe('when user starts a new declaration', () => { ) app = testApp.app store = testApp.store + store.dispatch(storeDeclaration(draft)) + + setScopes([SCOPES.RECORD_DECLARE_BIRTH], store) await store.dispatch(storeDeclaration(draft)) }) @@ -97,6 +104,9 @@ describe('when user starts a new declaration', () => { app = testApp.app store = testApp.store router = testApp.router + + setScopes(REGISTRATION_AGENT_DEFAULT_SCOPES, store) + await waitForReady(app) }) describe('when user is in birth registration by parent informant view', () => { diff --git a/packages/client/src/views/RegisterForm/PreviewForm.test.tsx b/packages/client/src/views/RegisterForm/PreviewForm.test.tsx index 187b140f933..3521d7e8e05 100644 --- a/packages/client/src/views/RegisterForm/PreviewForm.test.tsx +++ b/packages/client/src/views/RegisterForm/PreviewForm.test.tsx @@ -14,9 +14,9 @@ import { mockDeclarationData, goToEndOfForm, waitForReady, - validateScopeToken, - registerScopeToken, - flushPromises + flushPromises, + setScopes, + REGISTRAR_DEFAULT_SCOPES } from '@client/tests/util' import { DRAFT_BIRTH_PARENT_FORM, @@ -31,11 +31,11 @@ import { } from '@client/declarations' import { ReactWrapper } from 'enzyme' import { Store } from 'redux' +import { SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' import { v4 as uuid } from 'uuid' // eslint-disable-next-line no-restricted-imports import * as ReactApollo from '@apollo/client/react' -import { checkAuth } from '@opencrvs/client/src/profile/profileActions' import { waitForElement } from '@client/tests/wait-for-element' import { birthDraftData, @@ -66,6 +66,7 @@ describe('when user is previewing the form data', () => { router = testApp.router store = testApp.store + setScopes(REGISTRAR_DEFAULT_SCOPES, store) await waitForReady(app) }) @@ -77,11 +78,12 @@ describe('when user is previewing the form data', () => { beforeEach(async () => { getItem.mockReturnValue(registerScopeToken) - store.dispatch(checkAuth()) + await flushPromises() const data = deathReviewDraftData customDraft = { id: uuid(), data, review: true, event: EventType.Death } + store.dispatch(storeDeclaration(customDraft)) router.navigate( formatUrl(REVIEW_EVENT_PARENT_FORM_PAGE, { @@ -96,6 +98,7 @@ describe('when user is previewing the form data', () => { it('successfully submits the review form', async () => { vi.doMock('@apollo/client/react', () => ({ default: ReactApollo })) + app.update().find('#registerDeclarationBtn').hostNodes().simulate('click') app.update() app.update().find('#submit_confirm').hostNodes().simulate('click') @@ -118,7 +121,9 @@ describe('when user is previewing the form data', () => { app.find('#submit_reject_form').hostNodes().simulate('click') - expect(router.state.location.pathname).toEqual(REGISTRAR_HOME) + expect(router.state.location.pathname).toEqual( + `${REGISTRAR_HOME}/my-drafts/1` + ) }) }) @@ -134,6 +139,11 @@ describe('when user is previewing the form data', () => { event: EventType.Birth, submissionStatus: SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] } + setScopes( + [SCOPES.RECORD_DECLARE_BIRTH, SCOPES.RECORD_SUBMIT_FOR_REVIEW], + store + ) + await flushPromises() store.dispatch(storeDeclaration(customDraft)) router.navigate( DRAFT_BIRTH_PARENT_FORM.replace( @@ -151,19 +161,19 @@ describe('when user is previewing the form data', () => { }) it('check whether submit button is enabled or not', () => { - expect(app.find('#submit_form').hostNodes().prop('disabled')).toBe( - false - ) + expect( + app.find('#submit_for_review').hostNodes().prop('disabled') + ).toBe(false) }) describe('All sections visited', () => { it('Should be able to click SEND FOR REVIEW Button', () => { - expect(app.find('#submit_form').hostNodes().prop('disabled')).toBe( - false - ) + expect( + app.find('#submit_for_review').hostNodes().prop('disabled') + ).toBe(false) }) describe('button clicked', () => { beforeEach(async () => { - app.find('#submit_form').hostNodes().simulate('click') + app.find('#submit_for_review').hostNodes().simulate('click') }) it('confirmation screen should show up', () => { @@ -171,7 +181,9 @@ describe('when user is previewing the form data', () => { }) it('should redirect to home page', () => { app.find('#submit_confirm').hostNodes().simulate('click') - expect(router.state.location.pathname).toBe(REGISTRAR_HOME) + expect(router.state.location.pathname).toBe( + `${REGISTRAR_HOME}/my-drafts/1` + ) }) }) }) @@ -182,8 +194,8 @@ describe('when user is previewing the form data', () => { let customDraft: IDeclaration beforeEach(async () => { - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) + await waitForReady(app) await flushPromises() const data = birthReviewDraftData @@ -217,7 +229,9 @@ describe('when user is previewing the form data', () => { app.find('#submit_reject_form').hostNodes().simulate('click') - expect(router.state.location.pathname).toEqual(REGISTRAR_HOME) + expect(router.state.location.pathname).toEqual( + `${REGISTRAR_HOME}/my-drafts/1` + ) }) }) @@ -229,7 +243,6 @@ describe('when user is previewing the form data', () => { beforeEach(async () => { getItem.mockReturnValue(registerScopeToken) - store.dispatch(checkAuth()) await flushPromises() const data = marriageReviewDraftData @@ -256,8 +269,6 @@ describe('when user is previewing the form data', () => { app.find('#rejectDeclarationBtn').hostNodes().simulate('click') - // app.find('#rejectionReasonduplicate').hostNodes().simulate('change') - app .find('#rejectionCommentForHealthWorker') .hostNodes() @@ -269,14 +280,14 @@ describe('when user is previewing the form data', () => { }) app.find('#submit_reject_form').hostNodes().simulate('click') - expect(router.state.location.pathname).toEqual(REGISTRAR_HOME) + expect(router.state.location.pathname).toEqual( + `${REGISTRAR_HOME}/my-drafts/1` + ) }) }) describe('when user has validate scope', () => { beforeEach(async () => { - getItem.mockReturnValue(validateScopeToken) - await store.dispatch(checkAuth()) await flushPromises() const data = { _fhirIDMap: { diff --git a/packages/client/src/views/RegisterForm/RegisterForm.init.test.tsx b/packages/client/src/views/RegisterForm/RegisterForm.init.test.tsx index ebab496119b..56191deead8 100644 --- a/packages/client/src/views/RegisterForm/RegisterForm.init.test.tsx +++ b/packages/client/src/views/RegisterForm/RegisterForm.init.test.tsx @@ -27,7 +27,7 @@ import { import { DRAFT_BIRTH_PARENT_FORM_PAGE } from '@opencrvs/client/src/navigation/routes' import { vi } from 'vitest' -import { EventType, SystemRoleType, Status } from '@client/utils/gateway' +import { EventType, Status } from '@client/utils/gateway' import { storage } from '@client/storage' import { UserDetails } from '@client/utils/userUtils' import { formatUrl } from '@client/navigation' @@ -61,18 +61,18 @@ describe('when user logs in', () => { } ], mobile: '+260921111111', - systemRole: SystemRoleType.NationalSystemAdmin, role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'National System Admin' - } - ] + label: { + id: 'userRoles.nationalSystemAdmin', + defaultMessage: 'National System Admin', + description: 'National System Admin' + } }, status: 'active' as Status, - localRegistrar: { name: [], role: 'FIELD_AGENT' as SystemRoleType } + localRegistrar: { + name: [], + role: 'FIELD_AGENT' + } } const indexedDB = { diff --git a/packages/client/src/views/RegisterForm/RegisterForm.test.tsx b/packages/client/src/views/RegisterForm/RegisterForm.test.tsx index d252ac9a500..1b937d4ee0e 100644 --- a/packages/client/src/views/RegisterForm/RegisterForm.test.tsx +++ b/packages/client/src/views/RegisterForm/RegisterForm.test.tsx @@ -21,6 +21,7 @@ import { flushPromises, userDetails, mockOfflineData, + setScopes, TestComponentWithRouteMock } from '@client/tests/util' import { RegisterForm } from '@client/views/RegisterForm/RegisterForm' @@ -44,11 +45,11 @@ import { DRAFT_MARRIAGE_FORM_PAGE } from '@opencrvs/client/src/navigation/routes' import { IFormData } from '@opencrvs/client/src/forms' +import { SCOPES } from '@opencrvs/commons/client' import { EventType, RegStatus } from '@client/utils/gateway' import { draftToGqlTransformer } from '@client/transformer' import { IForm } from '@client/forms' import { clone, cloneDeep } from 'lodash' -import * as profileSelectors from '@client/profile/profileSelectors' import { getRegisterForm } from '@client/forms/register/declaration-selectors' import { waitForElement } from '@client/tests/wait-for-element' import { vi } from 'vitest' @@ -113,7 +114,7 @@ describe('when user is in the register form for birth event', () => { component.component.update() await flushPromises() expect(component.router.state.location.pathname).toEqual( - '/registration-home/progress/' + '/registration-home/my-drafts/1' ) }) it('takes registrar to declaration submitted page when save button is clicked', async () => { @@ -131,7 +132,7 @@ describe('when user is in the register form for birth event', () => { await flushPromises() expect( component.router.state.location.pathname.includes( - '/registration-home/progress' + '/registration-home/my-drafts/1' ) ).toBeTruthy() }) @@ -244,7 +245,7 @@ describe('when user is in the register form for marriage event', () => { }) }) -describe('when user is in the register form preview section', () => { +describe('when user is in the register form preview section and has the submit complete scope', () => { let component: TestComponentWithRouteMock let store: AppStore @@ -277,6 +278,7 @@ describe('when user is in the register form preview section', () => { } } } + setScopes([SCOPES.RECORD_SUBMIT_INCOMPLETE], store) store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(draft)) @@ -304,19 +306,22 @@ describe('when user is in the register form preview section', () => { it('submit button will be enabled when even if form is not fully filled-up', () => { expect( - component.component.find('#submit_form').hostNodes().prop('disabled') + component.component + .find('#submit_incomplete') + .hostNodes() + .prop('disabled') ).toBe(false) }) it('Displays submit confirm modal when submit button is clicked', () => { - component.component.find('#submit_form').hostNodes().simulate('click') + component.component.find('#submit_incomplete').hostNodes().simulate('click') expect( component.component.find('#submit_confirm').hostNodes() ).toHaveLength(1) }) - describe('User in the Preview section for submitting the Form', () => { + describe('User in the Preview section for submitting the Form and has the submit for review scope', () => { beforeEach(async () => { // @ts-ignore const nDeclaration = createReviewDeclaration( @@ -325,6 +330,7 @@ describe('when user is in the register form preview section', () => { EventType.Birth ) nDeclaration.submissionStatus = SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] + setScopes([SCOPES.RECORD_SUBMIT_FOR_REVIEW], store) store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(nDeclaration)) @@ -351,7 +357,11 @@ describe('when user is in the register form preview section', () => { }) it('should be able to submit the form', () => { - component.component.find('#submit_form').hostNodes().simulate('click') + component.component + .find('#submit_for_review') + .hostNodes() + .simulate('click') + component.component.update() const cancelBtn = component.component.find('#cancel-btn').hostNodes() @@ -364,10 +374,13 @@ describe('when user is in the register form preview section', () => { component.component.find('#submit_confirm').hostNodes().length ).toEqual(0) expect( - component.component.find('#submit_form').hostNodes().length + component.component.find('#submit_for_review').hostNodes().length ).toEqual(1) - component.component.find('#submit_form').hostNodes().simulate('click') + component.component + .find('#submit_for_review') + .hostNodes() + .simulate('click') component.component.update() const confirmBtn = component.component.find('#submit_confirm').hostNodes() @@ -390,11 +403,10 @@ describe('when user is in the register form review section', () => { mockDeclarationData, EventType.Birth ) + setScopes([SCOPES.RECORD_REGISTER, SCOPES.RECORD_SUBMIT_FOR_UPDATES], store) store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(declaration)) - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) - const form = await getReviewFormFromStore(store, EventType.Birth) const { component: testComponent } = await createTestComponent( @@ -439,11 +451,10 @@ describe('when user is in the register form from review edit', () => { mockDeclarationData, EventType.Birth ) + setScopes([SCOPES.RECORD_REGISTER], store) store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(declaration)) - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) - const form = await getReviewFormFromStore(store, EventType.Birth) const { component: testComponent, router: testRouter } = @@ -492,11 +503,10 @@ describe('when user is in the register form from sent for review edit', () => { EventType.Birth, RegStatus.Declared ) + store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(declaration)) - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) - const form = await getReviewFormFromStore(store, EventType.Birth) const { component: testComponent } = await createTestComponent( @@ -531,6 +541,7 @@ describe('when user is in the register form from sent for review edit', () => { ) expect(saveDraftConfirmationModal.hostNodes()).toHaveLength(1) }) + it('clicking save confirm saves the draft', async () => { const DRAFT_MODIFY_TIME = 1582525379383 Date.now = vi.fn(() => DRAFT_MODIFY_TIME) @@ -583,12 +594,11 @@ describe('When user is in Preview section death event', () => { mockDeathDeclarationData, EventType.Death ) + setScopes([SCOPES.RECORD_SUBMIT_INCOMPLETE], store) deathDraft.submissionStatus = SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(deathDraft)) - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['declare']) - deathForm = await getRegisterFormFromStore(store, EventType.Death) const nTestComponent = await createTestComponent( { }) it('Should be able to submit the form', () => { - component.component.find('#submit_form').hostNodes().simulate('click') + component.component.find('#submit_incomplete').hostNodes().simulate('click') const confirmBtn = component.component.find('#submit_confirm').hostNodes() expect(confirmBtn.length).toEqual(1) @@ -646,6 +656,7 @@ describe('When user is in Preview section death event', () => { expect(component.router.state.location.pathname).toBe(HOME) }) + it('Check if death location as hospital is parsed properly', () => { const hospitalLocatioMockDeathDeclarationData = clone( mockDeathDeclarationData @@ -727,6 +738,8 @@ describe('When user is in Preview section death event in offline mode', () => { mockDeathDeclarationData, EventType.Death ) + + setScopes([SCOPES.RECORD_SUBMIT_INCOMPLETE], store) deathDraft.submissionStatus = SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(deathDraft)) @@ -754,7 +767,7 @@ describe('When user is in Preview section death event in offline mode', () => { }) it('Should be able to submit the form', async () => { - component.component.find('#submit_form').hostNodes().simulate('click') + component.component.find('#submit_incomplete').hostNodes().simulate('click') const confirmBtn = component.component.find('#submit_confirm').hostNodes() expect(confirmBtn.length).toEqual(1) @@ -788,13 +801,14 @@ describe('When user is in Preview section marriage event', () => { mockDeathDeclarationData, EventType.Marriage ) + + setScopes([SCOPES.RECORD_SUBMIT_INCOMPLETE], store) marriageDraft.submissionStatus = SUBMISSION_STATUS[SUBMISSION_STATUS.DRAFT] store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(marriageDraft)) - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['declare']) - marriageForm = await getRegisterFormFromStore(store, EventType.Marriage) + const nTestComponent = await createTestComponent( // @ts-ignore { }) it('Should be able to submit the form', () => { - component.component.find('#submit_form').hostNodes().simulate('click') + component.component.find('#submit_incomplete').hostNodes().simulate('click') const confirmBtn = component.component.find('#submit_confirm').hostNodes() expect(confirmBtn.length).toEqual(1) diff --git a/packages/client/src/views/RegisterForm/RegisterForm.tsx b/packages/client/src/views/RegisterForm/RegisterForm.tsx index c3322bc1afa..d8b650600e2 100644 --- a/packages/client/src/views/RegisterForm/RegisterForm.tsx +++ b/packages/client/src/views/RegisterForm/RegisterForm.tsx @@ -8,6 +8,7 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ +import { Stack, ToggleMenu } from '@opencrvs/components/lib' import * as React from 'react' import { FormikTouched, FormikValues } from 'formik' import { @@ -42,13 +43,12 @@ import { writeDeclaration, DOWNLOAD_STATUS } from '@client/declarations' -import { Stack, ToggleMenu } from '@client/../../components/lib' import { FormFieldGenerator, ITouchedNestedFields, mapFieldsToValues } from '@client/components/form' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { RejectRegistrationForm } from '@client/components/review/RejectRegistrationForm' import { TimeMounted } from '@client/components/TimeMounted' import { @@ -88,13 +88,13 @@ import { getOfflineData } from '@client/offline/selectors' import { getScope, getUserDetails } from '@client/profile/profileSelectors' import { IStoreState } from '@client/store' import { client } from '@client/utils/apolloClient' -import { Scope } from '@client/utils/authUtils' import { ACCUMULATED_FILE_SIZE, DECLARED, REJECTED, VALIDATED } from '@client/utils/constants' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { EventType, RegStatus } from '@client/utils/gateway' import { UserDetails } from '@client/utils/userUtils' import { @@ -177,7 +177,7 @@ type Props = { fieldsToShowValidationErrors?: IFormField[] isWritingDraft: boolean userDetails: UserDetails | null - scope: Scope | null + scope: Scope[] | null config: IOfflineData } @@ -288,7 +288,7 @@ function FormAppBar({ case 'DECLARED': return WORKQUEUE_TABS.readyForReview case 'DRAFT': - return WORKQUEUE_TABS.inProgress + return WORKQUEUE_TABS.myDrafts case 'IN_PROGRESS': return WORKQUEUE_TABS.inProgressFieldAgent case 'REJECTED': @@ -713,11 +713,20 @@ class RegisterFormView extends React.Component { } userHasRegisterScope() { - return this.props.scope && this.props.scope.includes('register') + return this.props.scope && this.props.scope.includes(SCOPES.RECORD_REGISTER) } userHasValidateScope() { - return this.props.scope && this.props.scope.includes('validate') + const validateScopes = [ + SCOPES.RECORD_REGISTER, + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES + ] as Scope[] + + return ( + this.props.scope && + this.props.scope.some((x) => validateScopes.includes(x)) + ) } componentDidMount() { @@ -892,13 +901,13 @@ class RegisterFormView extends React.Component { onSaveAsDraftClicked = async () => { const { declaration } = this.props - const isRegistrarOrRegistrationAgent = + const canApproveOrRegister = this.userHasRegisterScope() || this.userHasValidateScope() const isConfirmationModalApplicable = declaration.registrationStatus === DECLARED || declaration.registrationStatus === VALIDATED || declaration.registrationStatus === REJECTED - if (isRegistrarOrRegistrationAgent && isConfirmationModalApplicable) { + if (canApproveOrRegister && isConfirmationModalApplicable) { this.toggleConfirmationModal() } else { this.writeDeclarationAndGoToHome() @@ -1000,7 +1009,7 @@ class RegisterFormView extends React.Component { case 'DECLARED': return WORKQUEUE_TABS.readyForReview case 'DRAFT': - return WORKQUEUE_TABS.inProgress + return WORKQUEUE_TABS.myDrafts case 'IN_PROGRESS': return WORKQUEUE_TABS.inProgressFieldAgent case 'REJECTED': diff --git a/packages/client/src/views/RegisterForm/ReviewForm.test.tsx b/packages/client/src/views/RegisterForm/ReviewForm.test.tsx index 117d690529c..5a640c495ff 100644 --- a/packages/client/src/views/RegisterForm/ReviewForm.test.tsx +++ b/packages/client/src/views/RegisterForm/ReviewForm.test.tsx @@ -16,6 +16,7 @@ import { storeDeclaration } from '@opencrvs/client/src/declarations' import { IForm, IFormSectionData } from '@opencrvs/client/src/forms' +import { SCOPES } from '@opencrvs/commons/client' import { EventType, RegStatus } from '@client/utils/gateway' import { REVIEW_EVENT_PARENT_FORM_PAGE } from '@opencrvs/client/src/navigation/routes' import { checkAuth } from '@opencrvs/client/src/profile/profileActions' @@ -29,12 +30,13 @@ import { getReviewFormFromStore, createTestStore, mockDeathDeclarationData, + setScopes, + REGISTRAR_DEFAULT_SCOPES, flushPromises } from '@client/tests/util' import { v4 as uuid } from 'uuid' import { ReviewForm } from '@client/views/RegisterForm/ReviewForm' - -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' import { birthDraftData } from '@client/tests/mock-drafts' import { vi, Mock } from 'vitest' import { formatUrl } from '@client/navigation' @@ -256,7 +258,7 @@ const mockFetchUserDetails = vi.fn() mockFetchUserDetails.mockReturnValue(mockUserResponseWithName) queries.fetchUserDetails = mockFetchUserDetails describe('ReviewForm tests', () => { - const scope = ['register'] + const scope = [SCOPES.RECORD_REGISTER] let form: IForm let store: AppStore @@ -265,9 +267,10 @@ describe('ReviewForm tests', () => { const testStore = await createTestStore() store = testStore.store + setScopes(REGISTRAR_DEFAULT_SCOPES, store) + form = await getReviewFormFromStore(store, EventType.Birth) getItem.mockReturnValue(registerScopeToken) - store.dispatch(checkAuth()) }) it('Shared contact phone number should be set properly', async () => { diff --git a/packages/client/src/views/RegisterForm/ReviewForm.tsx b/packages/client/src/views/RegisterForm/ReviewForm.tsx index 9f7057e6a9f..44c42ff646b 100644 --- a/packages/client/src/views/RegisterForm/ReviewForm.tsx +++ b/packages/client/src/views/RegisterForm/ReviewForm.tsx @@ -27,20 +27,19 @@ import { connect } from 'react-redux' import { getReviewForm } from '@opencrvs/client/src/forms/register/review-selectors' import { IDeclaration } from '@opencrvs/client/src/declarations' import { getScope } from '@client/profile/profileSelectors' -import { Scope } from '@opencrvs/client/src/utils/authUtils' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' - import { REGISTRAR_HOME_TAB, REVIEW_EVENT_PARENT_FORM_PAGE_GROUP } from '@client/navigation/routes' import { errorMessages } from '@client/i18n/messages' import { formatUrl } from '@client/navigation' -import { WORKQUEUE_TABS } from '@client/components/interface/Navigation' +import { WORKQUEUE_TABS } from '@client/components/interface/WorkQueueTabs' interface IReviewProps { theme: ITheme - scope: Scope | null + scope: Scope[] | null event: EventType } interface IDeclarationProp { @@ -63,11 +62,20 @@ const ErrorText = styled.div` class ReviewFormView extends React.Component { userHasRegisterScope() { - return this.props.scope && this.props.scope.includes('register') + return this.props.scope && this.props.scope.includes(SCOPES.RECORD_REGISTER) } userHasValidateScope() { - return this.props.scope && this.props.scope.includes('validate') + const validateScopes = [ + SCOPES.RECORD_REGISTER, + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES + ] as Scope[] + + return ( + this.props.scope && + this.props.scope.some((scope) => validateScopes.includes(scope)) + ) } render() { diff --git a/packages/client/src/views/RegisterForm/duplicate/DuplicateFormTabs.test.tsx b/packages/client/src/views/RegisterForm/duplicate/DuplicateFormTabs.test.tsx index 5145b28c330..b66854c19ba 100644 --- a/packages/client/src/views/RegisterForm/duplicate/DuplicateFormTabs.test.tsx +++ b/packages/client/src/views/RegisterForm/duplicate/DuplicateFormTabs.test.tsx @@ -13,6 +13,7 @@ import { createTestComponent, getReviewFormFromStore, createTestStore, + setScopes, flushPromises } from '@client/tests/util' import { RegisterForm } from '@client/views/RegisterForm/RegisterForm' @@ -24,8 +25,8 @@ import { } from '@client/declarations' import { v4 as uuid } from 'uuid' import { REVIEW_EVENT_PARENT_FORM_PAGE_GROUP } from '@opencrvs/client/src/navigation/routes' +import { SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' -import * as profileSelectors from '@client/profile/profileSelectors' import { vi } from 'vitest' import { ViewRecordQueries } from '@client/views/ViewRecord/query' import { formatUrl } from '@client/navigation' @@ -1174,7 +1175,7 @@ describe('when user is in the register form review section', () => { store.dispatch(setInitialDeclarations()) store.dispatch(storeDeclaration(declaration)) - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) + setScopes([SCOPES.RECORD_REGISTER], store) const form = await getReviewFormFromStore(store, EventType.Birth) diff --git a/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx b/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx index b83268ee1f5..a5a2c13ad26 100644 --- a/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx +++ b/packages/client/src/views/RegisterForm/review/ReviewSection.test.tsx @@ -28,7 +28,6 @@ import { formMessages } from '@client/i18n/messages' import { formatUrl } from '@client/navigation' import { REVIEW_EVENT_PARENT_FORM_PAGE } from '@client/navigation/routes' import { offlineDataReady } from '@client/offline/actions' -import * as profileSelectors from '@client/profile/profileSelectors' import { createStore } from '@client/store' import { createTestComponent, @@ -37,11 +36,13 @@ import { mockOfflineData, mockOfflineDataDispatch, resizeWindow, + setScopes, TestComponentWithRouteMock, userDetails } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' import { isMobileDevice } from '@client/utils/commonUtils' +import { SCOPES } from '@opencrvs/commons/client' import { EventType as DeclarationEvent, EventType, @@ -242,7 +243,8 @@ describe('when in device of large viewport', () => { describe('when user is in the review page for rejected birth declaration', () => { let reviewSectionComponent: ReactWrapper<{}, {}> beforeEach(async () => { - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) + setScopes([SCOPES.RECORD_REGISTER], store) + const { component: testComponent } = await createTestComponent( { let reviewSectionRouter: TestComponentWithRouteMock['router'] beforeEach(async () => { - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['validator']) + setScopes( + [SCOPES.RECORD_SUBMIT_FOR_APPROVAL, SCOPES.RECORD_SUBMIT_FOR_UPDATES], + store + ) + const { component: testComponent, router } = await createTestComponent( { }) beforeEach(async () => { - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) + setScopes([SCOPES.RECORD_REGISTER], store) const form = { sections: [ { @@ -508,7 +514,7 @@ describe('when in device of large viewport', () => { }) beforeEach(async () => { - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) + setScopes([SCOPES.RECORD_REGISTER], store) const form = { sections: [ { @@ -624,7 +630,7 @@ describe('when in device of large viewport', () => { }) beforeEach(async () => { - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) + setScopes([SCOPES.RECORD_REGISTER], store) const form = { sections: [ { @@ -788,7 +794,7 @@ describe('when in device of small viewport', () => { beforeEach(async () => { userAgentMock = vi.spyOn(window.navigator, 'userAgent', 'get') userAgentMock.mockReturnValue('Android') - vi.spyOn(profileSelectors, 'getScope').mockReturnValue(['register']) + setScopes([SCOPES.RECORD_REGISTER], store) const form = { sections: [ { diff --git a/packages/client/src/views/RegisterForm/review/ReviewSection.tsx b/packages/client/src/views/RegisterForm/review/ReviewSection.tsx index 7db662cfe33..a3e1433d9cd 100644 --- a/packages/client/src/views/RegisterForm/review/ReviewSection.tsx +++ b/packages/client/src/views/RegisterForm/review/ReviewSection.tsx @@ -72,10 +72,10 @@ import { DIVIDER, HIDDEN } from '@client/forms' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { EventType, RegStatus } from '@client/utils/gateway' import { getConditionalActionsForField, - getListOfLocations, getSectionFields, getVisibleSectionGroupsBasedOnConditions } from '@client/forms/utils' @@ -103,7 +103,7 @@ import { getOfflineData } from '@client/offline/selectors' import { getScope } from '@client/profile/profileSelectors' import { IStoreState } from '@client/store' import styled from 'styled-components' -import { Scope } from '@client/utils/authUtils' + import { ACCUMULATED_FILE_SIZE, REJECTED } from '@client/utils/constants' import { formatPlainDate, @@ -119,7 +119,7 @@ import { } from 'react-intl' import { connect } from 'react-redux' import { ReviewHeader } from './ReviewHeader' -import { IValidationResult } from '@client/utils/validate' +import { getListOfLocations, IValidationResult } from '@client/utils/validate' import { DocumentListPreview } from '@client/components/form/DocumentUploadField/DocumentListPreview' import { DocumentPreview } from '@client/components/form/DocumentUploadField/DocumentPreview' import { generateLocations } from '@client/utils/locationUtils' @@ -278,7 +278,7 @@ interface IProps { submissionStatus: string, action: SubmissionAction ) => void - scope: Scope | null + scope: Scope[] | null offlineCountryConfiguration: IOfflineData language: string onChangeReviewForm?: onChangeReviewForm @@ -829,15 +829,9 @@ class ReviewSectionComp extends React.Component { userHasRegisterScope() { if (this.props.scope) { - return this.props.scope && this.props.scope.includes('register') - } else { - return false - } - } - - userHasValidateScope() { - if (this.props.scope) { - return this.props.scope && this.props.scope.includes('validate') + return ( + this.props.scope && this.props.scope.includes(SCOPES.RECORD_REGISTER) + ) } else { return false } @@ -1972,8 +1966,6 @@ class ReviewSectionComp extends React.Component { { const { store } = createStore() + setScopes([SCOPES.SEARCH_BIRTH, SCOPES.SEARCH_DEATH], store) testComponent = ( await createTestComponent(, { store }) )?.component @@ -51,6 +58,7 @@ describe('when advancedSearchPage renders with 2 or more active params in store' let router: ReturnType beforeEach(async () => { const { store } = createStore() + setScopes([SCOPES.SEARCH_BIRTH, SCOPES.SEARCH_DEATH], store) store.dispatch( setAdvancedSearchParam({ event: 'birth', @@ -58,6 +66,7 @@ describe('when advancedSearchPage renders with 2 or more active params in store' registrationStatuses: ['IN_PROGRESS'] }) ) + store.dispatch(setUserDetails(mockUserResponse as any)) ;({ component: testComponent, router } = await createTestComponent( , { diff --git a/packages/client/src/views/SearchResult/AdvancedSearch.tsx b/packages/client/src/views/SearchResult/AdvancedSearch.tsx index ed98db340c2..b5008413721 100644 --- a/packages/client/src/views/SearchResult/AdvancedSearch.tsx +++ b/packages/client/src/views/SearchResult/AdvancedSearch.tsx @@ -13,7 +13,7 @@ import React, { useState } from 'react' import { connect, useDispatch, useSelector } from 'react-redux' import { injectIntl, useIntl } from 'react-intl' -import { getScope } from '@client/profile/profileSelectors' +import { getScope, getUserDetails } from '@client/profile/profileSelectors' import { IStoreState } from '@client/store' import { SysAdminContentWrapper } from '@client/views/SysAdmin/SysAdminContentWrapper' import { messages } from '@client/i18n/messages/views/config' @@ -22,8 +22,8 @@ import { Content, ContentSize, FormTabs } from '@opencrvs/components' import { FormFieldGenerator } from '@client/components/form/FormFieldGenerator' import { Button } from '@opencrvs/components/lib/Button' import { Icon } from '@opencrvs/components/lib/Icon' -import { advancedSearchBirthSections } from '@client/forms/advancedSearch/fieldDefinitions/Birth' -import { advancedSearchDeathSections } from '@client/forms/advancedSearch/fieldDefinitions/Death' +import { createAdvancedSearchBirthSections } from '@client/forms/advancedSearch/fieldDefinitions/Birth' +import { createAdvancedSearchDeathSections } from '@client/forms/advancedSearch/fieldDefinitions/Death' import { buttonMessages } from '@client/i18n/messages' import { messages as advancedSearchFormMessages } from '@client/i18n/messages/views/advancedSearchForm' import { getAdvancedSearchParamsState as AdvancedSearchParamsSelector } from '@client/search/advancedSearch/advancedSearchSelectors' @@ -42,8 +42,10 @@ import { } from '@client/search/advancedSearch/utils' import styled from 'styled-components' import { advancedSearchInitialState } from '@client/search/advancedSearch/reducer' +import { usePermissions } from '@client/hooks/useAuthorization' import { useNavigate } from 'react-router-dom' import * as routes from '@client/navigation/routes' +import { UUID } from '@opencrvs/commons/client' enum TabId { BIRTH = 'birth', @@ -54,21 +56,6 @@ const SearchButton = styled(Button)` margin-top: 32px; ` -const { - birthSearchRegistrationSection, - birthSearchChildSection, - birthSearchMotherSection, - birthSearchFatherSection, - birthSearchEventSection, - birthSearchInformantSection -} = advancedSearchBirthSections -const { - deathSearchRegistrationSection, - deathSearchDeceasedSection, - deathSearchEventSection, - deathSearchInformantSection -} = advancedSearchDeathSections - export const isAdvancedSearchFormValid = (value: IAdvancedSearchFormState) => { const validNonDateFields = Object.keys(value).filter( (key) => @@ -106,7 +93,34 @@ export const isAdvancedSearchFormValid = (value: IAdvancedSearchFormState) => { ) } -const BirthSection = () => { +interface BirthSectionProps { + hasBirthSearchJurisdictionScope: boolean + userOfficeId: UUID +} + +interface DeathSectionProps { + hasDeathSearchJurisdictionScope: boolean + userOfficeId: UUID +} + +const BirthSection: React.FC = ({ + hasBirthSearchJurisdictionScope, + userOfficeId +}) => { + const advancedSearchBirthSections = createAdvancedSearchBirthSections( + hasBirthSearchJurisdictionScope, + userOfficeId + ) + + const { + birthSearchRegistrationSection, + birthSearchChildSection, + birthSearchMotherSection, + birthSearchFatherSection, + birthSearchEventSection, + birthSearchInformantSection + } = advancedSearchBirthSections + const intl = useIntl() const navigate = useNavigate() const advancedSearchParamsState = useSelector(AdvancedSearchParamsSelector) @@ -119,7 +133,12 @@ const BirthSection = () => { ) }) const [accordionActiveStateMap] = useState( - getAccordionActiveStateMap(advancedSearchParamsState) + getAccordionActiveStateMap( + advancedSearchParamsState, + hasBirthSearchJurisdictionScope, + undefined, + userOfficeId + ) ) const isDisabled = !isAdvancedSearchFormValid(formState) @@ -318,7 +337,10 @@ const BirthSection = () => { ) } -const DeathSection = () => { +const DeathSection: React.FC = ({ + hasDeathSearchJurisdictionScope, + userOfficeId +}) => { const intl = useIntl() const navigate = useNavigate() const advancedSearchParamsState = useSelector(AdvancedSearchParamsSelector) @@ -331,7 +353,12 @@ const DeathSection = () => { ) }) const [accordionActiveStateMap] = useState( - getAccordionActiveStateMap(advancedSearchParamsState) + getAccordionActiveStateMap( + advancedSearchParamsState, + undefined, + hasDeathSearchJurisdictionScope, + userOfficeId + ) ) const isDisable = !isAdvancedSearchFormValid(formState) @@ -343,6 +370,18 @@ const DeathSection = () => { advancedSearchFormMessages.hide ) + const advancedSearchDeathSections = createAdvancedSearchDeathSections( + hasDeathSearchJurisdictionScope, + userOfficeId + ) + + const { + deathSearchRegistrationSection, + deathSearchDeceasedSection, + deathSearchEventSection, + deathSearchInformantSection + } = advancedSearchDeathSections + return ( <> { const AdvancedSearch = () => { const intl = useIntl() + const { + canSearchBirthRecords, + canSearchDeathRecords, + hasBirthSearchJurisdictionScope, + hasDeathSearchJurisdictionScope + } = usePermissions() const advancedSearchParamState = useSelector(AdvancedSearchParamsSelector) - const activeTabId = advancedSearchParamState.event || TabId.BIRTH - const dispatch = useDispatch() + const currentUser = useSelector(getUserDetails) + const userPrimaryOffice = currentUser?.primaryOffice + + if (!userPrimaryOffice) + throw new Error( + 'Something went wrong. Could not find any office assigned to the user' + ) + const activeTabId = + advancedSearchParamState.event === TabId.BIRTH && canSearchBirthRecords + ? TabId.BIRTH + : advancedSearchParamState.event === TabId.DEATH && canSearchDeathRecords + ? TabId.DEATH + : canSearchBirthRecords + ? TabId.BIRTH + : canSearchDeathRecords + ? TabId.DEATH + : '' + + const dispatch = useDispatch() const tabSections = [ { id: TabId.BIRTH, - title: intl.formatMessage(messages.birthTabTitle) + title: intl.formatMessage(messages.birthTabTitle), + showTab: canSearchBirthRecords }, { id: TabId.DEATH, - title: intl.formatMessage(messages.deathTabTitle) + title: intl.formatMessage(messages.deathTabTitle), + showTab: canSearchDeathRecords } ] + + const filteredTabSections = tabSections + .filter((section) => section.showTab) + .map((sec) => ({ id: sec.id, title: sec.title })) + return ( <> { size={ContentSize.SMALL} tabBarContent={ { dispatch( @@ -526,8 +595,18 @@ const AdvancedSearch = () => { } subtitle={intl.formatMessage(messages.advancedSearchInstruction)} > - {activeTabId === TabId.BIRTH && } - {activeTabId === TabId.DEATH && } + {activeTabId === TabId.BIRTH && ( + + )} + {activeTabId === TabId.DEATH && ( + + )} diff --git a/packages/client/src/views/SearchResult/SearchResult.test.tsx b/packages/client/src/views/SearchResult/SearchResult.test.tsx index 64e64221915..7edee07d21e 100644 --- a/packages/client/src/views/SearchResult/SearchResult.test.tsx +++ b/packages/client/src/views/SearchResult/SearchResult.test.tsx @@ -10,23 +10,25 @@ */ import { Spinner } from '@opencrvs/components/lib/Spinner' import { Workqueue } from '@opencrvs/components/lib/Workqueue' -import { checkAuth } from '@opencrvs/client/src/profile/profileActions' + import { merge } from 'lodash' import * as React from 'react' import { queries } from '@client/profile/queries' import { SEARCH_EVENTS } from '@client/search/queries' import { createStore } from '@client/store' -import { createTestComponent, mockUserResponse } from '@client/tests/util' +import { + createTestComponent, + mockUserResponse, + REGISTRAR_DEFAULT_SCOPES, + setScopes +} from '@client/tests/util' import { SearchResult } from '@client/views/SearchResult/SearchResult' import { waitForElement } from '@client/tests/wait-for-element' import { EventType } from '@client/utils/gateway' import { storeDeclaration } from '@client/declarations' -import { vi, Mock } from 'vitest' +import { vi } from 'vitest' -const registerScopeToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWdpc3RlciIsImNlcnRpZnkiLCJkZW1vIl0sImlhdCI6MTU0MjY4ODc3MCwiZXhwIjoxNTQzMjkzNTcwLCJhdWQiOlsib3BlbmNydnM6YXV0aC11c2VyIiwib3BlbmNydnM6dXNlci1tZ250LXVzZXIiLCJvcGVuY3J2czpoZWFydGgtdXNlciIsIm9wZW5jcnZzOmdhdGV3YXktdXNlciIsIm9wZW5jcnZzOm5vdGlmaWNhdGlvbi11c2VyIiwib3BlbmNydnM6d29ya2Zsb3ctdXNlciJdLCJpc3MiOiJvcGVuY3J2czphdXRoLXNlcnZpY2UiLCJzdWIiOiI1YmVhYWY2MDg0ZmRjNDc5MTA3ZjI5OGMifQ.ElQd99Lu7WFX3L_0RecU_Q7-WZClztdNpepo7deNHqzro-Cog4WLN7RW3ZS5PuQtMaiOq1tCb-Fm3h7t4l4KDJgvC11OyT7jD6R2s2OleoRVm3Mcw5LPYuUVHt64lR_moex0x_bCqS72iZmjrjS-fNlnWK5zHfYAjF2PWKceMTGk6wnI9N49f6VwwkinJcwJi6ylsjVkylNbutQZO0qTc7HRP-cBfAzNcKD37FqTRNpVSvHdzQSNcs7oiv3kInDN5aNa2536XSd3H-RiKR9hm9eID9bSIJgFIGzkWRd5jnoYxT70G0t03_mTVnDnqPXDtyI-lmerx24Ost0rQLUNIg' -const getItem = window.localStorage.getItem as Mock const mockFetchUserDetails = vi.fn() const nameObj = { @@ -41,7 +43,14 @@ const nameObj = { }, { use: 'bn', firstNames: '', familyName: '', __typename: 'HumanName' } ], - systemRole: 'DISTRICT_REGISTRAR' + role: { + id: 'DISTRICT_REGISTRAR', + label: { + defaultMessage: 'District Registrar', + description: 'Name for user role Field Agent', + id: 'userRole.fieldAgent' + } + } } } } @@ -55,8 +64,7 @@ describe('SearchResult tests', () => { beforeEach(async () => { ;({ store } = createStore()) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) }) it('sets loading state while waiting for data', async () => { @@ -77,7 +85,6 @@ describe('SearchResult tests', () => { query: SEARCH_EVENTS, variables: { advancedSearchParameters: { - declarationLocationId: '2a83cf14-b959-47f4-8097-f75a75d1867f', trackingId: '', nationalId: '', registrationNumber: '', @@ -320,7 +327,6 @@ describe('SearchResult tests', () => { nationalId: '', registrationNumber: '', contactNumber: '+8801622688232', - declarationLocationId: '1234567s2323289', contactEmail: '' }, sort: 'DESC' @@ -392,7 +398,6 @@ describe('SearchResult tests', () => { query: SEARCH_EVENTS, variables: { advancedSearchParameters: { - declarationLocationId: '2a83cf14-b959-47f4-8097-f75a75d1867f', trackingId: 'DW0UTHR', nationalId: '', registrationNumber: '', @@ -485,7 +490,6 @@ describe('SearchResult tests', () => { query: SEARCH_EVENTS, variables: { advancedSearchParameters: { - declarationLocationId: '2a83cf14-b959-47f4-8097-f75a75d1867f', trackingId: 'DW0UTHR', nationalId: '', registrationNumber: '', @@ -586,7 +590,6 @@ describe('SearchResult tests', () => { query: SEARCH_EVENTS, variables: { advancedSearchParameters: { - declarationLocationId: '2a83cf14-b959-47f4-8097-f75a75d1867f', trackingId: 'DW0UTHR', nationalId: '', registrationNumber: '', @@ -671,8 +674,7 @@ describe('SearchResult downloadButton tests', () => { beforeEach(async () => { ;({ store } = createStore()) - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) }) it('renders review button in search page', async () => { const declaration = { @@ -692,7 +694,6 @@ describe('SearchResult downloadButton tests', () => { query: SEARCH_EVENTS, variables: { advancedSearchParameters: { - declarationLocationId: '2a83cf14-b959-47f4-8097-f75a75d1867f', trackingId: 'DW0UTHR', nationalId: '', registrationNumber: '', @@ -788,7 +789,6 @@ describe('SearchResult downloadButton tests', () => { query: SEARCH_EVENTS, variables: { advancedSearchParameters: { - declarationLocationId: '2a83cf14-b959-47f4-8097-f75a75d1867f', trackingId: 'DW0UTHR', nationalId: '', registrationNumber: '', diff --git a/packages/client/src/views/SearchResult/SearchResult.tsx b/packages/client/src/views/SearchResult/SearchResult.tsx index 2124bf74810..d74c9918b18 100644 --- a/packages/client/src/views/SearchResult/SearchResult.tsx +++ b/packages/client/src/views/SearchResult/SearchResult.tsx @@ -8,15 +8,15 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ +import { Header } from '@client/components/Header/Header' +import { DownloadButton } from '@client/components/interface/DownloadButton' +import { Query } from '@client/components/Query' import { DOWNLOAD_STATUS, + getProcessingDeclarationIds, IDeclaration, - SUBMISSION_STATUS, - getProcessingDeclarationIds + SUBMISSION_STATUS } from '@client/declarations' -import { DownloadButton } from '@client/components/interface/DownloadButton' -import { Header } from '@client/components/Header/Header' -import { Query } from '@client/components/Query' import { DownloadAction } from '@client/forms' import { buttonMessages, @@ -41,36 +41,34 @@ import { getScope, getUserDetails } from '@client/profile/profileSelectors' import { SEARCH_EVENTS } from '@client/search/queries' import { transformData } from '@client/search/transformer' import { IStoreState } from '@client/store' -import styled, { withTheme } from 'styled-components' -import { ITheme } from '@opencrvs/components/lib/theme' -import { Scope } from '@client/utils/authUtils' import { SEARCH_RESULT_SORT } from '@client/utils/constants' +import { Scope, SCOPES } from '@opencrvs/commons/client' +import { SearchEventsQuery } from '@client/utils/gateway' import { getUserLocation, UserDetails } from '@client/utils/userUtils' -import { SearchEventsQuery, SystemRoleType } from '@client/utils/gateway' - +import { ITheme } from '@opencrvs/components/lib/theme' +import styled, { withTheme } from 'styled-components' +import { Frame } from '@opencrvs/components/lib/Frame' import { ColumnContentAlignment, - Workqueue, + COLUMNS, IAction, - COLUMNS + Workqueue } from '@opencrvs/components/lib/Workqueue' -import { Frame } from '@opencrvs/components/lib/Frame' - -import * as React from 'react' +import { Navigation } from '@client/components/interface/Navigation' +import React from 'react' import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl' import { connect } from 'react-redux' import ReactTooltip from 'react-tooltip' import { convertToMSISDN } from '@client/forms/utils' import { formattedDuration } from '@client/utils/date-formatting' -import { Navigation } from '@client/components/interface/Navigation' import { IconWithName, IconWithNameEvent, - NoNameContainer, - NameContainer + NameContainer, + NoNameContainer } from '@client/views/OfficeHome/components' -import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' import { LoadingIndicator } from '@client/views/OfficeHome/LoadingIndicator' +import { WQContentWrapper } from '@client/views/OfficeHome/WQContentWrapper' import { SearchCriteria } from '@client/utils/referenceApi' import { useWindowSize } from '@opencrvs/components/src/hooks' import * as routes from '@client/navigation/routes' @@ -105,7 +103,7 @@ export function getRejectionReasonDisplayValue(reason: string) { interface IBaseSearchResultProps { theme: ITheme language: string - scope: Scope | null + scope: Scope[] | null userDetails: UserDetails | null outboxDeclarations: IDeclaration[] } @@ -163,15 +161,36 @@ function SearchResultView(props: ISearchResultProps) { } function userHasRegisterScope() { - return props.scope && props.scope.includes('register') + return props.scope && props.scope.includes(SCOPES.RECORD_REGISTER) } function userHasValidateScope() { - return props.scope && props.scope.includes('validate') + const validateScopes = [ + SCOPES.RECORD_REGISTER, + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES + ] as Scope[] + + return ( + props.scope && props.scope.some((scope) => validateScopes.includes(scope)) + ) + } + + function hasIssueScope() { + return props.scope?.includes(SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES) } - function userHasCertifyScope() { - return props.scope && props.scope.includes('certify') + function hasPrintScope() { + return props.scope?.includes(SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES) + } + + function canSearchAnywhere() { + const searchScopes: Scope[] = [ + SCOPES.SEARCH_BIRTH, + SCOPES.SEARCH_DEATH, + SCOPES.SEARCH_MARRIAGE + ] + return props.scope?.some((scope) => searchScopes.includes(scope)) } const transformSearchContent = (data: QueryData) => { @@ -235,7 +254,7 @@ function SearchResultView(props: ISearchResultProps) { if (width > props.theme.grid.breakpoints.lg) { if ( (declarationIsRegistered || declarationIsIssued) && - userHasCertifyScope() + hasPrintScope() ) { actions.push({ label: props.intl.formatMessage(buttonMessages.print), @@ -253,7 +272,7 @@ function SearchResultView(props: ISearchResultProps) { }, disabled: downloadStatus !== DOWNLOAD_STATUS.DOWNLOADED }) - } else if (declarationIsCertified && userHasCertifyScope()) { + } else if (declarationIsCertified && hasIssueScope()) { actions.push({ label: props.intl.formatMessage(buttonMessages.issue), handler: ( @@ -324,12 +343,7 @@ function SearchResultView(props: ISearchResultProps) { name: searchType === SearchCriteria.NAME ? searchText : '', declarationLocationId: - userDetails && - ![ - SystemRoleType.LocalRegistrar, - SystemRoleType.NationalRegistrar, - SystemRoleType.RegistrationAgent - ].includes(userDetails.systemRole) + !canSearchAnywhere() && userDetails ? getUserLocation(userDetails).id : '' }, @@ -345,6 +359,7 @@ function SearchResultView(props: ISearchResultProps) { DownloadAction.LOAD_REVIEW_DECLARATION }} status={downloadStatus as DOWNLOAD_STATUS} + declarationStatus={reg.declarationStatus as SUBMISSION_STATUS} /> ) }) @@ -447,15 +462,6 @@ function SearchResultView(props: ISearchResultProps) { query={SEARCH_EVENTS} variables={{ advancedSearchParameters: { - declarationLocationId: - userDetails && - ![ - SystemRoleType.LocalRegistrar, - SystemRoleType.NationalRegistrar, - SystemRoleType.RegistrationAgent - ].includes(userDetails.systemRole) - ? getUserLocation(userDetails).id - : '', trackingId: searchType === SearchCriteria.TRACKING_ID ? searchText : '', nationalId: diff --git a/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.test.tsx b/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.test.tsx index 31b99ae5ff9..ce7d7813332 100644 --- a/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.test.tsx +++ b/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.test.tsx @@ -8,26 +8,43 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ +import { + createTestApp, + flushPromises, + setScopes, + waitForReady +} from '@client/tests/util' import { SELECT_VITAL_EVENT } from '@client/navigation/routes' -import { createTestApp, flushPromises, waitForReady } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' +import { AppStore } from '@client/store' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { ReactWrapper } from 'enzyme' import { createMemoryRouter } from 'react-router-dom' describe('when user is selecting the vital event', () => { let app: ReactWrapper let router: ReturnType + let store: AppStore beforeEach(async () => { const testApp = await createTestApp() app = testApp.app router = testApp.router + store = testApp.store await waitForReady(app) }) describe('when user is in vital event selection view', () => { beforeEach(async () => { + setScopes( + [ + SCOPES.RECORD_DECLARE_BIRTH, + SCOPES.RECORD_DECLARE_DEATH, + SCOPES.RECORD_DECLARE_MARRIAGE + ], + store + ) await flushPromises() router.navigate(SELECT_VITAL_EVENT, { replace: true }) await waitForElement(app, '#select_vital_event_view') @@ -35,6 +52,7 @@ describe('when user is selecting the vital event', () => { it('lists the options', () => { expect(app.find('#select_vital_event_view').hostNodes()).toHaveLength(1) }) + describe('when selects "Birth"', () => { beforeEach(async () => { await flushPromises() @@ -81,4 +99,57 @@ describe('when user is selecting the vital event', () => { }) }) }) + + describe('Birth option', () => { + const tests = [ + [[SCOPES.RECORD_DECLARE_BIRTH], true], + [[SCOPES.RECORD_DECLARE_BIRTH_MY_JURISDICTION], true], + [[SCOPES.RECORD_DECLARE_DEATH, SCOPES.RECORD_DECLARE_MARRIAGE], false] + ] + + tests.forEach(([scopes, length]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + router.navigate(SELECT_VITAL_EVENT, { replace: true }) + await waitForElement(app, '#select_vital_event_view') + expect(app.exists('#select_birth_event')).toBe(length) + }) + }) + }) + + describe('Death option', () => { + const tests = [ + [[SCOPES.RECORD_DECLARE_DEATH], true], + [[SCOPES.RECORD_DECLARE_DEATH_MY_JURISDICTION], true], + [[SCOPES.RECORD_DECLARE_BIRTH, SCOPES.RECORD_DECLARE_MARRIAGE], false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + router.navigate(SELECT_VITAL_EVENT, { replace: true }) + + await waitForElement(app, '#select_vital_event_view') + expect(app.exists('#select_death_event')).toBe(exists) + }) + }) + }) + + describe('Marriage option', () => { + const tests = [ + [[SCOPES.RECORD_DECLARE_MARRIAGE], true], + [[SCOPES.RECORD_DECLARE_MARRIAGE_MY_JURISDICTION], true], + [[SCOPES.RECORD_DECLARE_BIRTH, SCOPES.RECORD_DECLARE_DEATH], false] + ] + + tests.forEach(([scopes, exists]) => { + it(`should render when user has correct scopes ${scopes}`, async () => { + setScopes(scopes as Scope[], store) + router.navigate(SELECT_VITAL_EVENT, { replace: true }) + + await waitForElement(app, '#select_vital_event_view') + expect(app.exists('#select_marriage_event')).toBe(exists) + }) + }) + }) }) diff --git a/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.tsx b/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.tsx index 782204660ab..bdcbd1e2c12 100644 --- a/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.tsx +++ b/packages/client/src/views/SelectVitalEvent/SelectVitalEvent.tsx @@ -19,6 +19,7 @@ import { Content, ContentSize } from '@opencrvs/components/lib/Content' import { Stack } from '@opencrvs/components/lib/Stack' import { Button } from '@opencrvs/components/lib/Button' import { Icon } from '@opencrvs/components/lib/Icon' +import { SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' import { formatUrl } from '@client/navigation' import { messages } from '@client/i18n/messages/views/selectVitalEvent' @@ -28,6 +29,7 @@ import { IDeclaration, createDeclaration } from '@client/declarations' +import ProtectedComponent from '@client/components/ProtectedComponent' import { RouteComponentProps, @@ -149,48 +151,69 @@ const SelectVitalEventView = ( alignItems="left" gap={0} > - { - setGoTo(EventType.Birth) - setNoEventSelectedError(false) - }} - /> - {window.config.FEATURES.DEATH_REGISTRATION && ( + { - setGoTo(EventType.Death) + setGoTo(EventType.Birth) setNoEventSelectedError(false) }} /> + + {window.config.FEATURES.DEATH_REGISTRATION && ( + + { + setGoTo(EventType.Death) + setNoEventSelectedError(false) + }} + /> + )} {window.config.FEATURES.MARRIAGE_REGISTRATION && ( - { - setGoTo(EventType.Marriage) - setNoEventSelectedError(false) - }} - /> + + { + setGoTo(EventType.Marriage) + setNoEventSelectedError(false) + }} + /> + )} diff --git a/packages/client/src/views/Settings/items/Role.tsx b/packages/client/src/views/Settings/items/Role.tsx index ed7ac72c9e1..da7ee7e780f 100644 --- a/packages/client/src/views/Settings/items/Role.tsx +++ b/packages/client/src/views/Settings/items/Role.tsx @@ -8,27 +8,24 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import * as React from 'react' -import { ListViewItemSimplified } from '@opencrvs/components/lib/ListViewSimplified' -import { useIntl } from 'react-intl' -import { constantsMessages, buttonMessages } from '@client/i18n/messages' +import { buttonMessages, constantsMessages } from '@client/i18n/messages' +import { getUserDetails } from '@client/profile/profileSelectors' +import { IStoreState } from '@client/store' import { + DynamicHeightLinkButton, LabelContainer, - ValueContainer, - DynamicHeightLinkButton + ValueContainer } from '@client/views/Settings/items/components' +import { ListViewItemSimplified } from '@opencrvs/components/lib/ListViewSimplified' +import * as React from 'react' +import { useIntl } from 'react-intl' import { useSelector } from 'react-redux' -import { IStoreState } from '@client/store' -import { getLanguage } from '@client/i18n/selectors' -import { getUserDetails } from '@client/profile/profileSelectors' -import { getUserRole } from '@client/utils' export function Role() { const intl = useIntl() - const language = useSelector(getLanguage) - const systemRole = useSelector((state) => { + const role = useSelector((state) => { const userDetails = getUserDetails(state) - return (userDetails && getUserRole(language, userDetails.role)) ?? '' + return (userDetails && intl.formatMessage(userDetails.role.label)) || '' }) return ( } - value={{systemRole}} + value={{role}} actions={ {intl.formatMessage(buttonMessages.change)} diff --git a/packages/client/src/views/Settings/queries.ts b/packages/client/src/views/Settings/queries.ts index d4bfcba9a5d..3921bfa875c 100644 --- a/packages/client/src/views/Settings/queries.ts +++ b/packages/client/src/views/Settings/queries.ts @@ -17,9 +17,8 @@ export const GET_USER_BY_MOBILE = gql` username mobile email - systemRole role { - _id + id } status } @@ -33,9 +32,8 @@ export const GET_USER_BY_EMAIL = gql` username mobile email - systemRole role { - _id + id } status } diff --git a/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx b/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx index f2cfe773945..81c82d039ac 100644 --- a/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx +++ b/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx @@ -214,6 +214,7 @@ export function SystemList() { const systemTypeLabels = { HEALTH: intl.formatMessage(integrationMessages.eventNotification), RECORD_SEARCH: intl.formatMessage(integrationMessages.recordSearch), + NATIONAL_ID: intl.formatMessage(integrationMessages.nationalId), WEBHOOK: intl.formatMessage(integrationMessages.webhook) } diff --git a/packages/client/src/views/SysAdmin/Performance/CompletenessRates.tsx b/packages/client/src/views/SysAdmin/Performance/CompletenessRates.tsx index 5d5cf65d910..36713864fec 100644 --- a/packages/client/src/views/SysAdmin/Performance/CompletenessRates.tsx +++ b/packages/client/src/views/SysAdmin/Performance/CompletenessRates.tsx @@ -161,7 +161,7 @@ function Filter({ type === 'ADMIN_STRUCTURE'} onChangeLocation={(newLocationId) => { navigate( generateCompletenessRatesUrl({ diff --git a/packages/client/src/views/SysAdmin/Performance/PerformanceHome.tsx b/packages/client/src/views/SysAdmin/Performance/PerformanceHome.tsx index 2588f819f60..70c28770c41 100644 --- a/packages/client/src/views/SysAdmin/Performance/PerformanceHome.tsx +++ b/packages/client/src/views/SysAdmin/Performance/PerformanceHome.tsx @@ -29,9 +29,10 @@ import { Content, ContentSize } from '@opencrvs/components/lib/Content' import { DateRangePicker } from '@client/components/DateRangePicker' import subMonths from 'date-fns/subMonths' import { PerformanceSelect } from '@client/views/SysAdmin/Performance/PerformanceSelect' +import { Scope, SCOPES } from '@opencrvs/commons/client' import { EventType } from '@client/utils/gateway' import { LocationPicker } from '@client/components/LocationPicker' -import { getUserDetails } from '@client/profile/profileSelectors' +import { getScope, getUserDetails } from '@client/profile/profileSelectors' import { Query } from '@client/components/Query' import { CORRECTION_TOTALS, @@ -72,7 +73,6 @@ import { } from '@client/navigation' import { withOnlineStatus } from '@client/views/OfficeHome/LoadingIndicator' import { NoWifi } from '@opencrvs/components/lib/icons' -import { REGISTRAR_ROLES } from '@client/utils/constants' import { ICurrency } from '@client/utils/referenceApi' import { Box } from '@opencrvs/components/lib/Box' import startOfMonth from 'date-fns/startOfMonth' @@ -202,7 +202,10 @@ type IOnlineStatusProps = { } type Props = WrappedComponentProps & - IOnlineStatusProps & { userDetails: UserDetails | null } & IConnectProps & { + IOnlineStatusProps & { + userDetails: UserDetails | null + scopes: Scope[] | null + } & IConnectProps & { theme: ITheme } @@ -239,6 +242,17 @@ const PerformanceHomeComponent = (props: Props) => { event: parsedSearch.event || EventType.Birth } + let { locationId } = searchParams + + // Defaults empty URL locationId to your primary office if you don't have access to all locations via scopes + if ( + userDetails && + !locationId && + !props.scopes?.includes(SCOPES.ORGANISATION_READ_LOCATIONS) + ) { + locationId = userDetails.primaryOffice.id + } + const selectedLocation = !searchParams.locationId ? getAdditionalLocations(props.intl)[0] : selectLocation( @@ -263,14 +277,14 @@ const PerformanceHomeComponent = (props: Props) => { if ( selectedLocation && isOfficeSelected(selectedLocation) && - userDetails && - userDetails.systemRole + userDetails ) { - if (userDetails?.systemRole === 'NATIONAL_REGISTRAR') { + if (props.scopes?.includes(SCOPES.ORGANISATION_READ_LOCATIONS)) { return true - } - if ( - REGISTRAR_ROLES.includes(userDetails?.systemRole) && + } else if ( + props.scopes?.includes( + SCOPES.ORGANISATION_READ_LOCATIONS_MY_OFFICE + ) && userDetails.primaryOffice?.id === selectedLocation.id ) { return true @@ -278,7 +292,7 @@ const PerformanceHomeComponent = (props: Props) => { } return false }, - [isOfficeSelected, userDetails] + [isOfficeSelected, userDetails, props.scopes] ) const [toggleStatus, setToggleStatus] = useState(false) @@ -697,7 +711,7 @@ const PerformanceHomeComponent = (props: Props) => { function mapStateToProps(state: IStoreState) { const offlineCountryConfiguration = getOfflineData(state) - + const scopes = getScope(state) const locations = offlineCountryConfiguration.locations const offices = offlineCountryConfiguration.offices @@ -705,7 +719,8 @@ function mapStateToProps(state: IStoreState) { locations, offices, userDetails: getUserDetails(state), - currency: offlineCountryConfiguration.config.CURRENCY + currency: offlineCountryConfiguration.config.CURRENCY, + scopes } } diff --git a/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.test.tsx b/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.test.tsx index 2f6927af5b6..8346e3cc993 100644 --- a/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.test.tsx +++ b/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.test.tsx @@ -17,13 +17,14 @@ import { TestComponentWithRouteMock } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' -import { EventType } from '@client/utils/gateway' +import { GetEventsWithProgressQuery, EventType } from '@client/utils/gateway' import { WorkflowStatus } from '@client/views/SysAdmin/Performance/WorkflowStatus' import { ReactWrapper } from 'enzyme' import { GraphQLError } from 'graphql' import { parse, stringify } from 'query-string' import * as React from 'react' import { vi } from 'vitest' +import { PlainDate } from '@client/utils/date-formatting' import { FETCH_EVENTS_WITH_PROGRESS } from './queries' describe('Workflow status tests', () => { @@ -71,7 +72,7 @@ describe('Workflow status tests', () => { familyName: 'মায়ের পারিবারিক নাম ' } ], - dateOfEvent: '2020-05-17', + dateOfEvent: '2020-05-17' as unknown as PlainDate, registration: { status: null, contactNumber: null, @@ -92,13 +93,12 @@ describe('Workflow status tests', () => { } ], role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'LOCAL_REGISTRAR' - } - ] + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } } }, startedByFacility: null, @@ -126,7 +126,7 @@ describe('Workflow status tests', () => { familyName: 'আমিনা' } ], - dateOfEvent: '2020-02-15', + dateOfEvent: '2020-02-15' as unknown as PlainDate, registration: { status: 'REGISTERED', contactNumber: '+8801959595999', @@ -147,13 +147,12 @@ describe('Workflow status tests', () => { } ], role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'LOCAL_REGISTRAR' - } - ] + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } } }, progressReport: { @@ -180,7 +179,7 @@ describe('Workflow status tests', () => { familyName: 'আমিনা' } ], - dateOfEvent: '2020-03-15', + dateOfEvent: '2020-03-15' as unknown as PlainDate, registration: { status: 'CERTIFIED', contactNumber: '+8801656568682', @@ -201,13 +200,12 @@ describe('Workflow status tests', () => { } ], role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'LOCAL_REGISTRAR' - } - ] + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } } }, progressReport: { @@ -220,7 +218,7 @@ describe('Workflow status tests', () => { } } ] - } + } satisfies GetEventsWithProgressQuery['getEventsWithProgress'] } } } diff --git a/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.tsx b/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.tsx index 1987647d1e1..4b0b2237a5c 100644 --- a/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.tsx +++ b/packages/client/src/views/SysAdmin/Performance/WorkflowStatus.tsx @@ -39,9 +39,8 @@ import type { } from '@client/utils/gateway-deprecated-do-not-use' import { orderBy } from 'lodash' import { parse } from 'query-string' -import * as React from 'react' +import React from 'react' import { injectIntl, WrappedComponentProps } from 'react-intl' -import { useSelector } from 'react-redux' import ReactTooltip from 'react-tooltip' import styled from 'styled-components' import { checkExternalValidationStatus } from '@client/views/SysAdmin/Team/utils' @@ -56,8 +55,6 @@ import { Content, ContentSize } from '@opencrvs/components/lib/Content' import { Spinner } from '@opencrvs/components/lib/Spinner' import { Table } from '@opencrvs/components/lib/Table' import { Pagination } from '@opencrvs/components/lib/Pagination' -import { getLanguage } from '@client/i18n/selectors' -import { getUserRole } from '@client/utils' import * as routes from '@client/navigation/routes' import { useLocation, useNavigate } from 'react-router-dom' @@ -203,7 +200,6 @@ function WorkflowStatusComponent(props: WorkflowStatusProps) { 'declarationStartedOn' ) const pageSize = 10 - const language = useSelector(getLanguage) let timeStart: string | Date = subYears(new Date(Date.now()), 1) let timeEnd: string | Date = new Date(Date.now()) @@ -497,7 +493,7 @@ function WorkflowStatusComponent(props: WorkflowStatusProps) { if (eventProgress.startedBy != null) { const user = eventProgress.startedBy starterPractitionerRole = - (user.role && getUserRole(language, user.role)) || '' + (user.role && intl.formatMessage(user.role.label)) || '' } const event = @@ -713,8 +709,16 @@ function WorkflowStatusComponent(props: WorkflowStatusProps) { }) ) }} - requiredJurisdictionTypes={ + locationFilter={ window.config.DECLARATION_AUDIT_LOCATIONS + ? ({ jurisdictionType }) => + Boolean( + jurisdictionType && + window.config.DECLARATION_AUDIT_LOCATIONS.split( + ',' + ).includes(jurisdictionType) + ) + : undefined } /> { const storeContext = await createTestStore() store = storeContext.store - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) store.dispatch(offlineDataReady(mockOfflineDataDispatch)) await flushPromises() vi.spyOn(locationUtils, 'getJurisidictionType').mockReturnValue('UNION') - vi.spyOn(performanceUtils, 'isUnderJurisdictionOfUser').mockReturnValue( - true - ) }) describe('when it has data in props', () => { diff --git a/packages/client/src/views/SysAdmin/Performance/utils.test.ts b/packages/client/src/views/SysAdmin/Performance/utils.test.ts index c00b51db378..163f8b67702 100644 --- a/packages/client/src/views/SysAdmin/Performance/utils.test.ts +++ b/packages/client/src/views/SysAdmin/Performance/utils.test.ts @@ -9,8 +9,7 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { getJurisidictionType, isUnderJurisdictionOfUser } from './utils' -import { mockOfflineData } from '@client/tests/util' +import { getJurisidictionType } from './utils' describe('Performance util tests', () => { describe('getJurisidictionType tests', () => { @@ -52,26 +51,4 @@ describe('Performance util tests', () => { expect(getJurisidictionType(mockFacilitiesData)).toEqual(null) }) }) - - describe('isUnderJurisdictionOfUser', () => { - it('returns true if given location is a child location of jurisdictionLocation', () => { - const locations = mockOfflineData.locations - const locationId = 'a3455e64-164c-4bf4-b834-16640a85efd8' //Cox's Bazaar District - const jurisdictionLocation = '8cbc862a-b817-4c29-a490-4a8767ff023c' //Chittagong Division - - expect( - isUnderJurisdictionOfUser(locations, locationId, jurisdictionLocation) - ).toEqual(true) - }) - - it('returns false if given location is not a child location of jurisdictionLocation', () => { - const locations = mockOfflineData.locations - const locationId = 'a3455e64-164c-4bf4-b834-16640a85efd8' //Cox's Bazaar District - const jurisdictionLocation = '65cf62cb-864c-45e3-9c0d-5c70f0074cb4' //Barisal Division - - expect( - isUnderJurisdictionOfUser(locations, locationId, jurisdictionLocation) - ).toEqual(false) - }) - }) }) diff --git a/packages/client/src/views/SysAdmin/Performance/utils.tsx b/packages/client/src/views/SysAdmin/Performance/utils.tsx index 26939bbba05..80b550567b4 100644 --- a/packages/client/src/views/SysAdmin/Performance/utils.tsx +++ b/packages/client/src/views/SysAdmin/Performance/utils.tsx @@ -184,20 +184,6 @@ export function getJurisidictionType(location: GQLLocation): string | null { return jurisdictionType } -export function isUnderJurisdictionOfUser( - locations: { [key: string]: ILocation }, - locationId: string, - jurisdictionLocation: string | undefined | null -) { - if (!jurisdictionLocation) return false - - while (locationId !== jurisdictionLocation && locationId !== '0') { - locationId = locations[locationId].partOf.split('/')[1] - } - - return locationId !== '0' -} - export function getPrimaryLocationIdOfOffice( locations: { [key: string]: ILocation }, office: ILocation diff --git a/packages/client/src/views/SysAdmin/Team/TeamSearch.test.tsx b/packages/client/src/views/SysAdmin/Team/TeamSearch.test.tsx index 8191dfa0902..2bb216d5d16 100644 --- a/packages/client/src/views/SysAdmin/Team/TeamSearch.test.tsx +++ b/packages/client/src/views/SysAdmin/Team/TeamSearch.test.tsx @@ -9,7 +9,6 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import { TEAM_USER_LIST } from '@client/navigation/routes' -import { checkAuth } from '@client/profile/profileActions' import { queries } from '@client/profile/queries' import { AppStore } from '@client/store' import { @@ -17,16 +16,18 @@ import { createTestStore, flushPromises, mockUserResponse, - registerScopeToken + REGISTRAR_DEFAULT_SCOPES, + setScopes } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' import { ReactWrapper } from 'enzyme' import { merge } from 'lodash' import { parse } from 'query-string' import * as React from 'react' -import { createMemoryRouter } from 'react-router-dom' -import { Mock, vi } from 'vitest' import { TeamSearch } from './TeamSearch' +import { vi } from 'vitest' +import { SCOPES } from '@opencrvs/commons/client' +import { createMemoryRouter } from 'react-router-dom' describe('Team search test', () => { let store: AppStore @@ -44,6 +45,8 @@ describe('Team search test', () => { ;({ component: app, router } = await createTestComponent(, { store })) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) + app.update() }) @@ -89,7 +92,7 @@ describe('Team search test', () => { describe('Team search with location in props', () => { let testComponent: ReactWrapper<{}, {}> - const getItem = window.localStorage.getItem as Mock + const mockFetchUserDetails = vi.fn() const nameObj = { data: { @@ -127,12 +130,11 @@ describe('Team search test', () => { queries.fetchUserDetails = mockFetchUserDetails }) - beforeAll(async () => { - getItem.mockReturnValue(registerScopeToken) - await store.dispatch(checkAuth()) - }) + beforeAll(async () => {}) beforeEach(async () => { + setScopes([SCOPES.USER_READ], store) + testComponent = ( await createTestComponent(, { store, diff --git a/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.test.tsx b/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.test.tsx index 24ff7cdeae2..7cff46352c4 100644 --- a/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.test.tsx +++ b/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.test.tsx @@ -14,10 +14,10 @@ import { UserAuditActionModal, AUDIT_ACTION } from './UserAuditActionModal' import { createTestComponent } from '@client/tests/util' import { AppStore, createStore } from '@client/store' import { waitFor, waitForElement } from '@client/tests/wait-for-element' -import { USER_AUDIT_ACTION } from '@client/user/queries' +import { GET_USER, USER_AUDIT_ACTION } from '@client/user/queries' import { GraphQLError } from 'graphql' -import { vi, Mock } from 'vitest' -import { SystemRoleType, Status } from '@client/utils/gateway' +import { vi } from 'vitest' +import { Status } from '@client/utils/gateway' import { UserDetails } from '@client/utils/userUtils' const users: UserDetails[] = [ @@ -31,7 +31,6 @@ const users: UserDetails[] = [ } ], username: 'r.tagore', - systemRole: SystemRoleType.RegistrationAgent, localRegistrar: { name: [ { @@ -40,17 +39,15 @@ const users: UserDetails[] = [ familyName: 'Huq' } ], - role: SystemRoleType.LocalRegistrar, + role: 'LOCAL_REGISTRAR', signature: undefined }, role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'ENTREPENEUR' - } - ] + label: { + id: 'userRoles.entrepreneur', + defaultMessage: 'Entrepreneur', + description: 'Entrepreneur' + } }, status: Status.Active, creationDate: '2022-10-03T10:42:46.920Z', @@ -74,15 +71,12 @@ const users: UserDetails[] = [ } ], username: 'np.huq', - systemRole: SystemRoleType.LocalRegistrar, role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'MAYOR' - } - ] + label: { + id: 'userRoles.mayor', + defaultMessage: 'Mayor', + description: 'Mayor' + } }, status: Status.Deactivated, localRegistrar: { @@ -93,7 +87,7 @@ const users: UserDetails[] = [ familyName: 'Islam' } ], - role: SystemRoleType.LocalRegistrar, + role: 'LOCAL_REGISTRAR', signature: undefined }, creationDate: '2022-10-03T10:42:46.920Z', @@ -110,6 +104,19 @@ const users: UserDetails[] = [ ] const graphqlMocksOfDeactivate = [ + { + request: { + query: GET_USER, + variables: { + userId: users[0].id + } + }, + result: { + data: { + getUser: users[0] + } + } + }, { request: { query: USER_AUDIT_ACTION, @@ -143,6 +150,19 @@ const graphqlMocksOfDeactivate = [ ] const graphqlMocksOfReactivate = [ + { + request: { + query: GET_USER, + variables: { + userId: users[1].id + } + }, + result: { + data: { + getUser: users[1] + } + } + }, { request: { query: USER_AUDIT_ACTION, @@ -178,12 +198,10 @@ const graphqlMocksOfReactivate = [ describe('user audit action modal tests', () => { let component: ReactWrapper<{}, {}> let store: AppStore - let onCloseMock: Mock + const onCloseMock = vi.fn() beforeEach(async () => { ;({ store } = await createStore()) - - onCloseMock = vi.fn() }) afterEach(() => { @@ -192,14 +210,15 @@ describe('user audit action modal tests', () => { describe('in case of successful deactivate audit action', () => { beforeEach(async () => { - const [successMock] = graphqlMocksOfDeactivate + const [userDetails, successMock] = graphqlMocksOfDeactivate + const { component: testComponent } = await createTestComponent( , - { store, graphqlMocks: [successMock] } + { store, graphqlMocks: [userDetails, successMock] } ) component = testComponent }) @@ -254,14 +273,15 @@ describe('user audit action modal tests', () => { describe('in case of failed deactivate audit action', () => { beforeEach(async () => { - const [, errorMock] = graphqlMocksOfDeactivate + const [userDetails, , errorMock] = graphqlMocksOfDeactivate + const { component: testComponent } = await createTestComponent( , - { store, graphqlMocks: [errorMock] } + { store, graphqlMocks: [userDetails, errorMock] } ) component = testComponent }) @@ -288,14 +308,15 @@ describe('user audit action modal tests', () => { describe('in case of successful reactivate audit action', () => { beforeEach(async () => { - const [successMock] = graphqlMocksOfReactivate + const [userDetails, successMock] = graphqlMocksOfReactivate + const { component: testComponent } = await createTestComponent( , - { store, graphqlMocks: [successMock] } + { store, graphqlMocks: [userDetails, successMock] } ) component = testComponent }) @@ -337,14 +358,15 @@ describe('user audit action modal tests', () => { describe('in case of failed reactivate audit action', () => { beforeEach(async () => { - const [, errorMock] = graphqlMocksOfReactivate + const [userDetails, , errorMock] = graphqlMocksOfReactivate + const { component: testComponent } = await createTestComponent( , - { store, graphqlMocks: [errorMock] } + { store, graphqlMocks: [userDetails, errorMock] } ) component = testComponent }) diff --git a/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.tsx b/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.tsx index 887037b3f29..e80c83c6078 100644 --- a/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.tsx +++ b/packages/client/src/views/SysAdmin/Team/user/UserAuditActionModal.tsx @@ -28,17 +28,21 @@ import styled from 'styled-components' import { IFormSectionData } from '@client/forms' import { hasFormError } from '@client/forms/utils' import { ErrorText } from '@opencrvs/components/lib/ErrorText' -import { USER_AUDIT_ACTION } from '@client/user/queries' +import { GET_USER, USER_AUDIT_ACTION } from '@client/user/queries' import { Dispatch } from 'redux' import { showUserAuditSuccessToast, showSubmitFormErrorToast } from '@client/notification/actions' import { TOAST_MESSAGES } from '@client/user/userReducer' -import { ApolloClient, InternalRefetchQueriesInclude } from '@apollo/client' +import { + ApolloClient, + InternalRefetchQueriesInclude, + useQuery +} from '@apollo/client' import { withApollo, WithApolloClient } from '@apollo/client/react/hoc' -import { UserDetails } from '@client/utils/userUtils' import { getOfflineData } from '@client/offline/selectors' +import { GetUserQuery, GetUserQueryVariables } from '@client/utils/gateway' const { useState, useEffect } = React @@ -55,7 +59,7 @@ interface ToggleUserActivationModalProps extends WrappedComponentProps, ConnectProps, DispatchProps { - user: UserDetails | null + userId: string show: boolean onConfirmRefetchQueries?: InternalRefetchQueriesInclude onClose: () => void @@ -95,11 +99,16 @@ let makeAllFieldsDirty: (touched: {}) => void function UserAuditActionModalComponent( props: WithApolloClient ) { - const { intl, user, onClose, show, form } = props + const { intl, userId, onClose, show, form } = props const [formValues, setFormValues] = useState({}) const [formError, setFormError] = useState(null) const [isErrorVisible, makeErrorVisible] = useState(false) const config = useSelector(getOfflineData) + const { data } = useQuery(GET_USER, { + variables: { userId }, + fetchPolicy: 'cache-first' + }) + const user = data?.getUser ?? null let name = '' let modalTitle = '' @@ -165,7 +174,7 @@ function UserAuditActionModalComponent( } makeErrorVisible(true) if (!formError) { - const userId = props?.user?.id ?? '' + const userId = user?.id ?? '' ;(props.client as ApolloClient) .mutate({ diff --git a/packages/client/src/views/SysAdmin/Team/user/UserList.test.tsx b/packages/client/src/views/SysAdmin/Team/user/UserList.test.tsx index 0ff7ec194ac..d5b9a5bd8cf 100644 --- a/packages/client/src/views/SysAdmin/Team/user/UserList.test.tsx +++ b/packages/client/src/views/SysAdmin/Team/user/UserList.test.tsx @@ -13,9 +13,10 @@ import { mockLocalSysAdminUserResponse, createTestComponent, flushPromises, + mockRoles, + setScopes, mockOfflineDataDispatch, - mockUserResponse, - TestComponentWithRouteMock + fetchUserMock } from '@client/tests/util' import { waitForElement } from '@client/tests/wait-for-element' import { SEARCH_USERS } from '@client/user/queries' @@ -26,67 +27,400 @@ import { UserList } from './UserList' import { userMutations } from '@client/user/mutations' import * as actions from '@client/profile/profileActions' import { offlineDataReady } from '@client/offline/actions' +import { roleQueries } from '@client/forms/user/query/queries' import { vi, Mock } from 'vitest' +import { SCOPES } from '@opencrvs/commons/client' +import { SearchUsersQuery, Status } from '@client/utils/gateway' +import { NetworkStatus } from '@apollo/client' import { TEAM_USER_LIST } from '@client/navigation/routes' +import { createMemoryRouter } from 'react-router-dom' -describe('user list without admin scope', () => { - let store: AppStore +const searchUserResultsMock = ( + officeId: string, + searchUserResults?: NonNullable['results'] +) => [ + { + request: { + query: SEARCH_USERS, + variables: { + primaryOfficeId: officeId, + count: 10, + skip: 0 + } + }, + result: { + data: { + searchUsers: { + totalItems: 0, + results: searchUserResults ?? [] + } + } + } + } +] - it('no add user button', async () => { - Date.now = vi.fn(() => 1487076708000) - ;({ store } = await createStore()) - const action = { - type: actions.SET_USER_DETAILS, - payload: mockUserResponse +const mockRegistrationAgent = (officeId: string) => ({ + id: '5d08e102542c7a19fc55b790', + name: [ + { + use: 'en', + firstNames: 'Rabindranath', + familyName: 'Tagore' } - await store.dispatch(action) - store.dispatch(offlineDataReady(mockOfflineDataDispatch)) + ], + primaryOffice: { + id: officeId + }, + role: { + id: 'REGISTRATION_AGENT', + label: { + id: 'userRoles.registrationAgent', + defaultMessage: 'Registration_agent', + description: '' + } + }, + status: Status.Active, + underInvestigation: false +}) +const mockNationalSystemAdmin = (officeId: string) => ({ + id: '5d08e102542c7a19fc55b791', + name: [ + { + use: 'en', + firstNames: 'Mohammad', + familyName: 'Ashraful' + } + ], + primaryOffice: { + id: officeId + }, + role: { + id: 'NATIONAL_SYSTEM_ADMIN', + label: { + id: 'userRoles.nationalSystemAdmin', + defaultMessage: 'Natinoal System Admin', + description: '' + } + }, + status: Status.Active, + underInvestigation: false +}) + +describe('for user with create my jurisdiction scope', () => { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.USER_CREATE_MY_JURISDICTION], store) + }) + + it('should show add user button if office is under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' // This office is under the user's office in hierarchy + + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId) + }) + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#add-user').hostNodes().length).toBe(1) + }) + + it('should not show add user button if office is not under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '213ec5f3-e306-4f95-8058-f37893dbfbb6' // This office is not under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId) + }) + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#add-user').hostNodes().length).toBe(0) + }) +}) + +describe('for user with create scope', () => { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.USER_CREATE], store) + }) + + it('should show add user button if office is under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' // This office is under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId) + }) + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#add-user').hostNodes().length).toBe(1) + }) + + it('should show add user button even if office is not under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '213ec5f3-e306-4f95-8058-f37893dbfbb6' // This office is not under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId) + }) + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#add-user').hostNodes().length).toBe(1) + }) +}) + +describe('for user with update my jurisdiction scope', () => { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.USER_UPDATE_MY_JURISDICTION], store) + ;(roleQueries.fetchRoles as Mock).mockReturnValue(mockRoles) + }) + + it('should show edit user button if office is under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' // This office is under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId, [ + mockRegistrationAgent(selectedOfficeId) + ]) + }) + await flushPromises() + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#user-item-0-menu').length >= 1).toBe(true) + }) + + it('should not show edit user button if the other user has update all scope even if under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' // This office is under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId, [ + mockNationalSystemAdmin(selectedOfficeId) + ]) + }) await flushPromises() + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#user-item-0-menu').length >= 1).toBe(false) + }) - const userListMock = [ - { - request: { - query: SEARCH_USERS, - variables: { - primaryOfficeId: '65cf62cb-864c-45e3-9c0d-5c70f0074cb4', - count: 10 - } - }, - result: { - data: { - searchUsers: { - totalItems: 0, - results: [] - } - } - } - } - ] - - const { component } = await createTestComponent( - { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '213ec5f3-e306-4f95-8058-f37893dbfbb6' // This office is not under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId }) - }} - />, - { - store, - graphqlMocks: userListMock, - initialEntries: [ - '/' + - '?' + - stringify({ - locationId: '0d8474da-0361-4d32-979e-af91f012340a' - }) - ] - } + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId, [ + mockRegistrationAgent(selectedOfficeId) + ]) + }) + + await flushPromises() + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#user-item-0-menu').length >= 1).toBe(false) + }) +}) + +describe('for user with update scope', () => { + let store: AppStore + + beforeEach(async () => { + ;({ store } = createStore()) + setScopes([SCOPES.USER_UPDATE], store) + ;(roleQueries.fetchRoles as Mock).mockReturnValue(mockRoles) + }) + + it('should show edit user button if office is under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' // This office is under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId, [ + mockRegistrationAgent(selectedOfficeId) + ]) + }) + await flushPromises() + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) + component.update() + expect(component.find('#user-item-0-menu').length >= 1).toBe(true) + }) + + it('should show edit user button even if the other user has update all scope', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '0d8474da-0361-4d32-979e-af91f012340a' // This office is under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId, [ + mockNationalSystemAdmin(selectedOfficeId) + ]) + }) + await flushPromises() + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) ) + component.update() + expect(component.find('#user-item-0-menu').length >= 1).toBe(true) + }) + it('should show edit user button even if office is not under jurisdiction', async () => { + const userOfficeId = 'da672661-eb0a-437b-aa7a-a6d9a1711dd1' + const selectedOfficeId = '213ec5f3-e306-4f95-8058-f37893dbfbb6' // This office is not under the user's office in hierarchy + const { component } = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: selectedOfficeId + }) + ], + graphqlMocks: searchUserResultsMock(selectedOfficeId, [ + mockRegistrationAgent(selectedOfficeId) + ]) + }) + await flushPromises() + store.dispatch( + actions.setUserDetails({ + loading: false, + data: fetchUserMock(userOfficeId), + networkStatus: NetworkStatus.ready + }) + ) component.update() - expect(component.find('#add-user').length).toBe(0) + expect(component.find('#user-item-0-menu').length >= 1).toBe(true) }) }) @@ -95,13 +429,14 @@ describe('User list tests', () => { beforeAll(async () => { Date.now = vi.fn(() => 1487076708000) - ;({ store } = await createStore()) + ;({ store } = createStore()) + setScopes([SCOPES.USER_UPDATE, SCOPES.USER_CREATE], store) const action = { type: actions.SET_USER_DETAILS, payload: mockLocalSysAdminUserResponse } - await store.dispatch(action) + store.dispatch(action) store.dispatch(offlineDataReady(mockOfflineDataDispatch)) await flushPromises() }) @@ -132,7 +467,6 @@ describe('User list tests', () => { store, path: TEAM_USER_LIST, initialEntries: [ - '/', TEAM_USER_LIST + '?' + stringify({ @@ -215,28 +549,26 @@ describe('User list tests', () => { } } ] - const { component: testComponent } = await createTestComponent( - , - { - store, - initialEntries: [ - TEAM_USER_LIST + - '?' + - stringify({ - locationId: '0d8474da-0361-4d32-979e-af91f012340a' - }) - ], - graphqlMocks: userListMock - } - ) + const testComponent = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: '0d8474da-0361-4d32-979e-af91f012340a' + }) + ], + graphqlMocks: userListMock + }) // wait for mocked data to load mockedProvider await new Promise((resolve) => { setTimeout(resolve, 100) }) - testComponent.update() - const app = testComponent + testComponent.component.update() + const app = testComponent.component expect(app.find('#no-record').hostNodes()).toHaveLength(1) }) @@ -245,7 +577,7 @@ describe('User list tests', () => { userMutations.usernameReminderSend = vi.fn() userMutations.sendResetPasswordInvite = vi.fn() let component: ReactWrapper<{}, {}> - let router: TestComponentWithRouteMock['router'] + let router: ReturnType const userListMock = [ { request: { @@ -270,10 +602,18 @@ describe('User list tests', () => { familyName: 'Tagore' } ], - username: 'r.tagore', - role: 'REGISTRATION_AGENT', - type: 'ENTREPENEUR', - status: 'active', + primaryOffice: { + id: '0d8474da-0361-4d32-979e-af91f012340a' + }, + role: { + id: 'REGISTRATION_AGENT', + label: { + id: 'userRoles.registrationAgent', + defaultMessage: 'Registration_agent', + description: '' + } + }, + status: Status.Active, underInvestigation: false }, { @@ -285,10 +625,18 @@ describe('User list tests', () => { familyName: 'Ashraful' } ], - username: 'm.ashraful', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active', + primaryOffice: { + id: '0d8474da-0361-4d32-979e-af91f012340a' + }, + role: { + id: 'LOCAL_REGISTRAR', + label: { + id: 'userRoles.localRegistrar', + defaultMessage: 'Local Registrar', + description: '' + } + }, + status: Status.Active, underInvestigation: false }, { @@ -300,10 +648,18 @@ describe('User list tests', () => { familyName: 'Muid Khan' } ], - username: 'ma.muidkhan', - role: 'DISTRICT_REGISTRAR', - type: 'MAYOR', - status: 'pending', + primaryOffice: { + id: '0d8474da-0361-4d32-979e-af91f012340a' + }, + role: { + id: 'DISTRICT_REGISTRAR', + label: { + id: 'userRoles.districtRegistrar', + defaultMessage: 'District Registrar', + description: '' + } + }, + status: Status.Pending, underInvestigation: false }, { @@ -315,10 +671,18 @@ describe('User list tests', () => { familyName: 'Huq' } ], - username: 'np.huq', - role: 'STATE_REGISTRAR', - type: 'MAYOR', - status: 'deactivated', + primaryOffice: { + id: '0d8474da-0361-4d32-979e-af91f012340a' + }, + role: { + id: 'STATE_REGISTRAR', + label: { + id: 'userRoles.stateRegistrar', + defaultMessage: 'State Registrar', + description: '' + } + }, + status: Status.Deactivated, underInvestigation: true }, { @@ -330,14 +694,22 @@ describe('User list tests', () => { familyName: 'Islam' } ], - username: 'ma.islam', - role: 'FIELD_AGENT', - type: 'HOSPITAL', - status: 'disabled', + primaryOffice: { + id: '0d8474da-0361-4d32-979e-af91f012340a' + }, + role: { + id: 'FIELD_AGENT', + label: { + id: 'userRoles.fieldAgent', + defaultMessage: 'Field Agent', + description: '' + } + }, + status: Status.Disabled, underInvestigation: false } ] - } + } satisfies SearchUsersQuery['searchUsers'] } } } @@ -349,29 +721,27 @@ describe('User list tests', () => { configurable: true, value: 1100 }) - const { component: testComponent, router: testRouter } = - await createTestComponent(, { - store, - path: TEAM_USER_LIST, - initialEntries: [ - '/', - TEAM_USER_LIST + - '?' + - stringify({ - locationId: '0d8474da-0361-4d32-979e-af91f012340a' - }) - ], - graphqlMocks: userListMock - }) + const testComponent = await createTestComponent(, { + store, + path: TEAM_USER_LIST, + initialEntries: [ + TEAM_USER_LIST + + '?' + + stringify({ + locationId: '0d8474da-0361-4d32-979e-af91f012340a' + }) + ], + graphqlMocks: userListMock + }) // wait for mocked data to load mockedProvider await new Promise((resolve) => { setTimeout(resolve, 100) }) - testComponent.update() - component = testComponent - router = testRouter + testComponent.component.update() + component = testComponent.component + router = testComponent.router }) it('renders list of users', () => { @@ -670,747 +1040,4 @@ describe('User list tests', () => { }) }) }) - - /* Todo: fix after adding pagination in ListView */ - - /*describe('Pagination test', () => { - it('renders no pagination block when the total amount of data is not applicable for pagination', async () => { - const userListMock = [ - { - request: { - query: SEARCH_USERS, - variables: { - primaryOfficeId: '0d8474da-0361-4d32-979e-af91f012340a', - count: 10 - } - }, - result: { - data: { - searchUsers: { - totalItems: 5, - results: [ - { - id: '5d08e102542c7a19fc55b790', - name: [ - { - use: 'en', - firstNames: 'Rabindranath', - familyName: 'Tagore' - } - ], - username: 'r.tagore', - role: 'REGISTRATION_AGENT', - type: 'ENTREPENEUR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b791', - name: [ - { - use: 'en', - firstNames: 'Mohammad', - familyName: 'Ashraful' - } - ], - username: 'm.ashraful', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b792', - name: [ - { - use: 'en', - firstNames: 'Muhammad Abdul', - familyName: 'Muid Khan' - } - ], - username: 'ma.muidkhan', - role: 'DISTRICT_REGISTRAR', - type: 'MAYOR', - status: 'pending', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b793', - name: [ - { - use: 'en', - firstNames: 'Nasreen Pervin', - familyName: 'Huq' - } - ], - username: 'np.huq', - role: 'STATE_REGISTRAR', - type: 'MAYOR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b795', - name: [ - { - use: 'en', - firstNames: 'Md. Ariful', - familyName: 'Islam' - } - ], - username: 'ma.islam', - role: 'FIELD_AGENT', - type: 'HOSPITAL', - status: 'disabled', - underInvestigation: false - } - ] - } - } - } - } - ] - const {router: testComponent} = await createTestComponent( - , - { store, history, graphqlMocks: userListMock } - ) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() - const app = testComponent - expect(app.find('#pagination').hostNodes()).toHaveLength(0) - }) - it('renders pagination block with proper page value when the total amount of data is applicable for pagination', async () => { - const userListMock = [ - { - request: { - query: SEARCH_USERS, - variables: { - primaryOfficeId: '0d8474da-0361-4d32-979e-af91f012340a', - count: 10 - } - }, - result: { - data: { - searchUsers: { - totalItems: 15, - results: [ - { - id: '5d08e102542c7a19fc55b790', - name: [ - { - use: 'en', - firstNames: 'Rabindranath', - familyName: 'Tagore' - } - ], - username: 'r.tagore', - role: 'REGISTRATION_AGENT', - type: 'ENTREPENEUR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b791', - name: [ - { - use: 'en', - firstNames: 'Mohammad', - familyName: 'Ashraful' - } - ], - username: 'm.ashraful', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b792', - name: [ - { - use: 'en', - firstNames: 'Muhammad Abdul', - familyName: 'Muid Khan' - } - ], - username: 'ma.muidkhan', - role: 'DISTRICT_REGISTRAR', - type: 'MAYOR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b793', - name: [ - { - use: 'en', - firstNames: 'Nasreen Pervin', - familyName: 'Huq' - } - ], - username: 'np.huq', - role: 'STATE_REGISTRAR', - type: 'MAYOR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b795', - name: [ - { - use: 'en', - firstNames: 'Md. Ariful', - familyName: 'Islam' - } - ], - username: 'ma.islam', - role: 'FIELD_AGENT', - type: 'HOSPITAL', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b796', - name: [ - { - use: 'en', - firstNames: 'Md. Ashraful', - familyName: 'Alam' - } - ], - username: 'ma.alam', - role: 'FIELD_AGENT', - type: 'CHA', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b797', - name: [ - { - use: 'en', - firstNames: 'Lovely', - familyName: 'Khatun' - } - ], - username: 'l.khatun', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b794', - name: [ - { - use: 'en', - firstNames: 'Mohamed Abu', - familyName: 'Abdullah' - } - ], - username: 'ma.abdullah', - role: 'NATIONAL_REGISTRAR', - type: 'SECRETARY', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b798', - name: [ - { - use: 'en', - firstNames: 'Md. Seikh', - familyName: 'Farid' - } - ], - username: 'ms.farid', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b799', - name: [ - { - use: 'en', - firstNames: 'Md. Jahangir', - familyName: 'Alam' - } - ], - username: 'mj.alam', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active', - underInvestigation: false - } - ] - } - } - } - } - ] - const {router: testComponent} = await createTestComponent( - , - { store, history, graphqlMocks: userListMock } - ) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() - const app = testComponent - expect(app.find('#load_more_button').hostNodes().text()).toContain( - 'Show next 10' - ) - }) - it('renders next page of the user list when the next page button is pressed', async () => { - const userListMock = [ - { - request: { - query: SEARCH_USERS, - variables: { - primaryOfficeId: '0d8474da-0361-4d32-979e-af91f012340a', - count: 10 - } - }, - result: { - data: { - searchUsers: { - totalItems: 15, - results: [ - { - id: '5d08e102542c7a19fc55b790', - name: [ - { - use: 'en', - firstNames: 'Rabindranath', - familyName: 'Tagore' - } - ], - username: 'r.tagore', - role: 'REGISTRATION_AGENT', - type: 'ENTREPENEUR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b791', - name: [ - { - use: 'en', - firstNames: 'Mohammad', - familyName: 'Ashraful' - } - ], - username: 'm.ashraful', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b792', - name: [ - { - use: 'en', - firstNames: 'Muhammad Abdul', - familyName: 'Muid Khan' - } - ], - username: 'ma.muidkhan', - role: 'DISTRICT_REGISTRAR', - type: 'MAYOR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b793', - name: [ - { - use: 'en', - firstNames: 'Nasreen Pervin', - familyName: 'Huq' - } - ], - username: 'np.huq', - role: 'STATE_REGISTRAR', - type: 'MAYOR', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b795', - name: [ - { - use: 'en', - firstNames: 'Md. Ariful', - familyName: 'Islam' - } - ], - username: 'ma.islam', - role: 'FIELD_AGENT', - type: 'HOSPITAL', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b796', - name: [ - { - use: 'en', - firstNames: 'Md. Ashraful', - familyName: 'Alam' - } - ], - username: 'ma.alam', - role: 'FIELD_AGENT', - type: 'CHA', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b797', - name: [ - { - use: 'en', - firstNames: 'Lovely', - familyName: 'Khatun' - } - ], - username: 'l.khatun', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b794', - name: [ - { - use: 'en', - firstNames: 'Mohamed Abu', - familyName: 'Abdullah' - } - ], - username: 'ma.abdullah', - role: 'NATIONAL_REGISTRAR', - type: 'SECRETARY', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b798', - name: [ - { - use: 'en', - firstNames: 'Md. Seikh', - familyName: 'Farid' - } - ], - username: 'ms.farid', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active', - underInvestigation: false - }, - { - id: '5d08e102542c7a19fc55b799', - name: [ - { - use: 'en', - firstNames: 'Md. Jahangir', - familyName: 'Alam' - } - ], - username: 'mj.alam', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active', - underInvestigation: false - } - ] - } - } - } - }, - { - request: { - query: SEARCH_USERS, - variables: { - primaryOfficeId: '0d8474da-0361-4d32-979e-af91f012340a', - count: 20 - } - }, - result: { - data: { - searchUsers: { - totalItems: 15, - results: [ - { - id: '5d08e102542c7a19fc55b790', - name: [ - { - use: 'en', - firstNames: 'Rabindranath', - familyName: 'Tagore' - } - ], - username: 'r.tagore', - role: 'REGISTRATION_AGENT', - type: 'ENTREPENEUR', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b791', - name: [ - { - use: 'en', - firstNames: 'Mohammad', - familyName: 'Ashraful' - } - ], - username: 'm.ashraful', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b792', - name: [ - { - use: 'en', - firstNames: 'Muhammad Abdul', - familyName: 'Muid Khan' - } - ], - username: 'ma.muidkhan', - role: 'DISTRICT_REGISTRAR', - type: 'MAYOR', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b793', - name: [ - { - use: 'en', - firstNames: 'Nasreen Pervin', - familyName: 'Huq' - } - ], - username: 'np.huq', - role: 'STATE_REGISTRAR', - type: 'MAYOR', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b795', - name: [ - { - use: 'en', - firstNames: 'Md. Ariful', - familyName: 'Islam' - } - ], - username: 'ma.islam', - role: 'FIELD_AGENT', - type: 'HOSPITAL', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b796', - name: [ - { - use: 'en', - firstNames: 'Md. Ashraful', - familyName: 'Alam' - } - ], - username: 'ma.alam', - role: 'FIELD_AGENT', - type: 'CHA', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b797', - name: [ - { - use: 'en', - firstNames: 'Lovely', - familyName: 'Khatun' - } - ], - username: 'l.khatun', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b794', - name: [ - { - use: 'en', - firstNames: 'Mohamed Abu', - familyName: 'Abdullah' - } - ], - username: 'ma.abdullah', - role: 'NATIONAL_REGISTRAR', - type: 'SECRETARY', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b798', - name: [ - { - use: 'en', - firstNames: 'Md. Seikh', - familyName: 'Farid' - } - ], - username: 'ms.farid', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b799', - name: [ - { - use: 'en', - firstNames: 'Md. Jahangir', - familyName: 'Alam' - } - ], - username: 'mj.alam', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b800', - name: [ - { - use: 'en', - firstNames: 'Ashraful', - familyName: 'Alam' - } - ], - username: 'a.alam', - role: 'FIELD_AGENT', - type: 'CHA', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b801', - name: [ - { - use: 'en', - firstNames: 'Beauty', - familyName: 'Khatun' - } - ], - username: 'b.khatun', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b802', - name: [ - { - use: 'en', - firstNames: 'Abu', - familyName: 'Abdullah' - } - ], - username: 'a.abdullah', - role: 'NATIONAL_REGISTRAR', - type: 'SECRETARY', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b803', - name: [ - { - use: 'en', - firstNames: 'Seikh', - familyName: 'Farid' - } - ], - username: 's.farid', - role: 'REGISTRATION_AGENT', - type: 'DATA_ENTRY_CLERK', - status: 'active' - }, - { - id: '5d08e102542c7a19fc55b804', - name: [ - { - use: 'en', - firstNames: 'Jahangir', - familyName: 'Alam' - } - ], - username: 'j.alam', - role: 'LOCAL_REGISTRAR', - type: 'CHAIRMAN', - status: 'active' - } - ] - } - } - } - } - ] - const {router: testComponent} = await createTestComponent( - , - { store, history, graphqlMocks: userListMock } - ) - - // wait for mocked data to load mockedProvider - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - testComponent.update() - const app = testComponent - expect(app.find('#load_more_button').hostNodes()).toHaveLength(1) - - app.find('#load_more_button').hostNodes().simulate('click') - await new Promise((resolve) => { - setTimeout(resolve, 100) - }) - - expect(app.find('#load_more_button').hostNodes()).toHaveLength(0) - }) - })*/ }) diff --git a/packages/client/src/views/SysAdmin/Team/user/UserList.tsx b/packages/client/src/views/SysAdmin/Team/user/UserList.tsx index 6c9a4d09788..49b58eb2e61 100644 --- a/packages/client/src/views/SysAdmin/Team/user/UserList.tsx +++ b/packages/client/src/views/SysAdmin/Team/user/UserList.tsx @@ -22,18 +22,10 @@ import { getOfflineData } from '@client/offline/selectors' import { IStoreState } from '@client/store' import styled, { withTheme } from 'styled-components' import { SEARCH_USERS } from '@client/user/queries' -import { - LANG_EN, - NATL_ADMIN_ROLES, - SYS_ADMIN_ROLES -} from '@client/utils/constants' +import { LANG_EN } from '@client/utils/constants' import { createNamesMap } from '@client/utils/data-formatting' import { SysAdminContentWrapper } from '@client/views/SysAdmin/SysAdminContentWrapper' -import { - getAddressName, - getUserRoleIntlKey, - UserStatus -} from '@client/views/SysAdmin/Team/utils' +import { getAddressName, UserStatus } from '@client/views/SysAdmin/Team/utils' import { LinkButton } from '@opencrvs/components/lib/buttons' import { Button } from '@opencrvs/components/lib/Button' import { Pill } from '@opencrvs/components/lib/Pill' @@ -51,7 +43,6 @@ import { } from '@opencrvs/components/lib/Content' import { ITheme } from '@opencrvs/components/lib/theme' import { parse } from 'query-string' -import * as React from 'react' import { injectIntl, useIntl, @@ -64,16 +55,17 @@ import { userMutations } from '@client/user/mutations' import { Pagination } from '@opencrvs/components/lib/Pagination' import { Icon } from '@opencrvs/components/lib/Icon' import { ListUser } from '@opencrvs/components/lib/ListUser' -import { useCallback } from 'react' +import React, { useCallback, useState } from 'react' import { withOnlineStatus, LoadingIndicator } from '@client/views/OfficeHome/LoadingIndicator' import { LocationPicker } from '@client/components/LocationPicker' -import { Query as QueryType, User } from '@client/utils/gateway' +import { SearchUsersQuery } from '@client/utils/gateway' import { UserDetails } from '@client/utils/userUtils' import { Link } from '@opencrvs/components' import { getLocalizedLocationName } from '@client/utils/locationUtils' +import { usePermissions } from '@client/hooks/useAuthorization' import * as routes from '@client/navigation/routes' import { UserSection } from '@client/forms' import { stringify } from 'querystring' @@ -81,7 +73,9 @@ import { stringify } from 'querystring' const DEFAULT_FIELD_AGENT_LIST_SIZE = 10 const DEFAULT_PAGE_NUMBER = 1 -const { useState } = React +type User = NonNullable< + NonNullable['results']>[number] +> const UserTable = styled(BodyContent)` padding: 0px; @@ -226,13 +220,11 @@ function UserListComponent(props: IProps) { const [showResetPasswordSuccess, setShowResetPasswordSuccess] = useState(false) const [showResetPasswordError, setResetPasswordError] = useState(false) + const { canReadUser, canEditUser, canAddOfficeUsers, canAccessOffice } = + usePermissions() + const { intl, userDetails, offlineOffices, isOnline, offlineCountryConfig } = props - const isNatlSysAdmin = userDetails?.systemRole - ? NATL_ADMIN_ROLES.includes(userDetails.systemRole) - ? true - : false - : false const { locationId } = parse(location.search) as unknown as ISearchParams const [toggleUsernameReminder, setToggleUsernameReminder] = @@ -257,6 +249,9 @@ function UserListComponent(props: IProps) { ) const deliveryMethod = window.config.USER_NOTIFICATION_DELIVERY_METHOD + const isMultipleOfficeUnderJurisdiction = + offlineOffices.filter(canAccessOffice).length > 1 + const getParentLocation = ({ partOf }: ILocation) => { const parentLocationId = partOf.split('/')[1] return offlineCountryConfig.locations[parentLocationId] @@ -441,31 +436,6 @@ function UserListComponent(props: IProps) { ] ) - function getViewOnly( - locationId: string, - userDetails: UserDetails | null, - onlyNational: boolean - ) { - if ( - userDetails && - userDetails.systemRole && - userDetails.primaryOffice && - SYS_ADMIN_ROLES.includes(userDetails.systemRole) && - locationId === userDetails.primaryOffice.id && - !onlyNational - ) { - return false - } else if ( - userDetails && - userDetails.systemRole && - NATL_ADMIN_ROLES.includes(userDetails.systemRole) - ) { - return false - } else { - return true - } - } - const getUserName = (user: User) => { const userName = (user && @@ -478,8 +448,6 @@ function UserListComponent(props: IProps) { const StatusMenu = useCallback( function StatusMenu({ - userDetails, - locationId, user, index, status, @@ -492,12 +460,6 @@ function UserListComponent(props: IProps) { status?: string underInvestigation?: boolean }) { - const canEditUserDetails = - userDetails?.systemRole === 'NATIONAL_SYSTEM_ADMIN' || - (userDetails?.systemRole === 'LOCAL_SYSTEM_ADMIN' && - userDetails?.primaryOffice?.id === locationId) - ? true - : false return ( {underInvestigation && } - {canEditUserDetails && ( + {canEditUser(user) && ( ) }, - [getMenuItems] + [canEditUser, getMenuItems] ) const generateUserContents = useCallback( function generateUserContents( - data: QueryType, + data: SearchUsersQuery, locationId: string, userDetails: UserDetails | null ) { @@ -532,69 +494,67 @@ function UserListComponent(props: IProps) { return [] } - return data.searchUsers.results.map( - (user: User | null, index: number) => { - if (user !== null) { - const name = - (user && - user.name && - ((createNamesMap(user.name)[intl.locale] as string) || - (createNamesMap(user.name)[LANG_EN] as string))) || - '' - const role = intl.formatMessage({ - id: getUserRoleIntlKey(user.role._id) - }) - const avatar = user.avatar - - return { - image: ( - - navigate( - formatUrl(routes.USER_PROFILE, { - userId: String(user.id) - }) - ) - } - /> - ), - label: ( - - navigate( - formatUrl(routes.USER_PROFILE, { - userId: String(user.id) - }) - ) - } - > - {name} - - ), - value: {role}, - actions: ( - - ) - } - } + return data.searchUsers.results.map((user, index) => { + if (user !== null) { + const name = + (user && + user.name && + ((createNamesMap(user.name)[intl.locale] as string) || + (createNamesMap(user.name)[LANG_EN] as string))) || + '' + const role = intl.formatMessage(user.role.label) + const avatar = user.avatar + return { - label: '', - value: <> + image: ( + + navigate( + formatUrl(routes.USER_PROFILE, { + userId: String(user.id) + }) + ) + } + disabled={!canReadUser(user)} + > + + + ), + label: ( + + navigate( + formatUrl(routes.USER_PROFILE, { + userId: String(user.id) + }) + ) + } + disabled={!canReadUser(user)} + > + {name} + + ), + value: {role}, + actions: ( + + ) } } - ) + return { + label: '', + value: <> + } + }) }, - [StatusMenu, intl, navigate] + [StatusMenu, intl, navigate, canReadUser] ) const onClickAddUser = useCallback( @@ -624,13 +584,9 @@ function UserListComponent(props: IProps) { } } - const LocationButton = ( - locationId: string, - userDetails: UserDetails | null, - onlyNational: boolean - ) => { + const LocationButton = (locationId: string) => { const buttons: React.ReactElement[] = [] - if (!getViewOnly(locationId, userDetails, onlyNational)) { + if (isMultipleOfficeUnderJurisdiction) { buttons.push( + location.type === 'CRVS_OFFICE' && canAccessOffice(location) + } /> ) + } + if (canAddOfficeUsers({ id: locationId })) { buttons.push( , - - ]} - show={true} - handleClose={() => props.closeCallback(null)} - > - - {intl.formatMessage(messages.roleUpdateInstruction, { - systemRole: - getUserSystemRole({ systemRole: props.systemRole.value }, intl) || - '' - })} - - - - { - setCurrentLanguage(val) - }} - value={currentLanguage} - options={langChoice} - placeholder="" - /> - {userRoles.map((item, index) => { - return ( - - e.lang === currentLanguage)?.label || - '' - } - isDisabled={actives[index]} - focusInput={!actives[index]} - onChange={(e) => { - const newUserRoles = userRoles.map((userRole, idx) => { - if (index !== idx) return userRole - return { - ...userRole, - labels: userRole.labels.map((label) => { - if (label.lang === currentLanguage) { - return { ...label, label: e.target.value } - } - return label - }) - } - }) - setUserRoles(newUserRoles) - }} - onBlur={() => - setActives(new Array(userRoles.length).fill(true)) - } - /> - {actives[index] && !item._id && ( - - )} - - ) - })} - - - { - setCurrentClipBoard(e.target.value) - }} - /> - - - - - ) -} diff --git a/packages/client/src/views/UserSetup/SetupConfirmationPage.test.tsx b/packages/client/src/views/UserSetup/SetupConfirmationPage.test.tsx index 9f9034fe400..8788989ff53 100644 --- a/packages/client/src/views/UserSetup/SetupConfirmationPage.test.tsx +++ b/packages/client/src/views/UserSetup/SetupConfirmationPage.test.tsx @@ -9,19 +9,19 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import * as React from 'react' -import { createTestComponent, validToken } from '@client/tests/util' +import { + createTestComponent, + REGISTRAR_DEFAULT_SCOPES, + setScopes +} from '@client/tests/util' import { createStore } from '@client/store' -import { checkAuth } from '@client/profile/profileActions' -import { SetupConfirmationPage } from '@client/views/UserSetup/SetupConfirmationPage' -import { Mock } from 'vitest' -const getItem = window.localStorage.getItem as Mock +import { SetupConfirmationPage } from '@client/views/UserSetup/SetupConfirmationPage' describe('Setup confirmation page tests', () => { const { store } = createStore() beforeAll(async () => { - getItem.mockReturnValue(validToken) - await store.dispatch(checkAuth()) + setScopes(REGISTRAR_DEFAULT_SCOPES, store) }) it('renders page successfully', async () => { const { component: testComponent } = await createTestComponent( diff --git a/packages/client/src/views/UserSetup/SetupReviewPage.test.tsx b/packages/client/src/views/UserSetup/SetupReviewPage.test.tsx index 49f658d0428..369676ba04a 100644 --- a/packages/client/src/views/UserSetup/SetupReviewPage.test.tsx +++ b/packages/client/src/views/UserSetup/SetupReviewPage.test.tsx @@ -27,13 +27,12 @@ describe('SetupReviewPage page tests', () => { JSON.stringify({ ...userDetails, role: { - _id: '778464c0-08f8-4fb7-8a37-b86d1efc462a', - labels: [ - { - lang: 'en', - label: 'ENTREPENEUR' - } - ] + id: 'ENTREPRENEUR', + label: { + defaultMessage: 'Entrepreneur', + description: 'Name for user role Entrepreneur', + id: 'userRole.entrepreneur' + } } }) ) @@ -72,7 +71,7 @@ describe('SetupReviewPage page tests', () => { { store } ) const role = testComponent.find('#Role_value').hostNodes().text() - expect(role).toEqual('ENTREPENEUR') + expect(role).toEqual('Field Agent') }) it('clicks question to change', async () => { const { component: testComponent } = await createTestComponent( diff --git a/packages/client/src/views/UserSetup/SetupReviewPage.tsx b/packages/client/src/views/UserSetup/SetupReviewPage.tsx index 227d63dc1c2..ece42bbd416 100644 --- a/packages/client/src/views/UserSetup/SetupReviewPage.tsx +++ b/packages/client/src/views/UserSetup/SetupReviewPage.tsx @@ -8,41 +8,38 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import * as React from 'react' -import { useSelector } from 'react-redux' -import { useIntl } from 'react-intl' -import styled from 'styled-components' -import { ActionPageLight } from '@opencrvs/components/lib/ActionPageLight' -import { DataRow, IDataProps } from '@opencrvs/components/lib/ViewData' -import { Button } from '@opencrvs/components/lib/Button' -import { Icon } from '@opencrvs/components/lib/Icon' -import { Loader } from '@opencrvs/components/lib/Loader' +import { Mutation } from '@apollo/client/react/components' import { - ProtectedAccoutStep, IProtectedAccountSetupData, - ISecurityQuestionAnswer + ISecurityQuestionAnswer, + ProtectedAccoutStep } from '@client/components/ProtectedAccount' +import { + buttonMessages, + constantsMessages, + errorMessages, + userMessages +} from '@client/i18n/messages' +import { messages } from '@client/i18n/messages/views/userSetup' import { getUserDetails } from '@client/profile/profileSelectors' import { IStoreState } from '@client/store' -import { getUserName, UserDetails } from '@client/utils/userUtils' import { SubmitActivateUserMutation, SubmitActivateUserMutationVariables } from '@client/utils/gateway' -import { Mutation } from '@apollo/client/react/components' -import { - userMessages, - buttonMessages, - constantsMessages, - errorMessages -} from '@client/i18n/messages' +import { getUserName, UserDetails } from '@client/utils/userUtils' import { activateUserMutation } from '@client/views/UserSetup/queries' -import { messages } from '@client/i18n/messages/views/userSetup' -import { Content } from '@opencrvs/components/lib/Content' - -import { getLanguage } from '@client/i18n/selectors' import { ErrorText } from '@opencrvs/components/lib/' -import { getUserRole } from '@client/utils' +import { ActionPageLight } from '@opencrvs/components/lib/ActionPageLight' +import { Button } from '@opencrvs/components/lib/Button' +import { Content } from '@opencrvs/components/lib/Content' +import { Icon } from '@opencrvs/components/lib/Icon' +import { Loader } from '@opencrvs/components/lib/Loader' +import { DataRow, IDataProps } from '@opencrvs/components/lib/ViewData' +import * as React from 'react' +import { useIntl } from 'react-intl' +import { useSelector } from 'react-redux' +import styled from 'styled-components' const GlobalError = styled.div` color: ${({ theme }) => theme.colors.negative}; @@ -65,8 +62,8 @@ export function UserSetupReview({ setupData, goToStep }: IProps) { const englishName = getUserName(userDetails) const mobile = (userDetails && (userDetails.mobile as string)) || '' const email = (userDetails && (userDetails.email as string)) || '' - const language = useSelector(getLanguage) - const role = userDetails && getUserRole(language, userDetails.role) + const role = userDetails && intl.formatMessage(userDetails.role.label) + const primaryOffice = (userDetails && userDetails.primaryOffice && diff --git a/packages/client/src/views/ViewRecord/ViewRecord.test.tsx b/packages/client/src/views/ViewRecord/ViewRecord.test.tsx index abe538431af..0bade52303b 100644 --- a/packages/client/src/views/ViewRecord/ViewRecord.test.tsx +++ b/packages/client/src/views/ViewRecord/ViewRecord.test.tsx @@ -234,8 +234,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -273,8 +279,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -312,8 +324,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -351,8 +369,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -390,8 +414,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -429,8 +459,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -468,8 +504,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -507,8 +549,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -546,8 +594,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -585,8 +639,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -624,8 +684,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -663,8 +729,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -702,8 +774,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -741,8 +819,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -780,8 +864,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -819,8 +909,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -858,8 +954,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -897,8 +999,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -940,8 +1048,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -979,8 +1093,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '62b99cd98f113a700cc19a1a', - role: '', - systemRole: 'LOCAL_REGISTRAR', + role: { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + } + }, name: [ { firstNames: 'Kennedy', @@ -1018,8 +1138,14 @@ describe('View Record for loading and success state', () => { }, user: { id: '635fd1c82ef11238798ad666', - role: 'LOCAL_LEADER', - systemRole: 'FIELD_AGENT', + role: { + id: 'FIELD_AGENT', + label: { + defaultMessage: 'Field Agent', + description: 'Name for user role Field Agent', + id: 'userRole.fieldAgent' + } + }, name: [ { firstNames: 'Terrance', diff --git a/packages/client/src/views/ViewRecord/query.ts b/packages/client/src/views/ViewRecord/query.ts index 1bfe0030d5d..c8f71a32aaa 100644 --- a/packages/client/src/views/ViewRecord/query.ts +++ b/packages/client/src/views/ViewRecord/query.ts @@ -77,9 +77,8 @@ export const FETCH_VIEW_RECORD_BY_COMPOSITION = gql` user { id role { - _id + id } - systemRole name { firstNames familyName diff --git a/packages/client/src/workqueue/actions.ts b/packages/client/src/workqueue/actions.ts index bd2e0c505a6..9d010b02fc8 100644 --- a/packages/client/src/workqueue/actions.ts +++ b/packages/client/src/workqueue/actions.ts @@ -9,7 +9,19 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import { UserDetailsAvailable } from '@client/profile/profileActions' -import { IQueryData } from './reducer' +import { GQLEventSearchResultSet } from '@client/utils/gateway-deprecated-do-not-use' + +export interface IQueryData { + inProgressTab: GQLEventSearchResultSet + notificationTab: GQLEventSearchResultSet + reviewTab: GQLEventSearchResultSet + rejectTab: GQLEventSearchResultSet + sentForReviewTab: GQLEventSearchResultSet + approvalTab: GQLEventSearchResultSet + printTab: GQLEventSearchResultSet + issueTab: GQLEventSearchResultSet + externalValidationTab: GQLEventSearchResultSet +} export const GET_WORKQUEUE_SUCCESS = 'DECLARATION/GET_WORKQUEUE_SUCCESS' export const GET_WORKQUEUE_FAILED = 'DECLARATION/GET_WORKQUEUE_FAILED' @@ -43,7 +55,6 @@ export interface UpdateRegistrarWorkqueueAction { payload: { pageSize: number userId?: string - isFieldAgent: boolean } } diff --git a/packages/client/src/workqueue/reducer.ts b/packages/client/src/workqueue/reducer.ts index 091cd9b1d59..7ce857d7640 100644 --- a/packages/client/src/workqueue/reducer.ts +++ b/packages/client/src/workqueue/reducer.ts @@ -25,10 +25,7 @@ import { IStoreState } from '@client/store' import { getUserDetails, getScope } from '@client/profile/profileSelectors' import { getUserLocation, UserDetails } from '@client/utils/userUtils' import { syncRegistrarWorkqueue } from '@client/ListSyncController' -import type { - GQLEventSearchResultSet, - GQLEventSearchSet -} from '@client/utils/gateway-deprecated-do-not-use' +import type { GQLEventSearchSet } from '@client/utils/gateway-deprecated-do-not-use' import { UpdateRegistrarWorkqueueAction, UPDATE_REGISTRAR_WORKQUEUE, @@ -41,19 +38,10 @@ import { getCurrentUserWorkqueueFailed, GET_WORKQUEUE_SUCCESS, UPDATE_REGISTRAR_WORKQUEUE_SUCCESS, - UPDATE_WORKQUEUE_PAGINATION + UPDATE_WORKQUEUE_PAGINATION, + IQueryData } from './actions' - -export interface IQueryData { - inProgressTab: GQLEventSearchResultSet - notificationTab: GQLEventSearchResultSet - reviewTab: GQLEventSearchResultSet - rejectTab: GQLEventSearchResultSet - approvalTab: GQLEventSearchResultSet - printTab: GQLEventSearchResultSet - issueTab: GQLEventSearchResultSet - externalValidationTab: GQLEventSearchResultSet -} +import { SCOPES } from '@opencrvs/commons/client' export const EVENT_STATUS = { IN_PROGRESS: 'IN_PROGRESS', @@ -86,6 +74,7 @@ const workqueueInitialState: WorkqueueState = { notificationTab: { totalItems: 0, results: [] }, reviewTab: { totalItems: 0, results: [] }, rejectTab: { totalItems: 0, results: [] }, + sentForReviewTab: { totalItems: 0, results: [] }, approvalTab: { totalItems: 0, results: [] }, printTab: { totalItems: 0, results: [] }, issueTab: { totalItems: 0, results: [] }, @@ -98,6 +87,7 @@ const workqueueInitialState: WorkqueueState = { notificationTab: 1, reviewTab: 1, rejectTab: 1, + sentForReviewTab: 1, approvalTab: 1, externalValidationTab: 1, printTab: 1, @@ -106,13 +96,12 @@ const workqueueInitialState: WorkqueueState = { } interface IWorkqueuePaginationParams { - userId?: string pageSize: number - isFieldAgent: boolean inProgressSkip: number healthSystemSkip: number reviewSkip: number rejectSkip: number + sentForReviewSkip: number approvalSkip: number externalValidationSkip: number printSkip: number @@ -121,15 +110,13 @@ interface IWorkqueuePaginationParams { export function updateRegistrarWorkqueue( userId?: string, - pageSize = 10, - isFieldAgent = false + pageSize = 10 ): UpdateRegistrarWorkqueueAction { return { type: UPDATE_REGISTRAR_WORKQUEUE, payload: { userId, - pageSize, - isFieldAgent + pageSize } } } @@ -142,7 +129,6 @@ async function getFilteredDeclarations( unassignedDeclarations: IDeclaration[] }> { const state = getState() - const scope = getScope(state) const savedDeclarations = state.declarationsState.declarations const workqueueDeclarations = Object.entries(workqueue.data).flatMap( @@ -151,18 +137,6 @@ async function getFilteredDeclarations( } ) as Array - // for field agent, no declarations should be unassigned - // for registration agent, sent for approval declarations should not be unassigned - - // for other agents, check if the status of workqueue declaration - // has changed and if that declaration is saved in the store - // also declaration should not show as unassigned when it is being submitted - if (scope?.includes('declare')) - return { - currentlyDownloadedDeclarations: savedDeclarations, - unassignedDeclarations: [] - } - const unassignedDeclarations = workqueueDeclarations .filter( (dec) => @@ -346,7 +320,7 @@ async function getWorkqueueData( const scope = getScope(state) const reviewStatuses = - scope && scope.includes('register') + scope && scope.includes(SCOPES.RECORD_REGISTER) ? [ EVENT_STATUS.DECLARED, EVENT_STATUS.VALIDATED, @@ -355,33 +329,32 @@ async function getWorkqueueData( : [EVENT_STATUS.DECLARED] const { - userId, pageSize, - isFieldAgent, inProgressSkip, healthSystemSkip, reviewSkip, rejectSkip, approvalSkip, + sentForReviewSkip, externalValidationSkip, printSkip, issueSkip } = workqueuePaginationParams const result = await syncRegistrarWorkqueue( + userDetails.practitionerId, registrationLocationId, reviewStatuses, pageSize, - isFieldAgent, inProgressSkip, healthSystemSkip, reviewSkip, rejectSkip, + sentForReviewSkip, approvalSkip, externalValidationSkip, printSkip, - issueSkip, - userId + issueSkip ) let workqueue const { currentUserData } = await getUserData( @@ -407,14 +380,7 @@ async function getWorkqueueData( initialSyncDone: false } } - if (isFieldAgent) { - return mergeWorkQueueData( - state, - ['reviewTab', 'rejectTab'], - currentUserData && currentUserData.declarations, - workqueue - ) - } + return mergeWorkQueueData( state, ['inProgressTab', 'notificationTab', 'reviewTab', 'rejectTab'], @@ -466,6 +432,7 @@ export const workqueueReducer: LoopReducer = ( const { printTab, reviewTab, + sentForReviewTab, approvalTab, inProgressTab, externalValidationTab, @@ -482,6 +449,7 @@ export const workqueueReducer: LoopReducer = ( healthSystemSkip: Math.max(notificationTab - 1, 0) * pageSize, reviewSkip: Math.max(reviewTab - 1, 0) * pageSize, rejectSkip: Math.max(rejectTab - 1, 0) * pageSize, + sentForReviewSkip: Math.max(sentForReviewTab - 1, 0) * pageSize, approvalSkip: Math.max(approvalTab - 1, 0) * pageSize, externalValidationSkip: Math.max(externalValidationTab - 1, 0) * pageSize, diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index fd07413df5f..904801ab0a3 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -90,7 +90,9 @@ export default defineConfig(({ mode }) => { }, resolve: { alias: { - crypto: 'crypto-js' + crypto: 'crypto-js', + '@opencrvs/commons/build/dist/authentication': + '@opencrvs/commons/authentication' } }, plugins: [ diff --git a/packages/commons/src/authentication.ts b/packages/commons/src/authentication.ts index 023236ac1f3..45003118427 100644 --- a/packages/commons/src/authentication.ts +++ b/packages/commons/src/authentication.ts @@ -14,71 +14,181 @@ import decode from 'jwt-decode' import { Nominal } from './nominal' import { z } from 'zod' -/** All the scopes user can be assigned to */ -export const userScopes = { - demo: 'demo', - declare: 'declare', - register: 'register', - certify: 'certify', - performance: 'performance', - systemAdmin: 'sysadmin', - validate: 'validate', - nationalSystemAdmin: 'natlsysadmin', - /** Bypasses the rate limiting in gateway. Useful for data seeder. */ - bypassRateLimit: 'bypassratelimit', - teams: 'teams', - config: 'config', - confirmRegistration: 'record.confirm-registration', - rejectRegistration: 'record.reject-registration' -} as const - -export const userRoleScopes = { - FIELD_AGENT: [userScopes.declare], - REGISTRATION_AGENT: [ - userScopes.validate, - userScopes.performance, - userScopes.certify - ], - LOCAL_REGISTRAR: [ - userScopes.register, - userScopes.performance, - userScopes.certify - ], - LOCAL_SYSTEM_ADMIN: [userScopes.systemAdmin], - NATIONAL_SYSTEM_ADMIN: [ - userScopes.systemAdmin, - userScopes.nationalSystemAdmin - ], - PERFORMANCE_MANAGEMENT: [userScopes.performance], - NATIONAL_REGISTRAR: [ - userScopes.register, - userScopes.performance, - userScopes.certify, - userScopes.config, - userScopes.teams - ] -} +import { Scope, SCOPES } from './scopes' +export { scopes, Scope, SCOPES } from './scopes' /** All the scopes system/integration can be assigned to */ -const systemScopes = { - recordsearch: 'recordsearch', - declare: 'declare', - notificationApi: 'notification-api', - webhook: 'webhook', - nationalId: 'nationalId' +export const SYSTEM_INTEGRATION_SCOPES = { + recordsearch: SCOPES.RECORDSEARCH, + webhook: SCOPES.WEBHOOK, + nationalId: SCOPES.NATIONALID } as const -export const systemRoleScopes = { - HEALTH: [systemScopes.declare, systemScopes.notificationApi], - NATIONAL_ID: [systemScopes.nationalId], - RECORD_SEARCH: [systemScopes.recordsearch], - WEBHOOK: [systemScopes.webhook] -} +export const DEFAULT_ROLES_DEFINITION = [ + { + id: 'FIELD_AGENT', + label: { + defaultMessage: 'Field Agent', + description: 'Name for user role Field Agent', + id: 'userRole.fieldAgent' + }, + scopes: [ + // new scopes + SCOPES.RECORD_DECLARE_BIRTH, + SCOPES.RECORD_DECLARE_DEATH, + SCOPES.RECORD_DECLARE_MARRIAGE, + SCOPES.RECORD_SUBMIT_INCOMPLETE, + SCOPES.RECORD_SUBMIT_FOR_REVIEW, + SCOPES.SEARCH_BIRTH, + SCOPES.SEARCH_DEATH, + SCOPES.SEARCH_MARRIAGE + ] + }, + { + id: 'REGISTRATION_AGENT', + label: { + defaultMessage: 'Registration Agent', + description: 'Name for user role Registration Agent', + id: 'userRole.registrationAgent' + }, + scopes: [ + SCOPES.PERFORMANCE, + SCOPES.CERTIFY, + SCOPES.RECORD_DECLARE_BIRTH, + SCOPES.RECORD_DECLARE_DEATH, + SCOPES.RECORD_DECLARE_MARRIAGE, + SCOPES.RECORD_SUBMIT_FOR_APPROVAL, + SCOPES.RECORD_SUBMIT_FOR_UPDATES, + SCOPES.RECORD_DECLARATION_ARCHIVE, + SCOPES.RECORD_DECLARATION_REINSTATE, + SCOPES.RECORD_REGISTRATION_REQUEST_CORRECTION, + SCOPES.RECORD_REGISTRATION_PRINT, + SCOPES.RECORD_PRINT_RECORDS_SUPPORTING_DOCUMENTS, + SCOPES.RECORD_EXPORT_RECORDS, + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES, + SCOPES.RECORD_REGISTRATION_VERIFY_CERTIFIED_COPIES, + SCOPES.RECORD_CREATE_COMMENTS, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.ORGANISATION_READ_LOCATIONS_MY_OFFICE, + SCOPES.SEARCH_BIRTH, + SCOPES.SEARCH_DEATH, + SCOPES.SEARCH_MARRIAGE + ] + }, + { + id: 'LOCAL_REGISTRAR', + label: { + defaultMessage: 'Local Registrar', + description: 'Name for user role Local Registrar', + id: 'userRole.localRegistrar' + }, + scopes: [ + SCOPES.PERFORMANCE, + SCOPES.CERTIFY, + SCOPES.RECORD_DECLARE_BIRTH, + SCOPES.RECORD_DECLARE_DEATH, + SCOPES.RECORD_DECLARE_MARRIAGE, + SCOPES.RECORD_SUBMIT_FOR_UPDATES, + SCOPES.RECORD_REVIEW_DUPLICATES, + SCOPES.RECORD_DECLARATION_ARCHIVE, + SCOPES.RECORD_DECLARATION_REINSTATE, + SCOPES.RECORD_REGISTER, + SCOPES.RECORD_REGISTRATION_CORRECT, + SCOPES.RECORD_REGISTRATION_PRINT, + SCOPES.RECORD_PRINT_RECORDS_SUPPORTING_DOCUMENTS, + SCOPES.RECORD_EXPORT_RECORDS, + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES, + SCOPES.RECORD_REGISTRATION_VERIFY_CERTIFIED_COPIES, + SCOPES.RECORD_CREATE_COMMENTS, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.ORGANISATION_READ_LOCATIONS_MY_OFFICE, + SCOPES.SEARCH_BIRTH, + SCOPES.SEARCH_DEATH, + SCOPES.SEARCH_MARRIAGE + ] + }, + { + id: 'LOCAL_SYSTEM_ADMIN', + label: { + defaultMessage: 'Local System Admin', + description: 'Name for user role Local System Admin', + id: 'userRole.localSystemAdmin' + }, + scopes: [ + SCOPES.SYSADMIN, + SCOPES.USER_READ_MY_OFFICE, + SCOPES.USER_CREATE_MY_JURISDICTION, + SCOPES.USER_UPDATE_MY_JURISDICTION, + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.PERFORMANCE_EXPORT_VITAL_STATISTICS + // 'organisation.read-users' ? + ] + }, + { + id: 'NATIONAL_SYSTEM_ADMIN', + label: { + defaultMessage: 'National System Admin', + description: 'Name for user role National System Admin', + id: 'userRole.nationalSystemAdmin' + }, + scopes: [ + SCOPES.SYSADMIN, + SCOPES.NATLSYSADMIN, + SCOPES.USER_CREATE, + SCOPES.USER_READ, + SCOPES.USER_UPDATE, + SCOPES.ORGANISATION_READ_LOCATIONS, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.PERFORMANCE_EXPORT_VITAL_STATISTICS + // 'organisation.read-users' ? + ] + }, + { + id: 'PERFORMANCE_MANAGER', + label: { + defaultMessage: 'Performance Manager', + description: 'Name for user role Performance Manager', + id: 'userRole.performanceManager' + }, + scopes: [ + SCOPES.PERFORMANCE, + SCOPES.PERFORMANCE_READ, + SCOPES.PERFORMANCE_READ_DASHBOARDS, + SCOPES.PERFORMANCE_EXPORT_VITAL_STATISTICS + ] + } +] satisfies Array<{ + id: string + label: { defaultMessage: string; description: string; id: string } + scopes: Scope[] +}> + +export const DEFAULT_SYSTEM_INTEGRATION_ROLE_SCOPES = { + HEALTH: [SCOPES.NOTIFICATION_API], + NATIONAL_ID: [SCOPES.NATIONALID], + RECORD_SEARCH: [SCOPES.RECORDSEARCH], + WEBHOOK: [SCOPES.WEBHOOK] +} satisfies Record + +/* + * Describes a "legacy" user role such as FIELD_AGENT, REGISTRATION_AGENT, etc. + * These are roles we are slowly sunsettings in favor of the new, more configurable user roles. + */ + +/** All the scopes user can be assigned to – old & new */ +export type UserScope = + | (typeof SCOPES)[keyof typeof SCOPES] + | 'profile.electronic-signature' -export type UserRole = keyof typeof userRoleScopes -type UserScope = (typeof userScopes)[keyof typeof userScopes] -type SystemScope = (typeof systemScopes)[keyof typeof systemScopes] -export type Scope = UserScope | SystemScope +export type SystemScope = + (typeof DEFAULT_SYSTEM_INTEGRATION_ROLE_SCOPES)[keyof typeof DEFAULT_SYSTEM_INTEGRATION_ROLE_SCOPES][number] export interface ITokenPayload { sub: string diff --git a/packages/commons/src/client.ts b/packages/commons/src/client.ts index 49b095340e4..c3eb9b45de1 100644 --- a/packages/commons/src/client.ts +++ b/packages/commons/src/client.ts @@ -10,8 +10,11 @@ */ export * from './search' export * from './events' +export * from './scopes' export * from './conditionals/conditionals' export * from './conditionals/validate' export * from './documents' +export * from './uuid' +export { DEFAULT_ROLES_DEFINITION } from './authentication' export type PartialBy = Omit & Partial> diff --git a/packages/commons/src/fhir/extension.ts b/packages/commons/src/fhir/extension.ts index 50607bfc166..b269c280007 100644 --- a/packages/commons/src/fhir/extension.ts +++ b/packages/commons/src/fhir/extension.ts @@ -84,6 +84,7 @@ export type StringExtensionType = { url: 'http://opencrvs.org/specs/extension/regVerified' valueString: string } + // @deprecated in 1.7, kept for backwards compatibility 'http://opencrvs.org/specs/extension/regDownloaded': { url: 'http://opencrvs.org/specs/extension/regDownloaded' valueString?: string @@ -151,6 +152,12 @@ export type KnownExtensionType = StringExtensionType & { reference: ResourceIdentifier | URNReference /* Unsaved */ } } + 'http://opencrvs.org/specs/extension/certifier': { + url: 'http://opencrvs.org/specs/extension/certifier' + valueReference: { + reference: ResourceIdentifier + } + } 'http://opencrvs.org/specs/extension/relatedperson-affidavittype': { url: 'http://opencrvs.org/specs/extension/relatedperson-affidavittype' valueAttachment: { diff --git a/packages/commons/src/fhir/practitioner.ts b/packages/commons/src/fhir/practitioner.ts index ca2650db3d4..5d796aedfd9 100644 --- a/packages/commons/src/fhir/practitioner.ts +++ b/packages/commons/src/fhir/practitioner.ts @@ -112,11 +112,9 @@ export const getUserRoleFromHistory = ( ) const targetCode = result?.code?.find((element) => { - return element.coding?.[0].system === 'http://opencrvs.org/specs/types' + return element.coding?.[0].system === 'http://opencrvs.org/specs/roles' }) const role = targetCode?.coding?.[0].code - const systemRole = result?.code?.[0].coding?.[0].code - - return { role, systemRole } + return role } diff --git a/packages/commons/src/fhir/transformers/input.ts b/packages/commons/src/fhir/transformers/input.ts index d55e67ab300..3bf6cd7b6d9 100644 --- a/packages/commons/src/fhir/transformers/input.ts +++ b/packages/commons/src/fhir/transformers/input.ts @@ -34,16 +34,6 @@ const enum Status { type DateString = string -const enum SystemRoleType { - FIELD_AGENT = 'FIELD_AGENT', - REGISTRATION_AGENT = 'REGISTRATION_AGENT', - LOCAL_REGISTRAR = 'LOCAL_REGISTRAR', - LOCAL_SYSTEM_ADMIN = 'LOCAL_SYSTEM_ADMIN', - NATIONAL_SYSTEM_ADMIN = 'NATIONAL_SYSTEM_ADMIN', - PERFORMANCE_MANAGEMENT = 'PERFORMANCE_MANAGEMENT', - NATIONAL_REGISTRAR = 'NATIONAL_REGISTRAR' -} - /* * Enums get converted to string unions so that types generated from GraphQL * can be converted to these our core types @@ -58,7 +48,6 @@ interface User { mobile?: string password?: string status?: EnumToStringUnion - systemRole: EnumToStringUnion role?: string email?: string primaryOffice?: string diff --git a/packages/commons/src/fixtures/birth-bundle.ts b/packages/commons/src/fixtures/birth-bundle.ts index 1d604280aff..5b27302bbc0 100644 --- a/packages/commons/src/fixtures/birth-bundle.ts +++ b/packages/commons/src/fixtures/birth-bundle.ts @@ -958,14 +958,6 @@ export const BIRTH_BUNDLE: SavedBundle< code: 'LOCAL_REGISTRAR' } ] - }, - { - coding: [ - { - system: 'http://opencrvs.org/specs/types', - code: '[{"lang":"en","label":"Local Registrar"},{"lang":"fr","label":"Registraire local"}]' - } - ] } ], location: [ diff --git a/packages/commons/src/fixtures/death-bundle.ts b/packages/commons/src/fixtures/death-bundle.ts index a0c94609195..546fea7919e 100644 --- a/packages/commons/src/fixtures/death-bundle.ts +++ b/packages/commons/src/fixtures/death-bundle.ts @@ -1137,14 +1137,6 @@ export const DEATH_BUNDLE: SavedBundle< code: 'REGISTRATION_AGENT' } ] - }, - { - coding: [ - { - system: 'http://opencrvs.org/specs/types', - code: '[{"lang":"en","label":"Registration Agent"},{"lang":"fr","label":"Agent d\'enregistrement"}]' - } - ] } ], location: [ diff --git a/packages/commons/src/fixtures/marriage-bundle.ts b/packages/commons/src/fixtures/marriage-bundle.ts index 8efe6f0d108..b30c51d808d 100644 --- a/packages/commons/src/fixtures/marriage-bundle.ts +++ b/packages/commons/src/fixtures/marriage-bundle.ts @@ -900,14 +900,6 @@ export const MARRIAGE_BUNDLE: Saved< code: 'LOCAL_REGISTRAR' } ] - }, - { - coding: [ - { - system: 'http://opencrvs.org/specs/types', - code: '[{"lang":"en","label":"Local Registrar"},{"lang":"fr","label":"Registraire local"}]' - } - ] } ], location: [ diff --git a/packages/commons/src/http.ts b/packages/commons/src/http.ts index 8343c1489a5..fed68065645 100644 --- a/packages/commons/src/http.ts +++ b/packages/commons/src/http.ts @@ -10,6 +10,7 @@ */ import type * as Hapi from '@hapi/hapi' import { uniqueId } from 'lodash' +import nodeFetch from 'node-fetch' export interface IAuthHeader { Authorization: string @@ -31,3 +32,26 @@ export function joinURL(base: string, path: string) { const baseWithSlash = base.endsWith('/') ? base : base + '/' return new URL(path, baseWithSlash) } + +export class NotFound extends Error { + constructor(message: string) { + super(message) + this.name = 'NotFound' + } +} + +export async function fetchJSON( + ...params: Parameters +) { + const res = await nodeFetch(...params) + + if (!res.ok) { + if (res.status === 404) { + throw new NotFound(res.statusText) + } + + throw new Error(res.statusText) + } + + return res.json() as ResponseType +} diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 542059ee54d..5cc2b0964cb 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -13,6 +13,7 @@ export * from './uuid' export * from './documents' export * from './http' export * from './logger' +export * from './roles' export * from './search' export * from './events' export * from './users/service' diff --git a/packages/commons/src/roles.ts b/packages/commons/src/roles.ts new file mode 100644 index 00000000000..a6f4daf1d27 --- /dev/null +++ b/packages/commons/src/roles.ts @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import { Scope } from './scopes' + +export type Roles = Array<{ + id: string + labels: Array<{ language: string; label: string }> + scopes: Scope[] +}> diff --git a/packages/commons/src/scopes.ts b/packages/commons/src/scopes.ts new file mode 100644 index 00000000000..5164d8a1250 --- /dev/null +++ b/packages/commons/src/scopes.ts @@ -0,0 +1,133 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +export const SCOPES = { + // TODO v1.8 legacy scopes + NATLSYSADMIN: 'natlsysadmin', + BYPASSRATELIMIT: 'bypassratelimit', + + DECLARE: 'declare', + REGISTER: 'register', + VALIDATE: 'validate', + + DEMO: 'demo', + CERTIFY: 'certify', + PERFORMANCE: 'performance', + SYSADMIN: 'sysadmin', + TEAMS: 'teams', + CONFIG: 'config', + + // systems / integrations + WEBHOOK: 'webhook', + NATIONALID: 'nationalId', + NOTIFICATION_API: 'notification-api', + RECORDSEARCH: 'recordsearch', + + // declare + RECORD_DECLARE_BIRTH: 'record.declare-birth', + RECORD_DECLARE_BIRTH_MY_JURISDICTION: 'record.declare-birth:my-jurisdiction', + RECORD_DECLARE_DEATH: 'record.declare-death', + RECORD_DECLARE_DEATH_MY_JURISDICTION: 'record.declare-death:my-jurisdiction', + RECORD_DECLARE_MARRIAGE: 'record.declare-marriage', + RECORD_DECLARE_MARRIAGE_MY_JURISDICTION: + 'record.declare-marriage:my-jurisdiction', + RECORD_SUBMIT_INCOMPLETE: 'record.declaration-submit-incomplete', + RECORD_SUBMIT_FOR_REVIEW: 'record.declaration-submit-for-review', + RECORD_UNASSIGN_OTHERS: 'record.unassign-others', + + // validate + RECORD_SUBMIT_FOR_APPROVAL: 'record.declaration-submit-for-approval', + RECORD_SUBMIT_FOR_UPDATES: 'record.declaration-submit-for-updates', + RECORD_DECLARATION_EDIT: 'record.declaration-edit', + RECORD_REVIEW_DUPLICATES: 'record.review-duplicates', + RECORD_DECLARATION_ARCHIVE: 'record.declaration-archive', + RECORD_DECLARATION_REINSTATE: 'record.declaration-reinstate', + + // register + RECORD_REGISTER: 'record.register', + + // certify + RECORD_EXPORT_RECORDS: 'record.export-records', + RECORD_DECLARATION_PRINT: 'record.declaration-print', + RECORD_PRINT_RECORDS_SUPPORTING_DOCUMENTS: + 'record.declaration-print-supporting-documents', + RECORD_REGISTRATION_PRINT: 'record.registration-print', // v1.8 + RECORD_PRINT_ISSUE_CERTIFIED_COPIES: + 'record.registration-print&issue-certified-copies', + RECORD_PRINT_CERTIFIED_COPIES: 'record.registration-print-certified-copies', // v1.8 + RECORD_BULK_PRINT_CERTIFIED_COPIES: + 'record.registration-bulk-print-certified-copies', // v1.8 + RECORD_REGISTRATION_VERIFY_CERTIFIED_COPIES: + 'record.registration-verify-certified-copies', // v1.8 + + // correct + RECORD_REGISTRATION_REQUEST_CORRECTION: + 'record.registration-request-correction', + RECORD_REGISTRATION_CORRECT: 'record.registration-correct', + RECORD_REGISTRATION_REQUEST_REVOCATION: + 'record.registration-request-revocation', // v1.8 + RECORD_REGISTRATION_REVOKE: 'record.registration-revoke', // v1.8 + RECORD_REGISTRATION_REQUEST_REINSTATEMENT: + 'record.registration-request-reinstatement', // v1.8 + RECORD_REGISTRATION_REINSTATE: 'record.registration-reinstate', // v1.8 + RECORD_CONFIRM_REGISTRATION: 'record.confirm-registration', + RECORD_REJECT_REGISTRATION: 'record.reject-registration', + + // search + SEARCH_BIRTH_MY_JURISDICTION: 'search.birth:my-jurisdiction', + SEARCH_BIRTH: 'search.birth', + SEARCH_DEATH_MY_JURISDICTION: 'search.death:my-jurisdiction', + SEARCH_DEATH: 'search.death', + SEARCH_MARRIAGE_MY_JURISDICTION: 'search.marriage:my-jurisdiction', + SEARCH_MARRIAGE: 'search.marriage', + + // audit v1.8 + RECORD_READ: 'record.read', + RECORD_READ_AUDIT: 'record.read-audit', + RECORD_READ_COMMENTS: 'record.read-comments', + RECORD_CREATE_COMMENTS: 'record.create-comments', + + // profile + PROFILE_UPDATE: 'profile.update', //v1.8 + PROFILE_ELECTRONIC_SIGNATURE: 'profile.electronic-signature', + + // performance + PERFORMANCE_READ: 'performance.read', + PERFORMANCE_READ_DASHBOARDS: 'performance.read-dashboards', + PERFORMANCE_EXPORT_VITAL_STATISTICS: 'performance.vital-statistics-export', + + // organisation + ORGANISATION_READ_LOCATIONS: 'organisation.read-locations:all', + ORGANISATION_READ_LOCATIONS_MY_OFFICE: + 'organisation.read-locations:my-office', + ORGANISATION_READ_LOCATIONS_MY_JURISDICTION: + 'organisation.read-locations:my-jurisdiction', + + // user + USER_READ: 'user.read:all', + USER_READ_MY_OFFICE: 'user.read:my-office', + USER_READ_MY_JURISDICTION: 'user.read:my-jurisdiction', + USER_READ_ONLY_MY_AUDIT: 'user.read:only-my-audit', //v1.8 + USER_CREATE: 'user.create:all', + USER_CREATE_MY_JURISDICTION: 'user.create:my-jurisdiction', + USER_UPDATE: 'user.update:all', + USER_UPDATE_MY_JURISDICTION: 'user.update:my-jurisdiction', + + // config + CONFIG_UPDATE_ALL: 'config.update:all', + + // data seeding + USER_DATA_SEEDING: 'user.data-seeding' +} as const + +export type Scope = (typeof SCOPES)[keyof typeof SCOPES] + +export const scopes: Scope[] = Object.values(SCOPES) diff --git a/packages/commons/src/test-resources.ts b/packages/commons/src/test-resources.ts index e948432e93e..7aa0cc9463f 100644 --- a/packages/commons/src/test-resources.ts +++ b/packages/commons/src/test-resources.ts @@ -695,14 +695,6 @@ export const REGISTERED_RECORD = { code: 'LOCAL_REGISTRAR' } ] - }, - { - coding: [ - { - system: 'http://opencrvs.org/specs/types', - code: '[{"lang":"en","label":"Local Registrar"},{"lang":"fr","label":"Registraire local"}]' - } - ] } ], location: [ diff --git a/packages/commons/src/users/service.ts b/packages/commons/src/users/service.ts index 9c3f2172dd1..092e8af2ee0 100644 --- a/packages/commons/src/users/service.ts +++ b/packages/commons/src/users/service.ts @@ -10,7 +10,6 @@ */ import fetch from 'node-fetch' -import { UserRole } from '../authentication' interface IUserName { use: string @@ -27,7 +26,6 @@ type User = { name: IUserName[] username: string email: string - systemRole: UserRole role: ObjectId practitionerId: string primaryOfficeId: string diff --git a/packages/components/src/Icon/all-icons.ts b/packages/components/src/Icon/all-icons.ts index 56b4cef3cfc..cffbbc0ef23 100644 --- a/packages/components/src/Icon/all-icons.ts +++ b/packages/components/src/Icon/all-icons.ts @@ -71,6 +71,7 @@ export { ArchiveBox, ArrowCircleDown, FileArrowUp, + FileDotted, PencilLine, PencilCircle, Handshake, diff --git a/packages/config/src/config/routes.ts b/packages/config/src/config/routes.ts index 50adc608592..b1b2cc286dc 100644 --- a/packages/config/src/config/routes.ts +++ b/packages/config/src/config/routes.ts @@ -8,15 +8,6 @@ * * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ -import { - createCertificateHandler, - getActiveCertificatesHandler, - getCertificateHandler, - requestActiveCertificate, - requestNewCertificate, - updateCertificate, - updateCertificateHandler -} from '@config/handlers/certificate/certificateHandler' import configHandler, { getLoginConfigHandler } from '@config/handlers/application/applicationConfigHandler' @@ -37,16 +28,7 @@ import { } from '@config/handlers/locations/handler' import { fetchLocationHandler } from '@config/handlers/locations/location' import { locationHierarchyHandler } from '@config/handlers/locations/hierarchy' - -export const enum RouteScope { - DECLARE = 'declare', - REGISTER = 'register', - CERTIFY = 'certify', - PERFORMANCE = 'performance', - SYSADMIN = 'sysadmin', - VALIDATE = 'validate', - NATLSYSADMIN = 'natlsysadmin' -} +import { SCOPES } from '@opencrvs/commons/authentication' export default function getRoutes(): ServerRoute[] { return [ @@ -71,19 +53,6 @@ export default function getRoutes(): ServerRoute[] { path: '/config', handler: configHandler, options: { - auth: { - scope: [ - RouteScope.NATLSYSADMIN, - RouteScope.DECLARE, - RouteScope.REGISTER, - RouteScope.CERTIFY, - RouteScope.PERFORMANCE, - RouteScope.SYSADMIN, - RouteScope.VALIDATE, - // @TODO: Refer to an enum / constant - 'record.confirm-registration' - ] - }, tags: ['api'], description: 'Retrieve all configuration' } @@ -116,76 +85,6 @@ export default function getRoutes(): ServerRoute[] { description: 'Retrieve forms' } }, - { - method: 'POST', - path: '/getCertificate', - handler: getCertificateHandler, - options: { - tags: ['api'], - description: 'Retrieves certificate', - auth: { - scope: [ - RouteScope.NATLSYSADMIN, - RouteScope.REGISTER, - RouteScope.CERTIFY, - RouteScope.VALIDATE - ] - }, - validate: { - payload: requestActiveCertificate - } - } - }, - { - method: 'GET', - path: '/getActiveCertificates', - handler: getActiveCertificatesHandler, - options: { - tags: ['api'], - description: 'Retrieves active certificates for birth and death', - auth: { - scope: [ - RouteScope.NATLSYSADMIN, - RouteScope.DECLARE, - RouteScope.REGISTER, - RouteScope.CERTIFY, - RouteScope.PERFORMANCE, - RouteScope.SYSADMIN, - RouteScope.VALIDATE - ] - } - } - }, - { - method: 'POST', - path: '/createCertificate', - handler: createCertificateHandler, - options: { - tags: ['api'], - description: 'Creates a new Certificate', - auth: { - scope: [RouteScope.NATLSYSADMIN] - }, - validate: { - payload: requestNewCertificate - } - } - }, - { - method: 'POST', - path: '/updateCertificate', - handler: updateCertificateHandler, - options: { - tags: ['api'], - description: 'Updates an existing Certificate', - auth: { - scope: [RouteScope.NATLSYSADMIN] - }, - validate: { - payload: updateCertificate - } - } - }, { method: 'GET', path: '/dashboardQueries', @@ -216,7 +115,7 @@ export default function getRoutes(): ServerRoute[] { options: { tags: ['api'], auth: { - scope: ['natlsysadmin'] + scope: [SCOPES.CONFIG_UPDATE_ALL, SCOPES.USER_DATA_SEEDING] }, description: 'Create a location', validate: { @@ -244,7 +143,7 @@ export default function getRoutes(): ServerRoute[] { options: { tags: ['api'], auth: { - scope: ['natlsysadmin'] + scope: [SCOPES.CONFIG_UPDATE_ALL] }, description: 'Update a location or facility', validate: { diff --git a/packages/config/src/handlers/application/applicationConfigHandler.test.ts b/packages/config/src/handlers/application/applicationConfigHandler.test.ts index 124717701eb..f30f1b1deae 100644 --- a/packages/config/src/handlers/application/applicationConfigHandler.test.ts +++ b/packages/config/src/handlers/application/applicationConfigHandler.test.ts @@ -18,7 +18,7 @@ import * as jwt from 'jsonwebtoken' import { readFileSync } from 'fs' const token = jwt.sign( - { scope: ['natlsysadmin'] }, + { scope: ['config.update:all'] }, readFileSync('./test/cert.key'), { algorithm: 'RS256', @@ -109,22 +109,4 @@ describe('applicationHandler', () => { }) expect(res.statusCode).toBe(200) }) - - it('return error when tries to save invalid data', async () => { - mockingoose(ApplicationConfig).toReturn(null, 'findOne') - mockingoose(ApplicationConfig).toReturn({}, 'update') - - const res = await server.server.inject({ - method: 'POST', - url: '/getCertificate', - payload: { - APPLICATION_NAME: 1234 - }, - headers: { - Authorization: `${token}` - } - }) - - expect(res.statusCode).toBe(400) - }) }) diff --git a/packages/config/src/handlers/application/applicationConfigHandler.ts b/packages/config/src/handlers/application/applicationConfigHandler.ts index fb122c5512f..19a2f5f06e0 100644 --- a/packages/config/src/handlers/application/applicationConfigHandler.ts +++ b/packages/config/src/handlers/application/applicationConfigHandler.ts @@ -19,7 +19,7 @@ import fetch from 'node-fetch' import { getToken } from '@config/utils/auth' import { pipe } from 'fp-ts/lib/function' import { verifyToken } from '@config/utils/verifyToken' -import { RouteScope } from '@config/config/routes' +import { SCOPES } from '@opencrvs/commons/authentication' const SystemRoleType = [ 'FIELD_AGENT', @@ -64,12 +64,7 @@ async function getCertificatesConfig( } const { scope } = decodedOrError.right - if ( - scope && - (scope.includes(RouteScope.CERTIFY) || - scope.includes(RouteScope.VALIDATE) || - scope.includes(RouteScope.NATLSYSADMIN)) - ) { + if (scope.includes(SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES)) { const url = new URL(`/certificates`, env.COUNTRY_CONFIG_URL).toString() const res = await fetch(url, { @@ -85,6 +80,7 @@ async function getCertificatesConfig( } return [] } + async function getConfigFromCountry(authToken?: string) { const url = new URL('application-config', env.COUNTRY_CONFIG_URL).toString() diff --git a/packages/config/src/handlers/certificate/certificateHandler.test.ts b/packages/config/src/handlers/certificate/certificateHandler.test.ts deleted file mode 100644 index cca060e6609..00000000000 --- a/packages/config/src/handlers/certificate/certificateHandler.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * OpenCRVS is also distributed under the terms of the Civil Registration - * & Healthcare Disclaimer located at http://opencrvs.org/license. - * - * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. - */ -import { createServer } from '@config/server' -import Certificate, { ICertificateModel } from '@config/models/certificate' -import * as fetchMock from 'jest-fetch-mock' -import * as mockingoose from 'mockingoose' -import * as jwt from 'jsonwebtoken' -import { readFileSync } from 'fs' - -export enum Event { - BIRTH = 'birth', - DEATH = 'death', - MARRIAGE = 'marriage' -} -export enum Status { - ACTIVE = 'ACTIVE', - INACTIVE = 'INACTIVE' -} - -const token = jwt.sign( - { scope: ['natlsysadmin'] }, - readFileSync('./test/cert.key'), - { - algorithm: 'RS256', - issuer: 'opencrvs:auth-service', - audience: 'opencrvs:config-user' - } -) - -const fetch = fetchMock as fetchMock.FetchMock - -const mockCertificate = { - svgCode: 'ocrvs/1234-6789.svg', - svgFilename: 'ocrvs.svg', - user: 'dde0846b-4b0f-4732-80e7-b0f06444fef5', - event: 'birth', - status: 'ACTIVE' -} as unknown as ICertificateModel - -describe('createCertificate handler', () => { - let server: any - const svgCode = 'ocrvs/1234-6789.svg' - - beforeEach(async () => { - mockingoose.resetAll() - server = await createServer() - fetch.resetMocks() - }) - - it('creates and saves new certificate using mongoose', async () => { - mockingoose(Certificate).toReturn(mockCertificate, 'save') - - const res = await server.server.inject({ - method: 'POST', - url: '/createCertificate', - payload: { - svgCode: svgCode, - svgFilename: 'sample_doc.jpeg', - user: 'dde0846b-4b0f-4732-80e7-b0f06444fef5', - event: 'birth', - status: 'ACTIVE' - }, - headers: { - Authorization: `Bearer ${token}` - } - }) - expect(res.statusCode).toBe(201) - }) - - it('returns error when tries to saves invalid svg code using mongoose', async () => { - mockingoose(Certificate).toReturn(mockCertificate, 'save') - - const res = await server.server.inject({ - method: 'POST', - url: '/createCertificate', - payload: { - svgCode: 1123, - svgFilename: 'sample_doc.jpeg', - user: 'dde0846b-4b0f-4732-80e7-b0f06444fef5', - event: 'birth', - status: 'ACTIVE' - }, - headers: { - Authorization: `Bearer ${token}` - } - }) - expect(res.statusCode).toBe(400) - }) -}) - -describe('getCertificate handler', () => { - let server: any - - beforeEach(async () => { - mockingoose.resetAll() - server = await createServer() - fetch.resetMocks() - }) - - it('get active certificate for birth using mongoose', async () => { - mockingoose(Certificate).toReturn(mockCertificate, 'findOne') - - const res = await server.server.inject({ - method: 'POST', - url: '/getCertificate', - payload: { - status: 'ACTIVE', - event: 'birth' - }, - headers: { - Authorization: `Bearer ${token}` - } - }) - expect(res.statusCode).toBe(200) - }) - - it('get active certificate for marriage using mongoose', async () => { - mockingoose(Certificate).toReturn(mockCertificate, 'findOne') - - const res = await server.server.inject({ - method: 'POST', - url: '/getCertificate', - payload: { - status: 'ACTIVE', - event: 'marriage' - }, - headers: { - Authorization: `Bearer ${token}` - } - }) - expect(res.statusCode).toBe(200) - }) - - it('return no result found for active death certificate', async () => { - mockingoose(Certificate).toReturn(null, 'findOne') - - const res = await server.server.inject({ - method: 'POST', - url: '/getCertificate', - payload: { - status: 'ACTIVE', - event: 'death' - }, - headers: { - Authorization: `Bearer ${token}` - } - }) - - expect(res.statusCode).toBe(404) - }) -}) - -describe('getActiveCertificates handler', () => { - let server: any - - beforeEach(async () => { - mockingoose.resetAll() - server = await createServer() - fetch.resetMocks() - }) - - it('get active certificate for birth and death using mongoose', async () => { - mockingoose(Certificate).toReturn(mockCertificate, 'find') - - const res = await server.server.inject({ - method: 'GET', - url: '/getActiveCertificates', - headers: { - Authorization: `Bearer ${token}` - } - }) - expect(res.statusCode).toBe(200) - }) -}) - -describe('updateCertificate handler', () => { - let server: any - - beforeEach(async () => { - mockingoose.resetAll() - server = await createServer() - fetch.resetMocks() - }) - - it('update certificate to inactive using mongoose', async () => { - mockCertificate.id = '61c4664e663fc6af203b63b8' - mockingoose(Certificate).toReturn(mockCertificate, 'findOne') - mockingoose(Certificate).toReturn(mockCertificate, 'update') - - const res = await server.server.inject({ - method: 'POST', - url: '/updateCertificate', - payload: { - id: '61c4664e663fc6af203b63b8', - status: 'INACTIVE' - }, - headers: { - Authorization: `${token}` - } - }) - expect(res.statusCode).toBe(201) - }) - - it('return error when tries to save invalid data', async () => { - mockingoose(Certificate).toReturn(null, 'findOne') - mockingoose(Certificate).toReturn({}, 'update') - - const res = await server.server.inject({ - method: 'POST', - url: '/getCertificate', - payload: { - id: '61c4664e663fc6af203b63b8', - svgFilename: 1234 - }, - headers: { - Authorization: `${token}` - } - }) - - expect(res.statusCode).toBe(400) - }) -}) - -describe('deleteCertificate handler', () => { - let server: any - - beforeEach(async () => { - mockingoose.resetAll() - server = await createServer() - fetch.resetMocks() - }) - - it('return error when there is no param', async () => { - mockingoose(Certificate).toReturn({}, 'findOneAndRemove') - - const res = await server.server.inject({ - method: 'DELETE', - url: '/certificate' - }) - - expect(res.statusCode).toBe(404) - }) -}) diff --git a/packages/config/src/handlers/certificate/certificateHandler.ts b/packages/config/src/handlers/certificate/certificateHandler.ts deleted file mode 100644 index 44a0d840794..00000000000 --- a/packages/config/src/handlers/certificate/certificateHandler.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * OpenCRVS is also distributed under the terms of the Civil Registration - * & Healthcare Disclaimer located at http://opencrvs.org/license. - * - * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. - */ -import * as Hapi from '@hapi/hapi' -import Certificate, { - ICertificateModel, - Status, - Event -} from '@config/models/certificate' // IDeclarationConfigurationModel -import { logger } from '@opencrvs/commons' -import * as Joi from 'joi' -import { badRequest, notFound } from '@hapi/boom' -import { verifyToken } from '@config/utils/verifyToken' -import { RouteScope } from '@config/config/routes' -import { pipe } from 'fp-ts/lib/function' - -interface IActivePayload { - status: Status - event: Event -} - -export async function getCertificateHandler( - request: Hapi.Request, - h: Hapi.ResponseToolkit -) { - const { status, event } = request.payload as IActivePayload - const certificate: ICertificateModel | null = await Certificate.findOne({ - status: status, - event: event - }) - - if (!certificate) { - throw notFound() - } - return certificate -} - -export async function getActiveCertificatesHandler( - request: Hapi.Request, - h: Hapi.ResponseToolkit -) { - const token = request.headers.authorization.replace('Bearer ', '') - const decodedOrError = pipe(token, verifyToken) - if (decodedOrError._tag === 'Left') { - return [] - } - const { scope } = decodedOrError.right - - if ( - scope && - (scope.includes(RouteScope.CERTIFY) || - scope.includes(RouteScope.VALIDATE) || - scope.includes(RouteScope.NATLSYSADMIN)) - ) { - const activeCertificates = await Certificate.find({ - status: Status.ACTIVE, - event: { $in: [Event.BIRTH, Event.DEATH, Event.MARRIAGE] } - }).lean() - return activeCertificates - } - return [] -} - -export async function createCertificateHandler( - request: Hapi.Request, - h: Hapi.ResponseToolkit -) { - const newCertificate = request.payload as ICertificateModel - // save new certificate - let certificateResponse - try { - certificateResponse = await Certificate.create(newCertificate) - } catch (err) { - logger.error(err) - // return 400 if there is a validation error when saving to mongo - return h.response().code(400) - } - return h.response(certificateResponse).code(201) -} - -export async function updateCertificateHandler( - request: Hapi.Request, - h: Hapi.ResponseToolkit -) { - try { - const certificate = request.payload as ICertificateModel - const existingCertificate: ICertificateModel | null = - await Certificate.findOne({ _id: certificate.id }) - if (!existingCertificate) { - throw badRequest(`No certificate found by given id: ${certificate.id}`) - } - // Update existing certificate's fields - existingCertificate.svgCode = certificate.svgCode - existingCertificate.svgFilename = certificate.svgFilename - existingCertificate.svgDateUpdated = Date.now() - existingCertificate.user = certificate.user - existingCertificate.event = certificate.event - existingCertificate.status = certificate.status - await Certificate.update( - { _id: existingCertificate._id }, - existingCertificate - ) - return h.response(existingCertificate).code(201) - } catch (err) { - logger.error(err) - // return 400 if there is a validation error when saving to mongo - return h.response().code(400) - } -} - -export const requestActiveCertificate = Joi.object({ - status: Joi.string().required(), - event: Joi.string().required() -}) - -export const requestNewCertificate = Joi.object({ - svgCode: Joi.string(), - svgFilename: Joi.string(), - svgDateUpdated: Joi.number(), - svgDateCreated: Joi.number(), - user: Joi.string(), - event: Joi.string(), - status: Joi.string() -}) - -export const updateCertificate = Joi.object({ - id: Joi.string().required(), - svgCode: Joi.string(), - svgFilename: Joi.string(), - svgDateUpdated: Joi.number(), - svgDateCreated: Joi.number(), - user: Joi.string(), - event: Joi.string(), - status: Joi.string() -}) diff --git a/packages/data-seeder/package.json b/packages/data-seeder/package.json index 4b6a64b80de..7c3c5afce01 100644 --- a/packages/data-seeder/package.json +++ b/packages/data-seeder/package.json @@ -19,6 +19,7 @@ "@types/fhir": "^0.0.37", "@types/node": "^16.18.39", "@types/node-fetch": "^2.5.12", + "@opencrvs/commons": "^1.3.0", "envalid": "^8.0.0", "graphql": "^15.0.0", "graphql-tag": "^2.12.6", @@ -28,7 +29,8 @@ "tsconfig-paths": "^3.13.0", "typescript": "5.6.3", "uuid": "^3.3.2", - "zod": "^3.17.3" + "zod": "^3.17.3", + "zod-validation-error": "^1.3.1" }, "lint-staged": { "src/**/*.ts": [ diff --git a/packages/data-seeder/src/index.ts b/packages/data-seeder/src/index.ts index aaf07bd820f..b42be225588 100644 --- a/packages/data-seeder/src/index.ts +++ b/packages/data-seeder/src/index.ts @@ -11,7 +11,6 @@ import { env } from './environment' import fetch from 'node-fetch' import { seedLocations, seedLocationsForV2Events } from './locations' -import { seedRoles } from './roles' import { seedUsers } from './users' import { parseGQLResponse, raise } from './utils' import { print } from 'graphql' @@ -31,7 +30,11 @@ async function getToken(): Promise { } }) if (!res.ok) { - raise('Could not login as the super user') + raise( + 'Could not login as the super user. This might because you have seeded the database already and the account has now been deactivated', + res.status, + res.statusText + ) } const body = await res.json() return body.token @@ -91,8 +94,7 @@ async function deactivateSuperuser(token: string) { async function main() { const token = await getToken() - console.log('Seeding roles') - const roleIdMap = await seedRoles(token) + console.log('Seeding locations for v1 system') await seedLocations(token) @@ -100,7 +102,7 @@ async function main() { await seedLocationsForV2Events(token) console.log('Seeding users') - await seedUsers(token, roleIdMap) + await seedUsers(token) await deactivateSuperuser(token) } diff --git a/packages/data-seeder/src/locations.ts b/packages/data-seeder/src/locations.ts index d348d5a26cb..c5171fe4494 100644 --- a/packages/data-seeder/src/locations.ts +++ b/packages/data-seeder/src/locations.ts @@ -13,7 +13,7 @@ import { OPENCRVS_SPECIFICATION_URL } from './constants' import { env } from './environment' import { TypeOf, z } from 'zod' import { raise } from './utils' -import { inspect } from 'util' +import { fromZodError } from 'zod-validation-error' const LOCATION_TYPES = [ 'ADMIN_STRUCTURE', @@ -167,9 +167,9 @@ async function getLocations() { const parsedLocations = LocationSchema.safeParse(await res.json()) if (!parsedLocations.success) { raise( - `Error when getting locations from country-config: ${inspect( - parsedLocations.error.issues - )}` + fromZodError(parsedLocations.error, { + prefix: `Error validating locations data returned from ${url}` + }) ) } const adminStructureMap = validateAdminStructure( diff --git a/packages/data-seeder/src/roles.ts b/packages/data-seeder/src/roles.ts deleted file mode 100644 index a1380221582..00000000000 --- a/packages/data-seeder/src/roles.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * OpenCRVS is also distributed under the terms of the Civil Registration - * & Healthcare Disclaimer located at http://opencrvs.org/license. - * - * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. - */ -import { env } from './environment' -import { raise, parseGQLResponse } from './utils' -import fetch from 'node-fetch' -import { z } from 'zod' -import { print } from 'graphql' -import gql from 'graphql-tag' -import { inspect } from 'util' - -const LabelSchema = z.array( - z.object({ - labels: z.array(z.object({ lang: z.string(), label: z.string() })) - }) -) - -/* - * at least LOCAL_REGISTRAR & NATIONAL_SYSTEM_ADMIN - * roles are required - */ -const CountryRoleSchema = z.object({ - FIELD_AGENT: LabelSchema.optional(), - LOCAL_REGISTRAR: LabelSchema, - LOCAL_SYSTEM_ADMIN: LabelSchema.optional(), - NATIONAL_REGISTRAR: LabelSchema.optional(), - NATIONAL_SYSTEM_ADMIN: LabelSchema, - PERFORMANCE_MANAGEMENT: LabelSchema.optional(), - REGISTRATION_AGENT: LabelSchema.optional() -}) - -const SYSTEM_ROLES = [ - 'FIELD_AGENT', - 'LOCAL_REGISTRAR', - 'LOCAL_SYSTEM_ADMIN', - 'NATIONAL_REGISTRAR', - 'NATIONAL_SYSTEM_ADMIN', - 'PERFORMANCE_MANAGEMENT', - 'REGISTRATION_AGENT' -] as const - -export interface Label { - lang: string - label: string -} - -export interface Role { - _id?: string - labels: Array