diff --git a/app/src/controllers/enquiry.ts b/app/src/controllers/enquiry.ts index b7cb952ce..577202a66 100644 --- a/app/src/controllers/enquiry.ts +++ b/app/src/controllers/enquiry.ts @@ -1,7 +1,7 @@ import { NIL, v4 as uuidv4 } from 'uuid'; -import { Initiatives, NOTE_TYPE_LIST } from '../components/constants'; -import { getCurrentIdentity } from '../components/utils'; +import { INTAKE_STATUS_LIST, Initiatives, NOTE_TYPE_LIST } from '../components/constants'; +import { getCurrentIdentity, isTruthy } from '../components/utils'; import { activityService, enquiryService, noteService, userService } from '../services'; import type { NextFunction, Request, Response } from '../interfaces/IExpress'; @@ -68,7 +68,8 @@ const controller = { activityId: activityId, submittedAt: data.submittedAt ?? new Date().toISOString(), // eslint-disable-next-line @typescript-eslint/no-explicit-any - submittedBy: (req.currentUser?.tokenPayload as any)?.idir_username + submittedBy: (req.currentUser?.tokenPayload as any)?.idir_username, + intakeStatus: data.submit ? INTAKE_STATUS_LIST.SUBMITTED : INTAKE_STATUS_LIST.DRAFT }; }, @@ -93,6 +94,40 @@ const controller = { } }, + deleteEnquiry: async (req: Request<{ enquiryId: string }>, res: Response, next: NextFunction) => { + try { + const response = await enquiryService.deleteEnquiry(req.params.enquiryId); + res.status(200).json(response); + } catch (e: unknown) { + next(e); + } + }, + + getEnquiries: async (req: Request, res: Response, next: NextFunction) => { + try { + // Pull from PCNS database + let response = await enquiryService.getEnquiries(); + + if (isTruthy(req.query.self)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = response.filter((x) => x?.submittedBy === (req.currentUser?.tokenPayload as any)?.idir_username); + } + + res.status(200).json(response); + } catch (e: unknown) { + next(e); + } + }, + + getEnquiry: async (req: Request<{ activityId: string }>, res: Response, next: NextFunction) => { + try { + const response = await enquiryService.getEnquiry(req.params.activityId); + res.status(200).json(response); + } catch (e: unknown) { + next(e); + } + }, + updateDraft: async (req: Request, res: Response, next: NextFunction) => { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/src/controllers/submission.ts b/app/src/controllers/submission.ts index 083dd7bd9..21f9c7db1 100644 --- a/app/src/controllers/submission.ts +++ b/app/src/controllers/submission.ts @@ -10,7 +10,7 @@ import { YesNo, YesNoUnsure } from '../components/constants'; -import { camelCaseToTitleCase, deDupeUnsure, getCurrentIdentity, toTitleCase } from '../components/utils'; +import { camelCaseToTitleCase, deDupeUnsure, getCurrentIdentity, isTruthy, toTitleCase } from '../components/utils'; import { activityService, submissionService, permitService, userService } from '../services'; import type { NextFunction, Request, Response } from '../interfaces/IExpress'; @@ -325,6 +325,15 @@ const controller = { } }, + deleteSubmission: async (req: Request<{ submissionId: string }>, res: Response, next: NextFunction) => { + try { + const response = await submissionService.deleteSubmission(req.params.submissionId); + res.status(200).json(response); + } catch (e: unknown) { + next(e); + } + }, + getStatistics: async ( req: Request, res: Response, @@ -338,22 +347,28 @@ const controller = { } }, - getSubmission: async (req: Request<{ activityId: string }>, res: Response, next: NextFunction) => { + getSubmission: async (req: Request<{ submissionId: string }>, res: Response, next: NextFunction) => { try { - const response = await submissionService.getSubmission(req.params.activityId); + const response = await submissionService.getSubmission(req.params.submissionId); res.status(200).json(response); } catch (e: unknown) { next(e); } }, - getSubmissions: async (req: Request, res: Response, next: NextFunction) => { + getSubmissions: async (req: Request, res: Response, next: NextFunction) => { try { // Check for and store new submissions in CHEFS await controller.checkAndStoreNewSubmissions(); // Pull from PCNS database - const response = await submissionService.getSubmissions(); + let response = await submissionService.getSubmissions(); + + if (isTruthy(req.query.self)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = response.filter((x) => x?.submittedBy === (req.currentUser?.tokenPayload as any)?.idir_username); + } + res.status(200).json(response); } catch (e: unknown) { next(e); diff --git a/app/src/db/migrations/20240516000000_005-shas-enquiry.ts b/app/src/db/migrations/20240516000000_005-shas-enquiry.ts index 329316c38..5fbe32f41 100644 --- a/app/src/db/migrations/20240516000000_005-shas-enquiry.ts +++ b/app/src/db/migrations/20240516000000_005-shas-enquiry.ts @@ -37,6 +37,7 @@ export async function up(knex: Knex): Promise { table.text('related_activity_id'); table.text('enquiry_description'); table.text('apply_for_permit_connect'); + table.text('intake_status'); stamps(knex, table); }) ) @@ -73,14 +74,8 @@ export async function down(knex: Knex): Promise { // Drop public schema table triggers .then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_enquiry_trigger ON "enquiry"')) - // Revert table alters - .then(() => - knex.schema.alterTable('submission', function (table) { - table.dropColumn('contact_first_name'); - table.dropColumn('contact_last_name'); - }) - ) - + // Not reverting submission table alters + // This migration is destructive and would result in data loss // Drop public schema tables .then(() => knex.schema.dropTableIfExists('enquiry')) ); diff --git a/app/src/db/models/enquiry.ts b/app/src/db/models/enquiry.ts index ededb6d4e..4a0d155e8 100644 --- a/app/src/db/models/enquiry.ts +++ b/app/src/db/models/enquiry.ts @@ -31,7 +31,8 @@ export default { is_related: input.isRelated, related_activity_id: input.relatedActivityId, enquiry_description: input.enquiryDescription, - apply_for_permit_connect: input.applyForPermitConnect + apply_for_permit_connect: input.applyForPermitConnect, + intake_status: input.intakeStatus }; }, @@ -52,6 +53,8 @@ export default { relatedActivityId: input.related_activity_id, enquiryDescription: input.enquiry_description, applyForPermitConnect: input.apply_for_permit_connect, + intakeStatus: input.intake_status, + updatedAt: input.updated_at?.toISOString() as string, user: null }; }, diff --git a/app/src/db/models/submission.ts b/app/src/db/models/submission.ts index f71560738..5d5ced6bd 100644 --- a/app/src/db/models/submission.ts +++ b/app/src/db/models/submission.ts @@ -138,6 +138,7 @@ export default { housingCoopDescription: input.housing_coop_description, contactFirstName: input.contact_first_name, contactLastName: input.contact_last_name, + updatedAt: input.updated_at?.toISOString() as string, user: null }; }, diff --git a/app/src/db/prisma/schema.prisma b/app/src/db/prisma/schema.prisma index 01145d102..7757b5f4b 100644 --- a/app/src/db/prisma/schema.prisma +++ b/app/src/db/prisma/schema.prisma @@ -236,6 +236,7 @@ model enquiry { related_activity_id String? enquiry_description String? apply_for_permit_connect String? + intake_status String? created_by String? @default("00000000-0000-0000-0000-000000000000") created_at DateTime? @default(now()) @db.Timestamptz(6) updated_by String? diff --git a/app/src/routes/v1/enquiry.ts b/app/src/routes/v1/enquiry.ts index 2c065ae4d..930b26da5 100644 --- a/app/src/routes/v1/enquiry.ts +++ b/app/src/routes/v1/enquiry.ts @@ -7,13 +7,28 @@ import type { NextFunction, Request, Response } from '../../interfaces/IExpress' const router = express.Router(); router.use(requireSomeAuth); -// Submission create draft endpoint +/** Gets a list of enquiries */ +router.get('/', (req: Request, res: Response, next: NextFunction): void => { + enquiryController.getEnquiries(req, res, next); +}); + +/** Gets a specific enquiry */ +router.get('/:enquiryId', (req: Request, res: Response, next: NextFunction): void => { + enquiryController.getEnquiry(req, res, next); +}); + +/** Deletes an enquiry */ +router.delete('/:enquiryId', (req: Request, res: Response, next: NextFunction): void => { + enquiryController.deleteEnquiry(req, res, next); +}); + +/** Creates an enquiry with Draft status */ router.put('/draft', (req: Request, res: Response, next: NextFunction): void => { enquiryController.createDraft(req, res, next); }); -// Submission update draft endpoint -router.put('/draft/:activityId', (req: Request, res: Response, next: NextFunction): void => { +/** Updates an enquiry with Draft status */ +router.put('/draft/:enquiryId', (req: Request, res: Response, next: NextFunction): void => { enquiryController.updateDraft(req, res, next); }); diff --git a/app/src/routes/v1/index.ts b/app/src/routes/v1/index.ts index 9acb6b43f..fc3e5733b 100644 --- a/app/src/routes/v1/index.ts +++ b/app/src/routes/v1/index.ts @@ -1,5 +1,5 @@ import { currentUser } from '../../middleware/authentication'; -import { hasAccess } from '../../middleware/authorization'; + import express from 'express'; import submission from './submission'; import document from './document'; @@ -11,7 +11,6 @@ import user from './user'; const router = express.Router(); router.use(currentUser); -router.use(hasAccess); // Base v1 Responder router.get('/', (_req, res) => { diff --git a/app/src/routes/v1/submission.ts b/app/src/routes/v1/submission.ts index f053794e8..786b192b4 100644 --- a/app/src/routes/v1/submission.ts +++ b/app/src/routes/v1/submission.ts @@ -8,12 +8,12 @@ import type { NextFunction, Request, Response } from '../../interfaces/IExpress' const router = express.Router(); router.use(requireSomeAuth); -// Submissions endpoint +/** Gets a list of submissions */ router.get('/', (req: Request, res: Response, next: NextFunction): void => { submissionController.getSubmissions(req, res, next); }); -// Statistics endpoint +/** Gets submission statistics*/ router.get( '/statistics', submissionValidator.getStatistics, @@ -22,30 +22,40 @@ router.get( } ); -// Submission create draft endpoint +/** Creates a submission with Draft status */ router.put('/draft', (req: Request, res: Response, next: NextFunction): void => { submissionController.createDraft(req, res, next); }); -// Submission update draft endpoint -router.put('/draft/:activityId', (req: Request, res: Response, next: NextFunction): void => { +/** Updates a submission with Draft status */ +router.put('/draft/:submissionId', (req: Request, res: Response, next: NextFunction): void => { submissionController.updateDraft(req, res, next); }); -// Submission create endpoint +/** Creates a submission */ router.put('/', submissionValidator.createSubmission, (req: Request, res: Response, next: NextFunction): void => { submissionController.createSubmission(req, res, next); }); +/** Deletes a submission */ +router.delete( + '/:submissionId', + submissionValidator.deleteSubmission, + (req: Request, res: Response, next: NextFunction): void => { + submissionController.deleteSubmission(req, res, next); + } +); + +/** Gets a specific submission */ router.get( - '/:activityId', + '/:submissionId', submissionValidator.getSubmission, (req: Request, res: Response, next: NextFunction): void => { submissionController.getSubmission(req, res, next); } ); -// Submission update endpoint +/** Updates a submission*/ router.put( '/:submissionId', submissionValidator.updateSubmission, diff --git a/app/src/services/activity.ts b/app/src/services/activity.ts index 0e265d231..61949f5d3 100644 --- a/app/src/services/activity.ts +++ b/app/src/services/activity.ts @@ -28,6 +28,23 @@ const service = { return activity.fromPrismaModel(response); }, + /** + * @function deleteActivity + * Delete an activity + * This action will cascade delete across all linked items + * @param {string} activityId Unique activity ID + * @returns {Promise} The result of running the findFirst operation + */ + deleteActivity: async (activityId: string) => { + const response = await prisma.activity.delete({ + where: { + activity_id: activityId + } + }); + + return activity.fromPrismaModel(response); + }, + /** * @function getActivity * Get an activity diff --git a/app/src/services/enquiry.ts b/app/src/services/enquiry.ts index 576f9c708..b785c5e64 100644 --- a/app/src/services/enquiry.ts +++ b/app/src/services/enquiry.ts @@ -18,6 +18,68 @@ const service = { return enquiry.fromPrismaModel(response); }, + /** + * @function deleteEnquiry + * Deletes the enquiry, followed by the associated activity + * This action will cascade delete across all linked items + * @param {string} enquiryId Enquiry ID + * @returns {Promise} The result of running the delete operation + */ + deleteEnquiry: async (enquiryId: string) => { + const response = await prisma.$transaction(async (trx) => { + const del = await trx.enquiry.delete({ + where: { + enquiry_id: enquiryId + } + }); + + await trx.activity.delete({ + where: { + activity_id: del.activity_id + } + }); + + return del; + }); + + return enquiry.fromPrismaModel(response); + }, + + /** + * @function getEnquiries + * Gets a list of enquiries + * @returns {Promise<(Enquiry | null)[]>} The result of running the findMany operation + */ + getEnquiries: async () => { + try { + const result = await prisma.enquiry.findMany({ include: { user: true } }); + + return result.map((x) => enquiry.fromPrismaModelWithUser(x)); + } catch (e: unknown) { + throw e; + } + }, + + /** + * @function getEnquiry + * Gets a specific enquiry from the PCNS database + * @param {string} activityId PCNS Activity ID + * @returns {Promise} The result of running the findFirst operation + */ + getEnquiry: async (activityId: string) => { + try { + const result = await prisma.enquiry.findFirst({ + where: { + activity_id: activityId + } + }); + + return result ? enquiry.fromPrismaModel(result) : null; + } catch (e: unknown) { + throw e; + } + }, + /** * @function updateEnquiry * Updates a specific enquiry diff --git a/app/src/services/submission.ts b/app/src/services/submission.ts index 0225609f3..662438d9d 100644 --- a/app/src/services/submission.ts +++ b/app/src/services/submission.ts @@ -94,6 +94,33 @@ const service = { }); }, + /** + * @function deleteSubmission + * Deletes the submission, followed by the associated activity + * This action will cascade delete across all linked items + * @param {string} submissionId Submission ID + * @returns {Promise} The result of running the delete operation + */ + deleteSubmission: async (submissionId: string) => { + const response = await prisma.$transaction(async (trx) => { + const del = await trx.submission.delete({ + where: { + submission_id: submissionId + } + }); + + await trx.activity.delete({ + where: { + activity_id: del.activity_id + } + }); + + return del; + }); + + return submission.fromPrismaModel(response); + }, + /** * @function getSubmission * Gets a full data export for the requested CHEFS form @@ -142,11 +169,11 @@ const service = { * @param {string} activityId PCNS Activity ID * @returns {Promise} The result of running the findFirst operation */ - getSubmission: async (activityId: string) => { + getSubmission: async (submissionId: string) => { try { const result = await prisma.submission.findFirst({ where: { - activity_id: activityId + submission_id: submissionId } }); diff --git a/app/src/types/Enquiry.ts b/app/src/types/Enquiry.ts index 5ad3c4a0a..2b863acdd 100644 --- a/app/src/types/Enquiry.ts +++ b/app/src/types/Enquiry.ts @@ -18,5 +18,6 @@ export type Enquiry = { relatedActivityId: string | null; enquiryDescription: string | null; applyForPermitConnect: string | null; + intakeStatus: string | null; user: User | null; } & Partial; diff --git a/app/src/validators/submission.ts b/app/src/validators/submission.ts index 27c17994f..7a8442c5e 100644 --- a/app/src/validators/submission.ts +++ b/app/src/validators/submission.ts @@ -22,6 +22,11 @@ const schema = { permits: permitsSchema }) }, + deleteSubmission: { + params: Joi.object({ + submissionId: uuidv4.required() + }) + }, getStatistics: { query: Joi.object({ dateFrom: Joi.date().allow(null), @@ -32,7 +37,7 @@ const schema = { }, getSubmission: { params: Joi.object({ - activityId: activityId + submissionId: uuidv4.required() }) }, updateSubmission: { @@ -89,8 +94,9 @@ const schema = { }; export default { + createSubmission: validate(schema.createSubmission), + deleteSubmission: validate(schema.deleteSubmission), getStatistics: validate(schema.getStatistics), getSubmission: validate(schema.getSubmission), - createSubmission: validate(schema.createSubmission), updateSubmission: validate(schema.updateSubmission) }; diff --git a/app/tests/unit/controllers/submission.spec.ts b/app/tests/unit/controllers/submission.spec.ts index e0e656f54..5b1eb2c1c 100644 --- a/app/tests/unit/controllers/submission.spec.ts +++ b/app/tests/unit/controllers/submission.spec.ts @@ -767,7 +767,7 @@ describe('getSubmission', () => { it('should return 200 if all good', async () => { const req = { - params: { activityId: 'ACT_ID' }, + params: { submissionId: 'SOMEID' }, currentUser: CURRENT_USER }; @@ -778,14 +778,14 @@ describe('getSubmission', () => { await submissionController.getSubmission(req as any, res as any, next); expect(submissionSpy).toHaveBeenCalledTimes(1); - expect(submissionSpy).toHaveBeenCalledWith(req.params.activityId); + expect(submissionSpy).toHaveBeenCalledWith(req.params.submissionId); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith(SUBMISSION_1); }); it('calls next if the submission service fails to get submission', async () => { const req = { - params: { activityId: 'ACT_ID' }, + params: { submissionId: 'SOMEID' }, currentUser: CURRENT_USER }; @@ -797,7 +797,7 @@ describe('getSubmission', () => { await submissionController.getSubmission(req as any, res as any, next); expect(submissionSpy).toHaveBeenCalledTimes(1); - expect(submissionSpy).toHaveBeenCalledWith(req.params.activityId); + expect(submissionSpy).toHaveBeenCalledWith(req.params.submissionId); expect(res.status).toHaveBeenCalledTimes(0); expect(next).toHaveBeenCalledTimes(1); }); @@ -814,7 +814,8 @@ describe('getSubmissions', () => { it('should return 200 if all good', async () => { const req = { - currentUser: CURRENT_USER + currentUser: CURRENT_USER, + query: {} }; checkAndStoreSpy.mockResolvedValue(); diff --git a/frontend/src/components/intake/CollectionDisclaimer.vue b/frontend/src/components/housing/intake/CollectionDisclaimer.vue similarity index 100% rename from frontend/src/components/intake/CollectionDisclaimer.vue rename to frontend/src/components/housing/intake/CollectionDisclaimer.vue diff --git a/frontend/src/components/intake/ShasEnquiryForm.vue b/frontend/src/components/housing/intake/ShasEnquiryForm.vue similarity index 92% rename from frontend/src/components/intake/ShasEnquiryForm.vue rename to frontend/src/components/housing/intake/ShasEnquiryForm.vue index 3c3bb2e5f..b2fb89e46 100644 --- a/frontend/src/components/intake/ShasEnquiryForm.vue +++ b/frontend/src/components/housing/intake/ShasEnquiryForm.vue @@ -5,14 +5,25 @@ import { useRouter } from 'vue-router'; import { object } from 'yup'; import { Dropdown, InputMask, RadioList, InputText, StepperNavigation, TextArea } from '@/components/form'; -import CollectionDisclaimer from '@/components/intake/CollectionDisclaimer.vue'; +import CollectionDisclaimer from '@/components/housing/intake/CollectionDisclaimer.vue'; import { Button, Card, Divider, Message, useConfirm, useToast } from '@/lib/primevue'; import { enquiryService, submissionService } from '@/services'; import { ContactPreferenceList, ProjectRelationshipList, RouteNames, YesNo } from '@/utils/constants'; -import { BASIC_RESPONSES } from '@/utils/enums'; +import { BASIC_RESPONSES, INTAKE_STATUS_LIST } from '@/utils/enums'; import type { Ref } from 'vue'; +// Props +type Props = { + activityId?: string; + enquiryId?: string; +}; + +const props = withDefaults(defineProps(), { + activityId: undefined, + enquiryId: undefined +}); + // State const assignedActivityId: Ref = ref(undefined); const editable: Ref = ref(true); @@ -133,8 +144,25 @@ async function onSubmit(data: any) { } onBeforeMount(async () => { + let response; + if (props.activityId) { + response = (await enquiryService.getEnquiry(props.activityId)).data; + editable.value = response.intakeStatus === INTAKE_STATUS_LIST.DRAFT; + } + // Default form values - initialFormValues.value = {}; + initialFormValues.value = { + activityId: response?.activityId, + enquiryId: response?.enquiryId, + applicant: { + firstName: response?.contactFirstName, + lastName: response?.contactLastName, + phoneNumber: response?.contactPhoneNumber, + email: response?.contactEmail, + relationshipToProject: response?.contactApplicantRelationship, + contactPreference: response?.contactPreference + } + }; }); diff --git a/frontend/src/components/intake/ShasIntakeForm.vue b/frontend/src/components/housing/intake/ShasIntakeForm.vue similarity index 98% rename from frontend/src/components/intake/ShasIntakeForm.vue rename to frontend/src/components/housing/intake/ShasIntakeForm.vue index d5b62a7d0..9fa90fe54 100644 --- a/frontend/src/components/intake/ShasIntakeForm.vue +++ b/frontend/src/components/housing/intake/ShasIntakeForm.vue @@ -17,8 +17,8 @@ import { StepperNavigation, TextArea } from '@/components/form'; -import CollectionDisclaimer from '@/components/intake/CollectionDisclaimer.vue'; -import { intakeSchema } from '@/components/intake/ShasIntakeSchema'; +import CollectionDisclaimer from '@/components/housing/intake/CollectionDisclaimer.vue'; +import { intakeSchema } from '@/components/housing/intake/ShasIntakeSchema'; import { Accordion, AccordionTab, @@ -42,7 +42,7 @@ import { YesNo, YesNoUnsure } from '@/utils/constants'; -import { BASIC_RESPONSES, INTAKE_FORM_CATEGORIES, PROJECT_LOCATION } from '@/utils/enums'; +import { BASIC_RESPONSES, INTAKE_FORM_CATEGORIES, INTAKE_STATUS_LIST, PROJECT_LOCATION } from '@/utils/enums'; import type { Ref } from 'vue'; @@ -178,8 +178,9 @@ async function onSubmit(data: any) { onBeforeMount(async () => { let response; - if (props.activityId) { - response = (await submissionService.getSubmission(props.activityId)).data; + if (props.submissionId) { + response = (await submissionService.getSubmission(props.submissionId)).data; + editable.value = response.intakeStatus === INTAKE_STATUS_LIST.DRAFT; } // Default form values @@ -194,6 +195,11 @@ onBeforeMount(async () => { relationshipToProject: response?.contactApplicantRelationship, contactPreference: response?.contactPreference }, + basic: { + isDevelopedByCompanyOrOrg: response?.isDevelopedByCompanyOrOrg, + isDevelopedInBC: response?.isDevelopedInBC, + registeredName: response?.companyNameRegistered + }, location: { province: 'BC' } @@ -1159,6 +1165,7 @@ onBeforeMount(async () => {