Skip to content

Commit a8d475b

Browse files
committed
feat: general project navigator form validation
1 parent ddcd122 commit a8d475b

17 files changed

Lines changed: 466 additions & 288 deletions

File tree

app/src/controllers/generalProject.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import config from 'config';
2+
import { Prisma } from '@prisma/client';
23
import { v4 as uuidv4 } from 'uuid';
34

45
import { transactionWrapper } from '../db/utils/transactionWrapper.ts';
@@ -205,11 +206,7 @@ const generateGeneralProjectData = async (
205206
queuePriority: null,
206207
relatedPermits: null,
207208
astNotes: null,
208-
astUpdated: false,
209-
addedToAts: false,
210209
atsClientId: null,
211-
ltsaCompleted: false,
212-
bcOnlineCompleted: false,
213210
checkProvincialPermits: null,
214211
atsEnquiryId: null
215212
} as GeneralProject,
@@ -420,12 +417,19 @@ export const upsertGeneralProjectDraftController = async (req: Request<never, ne
420417
res.status(update ? 200 : 201).json(response);
421418
};
422419

423-
export const updateGeneralProjectController = async (req: Request<never, never, GeneralProject>, res: Response) => {
420+
export const updateGeneralProjectController = async (
421+
req: Request<{ generalProjectId: string }, never, Omit<Prisma.general_projectUpdateInput, 'generalProjectId'>>,
422+
res: Response
423+
) => {
424424
const response = await transactionWrapper<GeneralProject>(async (tx: PrismaTransactionClient) => {
425-
return await updateGeneralProject(tx, {
426-
...req.body,
427-
...generateUpdateStamps(req.currentContext)
428-
});
425+
return await updateGeneralProject(
426+
tx,
427+
{
428+
...req.body,
429+
...generateUpdateStamps(req.currentContext)
430+
},
431+
req.params.generalProjectId
432+
);
429433
});
430434

431435
res.status(200).json(response);

app/src/routes/v1/generalProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ router.get(
113113
);
114114

115115
/** Updates a general project*/
116-
router.put(
116+
router.patch(
117117
'/:generalProjectId',
118118
hasAuthorization(Resource.GENERAL_PROJECT, Action.UPDATE),
119119
hasAccess('generalProjectId'),

app/src/services/generalProject.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,20 +204,19 @@ export const searchGeneralProjects = async (
204204
/**
205205
* Updates a specific general project
206206
* @param tx Prisma transaction client
207-
* @param data General project to update
207+
* @param data General project data to update
208+
* @param generalProjectId ID of the project to update
208209
* @returns A Promise that resolves to the updated general project
209210
*/
210211
export const updateGeneralProject = async (
211212
tx: PrismaTransactionClient,
212-
data: GeneralProjectBase
213+
data: Omit<Prisma.general_projectUpdateInput, 'generalProjectId'>,
214+
generalProjectId: string
213215
): Promise<GeneralProject> => {
214216
const result = await tx.general_project.update({
215-
data: {
216-
...data,
217-
geoJson: jsonToPrismaInputJson(data.geoJson)
218-
},
217+
data,
219218
where: {
220-
generalProjectId: data.generalProjectId
219+
generalProjectId
221220
},
222221
include: {
223222
activity: {

app/src/validators/generalProject.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { activityId, email, uuidv4 } from './common.ts';
66
import { contactSchema } from './contact.ts';
77

88
import { validate } from '../middleware/validation';
9-
import { YES_NO_UNSURE_LIST } from '../utils/constants/application.ts';
9+
import { YES_NO_LIST, YES_NO_UNSURE_LIST } from '../utils/constants/application.ts';
1010
import { APPLICATION_STATUS_LIST, SUBMISSION_TYPE_LIST } from '../utils/constants/projectCommon';
1111
import { ProjectApplicant } from '../utils/enums/housing.ts';
1212
import { PROJECT_APPLICANT_LIST } from '../utils/constants/housing.ts';
@@ -51,17 +51,6 @@ const schema = {
5151
})
5252
})
5353
},
54-
emailConfirmation: {
55-
body: Joi.object({
56-
bcc: Joi.array().items(email).allow(null),
57-
bodyType: Joi.string().required().allow(null),
58-
body: Joi.string().required(),
59-
cc: Joi.array().items(email),
60-
from: email.required(),
61-
subject: Joi.string().required(),
62-
to: Joi.array().items(email).required()
63-
})
64-
},
6554
deleteGeneralProject: {
6655
params: Joi.object({
6756
generalProjectId: uuidv4.required()
@@ -96,17 +85,15 @@ const schema = {
9685
},
9786
updateGeneralProject: {
9887
body: Joi.object({
99-
generalProjectId: uuidv4.required(),
100-
activityId: activityId,
101-
consentToFeedback: Joi.boolean(),
10288
queuePriority: Joi.number().required().integer().min(0).max(3),
10389
submissionType: Joi.string()
10490
.required()
10591
.valid(...SUBMISSION_TYPE_LIST),
106-
submittedAt: Joi.string().required(),
10792
companyNameRegistered: Joi.string().allow(null),
10893
companyIdRegistered: Joi.string().allow(null),
94+
isRegisteredInBc: Joi.boolean().required(),
10995
projectName: Joi.string().required(),
96+
activityType: Joi.string().required(),
11097
projectDescription: Joi.string().allow(null),
11198
streetAddress: Joi.string().allow(null).max(255),
11299
locality: Joi.string().allow(null).max(255),
@@ -118,12 +105,14 @@ const schema = {
118105
naturalDisaster: Joi.boolean().required(),
119106
projectLocationDescription: Joi.string().allow(null).max(4000),
120107
...atsValidator.atsEnquirySubmissionFields,
121-
ltsaCompleted: Joi.boolean().required(),
122-
bcOnlineCompleted: Joi.boolean().required(),
108+
addedToAts: Joi.optional(),
123109
aaiUpdated: Joi.boolean().required(),
124110
astNotes: Joi.string().allow(null).max(4000),
125111
assignedUserId: uuidv4.allow(null),
126-
applicationStatus: Joi.string().valid(...APPLICATION_STATUS_LIST)
112+
applicationStatus: Joi.string().valid(...APPLICATION_STATUS_LIST),
113+
region: Joi.string().allow(null),
114+
area: Joi.string().allow(null),
115+
businessArea: Joi.string() //TODO: add .valid() and make this list
127116
}),
128117
params: Joi.object({
129118
generalProjectId: uuidv4.required()
@@ -133,7 +122,6 @@ const schema = {
133122

134123
export default {
135124
createGeneralProject: validate(schema.createGeneralProject),
136-
emailConfirmation: validate(schema.emailConfirmation),
137125
deleteGeneralProject: validate(schema.deleteGeneralProject),
138126
deleteDraft: validate(schema.deleteDraft),
139127
getStatistics: validate(schema.getStatistics),

frontend/src/components/ats/ATSInfo.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ watch(visible, () => {
154154
<Select
155155
v-if="getInitiative === Initiative.GENERAL"
156156
class="w-full"
157-
name="submissionState.businessArea"
157+
name="atsInfo.businessArea"
158158
:label="t('i.housing.project.projectForm.businessAreaLabel')"
159159
:options="BUSINESS_AREA_LIST"
160160
/>

frontend/src/components/form/panel/CompanyProjectNamePanel.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ async function onRegisteredNameInput(e: AutoCompleteCompleteEvent) {
117117
:disabled="!getEditable"
118118
:options="YES_NO_LIST"
119119
/>
120+
<InputText
121+
v-if="getInitiative === Initiative.GENERAL"
122+
name="companyProjectName.projectNumber"
123+
:label="t('i.common.projectForm.projectNumber')"
124+
:disabled="true"
125+
/>
120126
</div>
121127
</Panel>
122128
</template>

frontend/src/components/general/project/ProjectFormNavigator.vue

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,20 @@ import RelatedEnquiriesSection from '@/components/form/section/RelatedEnquiriesS
2020
import SubmissionStateSection from '@/components/form/section/SubmissionStateSection.vue';
2121
import { Button, Message, useConfirm, useToast } from '@/lib/primevue';
2222
import { atsService, generalProjectService, mapService, userService } from '@/services';
23-
import { useAppStore, useProjectStore } from '@/store';
23+
import { useAppStore, useFormStore, useProjectStore } from '@/store';
2424
import { ATS_ENQUIRY_TYPE_CODE_PROJECT_INTAKE_SUFFIX, ATS_MANAGING_REGION } from '@/utils/constants/projectCommon';
2525
import { ATSCreateTypes, BasicResponse, GroupName, Initiative } from '@/utils/enums/application';
26-
import { ActivityContactRole, ApplicationStatus } from '@/utils/enums/projectCommon';
26+
import {
27+
ActivityContactRole,
28+
ApplicationStatus,
29+
Area,
30+
BusinessArea,
31+
FormState,
32+
FormType,
33+
Region
34+
} from '@/utils/enums/projectCommon';
2735
import { formatDate } from '@/utils/formatters';
28-
import { omit, scrollToFirstError, setEmptyStringsToNull, toTitleCase } from '@/utils/utils';
36+
import { scrollToFirstError, setEmptyStringsToNull, toTitleCase } from '@/utils/utils';
2937
3038
import type { Ref } from 'vue';
3139
import type {
@@ -34,7 +42,8 @@ import type {
3442
ATSEnquiryResource,
3543
Contact,
3644
DeepPartial,
37-
GeneralProject
45+
GeneralProject,
46+
User
3847
} from '@/types';
3948
import type { FormSchemaType } from '@/validators/general/projectFormNavigatorSchema';
4049
@@ -154,7 +163,7 @@ async function handleAtsCreate(values: GenericObject) {
154163
}
155164
156165
async function initializeFormValues(project: GeneralProject): Promise<DeepPartial<FormSchemaType>> {
157-
let assigneeOptions = [];
166+
let assigneeOptions: User[] = [];
158167
if (project.assignedUserId)
159168
assigneeOptions = (await userService.searchUsers({ userId: [project.assignedUserId] })).data;
160169
@@ -186,7 +195,10 @@ async function initializeFormValues(project: GeneralProject): Promise<DeepPartia
186195
companyProjectName: {
187196
companyIdRegistered: project.companyIdRegistered,
188197
companyNameRegistered: project.companyNameRegistered,
189-
projectName: project.projectName
198+
projectName: project.projectName,
199+
projectNumber: project.projectNumber,
200+
activityType: project.activityType,
201+
isRegisteredInBc: project.isRegisteredInBc ? BasicResponse.YES : BasicResponse.NO
190202
},
191203
192204
// Additional Info
@@ -202,14 +214,17 @@ async function initializeFormValues(project: GeneralProject): Promise<DeepPartia
202214
submissionState: {
203215
queuePriority: project.queuePriority,
204216
submissionType: project.submissionType,
205-
assignedUser: assigneeOptions[0] ?? null,
206-
applicationStatus: project.applicationStatus
217+
assignedUser: assigneeOptions[0]?.fullName ?? null,
218+
applicationStatus: project.applicationStatus,
219+
region: String(project.region),
220+
area: String(project.area)
207221
},
208222
209223
// ATS link
210224
atsInfo: {
211225
atsClientId: project.atsClientId,
212-
atsEnquiryId: project.atsEnquiryId
226+
atsEnquiryId: project.atsEnquiryId,
227+
businessArea: String(project.businessArea)
213228
},
214229
215230
relatedEnquiries: { csv: project.relatedEnquiries },
@@ -237,7 +252,6 @@ function onCancel() {
237252
}
238253
239254
function onInvalidSubmit(e: GenericObject) {
240-
console.log(e);
241255
const errors = Object.keys(e.errors);
242256
243257
if (errors.includes('contact.firstName')) {
@@ -262,40 +276,56 @@ function onReOpen() {
262276
263277
const onSubmit = async (formValues: GenericObject) => {
264278
try {
265-
const values: FormSchemaType = formValues as FormSchemaType;
279+
// vee-validate doesn't get transformed data from yup so
280+
// manually run the form values through it here
281+
const values: FormSchemaType = projectFormNavigatorSchema.cast(formValues);
266282
267283
await handleAtsCreate(values);
268284
269-
console.log(values);
270-
271-
// Generate final submission object
272-
const payload: GeneralProject = {
273-
...project,
274-
275-
assignedUserId: values.submissionState.assignedUser?.userId ?? undefined,
276-
applicationStatus: values.submissionState.applicationStatus,
277-
278-
companyIdRegistered: values.companyProjectName.companyIdRegistered,
279-
companyNameRegistered: values.companyProjectName.companyNameRegistered,
280-
queuePriority: values.submissionState.queuePriority,
281-
submissionType: values.submissionState.submissionType,
285+
// Generate final payload
286+
// TODO: Create a type using Pick instead of Partial?
287+
const payload: Partial<GeneralProject> = {
288+
// Company and Project Information
282289
projectName: values.companyProjectName.projectName,
283-
projectDescription: values.projectDescription.description,
284-
astNotes: values.astNotes.notes,
285-
atsClientId: values.atsInfo.atsClientId ?? null,
286-
atsEnquiryId: values.atsInfo.atsEnquiryId ?? null,
287-
aaiUpdated: values.projectAreasUpdated.aaiUpdated,
288-
projectNumber: values.companyProjectName.projectNumber, // need to add this
289-
// projectLocation: string; // was ist das
290-
projectLocationDescription: values.locationDescription.description,
290+
companyNameRegistered: values.companyProjectName.companyNameRegistered,
291+
companyIdRegistered: values.companyProjectName.companyIdRegistered,
292+
activityType: values.companyProjectName.activityType!, // Req for General but the type doesnt know until runtime
293+
isRegisteredInBc: values.companyProjectName.isRegisteredInBc === BasicResponse.YES,
294+
295+
// Location
291296
locality: values.location.locality,
292297
province: values.location.province,
293298
locationPids: values.location.locationPids,
294299
latitude: values.location.latitude,
295300
longitude: values.location.longitude,
296301
streetAddress: values.location.streetAddress,
297302
geomarkUrl: values.location.geomarkUrl,
298-
naturalDisaster: values.location.naturalDisaster === BasicResponse.YES
303+
naturalDisaster: values.location.naturalDisaster === BasicResponse.YES,
304+
305+
// Additional Location Information
306+
projectLocationDescription: values.locationDescription.description,
307+
308+
// Additional Project Information
309+
projectDescription: values.projectDescription.description,
310+
311+
// AST Notes
312+
astNotes: values.astNotes.notes,
313+
314+
// Submission State
315+
assignedUserId: values.submissionState.assignedUser
316+
? (values.submissionState.assignedUser as User).userId
317+
: undefined,
318+
region: values.submissionState.region as Region,
319+
area: values.submissionState.area as Area,
320+
applicationStatus: values.submissionState.applicationStatus,
321+
submissionType: values.submissionState.submissionType,
322+
queuePriority: values.submissionState.queuePriority,
323+
324+
// ATS
325+
businessArea: values.atsInfo.businessArea as BusinessArea,
326+
327+
// Updates
328+
aaiUpdated: values.projectAreasUpdated.aaiUpdated
299329
};
300330
301331
// Update project
@@ -307,9 +337,7 @@ const onSubmit = async (formValues: GenericObject) => {
307337
308338
// Reinitialize the form
309339
formRef.value?.resetForm({
310-
values: {
311-
...initializeFormValues(result.data)
312-
}
340+
values: await initializeFormValues(result.data)
313341
});
314342
315343
toast.success(t('i.common.form.savedMessage'));
@@ -346,6 +374,8 @@ watch(primaryContact, (newContact, oldContact) => {
346374
});
347375
348376
onBeforeMount(async () => {
377+
useFormStore().setFormType(FormType.NAVIGATOR);
378+
useFormStore().setFormState(FormState.UNLOCKED);
349379
locationPidsAuto.value = (await mapService.getPIDs(project.generalProjectId)).data;
350380
351381
// Default form values

frontend/src/components/general/project/ProjectIntakeForm.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { PermitNeeded, PermitStage } from '@/utils/enums/permit';
3232
import { ActivityContactRole, FormState, FormType } from '@/utils/enums/projectCommon';
3333
import { generalErrorHandler } from '@/utils/utils';
3434
35+
import type { GeoJSON } from 'geojson';
3536
import type { GenericObject } from 'vee-validate';
3637
import type { Ref } from 'vue';
3738
import type {
@@ -164,7 +165,7 @@ function loadProjectValues() {
164165
longitude: project.longitude,
165166
ltsaPidLookup: project.locationPids,
166167
geomarkUrl: project.geomarkUrl,
167-
projectLocationDescription: project?.projectLocationDescription,
168+
projectLocationDescription: project.projectLocationDescription,
168169
geoJson: project.geoJson
169170
},
170171
permits: {
@@ -249,7 +250,8 @@ async function onSubmit(data: FormSchemaType) {
249250
locality: data.location.locality,
250251
province: data.location.province,
251252
geomarkUrl: data.location.geomarkUrl,
252-
streetAddress: data.location.streetAddress
253+
streetAddress: data.location.streetAddress,
254+
geoJson: data.location.geoJson ? (data.location.geoJson as GeoJSON) : undefined
253255
},
254256
permits: {
255257
appliedPermits: data.permits.appliedPermits?.map((x) => ({

0 commit comments

Comments
 (0)