diff --git a/backend/package-lock.json b/backend/package-lock.json index a34474f3..b6bcc773 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,6 +24,7 @@ "apollo-server-express": "^3.13.0", "copyfiles": "^2.4.1", "dotenv": "^16.0.1", + "geojson": "^0.5.0", "graphql": "^16.8.1", "graphql-ws": "^5.14.3", "helmet": "^7.0.0", @@ -7019,6 +7020,14 @@ "node": ">=6.9.0" } }, + "node_modules/geojson": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/geojson/-/geojson-0.5.0.tgz", + "integrity": "sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index 151ed0ec..fca26f5c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -40,6 +40,7 @@ "apollo-server-express": "^3.13.0", "copyfiles": "^2.4.1", "dotenv": "^16.0.1", + "geojson": "^0.5.0", "graphql": "^16.8.1", "graphql-ws": "^5.14.3", "helmet": "^7.0.0", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 93e4ec0c..2f6db25b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -333,15 +333,15 @@ model configuration_h { /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model equipment { - equipment_guid String @id(map: "PK_equipment_guid") @db.Uuid - equipment_code String? @db.VarChar(10) - equipment_location_desc String? @db.VarChar(120) - equipment_geometry_point String? + equipment_guid String @id(map: "PK_equipment_guid") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + equipment_code String? @db.VarChar(10) + equipment_location_desc String? @db.VarChar(120) + equipment_geometry_point Unsupported("geometry")? active_ind Boolean - create_user_id String @db.VarChar(32) - create_utc_timestamp DateTime @db.Timestamp(6) - update_user_id String @db.VarChar(32) - update_utc_timestamp DateTime @db.Timestamp(6) + create_user_id String @db.VarChar(32) + create_utc_timestamp DateTime @db.Timestamp(6) + update_user_id String @db.VarChar(32) + update_utc_timestamp DateTime @db.Timestamp(6) action action[] - equipment_code_equipment_equipment_codeToequipment_code equipment_code? @relation("equipment_equipment_codeToequipment_code", fields: [equipment_code], references: [equipment_code], onDelete: NoAction, onUpdate: NoAction, map: "fk_equipment__equipment_code") + equipment_code_equipment_equipment_codeToequipment_code equipment_code? @relation("equipment_equipment_codeToequipment_code", fields: [equipment_code], references: [equipment_code], onDelete: NoAction, onUpdate: NoAction, map: "fk_equipment__equipment_code") } diff --git a/backend/src/case_file/case_file.graphql b/backend/src/case_file/case_file.graphql index 95000ec1..a4ed5e81 100644 --- a/backend/src/case_file/case_file.graphql +++ b/backend/src/case_file/case_file.graphql @@ -6,6 +6,7 @@ type CaseFile { assessmentDetails: Assessment preventionDetails: Prevention note: Note + equipment: [EquipmentDetails] isReviewRequired: Boolean reviewComplete: ReviewCompleteAction } @@ -27,6 +28,7 @@ type Assessment { } type Action{ + actionGuid: String actor: String! date: Date! actionCode: String! @@ -143,10 +145,66 @@ type Query { type Mutation { createAssessment(createAssessmentInput: CreateAssessmentInput!): CaseFile! updateAssessment(updateAssessmentInput: UpdateAssessmentInput!): CaseFile! + createPrevention(createPreventionInput: CreatePreventionInput!): CaseFile! updatePrevention(updatePreventionInput: UpdatePreventionInput!): CaseFile! + createReview(reviewInput: ReviewInput!): CaseFile! updateReview(reviewInput: ReviewInput!): CaseFile! + createNote(input: CreateSupplementalNoteInput!): CaseFile! updateNote(input: UpdateSupplementalNoteInput!): CaseFile! + + createEquipment(createEquipmentInput: CreateEquipmentInput!): CaseFile! + updateEquipment(updateEquipmentInput: UpdateEquipmentInput!): CaseFile! + deleteEquipment(deleteEquipmentInput: DeleteEquipmentInput!): Boolean! +} + +type EquipmentDetails { + equipmentGuid: String + equipmentTypeCode: String + equipmentTypeActiveIndicator: Boolean + address: String + xCoordinate: String + yCoordinate: String + actions: [Action] +} + +input CreateEquipmentInput { + leadIdentifier: String + createUserId: String + agencyCode: String + caseCode: String + equipment: [EquipmentDetailsInput] +} + +input UpdateEquipmentInput { + leadIdentifier: String + createUserId: String + agencyCode: String + caseCode: String + equipment: [EquipmentDetailsInput] +} + +input DeleteEquipmentInput { + equipmentGuid: String! + updateUserId: String! +} + +input EquipmentDetailsInput { + equipmentGuid: String + equipmentTypeCode: String + equipmentTypeActiveIndicator: Boolean + address: String + xCoordinate: String + yCoordinate: String + actions: [EquipmentActionInput] +} + +input EquipmentActionInput { + actionGuid: String + date: Date + actor: String + activeIndicator: Boolean + actionCode: String } diff --git a/backend/src/case_file/case_file.resolver.ts b/backend/src/case_file/case_file.resolver.ts index 6d974bbb..3904b6d1 100644 --- a/backend/src/case_file/case_file.resolver.ts +++ b/backend/src/case_file/case_file.resolver.ts @@ -1,7 +1,7 @@ import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'; import { CaseFileService } from './case_file.service'; -import { CreateAssessmentInput, CreatePreventionInput } from './dto/create-case_file.input'; -import { UpdateAssessmentInput, UpdatePreventionInput } from './dto/update-case_file.input'; +import { CreateAssessmentInput, CreateEquipmentInput, CreatePreventionInput } from './dto/create-case_file.input'; +import { UpdateAssessmentInput, UpdateEquipmentInput, UpdatePreventionInput } from './dto/update-case_file.input'; import { JwtRoleGuard } from "../auth/jwtrole.guard"; import { UseGuards } from "@nestjs/common"; import { Role } from "../enum/role.enum"; @@ -9,6 +9,7 @@ import { Roles } from "../auth/decorators/roles.decorator"; import { ReviewInput } from './dto/review-input'; import { CreateSupplementalNoteInput } from './dto/supplemental-note/create-supplemental-note.input'; import { UpdateSupplementalNoteInput } from './dto/supplemental-note/update-supplemental-note.input'; +import { DeleteEquipmentInput } from './dto/equipment/delete-equipment.input'; @UseGuards(JwtRoleGuard) @Resolver('CaseFile') @@ -57,6 +58,24 @@ export class CaseFileResolver { return this.caseFileService.updatePrevention(updatePreventionInput.caseIdentifier, updatePreventionInput); } + @Mutation('createEquipment') + @Roles(Role.COS_OFFICER) + createEquipment(@Args('createEquipmentInput') createEquipmentInput: CreateEquipmentInput) { + return this.caseFileService.createEquipment(createEquipmentInput); + } + + @Mutation('updateEquipment') + @Roles(Role.COS_OFFICER) + updateEquipment(@Args('updateEquipmentInput') updateEquipmentInput: UpdateEquipmentInput) { + return this.caseFileService.updateEquipment(updateEquipmentInput); + } + + @Mutation('deleteEquipment') + @Roles(Role.COS_OFFICER) + deleteEquipment(@Args('deleteEquipmentInput') deleteEquipmentInput: DeleteEquipmentInput) { + return this.caseFileService.deleteEquipment(deleteEquipmentInput); + } + @Mutation('updateReview') @Roles(Role.COS_OFFICER) updateReview(@Args('reviewInput') reviewInput: ReviewInput) { diff --git a/backend/src/case_file/case_file.service.ts b/backend/src/case_file/case_file.service.ts index fc634928..c9666db5 100644 --- a/backend/src/case_file/case_file.service.ts +++ b/backend/src/case_file/case_file.service.ts @@ -1,22 +1,38 @@ -import { Injectable } from "@nestjs/common"; -import { CreateAssessmentInput, CreateCaseInput, CreatePreventionInput } from "./dto/create-case_file.input"; -import { UpdateAssessmentInput, UpdatePreventionInput } from "./dto/update-case_file.input"; +import { Injectable, Logger } from "@nestjs/common"; +import { + CreateAssessmentInput, + CreateCaseInput, + CreateEquipmentInput, + CreatePreventionInput, +} from "./dto/create-case_file.input"; +import { + UpdateAssessmentInput, + UpdateEquipmentInput, + UpdatePreventionInput, +} from "./dto/update-case_file.input"; import { PrismaService } from "nestjs-prisma"; import { CaseFile } from "./entities/case_file.entity"; import { GraphQLError } from "graphql"; import { CreateSupplementalNoteInput } from "./dto/supplemental-note/create-supplemental-note.input"; import { ACTION_CODES } from "../common/action_codes"; import { UpdateSupplementalNoteInput } from "./dto/supplemental-note/update-supplemental-note.input"; -import { ACTION_TYPE_CODES } from "../common/action_type_codes" +import { ACTION_TYPE_CODES } from "../common/action_type_codes"; import { Action } from "./entities/case-action.entity"; import { CaseFileActionItem } from "./dto/case-file-action-item"; -import { ReviewInput } from './dto/review-input'; +import { Equipment } from "./entities/equipment.entity"; +import { ReviewInput } from "./dto/review-input"; +import { DeleteEquipmentInput } from "./dto/equipment/delete-equipment.input"; +import { Prisma } from "@prisma/client"; @Injectable() export class CaseFileService { constructor(private prisma: PrismaService) {} - async createAssessmentCase(createAssessmentInput: CreateAssessmentInput): Promise { + private readonly logger = new Logger(CaseFileService.name); + + async createAssessmentCase( + createAssessmentInput: CreateAssessmentInput + ): Promise { let caseFileGuid: string; try { @@ -28,17 +44,20 @@ export class CaseFileService { agency_code: createAssessmentInput.agencyCode, }, }, - inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: createAssessmentInput - .assessmentDetails.actionJustificationCode - ? { - connect: { - inaction_reason_code: createAssessmentInput.assessmentDetails.actionJustificationCode, - }, - } - : undefined, + inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: + createAssessmentInput.assessmentDetails.actionJustificationCode + ? { + connect: { + inaction_reason_code: + createAssessmentInput.assessmentDetails + .actionJustificationCode, + }, + } + : undefined, create_user_id: createAssessmentInput.createUserId, create_utc_timestamp: new Date(), - action_not_required_ind: createAssessmentInput.assessmentDetails.actionNotRequired, + action_not_required_ind: + createAssessmentInput.assessmentDetails.actionNotRequired, case_code_case_file_case_codeTocase_code: { connect: { case_code: createAssessmentInput.caseCode, @@ -59,7 +78,10 @@ export class CaseFileService { }); }); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } return caseFileGuid; } @@ -98,7 +120,10 @@ export class CaseFileService { }); }); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } return caseFileGuid; } @@ -141,14 +166,21 @@ export class CaseFileService { }); }); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", exception); + throw new GraphQLError( + "Exception occurred. See server log for details", + exception + ); } return caseFileGuid; } - async createAssessment(createAssessmentInput: CreateAssessmentInput): Promise { + async createAssessment( + createAssessmentInput: CreateAssessmentInput + ): Promise { let actiontypeCode: string = "COMPASSESS"; - let caseFileGuid: string = await this.createAssessmentCase(createAssessmentInput); + let caseFileGuid: string = await this.createAssessmentCase( + createAssessmentInput + ); let caseFileOutput: CaseFile; try { await this.prisma.$transaction(async (db) => { @@ -167,19 +199,21 @@ export class CaseFileService { } for (const action of createAssessmentInput.assessmentDetails.actions) { - let actionTypeActionXref = await db.action_type_action_xref.findFirstOrThrow({ - where: { - action_type_code: actiontypeCode, - action_code: action.actionCode, - }, - select: { - action_type_action_xref_guid: true, - }, - }); + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: actiontypeCode, + action_code: action.actionCode, + }, + select: { + action_type_action_xref_guid: true, + }, + }); await db.action.create({ data: { case_guid: caseFileGuid, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, actor_guid: action.actor, action_date: action.date, active_ind: action.activeIndicator, @@ -191,14 +225,21 @@ export class CaseFileService { }); caseFileOutput = await this.findOne(caseFileGuid); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } return caseFileOutput; } - async createPrevention(createPreventionInput: CreatePreventionInput): Promise { + async createPrevention( + createPreventionInput: CreatePreventionInput + ): Promise { let actiontypeCode: string = "COSPRV&EDU"; - let caseFileGuid: string = await this.createOtherCase(createPreventionInput); + let caseFileGuid: string = await this.createOtherCase( + createPreventionInput + ); let caseFileOutput: CaseFile; try { await this.prisma.$transaction(async (db) => { @@ -217,19 +258,21 @@ export class CaseFileService { } for (const action of createPreventionInput.preventionDetails.actions) { - let actionTypeActionXref = await db.action_type_action_xref.findFirstOrThrow({ - where: { - action_type_code: actiontypeCode, - action_code: action.actionCode, - }, - select: { - action_type_action_xref_guid: true, - }, - }); + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: actiontypeCode, + action_code: action.actionCode, + }, + select: { + action_type_action_xref_guid: true, + }, + }); await db.action.create({ data: { case_guid: caseFileGuid, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, actor_guid: action.actor, action_date: action.date, active_ind: action.activeIndicator, @@ -241,7 +284,10 @@ export class CaseFileService { }); caseFileOutput = await this.findOne(caseFileGuid); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } return caseFileOutput; } @@ -258,7 +304,7 @@ export class CaseFileService { private getCaseActions = async ( actions: Array, actionTypeCode: string, - actionCode: string = "", + actionCode: string = "" ): Promise> => { let items = []; @@ -266,7 +312,8 @@ export class CaseFileService { items = actions.filter((action) => { const { action_type_action_xref: { - action_type_code_action_type_action_xref_action_type_codeToaction_type_code: _actionTypeCode, + action_type_code_action_type_action_xref_action_type_codeToaction_type_code: + _actionTypeCode, }, } = action; @@ -276,17 +323,23 @@ export class CaseFileService { items = actions.filter((action) => { const { action_type_action_xref: { - action_code_action_type_action_xref_action_codeToaction_code: _actionCode, - action_type_code_action_type_action_xref_action_type_codeToaction_type_code: _actionTypeCode, + action_code_action_type_action_xref_action_codeToaction_code: + _actionCode, + action_type_code_action_type_action_xref_action_type_codeToaction_type_code: + _actionTypeCode, }, } = action; - return _actionTypeCode.action_type_code === actionTypeCode && _actionCode.action_code === actionCode; + return ( + _actionTypeCode.action_type_code === actionTypeCode && + _actionCode.action_code === actionCode + ); }); } const result = items.map( ({ + action_guid: actionGuid, actor_guid: actor, action_date: date, active_ind: activeIndicator, @@ -299,6 +352,7 @@ export class CaseFileService { }, }) => { return { + actionGuid, actor, date, actionCode, @@ -306,7 +360,7 @@ export class CaseFileService { longDescription, activeIndicator, } as Action; - }, + } ); return result; }; @@ -316,7 +370,7 @@ export class CaseFileService { private getCaseAction = async ( actions: Array, actionTypeCode: string, - actionCode: string = "", + actionCode: string = "" ): Promise => { let item: CaseFileActionItem; @@ -324,7 +378,8 @@ export class CaseFileService { item = actions.find((action) => { const { action_type_action_xref: { - action_type_code_action_type_action_xref_action_type_codeToaction_type_code: _actionTypeCode, + action_type_code_action_type_action_xref_action_type_codeToaction_type_code: + _actionTypeCode, }, } = action; @@ -334,17 +389,23 @@ export class CaseFileService { item = actions.find((action) => { const { action_type_action_xref: { - action_code_action_type_action_xref_action_codeToaction_code: _actionCode, - action_type_code_action_type_action_xref_action_type_codeToaction_type_code: _actionTypeCode, + action_code_action_type_action_xref_action_codeToaction_code: + _actionCode, + action_type_code_action_type_action_xref_action_type_codeToaction_type_code: + _actionTypeCode, }, } = action; - return _actionTypeCode.action_type_code === actionTypeCode && _actionCode.action_code === actionCode; + return ( + _actionTypeCode.action_type_code === actionTypeCode && + _actionCode.action_code === actionCode + ); }); } if (item) { const { + action_guid: actionGuid, actor_guid: actor, action_date: date, active_ind: activeIndicator, @@ -358,6 +419,7 @@ export class CaseFileService { } = item; return { + actionGuid, actor, date, actionCode, @@ -379,25 +441,29 @@ export class CaseFileService { action_not_required_ind: true, inaction_reason_code: true, note_text: true, - inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: { - select: { - short_description: true, - long_description: true, - active_ind: true, + inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: + { + select: { + short_description: true, + long_description: true, + active_ind: true, + }, }, - }, lead: { select: { lead_identifier: true, }, }, action: { - orderBy: [{ - action_type_action_xref: { - display_order: 'asc' - } - }], + orderBy: [ + { + action_type_action_xref: { + display_order: "asc", + }, + }, + ], select: { + action_guid: true, actor_guid: true, action_date: true, active_ind: true, @@ -411,14 +477,15 @@ export class CaseFileService { active_ind: true, }, }, - action_type_code_action_type_action_xref_action_type_codeToaction_type_code: { - select: { - action_type_code: true, - short_description: true, - long_description: true, - active_ind: true, + action_type_code_action_type_action_xref_action_type_codeToaction_type_code: + { + select: { + action_type_code: true, + short_description: true, + long_description: true, + active_ind: true, + }, }, - }, }, }, }, @@ -431,11 +498,18 @@ export class CaseFileService { lead, action_not_required_ind: actionNotRequired, inaction_reason_code: inactionReasonCode, - inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: reason, - review_required_ind: isReviewRequired + inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: + reason, + review_required_ind: isReviewRequired, } = queryResult; - const reviewCompleteAction = await this.getCaseAction(queryResult.action, ACTION_TYPE_CODES.CASEACTION, ACTION_CODES.COMPLTREVW) + const equipmentDetails = await this._findEquipmentDetails(caseFileId); + + const reviewCompleteAction = await this.getCaseAction( + queryResult.action, + ACTION_TYPE_CODES.CASEACTION, + ACTION_CODES.COMPLTREVW + ); const caseFile: CaseFile = { caseIdentifier: caseFileId, @@ -443,20 +517,35 @@ export class CaseFileService { assessmentDetails: { actionNotRequired: actionNotRequired, actionJustificationCode: inactionReasonCode, - actionJustificationShortDescription: !reason ? "" : reason.short_description, - actionJustificationLongDescription: !reason ? "" : reason.long_description, + actionJustificationShortDescription: !reason + ? "" + : reason.short_description, + actionJustificationLongDescription: !reason + ? "" + : reason.long_description, actionJustificationActiveIndicator: !reason ? false : reason.active_ind, - actions: await this.getCaseActions(queryResult.action, ACTION_TYPE_CODES.COMPASSESS), + actions: await this.getCaseActions( + queryResult.action, + ACTION_TYPE_CODES.COMPASSESS + ), }, preventionDetails: { - actions: await this.getCaseActions(queryResult.action, ACTION_TYPE_CODES.COSPRVANDEDU), + actions: await this.getCaseActions( + queryResult.action, + ACTION_TYPE_CODES.COSPRVANDEDU + ), }, isReviewRequired: isReviewRequired, reviewComplete: reviewCompleteAction ?? null, note: { note: queryResult.note_text, - action: await this.getCaseAction(queryResult.action, ACTION_TYPE_CODES.CASEACTION, ACTION_CODES.UPDATENOTE), + action: await this.getCaseAction( + queryResult.action, + ACTION_TYPE_CODES.CASEACTION, + ACTION_CODES.UPDATENOTE + ), }, + equipment: equipmentDetails, }; return caseFile; @@ -479,7 +568,10 @@ export class CaseFileService { return caseFileOutput; } - async updateAssessment(caseIdentifier: string, updateAssessmentInput: UpdateAssessmentInput) { + async updateAssessment( + caseIdentifier: string, + updateAssessmentInput: UpdateAssessmentInput + ) { let actionTypeCode: string = "COMPASSESS"; let caseFileOutput: CaseFile; @@ -488,15 +580,18 @@ export class CaseFileService { await db.case_file.update({ where: { case_file_guid: caseIdentifier }, data: { - inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: updateAssessmentInput - .assessmentDetails.actionJustificationCode - ? { - connect: { - inaction_reason_code: updateAssessmentInput.assessmentDetails.actionJustificationCode, - }, - } - : undefined, - action_not_required_ind: updateAssessmentInput.assessmentDetails.actionNotRequired, + inaction_reason_code_case_file_inaction_reason_codeToinaction_reason_code: + updateAssessmentInput.assessmentDetails.actionJustificationCode + ? { + connect: { + inaction_reason_code: + updateAssessmentInput.assessmentDetails + .actionJustificationCode, + }, + } + : undefined, + action_not_required_ind: + updateAssessmentInput.assessmentDetails.actionNotRequired, update_user_id: updateAssessmentInput.updateUserId, update_utc_timestamp: new Date(), }, @@ -512,21 +607,23 @@ export class CaseFileService { }); for (const action of updateAssessmentInput.assessmentDetails.actions) { - let actionTypeActionXref = await db.action_type_action_xref.findFirstOrThrow({ - where: { - action_type_code: actionTypeCode, - action_code: action.actionCode, - }, - select: { - action_type_action_xref_guid: true, - action_code: true, - action_type_code: true, - }, - }); + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: actionTypeCode, + action_code: action.actionCode, + }, + select: { + action_type_action_xref_guid: true, + action_code: true, + action_type_code: true, + }, + }); let actionXref = await db.action.findFirst({ where: { - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, case_guid: caseIdentifier, }, select: { @@ -538,7 +635,8 @@ export class CaseFileService { await db.action.updateMany({ where: { case_guid: caseIdentifier, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, }, data: { actor_guid: action.actor, @@ -552,7 +650,8 @@ export class CaseFileService { await db.action.create({ data: { case_guid: caseIdentifier, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, actor_guid: action.actor, action_date: action.date, active_ind: action.activeIndicator, @@ -563,7 +662,8 @@ export class CaseFileService { } } - let assessmentCount: number = updateAssessmentInput.assessmentDetails.actions.length; + let assessmentCount: number = + updateAssessmentInput.assessmentDetails.actions.length; if (assessmentCount === 0) { await db.action.updateMany({ where: { case_guid: caseIdentifier }, @@ -574,33 +674,41 @@ export class CaseFileService { caseFileOutput = await this.findOne(caseIdentifier); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } return caseFileOutput; } - async updatePrevention(caseIdentifier: string, updatePreventionInput: UpdatePreventionInput) { + async updatePrevention( + caseIdentifier: string, + updatePreventionInput: UpdatePreventionInput + ) { let actionTypeCode: string = "COSPRV&EDU"; let caseFileOutput: CaseFile; try { await this.prisma.$transaction(async (db) => { for (const action of updatePreventionInput.preventionDetails.actions) { - let actionTypeActionXref = await db.action_type_action_xref.findFirstOrThrow({ - where: { - action_type_code: actionTypeCode, - action_code: action.actionCode, - }, - select: { - action_type_action_xref_guid: true, - action_code: true, - action_type_code: true, - }, - }); + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: actionTypeCode, + action_code: action.actionCode, + }, + select: { + action_type_action_xref_guid: true, + action_code: true, + action_type_code: true, + }, + }); let actionXref = await db.action.findFirst({ where: { - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, case_guid: caseIdentifier, }, select: { @@ -612,7 +720,8 @@ export class CaseFileService { await db.action.updateMany({ where: { case_guid: caseIdentifier, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, }, data: { actor_guid: action.actor, @@ -626,7 +735,8 @@ export class CaseFileService { await db.action.create({ data: { case_guid: caseIdentifier, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, actor_guid: action.actor, action_date: action.date, active_ind: action.activeIndicator, @@ -636,7 +746,8 @@ export class CaseFileService { }); } } - let preventionCount: number = updatePreventionInput.preventionDetails.actions.length; + let preventionCount: number = + updatePreventionInput.preventionDetails.actions.length; if (preventionCount === 0) { await db.action.updateMany({ where: { case_guid: caseIdentifier }, @@ -646,16 +757,17 @@ export class CaseFileService { }); caseFileOutput = await this.findOne(caseIdentifier); } catch (exception) { - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } return caseFileOutput; } - //create new case and new lead if not exists when mutation createReview called - async createReviewCase(reviewInput: ReviewInput): Promise - { - try - { + //create new case and new lead if not exists when mutation createReview called + async createReviewCase(reviewInput: ReviewInput): Promise { + try { let caseFileId: string; await this.prisma.$transaction(async (db) => { //create case @@ -663,72 +775,70 @@ export class CaseFileService { data: { agency_code: { connect: { - agency_code: reviewInput.agencyCode - } + agency_code: reviewInput.agencyCode, + }, }, create_user_id: reviewInput.userId, create_utc_timestamp: new Date(), review_required_ind: true, case_code_case_file_case_codeTocase_code: { connect: { - case_code: reviewInput.caseCode - } + case_code: reviewInput.caseCode, + }, }, - } + }, }); - caseFileId = caseFile.case_file_guid + caseFileId = caseFile.case_file_guid; //create lead await db.lead.create({ data: { lead_identifier: reviewInput.leadIdentifier, case_identifier: caseFile.case_file_guid, create_user_id: reviewInput.userId, - create_utc_timestamp: new Date() - } + create_utc_timestamp: new Date(), + }, }); }); return caseFileId; - } - catch (err) { + } catch (err) { console.error(err); - throw new GraphQLError('Error in createReviewCase', {}); + throw new GraphQLError("Error in createReviewCase", {}); } } //Create review complete action in table action - async createReviewComplete(reviewInput: ReviewInput): Promise - { - try - { + async createReviewComplete(reviewInput: ReviewInput): Promise { + try { let actionId: string; await this.prisma.$transaction(async (db) => { - let actionTypeActionXref = await db.action_type_action_xref.findFirstOrThrow({ - where: { - action_type_code: ACTION_TYPE_CODES.CASEACTION, - action_code: ACTION_CODES.COMPLTREVW - }, - select: { - action_type_action_xref_guid: true - } - }); + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: ACTION_TYPE_CODES.CASEACTION, + action_code: ACTION_CODES.COMPLTREVW, + }, + select: { + action_type_action_xref_guid: true, + }, + }); const reviewAction = await db.action.create({ data: { case_guid: reviewInput.caseIdentifier, - action_type_action_xref_guid: actionTypeActionXref.action_type_action_xref_guid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, actor_guid: reviewInput.reviewComplete.actor, action_date: reviewInput.reviewComplete.date, active_ind: true, //True: review complete, false: review not complete create_user_id: reviewInput.userId, - create_utc_timestamp: new Date - } + create_utc_timestamp: new Date(), + }, }); - actionId = reviewAction.action_guid + actionId = reviewAction.action_guid; }); return actionId; - } - catch (err) { + } catch (err) { console.error(err); - throw new GraphQLError('Error in createReviewComplete', {}); + throw new GraphQLError("Error in createReviewComplete", {}); } } @@ -736,37 +846,40 @@ export class CaseFileService { async createReview(reviewInput: ReviewInput): Promise { try { let result = { - ...reviewInput - } + ...reviewInput, + }; //If case is not exists -> create case - if(!reviewInput.caseIdentifier) { + if (!reviewInput.caseIdentifier) { const caseFileId = await this.createReviewCase(reviewInput); - result.caseIdentifier = caseFileId - result.isReviewRequired = true + result.caseIdentifier = caseFileId; + result.isReviewRequired = true; } //Else update review_required_ind else { const caseFile = await this.prisma.case_file.update({ where: { - case_file_guid: reviewInput.caseIdentifier + case_file_guid: reviewInput.caseIdentifier, }, data: { - review_required_ind: reviewInput.isReviewRequired - } + review_required_ind: reviewInput.isReviewRequired, + }, }); - result.isReviewRequired = caseFile.review_required_ind + result.isReviewRequired = caseFile.review_required_ind; //if isReviewRequired && reviewComplete, create reviewComplete action - if(reviewInput.isReviewRequired && reviewInput.reviewComplete && !reviewInput.reviewComplete.actionId) { + if ( + reviewInput.isReviewRequired && + reviewInput.reviewComplete && + !reviewInput.reviewComplete.actionId + ) { const actionId = await this.createReviewComplete(reviewInput); reviewInput.reviewComplete.actionId = actionId; } } return result; - } - catch (err) { + } catch (err) { console.error(err); - throw new GraphQLError('Error in createReview', {}); + throw new GraphQLError("Error in createReview", {}); } } @@ -776,17 +889,16 @@ export class CaseFileService { //update review_required_ind in table case_file await this.prisma.case_file.update({ where: { - case_file_guid: caseIdentifier + case_file_guid: caseIdentifier, }, data: { - review_required_ind: isReviewRequired - } + review_required_ind: isReviewRequired, + }, }); return reviewInput; - } - catch (err) { + } catch (err) { console.error(err); - throw new GraphQLError('Error in updateReview', {}); + throw new GraphQLError("Error in updateReview", {}); } } @@ -794,7 +906,9 @@ export class CaseFileService { return `This action removes a #${id} caseFile`; } - createNote = async (input: CreateSupplementalNoteInput): Promise => { + createNote = async ( + input: CreateSupplementalNoteInput + ): Promise => { let caseFileId = ""; const { leadIdentifier, note, createUserId, actor } = input; @@ -810,13 +924,20 @@ export class CaseFileService { return await this._upsertNote(caseFileId, note, actor, createUserId); }; - updateNote = async (input: UpdateSupplementalNoteInput): Promise => { + updateNote = async ( + input: UpdateSupplementalNoteInput + ): Promise => { const { caseIdentifier: caseFileId, actor, note, updateUserId } = input; return await this._upsertNote(caseFileId, note, actor, updateUserId); }; - private _upsertNote = async (caseId: string, note: string, actor: string, userId: string): Promise => { + private _upsertNote = async ( + caseId: string, + note: string, + actor: string, + userId: string + ): Promise => { const _hasAction = async (caseId: string): Promise => { const xrefId = await _getNoteActionXref(); @@ -906,8 +1027,435 @@ export class CaseFileService { return await this.findOne(caseId); } catch (error) { console.log("exception: unable to create supplemental note", error); - throw new GraphQLError("Exception occurred. See server log for details", {}); + throw new GraphQLError( + "Exception occurred. See server log for details", + {} + ); } }; + + // find all equipment records, and their respective actions, for a given case + // Since we want to list the equipment related to a case, rather than the actions for a case, which may contain equipment, let's + // transform the actions with equipment to equipment with actions. + async _findEquipmentDetails(caseIdentifier: string): Promise { + const actions = await this.prisma.action.findMany({ + where: { case_guid: caseIdentifier }, + include: { + equipment: true, + }, + }); + + // Initialize a map to hold equipment details keyed by equipment_guid + const equipmentDetailsMap = new Map(); + + // get all the action codes, we're looking for the action codes matching the action performed on the equipment + const actionCodes = await this.prisma.action_type_action_xref.findMany({ + where: { + action_type_action_xref_guid: { + in: actions.map((item) => item.action_type_action_xref_guid), + }, + }, + select: { + action_code: true, + action_type_action_xref_guid: true, + action_code_action_type_action_xref_action_codeToaction_code: { + select: { + short_description: true, + long_description: true, + active_ind: true, + }, + }, + }, + }); + + // construct the equipmentDetails list + for (const action of actions) { + const equipment = action.equipment; + + // get the action xref for the action + let actionData = actionCodes.find( + (element) => + element.action_type_action_xref_guid === + action.action_type_action_xref_guid + ); + + if (equipment && equipment.active_ind) { + // prisma doesn't support the geometry type, for now, it's just treated as a string + // Parse the geometry string into a GeoJSON object + + // Correctly setting the search path using Prisma + await this.prisma + .$executeRaw`SET search_path TO public, case_management`; + + // get the latitude and longitude using a raw query + const result = await this.prisma.$queryRaw< + { longitude: number; latitude: number }[] + >` + SELECT + public.st_x(equipment_geometry_point::geometry) AS longitude, + public.st_y(equipment_geometry_point::geometry) AS latitude + FROM + ${Prisma.raw("case_management.equipment")} + WHERE + equipment_guid = ${Prisma.raw( + `'${equipment.equipment_guid}'::uuid` + )} + `; + + const { longitude, latitude } = result[0]; + + const longitudeString = longitude?.toString() ?? null; + const latitudeString = latitude?.toString() ?? null; + + let equipmentDetail = + equipmentDetailsMap.get(equipment.equipment_guid) || + ({ + equipmentGuid: equipment.equipment_guid, + equipmentTypeCode: equipment.equipment_code, + equipmentTypeActiveIndicator: equipment.active_ind, + address: equipment.equipment_location_desc, + xCoordinate: longitudeString, + yCoordinate: latitudeString, + actions: [], + } as Equipment); + + // Append the action to this equipment's list of actions + equipmentDetail.actions.push({ + actionGuid: action.action_guid, + actor: action.actor_guid, + date: action.action_date, + activeIndicator: action.active_ind, + actionCode: actionData.action_code, + }); + + equipmentDetailsMap.set(equipment.equipment_guid, equipmentDetail); + } + } + const equipmentDetails = Array.from( + equipmentDetailsMap.values() + ) as Equipment[]; + + return equipmentDetails; + } + + async deleteEquipment( + deleteEquipmentInput: DeleteEquipmentInput + ): Promise { + try { + // Find the equipment record by its ID + const equipment = await this.prisma.equipment.findUnique({ + where: { + equipment_guid: deleteEquipmentInput.equipmentGuid, + }, + }); + + if (!equipment) { + throw new Error( + `Equipment with ID ${deleteEquipmentInput.equipmentGuid} not found.` + ); + } + + // Update the active_ind field to false + await this.prisma.equipment.update({ + where: { + equipment_guid: deleteEquipmentInput.equipmentGuid, + }, + data: { + active_ind: false, + update_user_id: deleteEquipmentInput.updateUserId, + update_utc_timestamp: new Date(), + }, + }); + + this.logger.debug( + `Equipment with ID ${deleteEquipmentInput.equipmentGuid} has been updated successfully.` + ); + return true; + } catch (error) { + this.logger.error("Error deleting equipment:", error); + return false; + } finally { + await this.prisma.$disconnect(); + } + } + + async updateEquipment( + updateEquipmentInput: UpdateEquipmentInput + ): Promise { + let caseFileOutput: CaseFile; + + let caseFile = await this.findOneByLeadId( + updateEquipmentInput.leadIdentifier + ); + + try { + await this.prisma.$transaction(async (db) => { + // Find the existing equipment record + this.logger.debug( + `Updating equipment ${JSON.stringify(updateEquipmentInput)}` + ); + + // we're updating a single equipment record, so only one equipment was provided. + const equipmentRecord = updateEquipmentInput.equipment[0]; + const equipmentGuid = equipmentRecord.equipmentGuid; + const existingEquipment = await db.equipment.findUnique({ + where: { equipment_guid: equipmentGuid }, + }); + + if (!existingEquipment) { + this.logger.debug(`Equipment ${equipmentGuid} not found`); + throw new Error("Equipment not found"); + } + + // Update the equipment record + await db.equipment.update({ + where: { equipment_guid: equipmentGuid }, + data: { + equipment_code: equipmentRecord.equipmentTypeCode, + equipment_location_desc: equipmentRecord.address, + active_ind: equipmentRecord.actionEquipmentTypeActiveIndicator, + }, + }); + + this.logger.debug(`Found equipment to update`); + + // constructing a geometry type to update the equipment record with + // prisma doesn't handle geometry types, so we have to create this as a string and insert it + const xCoordinate = updateEquipmentInput.equipment[0].xCoordinate; + const yCoordinate = updateEquipmentInput.equipment[0].yCoordinate; + const pointWKT = xCoordinate && yCoordinate ? `POINT(${xCoordinate} ${yCoordinate})` : null; + + // update the equipment record to set the coordinates + // using raw query because prisma can't handle the awesomeness + await this.prisma + .$executeRaw`SET search_path TO public, case_management`; + const geometryUpdateQuery = ` + UPDATE case_management.equipment + SET equipment_geometry_point = public.ST_GeomFromText($1, 4326) + WHERE equipment_guid = $2::uuid; + `; + + // Execute the update with safe parameter binding + try { + await db.$executeRawUnsafe( + geometryUpdateQuery, + pointWKT, // WKT string for the POINT + equipmentGuid // UUID of the equipment + ); + this.logger.debug( + `Updated geometry for equipment GUID: ${equipmentGuid}` + ); + } catch (error) { + this.logger.error( + "An error occurred during the geometry update:", + error + ); + throw new Error( + "Failed to update equipment geometry due to a database error." + ); + } + + // Check for updated or added actions + const actions = equipmentRecord.actions; + for (const action of actions) { + if (action.actionGuid) { + this.logger.debug( + `Updating equipment action: ${JSON.stringify(action)}` + ); + // If actionGuid exists, it means the action already exists and needs to be updated + await db.action.update({ + where: { action_guid: action.actionGuid }, + data: { + action_date: action.date, + actor_guid: action.actor, + update_utc_timestamp: new Date(), + }, + }); + } else { + // we're adding a new action, so find the action type action xref needed for this + this.logger.debug( + `Creating new equipment action: ${JSON.stringify(action)}` + ); + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: ACTION_TYPE_CODES.EQUIPMENT, + action_code: action.actionCode, + }, + select: { + action_type_action_xref_guid: true, + }, + }); + + this.logger.debug(`Found action xref`); + const caseFileGuid = caseFile.caseIdentifier; + // create the action records (this may either be setting an equipment or removing an equipment) + const data = { + case_guid: caseFileGuid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, + actor_guid: action.actor, + action_date: action.date, + active_ind: action.activeIndicator, + create_user_id: updateEquipmentInput.updateUserId, + create_utc_timestamp: new Date(), + equipment_guid: equipmentGuid, + }; + + this.logger.debug( + `Adding new equipment action as part of an update: ${JSON.stringify( + data + )}` + ); + + await db.action.create({ + data: data, + }); + } + } + }); + + const caseFileGuid = caseFile.caseIdentifier; + caseFileOutput = await this.findOne(caseFileGuid); + } catch (error) { + this.logger.error("An error occurred during equipment update:", error); + throw new GraphQLError( + "An error occurred during equipment update. See server log for details", + error + ); + } + return caseFileOutput; + } + + // create an equipment record - with actions to either set the equipment, or set and remove the equipment + async createEquipment( + createEquipmentInput: CreateEquipmentInput + ): Promise { + let caseFileOutput: CaseFile; + let caseFileGuid; + try { + await this.prisma.$transaction(async (db) => { + let caseFile = await this.findOneByLeadId( + createEquipmentInput.leadIdentifier + ); + + if (caseFile?.caseIdentifier) { + caseFileGuid = caseFile.caseIdentifier; + } else { + caseFileGuid = await this.createCase(createEquipmentInput); + } + + const createdDate = new Date(); + + const newEquipmentJSON = { + active_ind: true, + create_user_id: createEquipmentInput.createUserId, + create_utc_timestamp: createdDate, + update_user_id: createEquipmentInput.createUserId, + update_utc_timestamp: createdDate, + equipment_code: createEquipmentInput.equipment[0].equipmentTypeCode, + equipment_location_desc: createEquipmentInput.equipment[0].address, + // exclude equipment_geometry_point because prisma can't handle this =gracefully + }; + + this.logger.debug( + `Creating equipment: ${JSON.stringify(newEquipmentJSON)}` + ); + + // create the equipment record + const newEquipment = await db.equipment.create({ + data: newEquipmentJSON, + }); + + // constructing a geometry type to update the equipment record with + // prisma doesn't handle geometry types, so we have to create this as a string and insert it + const xCoordinate = createEquipmentInput.equipment[0].xCoordinate; + const yCoordinate = createEquipmentInput.equipment[0].yCoordinate; + + if (xCoordinate && yCoordinate) { + const pointWKT = `POINT(${xCoordinate} ${yCoordinate})`; + + // update the equipment record to set the coordinates + // using raw query because prisma can't handle the awesomeness + await this.prisma + .$executeRaw`SET search_path TO public, case_management`; + const geometryUpdateQuery = ` + UPDATE case_management.equipment + SET equipment_geometry_point = public.ST_GeomFromText($1, 4326) + WHERE equipment_guid = $2::uuid; + `; + + // Execute the update with safe parameter binding + try { + await db.$executeRawUnsafe( + geometryUpdateQuery, + pointWKT, // WKT string for the POINT + newEquipment.equipment_guid // UUID of the equipment + ); + this.logger.debug( + `Updated geometry for equipment GUID: ${newEquipment.equipment_guid}` + ); + } catch (error) { + this.logger.error( + "An error occurred during the geometry update:", + error + ); + throw new Error( + "Failed to update equipment geometry due to a database error." + ); + } + } + + this.logger.debug(`New Equipment: ${JSON.stringify(newEquipment)}`); + + // we can only create one equipment at a time, so just grab the first one. + const equipmentDetailsInstance = createEquipmentInput.equipment[0]; + const actions = equipmentDetailsInstance.actions; + + // get the actions associated with the creation of the equipment. We may be setting an equipment, or setting and removing an equipment + for (const action of actions) { + let actionTypeActionXref = + await db.action_type_action_xref.findFirstOrThrow({ + where: { + action_type_code: ACTION_TYPE_CODES.EQUIPMENT, + action_code: action.actionCode, + }, + select: { + action_type_action_xref_guid: true, + }, + }); + + // create the action records (this may either be setting an equipment or removing an equipment) + const data = { + case_guid: caseFileGuid, + action_type_action_xref_guid: + actionTypeActionXref.action_type_action_xref_guid, + actor_guid: action.actor, + action_date: action.date, + active_ind: action.activeIndicator, + create_user_id: createEquipmentInput.createUserId, + create_utc_timestamp: new Date(), + equipment_guid: newEquipment.equipment_guid, + }; + + this.logger.debug( + `Creating new action record for equipment: ${JSON.stringify(data)}` + ); + await db.action.create({ + data: data, + }); + } + }); + caseFileOutput = await this.findOne(caseFileGuid); + } catch (exception) { + this.logger.error( + "An error occurred during equipment creation:", + exception + ); + throw new GraphQLError( + "An error occurred during equipment creation. See server log for details" + ); + } + return caseFileOutput; + } } - \ No newline at end of file diff --git a/backend/src/case_file/dto/case-file-action-item.ts b/backend/src/case_file/dto/case-file-action-item.ts index 9259a9fc..21643509 100644 --- a/backend/src/case_file/dto/case-file-action-item.ts +++ b/backend/src/case_file/dto/case-file-action-item.ts @@ -1,4 +1,5 @@ export interface CaseFileActionItem { + action_guid: string; actor_guid: string; action_date: Date; active_ind: boolean, diff --git a/backend/src/case_file/dto/create-case_file.input.ts b/backend/src/case_file/dto/create-case_file.input.ts index eb7b1821..a061e2bd 100644 --- a/backend/src/case_file/dto/create-case_file.input.ts +++ b/backend/src/case_file/dto/create-case_file.input.ts @@ -1,4 +1,5 @@ import { AssessmentDetailsInput } from "./assessment-details.input"; +import { CreateEquipmentDetailsInput } from "./equipment/create-equipment-details.input"; import { PreventionDetailsInput } from "./prevention-details.input"; export class CreateCaseInput { @@ -7,7 +8,6 @@ export class CreateCaseInput { caseCode: string; createUserId: string; } - export class CreateAssessmentInput { leadIdentifier: string; assessmentDetails: AssessmentDetailsInput; @@ -22,4 +22,12 @@ export class CreatePreventionInput { agencyCode: string; caseCode: string; createUserId: string; +} + +export class CreateEquipmentInput { + leadIdentifier: string; + equipment: [CreateEquipmentDetailsInput]; + agencyCode: string; + caseCode: string; + createUserId: string; } \ No newline at end of file diff --git a/backend/src/case_file/dto/equipment-action.ts b/backend/src/case_file/dto/equipment-action.ts new file mode 100644 index 00000000..77f246b3 --- /dev/null +++ b/backend/src/case_file/dto/equipment-action.ts @@ -0,0 +1,10 @@ +import { UUID } from "crypto"; + +export interface EquipmentActionItem { + actionGuid: UUID; + actor: string; + date: Date; + actionCode: string; + activeIndicator: boolean; + } + \ No newline at end of file diff --git a/backend/src/case_file/dto/equipment/create-equipment-details.input.ts b/backend/src/case_file/dto/equipment/create-equipment-details.input.ts new file mode 100644 index 00000000..fd6c8d14 --- /dev/null +++ b/backend/src/case_file/dto/equipment/create-equipment-details.input.ts @@ -0,0 +1,10 @@ +import { EquipmentActionItem } from "../equipment-action"; + +export interface CreateEquipmentDetailsInput { + equipmentTypeCode?: string; + address?: string; + xCoordinate: string; + yCoordinate: string; + equipmentTypeActiveIndicator: boolean; + actions: EquipmentActionItem[]; + } \ No newline at end of file diff --git a/backend/src/case_file/dto/equipment/delete-equipment.input.ts b/backend/src/case_file/dto/equipment/delete-equipment.input.ts new file mode 100644 index 00000000..53859461 --- /dev/null +++ b/backend/src/case_file/dto/equipment/delete-equipment.input.ts @@ -0,0 +1,6 @@ +import { EquipmentActionItem } from "../equipment-action"; + +export interface DeleteEquipmentInput { + equipmentGuid: string; + updateUserId: string; + } \ No newline at end of file diff --git a/backend/src/case_file/dto/equipment/update-equipment-details.input.ts b/backend/src/case_file/dto/equipment/update-equipment-details.input.ts new file mode 100644 index 00000000..8b31b5fa --- /dev/null +++ b/backend/src/case_file/dto/equipment/update-equipment-details.input.ts @@ -0,0 +1,11 @@ +import { EquipmentActionItem } from "../equipment-action"; + +export interface UpdateEquipmentDetailsInput { + equipmentGuid?: string; + equipmentTypeCode?: string; + address?: string; + xCoordinate: string; + yCoordinate: string; + actionEquipmentTypeActiveIndicator: boolean; + actions: EquipmentActionItem[]; + } \ No newline at end of file diff --git a/backend/src/case_file/dto/prevention-details.input.ts b/backend/src/case_file/dto/prevention-details.input.ts index 817fc386..ea4de978 100644 --- a/backend/src/case_file/dto/prevention-details.input.ts +++ b/backend/src/case_file/dto/prevention-details.input.ts @@ -1,5 +1,7 @@ import { PreventionActionInput } from "./prevention-action.input" export class PreventionDetailsInput { + actionNotRequired: boolean + actionJustificationCode: string actions: [PreventionActionInput] } diff --git a/backend/src/case_file/dto/update-case_file.input.ts b/backend/src/case_file/dto/update-case_file.input.ts index be41fb57..448a78dc 100644 --- a/backend/src/case_file/dto/update-case_file.input.ts +++ b/backend/src/case_file/dto/update-case_file.input.ts @@ -1,4 +1,5 @@ import { AssessmentDetailsInput } from "./assessment-details.input"; +import { UpdateEquipmentDetailsInput } from "./equipment/update-equipment-details.input"; import { PreventionDetailsInput } from "./prevention-details.input"; export class UpdateAssessmentInput { @@ -17,4 +18,11 @@ export class UpdatePreventionInput { agencyCode: string; caseCode: string; updateUserId: string; +} + +export class UpdateEquipmentInput { + caseIdentifier: string; + leadIdentifier: string; + equipment: [UpdateEquipmentDetailsInput]; + updateUserId: string; } \ No newline at end of file diff --git a/backend/src/case_file/entities/case-action.entity.ts b/backend/src/case_file/entities/case-action.entity.ts index 068b55e9..36423b2e 100644 --- a/backend/src/case_file/entities/case-action.entity.ts +++ b/backend/src/case_file/entities/case-action.entity.ts @@ -1,4 +1,5 @@ export class Action { + actionGuid: string; actor: string; date: Date; actionCode: string; diff --git a/backend/src/case_file/entities/case_file.entity.ts b/backend/src/case_file/entities/case_file.entity.ts index 864483be..252d2283 100644 --- a/backend/src/case_file/entities/case_file.entity.ts +++ b/backend/src/case_file/entities/case_file.entity.ts @@ -1,4 +1,5 @@ import { Assessment } from "./assessment.entity"; +import { Equipment } from "./equipment.entity"; import { Prevention } from "./prevention.entity"; import { ReviewComplete } from "./review_complete"; import { Note } from "./supplemental-note.entity"; @@ -8,6 +9,7 @@ export class CaseFile { leadIdentifier: string; assessmentDetails?: Assessment; preventionDetails?: Prevention; + equipment?: Equipment[]; note?: Note isReviewRequired?: boolean; reviewComplete?: ReviewComplete diff --git a/backend/src/case_file/entities/equipment.entity.ts b/backend/src/case_file/entities/equipment.entity.ts new file mode 100644 index 00000000..5489eec8 --- /dev/null +++ b/backend/src/case_file/entities/equipment.entity.ts @@ -0,0 +1,13 @@ +import { CaseFileActionItem } from "../dto/case-file-action-item"; + +export class Equipment { + equipmentGuid: string; + equipmentTypeCode: string; + equipmentTypeShortDescription?: string; + equipmentTypeLongDescription?: string; + equipmentTypeActiveIndicator: boolean; + address?: string; + xCoordinate?: string; + yCoordinate?: string; + actions: CaseFileActionItem[]; +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index bbcb238e..04d76f83 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ x-postgres-vars: &postgres-vars services: database: - image: postgis/postgis:15-3.3 + image: postgis/postgis:16-3.4 container_name: cm-database environment: <<: *postgres-vars diff --git a/migrations/sql/V1.16.1__CE-357.sql b/migrations/sql/V1.17.1__CE-357.sql similarity index 92% rename from migrations/sql/V1.16.1__CE-357.sql rename to migrations/sql/V1.17.1__CE-357.sql index f2fc3209..e2577222 100644 --- a/migrations/sql/V1.16.1__CE-357.sql +++ b/migrations/sql/V1.17.1__CE-357.sql @@ -1,3 +1,6 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "postgis"; + -- -- CREATE TABLE equipment -- @@ -6,7 +9,7 @@ CREATE TABLE equipment_guid uuid, equipment_code varchar(10), equipment_location_desc varchar(120), - equipment_geometry_point text, + equipment_geometry_point geometry, active_ind bool NOT NULL, create_user_id varchar(32) NOT NULL, create_utc_timestamp timestamp NOT NULL, @@ -20,6 +23,8 @@ ALTER TABLE case_management.equipment ADD CONSTRAINT FK_equipment__equipment_cod ALTER TABLE action ADD COLUMN equipment_guid uuid; +ALTER TABLE case_management.equipment ALTER COLUMN equipment_guid SET DEFAULT uuid_generate_v4(); + ALTER TABLE case_management.action ADD CONSTRAINT FK_action__equipment_guid FOREIGN KEY (equipment_guid) REFERENCES case_management.equipment(equipment_guid); comment on table case_management.equipment is 'Represents a piece of physical equipment that has been deployed in support of the case. Contains information about where and when the equipment was deployed.';