Skip to content

Commit

Permalink
Contact management - frontend changes: new - contactProfileView, cont…
Browse files Browse the repository at this point in the history
…actSearchParams type, contactService, tests, updated - HeaderMenu, en-CA, application enums, router, authzStore
  • Loading branch information
sanjaytkbabu committed Jan 10, 2025
1 parent f1ecaed commit 704948c
Show file tree
Hide file tree
Showing 17 changed files with 583 additions and 5 deletions.
6 changes: 6 additions & 0 deletions app/src/controllers/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const controller = {
let userIds = mixedQueryToArray(req.query.userId);

if (!userIds && req.currentContext.userId) userIds = [req.currentContext.userId];
const identityIds = mixedQueryToArray(req.query.identityId);

console.log('\n\n\nidentityIds>>>>>' + identityIds);

Check warning on line 20 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 20 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement

Check warning on line 20 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (22.0)

Unexpected console statement

Check warning on line 20 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 20 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement

Check warning on line 20 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (22.0)

Unexpected console statement
console.log('\n\n\ncontactIds>>>>>' + contactIds);

Check warning on line 21 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 21 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement

Check warning on line 21 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (22.0)

Unexpected console statement

Check warning on line 21 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 21 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement

Check warning on line 21 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (22.0)

Unexpected console statement
console.log('\n\n\nuserIds>>>>>' + userIds);

Check warning on line 22 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 22 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement

Check warning on line 22 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (22.0)

Unexpected console statement

Check warning on line 22 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (18.x)

Unexpected console statement

Check warning on line 22 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (20.x)

Unexpected console statement

Check warning on line 22 in app/src/controllers/contact.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (App) (22.0)

Unexpected console statement

const response = await contactService.searchContacts({
userId: userIds ? userIds.map((id) => addDashesToUuid(id)) : userIds,
Expand All @@ -30,6 +35,7 @@ const controller = {
next(e);
}
},

updateContact: async (req: Request<never, never, Contact, never>, res: Response, next: NextFunction) => {
try {
const response = await contactService.upsertContacts([req.body], req.currentContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,14 @@ export async function up(knex: Knex): Promise<void> {
// Add all navigator read only role mappings
await addResourceRoles(navigator_read_group_id[0].group_id, Resource.CONTACT, [Action.READ, Action.UPDATE]);


// Add all supervisor role mappings
await addResourceRoles(superviser_group_id[0].group_id, Resource.CONTACT, [Action.READ, Action.UPDATE]);

// Add all admin role mappings
await addResourceRoles(admin_group_id[0].group_id, Resource.CONTACT, [Action.READ, Action.UPDATE]);


// Add all proponent role mappings
await addResourceRoles(proponent_group_id[0].group_id, Resource.CONTACT, [Action.READ, Action.UPDATE]);

}
return knex('yars.group_role').insert(items);
});
Expand Down
1 change: 1 addition & 0 deletions app/src/types/ContactSearchParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type ContactSearchParameters = {
contactId?: string[];
email?: string;
firstName?: string;
identityId?: string;
lastName?: string;
phoneNumber?: string;
userId?: string[];
Expand Down
3 changes: 2 additions & 1 deletion app/src/validators/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const contacts = Joi.array()
const schema = {
searchContacts: {
query: Joi.object({
identityId: uuidv4.allow(null),
userId: Joi.array().items(uuidv4).allow(null),
contactId: Joi.array().items(uuidv4).allow(null),
email: Joi.string().max(255).allow(null),
Expand All @@ -45,7 +46,7 @@ const schema = {
email: Joi.string().max(255).required(),
firstName: Joi.string().max(255).required(),
lastName: Joi.string().max(255).required(),
phoneNumber: Joi.number().max(255).required(),
phoneNumber: phoneNumber.required(),
contactApplicantRelationship: Joi.string()
.required()
.valid(...PROJECT_RELATIONSHIP_LIST),
Expand Down
219 changes: 219 additions & 0 deletions app/tests/unit/controllers/contact.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { contactService } from '../../../src/services';
import contactController from '../../../src/controllers/contact';
import { Request, Response } from 'express';

// Mock config library - @see {@link https://stackoverflow.com/a/64819698}
jest.mock('config');

const mockResponse = () => {
const res: { status?: jest.Mock; json?: jest.Mock; end?: jest.Mock } = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);

return res;
};

let res = mockResponse();
beforeEach(() => {
res = mockResponse();
});

afterEach(() => {
jest.resetAllMocks();
});

const CURRENT_CONTEXT = { authType: 'BEARER', tokenPayload: null };

describe('contactController', () => {
const next = jest.fn();

describe('searchContacts', () => {
const searchContactsSpy = jest.spyOn(contactService, 'searchContacts');

it('should return 200 if all good', async () => {
const req = {
query: { userId: '5e3f0c19-8664-4a43-ac9e-210da336e923' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

const contacts = [
{
contactId: 'contact123',
userId: 'user123',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '123-456-7890',
email: '[email protected]',
contactPreference: 'email',
contactApplicantRelationship: 'applicant'
}
];

searchContactsSpy.mockResolvedValue(contacts);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
userId: ['5e3f0c19-8664-4a43-ac9e-210da336e923'],
contactId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(contacts);
});

it('adds dashes to user IDs', async () => {
const req = {
query: { userId: '5e3f0c1986644a43ac9e210da336e923,8b9dedd279d442c6b82f52844a8e2757' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

const contacts = [
{
contactId: 'contact123',
userId: 'user123',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '123-456-7890',
email: '[email protected]',
contactPreference: 'email',
contactApplicantRelationship: 'applicant'
}
];

searchContactsSpy.mockResolvedValue(contacts);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
userId: ['5e3f0c19-8664-4a43-ac9e-210da336e923', '8b9dedd2-79d4-42c6-b82f-52844a8e2757'],
contactId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
});

it('adds dashes to contact IDs', async () => {
const req = {
query: { contactId: '5e3f0c1986644a43ac9e210da336e923,8b9dedd279d442c6b82f52844a8e2757' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

const contacts = [
{
contactId: 'contact123',
userId: 'user123',
firstName: 'John',
lastName: 'Doe',
phoneNumber: '123-456-7890',
email: '[email protected]',
contactPreference: 'email',
contactApplicantRelationship: 'applicant'
}
];

searchContactsSpy.mockResolvedValue(contacts);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
contactId: ['5e3f0c19-8664-4a43-ac9e-210da336e923', '8b9dedd2-79d4-42c6-b82f-52844a8e2757'],
userId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
});

it('calls next if the contact service fails to list contacts', async () => {
const req = {
query: { userId: '5e3f0c19-8664-4a43-ac9e-210da336e923' },
currentContext: CURRENT_CONTEXT
} as unknown as Request;

searchContactsSpy.mockImplementationOnce(() => {
throw new Error();
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.searchContacts(req as any, res as unknown as Response, next);

expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledWith({
userId: ['5e3f0c19-8664-4a43-ac9e-210da336e923'],
contactId: undefined,
email: undefined,
firstName: undefined,
lastName: undefined,
contactApplicantRelationship: undefined,
phoneNumber: undefined
});
expect(res.status).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
});
});

describe('updateContact', () => {
const upsertContactsSpy = jest.spyOn(contactService, 'upsertContacts');

it('should return 200 if contact is updated successfully', async () => {
const req = {
body: {
userId: '5e3f0c19-8664-4a43-ac9e-210da336e923',
email: '[email protected]',
firstName: 'First',
lastName: 'Last'
},
currentContext: CURRENT_CONTEXT
} as unknown as Request;

upsertContactsSpy.mockResolvedValue();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.updateContact(req as any, res as unknown as Response, next);

expect(upsertContactsSpy).toHaveBeenCalledTimes(1);
expect(upsertContactsSpy).toHaveBeenCalledWith([req.body], req.currentContext);
expect(res.status).toHaveBeenCalledWith(200);
});

it('calls next if the contact service fails to update contact', async () => {
const req = {
body: {
userId: '5e3f0c19-8664-4a43-ac9e-210da336e923',
email: '[email protected]',
firstName: 'First',
lastName: 'Last'
},
currentContext: CURRENT_CONTEXT
} as unknown as Request;

upsertContactsSpy.mockImplementationOnce(() => {
throw new Error();
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
await contactController.updateContact(req as any, res as unknown as Response, next);

expect(upsertContactsSpy).toHaveBeenCalledTimes(1);
expect(upsertContactsSpy).toHaveBeenCalledWith([req.body], req.currentContext);
expect(res.status).toHaveBeenCalledTimes(0);
expect(next).toHaveBeenCalledTimes(1);
});
});
});
7 changes: 7 additions & 0 deletions frontend/src/components/common/HeaderMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const { getIsAuthenticated, getProfile } = storeToRefs(useAuthNStore());
// State
const { t } = useI18n();
const items = ref([
{
label: t('headerMenu.contactProfile'),
icon: 'pi pi-user',
command: () => {
router.push({ name: RouteName.CONTACT_PROFILE });
}
},
{
label: t('headerMenu.logout'),
icon: 'pi pi-sign-out',
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/locales/en-CA.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@
"collectionDisclaimer": {
"disclaimer": "This information is being collected under the legal authority of section 26 (c)(e) and 27 (1)(a)(i) of the Freedom of Information and Protection of Privacy Act (the Act) and is being used for the purpose of creating a client relationship between you or your organization and Government of British Columbia. It may also be shared when strictly necessary with partner agencies that are also subject to the provisions of the Act. Personal information may be used by the Permitting Solutions Branch for survey purposes. If you have any questions regarding the use of this personal information, please contact Housing Authorizations at"
},
"contactProfileView": {
"cancel": "Cancel",
"contactProfile": "Contact profile",
"edit": "Edit",
"email": "Email",
"failedToSaveTheForm": "Failed to save the form",
"fillOutProfile": "Please fill out your contact profile",
"firstName": "First name",
"formSaved": "Form saved",
"getStarted": "Get started",
"lastName": "Last name",
"phone": "Phone",
"preferredContact": "Preferred contact method",
"relationshipToProject": "Relationship to project",
"save": "Save"
},
"deleteDocument": {
"deleteTooltip": "Delete document"
},
Expand Down Expand Up @@ -71,6 +87,7 @@
"name": "Permit Connect Services"
},
"headerMenu": {
"contactProfile": "Contact profile",
"logout": "Log out"
},
"housing": {
Expand All @@ -84,7 +101,7 @@
"onThisPage": "On this page",
"of": "of",
"projectState": "Project state",
"projectsEmpty": "Submit a new project to the Navigator Service to found out what permits are required",
"projectsEmpty": "Submit a new project to the Navigator Service to find out what permits are required",
"projectsTooltip": "Submit a housing project to the Navigator service to find out what permits are required for your projects, track permit applications, and submit related enquiries.",
"submitNewEnquiry": "Submit a new enquiry",
"submitNewProject": "Submit a new project"
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ const routes: Array<RouteRecordRaw> = [
name: RouteName.HOME,
component: () => import('@/views/HomeView.vue')
},
{
path: '/contact/profile',
name: RouteName.CONTACT_PROFILE,
component: () => import('@/views/contact/ContactProfileView.vue'),
beforeEnter: accessHandler,
meta: { requiresAuth: true, access: [NavigationPermission.HOUSING_CONTACT_MANAGEMENT] }
},
{
path: '/developer',
name: RouteName.DEVELOPER,
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/services/contactService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { appAxios } from './interceptors';

import type { AxiosResponse } from 'axios';
import type { ContactSearchParameters } from '@/types';

const PATH = 'contact';

export default {
/**
* @function searchContacts
* Returns a list of users based on the provided filtering parameters
* @param {SearchUsersOptions} params SearchUsersOptions object containing the data to filter against
* @returns {Promise<AxiosResponse>} An axios response or empty array
*/
searchContacts(params: ContactSearchParameters): Promise<AxiosResponse> {
return appAxios().get(`${PATH}`, { params: params });
},
/**
* @function updateEnquiry
* @returns {Promise} An axios response
*/
updateContact(data?: any) {
return appAxios().put(`${PATH}/${data.contactId}`, data);
}
};
1 change: 1 addition & 0 deletions frontend/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as atsService } from './atsService';
export { default as AuthService } from './authService';
export { default as ConfigService } from './configService';
export { default as comsService } from './comsService';
export { default as contactService } from './contactService';
export { default as documentService } from './documentService';
export { default as enquiryService } from './enquiryService';
export { default as externalApiService } from './externalApiService';
Expand Down
Loading

0 comments on commit 704948c

Please sign in to comment.