diff --git a/packages/api/prisma/migrations/20220403064022_/migration.sql b/packages/api/prisma/migrations/20220403064022_/migration.sql new file mode 100644 index 000000000..a3671b831 --- /dev/null +++ b/packages/api/prisma/migrations/20220403064022_/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "MiscCadSettings" ADD COLUMN "maxAssignmentsToCalls" INTEGER, +ADD COLUMN "maxAssignmentsToIncidents" INTEGER; diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 66dcea717..2f327e77e 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -59,8 +59,10 @@ model MiscCadSettings { maxPlateLength Int @default(8) maxBusinessesPerCitizen Int? maxDivisionsPerOfficer Int? - // max units a user can create with a department + /// max units a user can create with a department maxDepartmentsEachPerUser Int? + maxAssignmentsToIncidents Int? + maxAssignmentsToCalls Int? callsignTemplate String @default("{department}{callsign1} - {callsign2}{division}") @db.Text pairedUnitTemplate String? @default("1A-{callsign1}") @db.Text pairedUnitSymbol String? @default("A") @db.VarChar(255) diff --git a/packages/api/src/controllers/admin/manage/cad-settings/CadSettings.ts b/packages/api/src/controllers/admin/manage/cad-settings/CadSettings.ts index b01c08405..ededf0e00 100644 --- a/packages/api/src/controllers/admin/manage/cad-settings/CadSettings.ts +++ b/packages/api/src/controllers/admin/manage/cad-settings/CadSettings.ts @@ -123,6 +123,8 @@ export class ManageCitizensController { authScreenHeaderImageId: data.authScreenHeaderImageId, maxOfficersPerUser: data.maxOfficersPerUser, maxDepartmentsEachPerUser: data.maxDepartmentsEachPerUser, + maxAssignmentsToCalls: data.maxAssignmentsToCalls, + maxAssignmentsToIncidents: data.maxAssignmentsToIncidents, }, }); diff --git a/packages/api/src/controllers/dispatch/911-calls/Calls911Controller.ts b/packages/api/src/controllers/dispatch/911-calls/Calls911Controller.ts index 278fd0b4c..07f05a1a1 100644 --- a/packages/api/src/controllers/dispatch/911-calls/Calls911Controller.ts +++ b/packages/api/src/controllers/dispatch/911-calls/Calls911Controller.ts @@ -18,6 +18,7 @@ import { DivisionValue, User, StatusValue, + MiscCadSettings, } from "@prisma/client"; import { sendDiscordWebhook } from "lib/discord/webhooks"; import type { cad, Call911 } from "@snailycad/types"; @@ -91,11 +92,12 @@ export class Calls911Controller { async create911Call( @BodyParams() body: unknown, @Context("user") user: User, - @Context("cad") cad: cad, + @Context("cad") cad: cad & { miscCadSettings: MiscCadSettings }, @HeaderParams("is-from-dispatch") isFromDispatchHeader: string | undefined, ) { const data = validateSchema(CREATE_911_CALL, body); const isFromDispatch = isFromDispatchHeader === "true" && user.isDispatch; + const maxAssignmentsToCalls = cad.miscCadSettings.maxAssignmentsToCalls ?? Infinity; const call = await prisma.call911.create({ data: { @@ -112,7 +114,7 @@ export class Calls911Controller { }); const units = (data.assignedUnits ?? []) as string[]; - await this.assignUnitsToCall(call.id, units); + await this.assignUnitsToCall(call.id, units, maxAssignmentsToCalls); await this.linkOrUnlinkCallDepartmentsAndDivisions({ type: "connect", departments: (data.departments ?? []) as string[], @@ -148,9 +150,11 @@ export class Calls911Controller { async update911Call( @PathParams("id") id: string, @BodyParams() body: unknown, - @Context() ctx: Context, + @Context("user") user: User, + @Context("cad") cad: cad & { miscCadSettings: MiscCadSettings }, ) { const data = validateSchema(CREATE_911_CALL, body); + const maxAssignmentsToCalls = cad.miscCadSettings.maxAssignmentsToCalls ?? Infinity; const call = await prisma.call911.findUnique({ where: { @@ -211,7 +215,7 @@ export class Calls911Controller { postal: String(data.postal), description: data.description, name: data.name, - userId: ctx.get("user").id, + userId: user.id, positionId: shouldRemovePosition ? null : position?.id ?? call.positionId, descriptionData: data.descriptionData, situationCodeId: data.situationCode === null ? null : data.situationCode, @@ -219,7 +223,7 @@ export class Calls911Controller { }); const units = (data.assignedUnits ?? []) as string[]; - await this.assignUnitsToCall(call.id, units); + await this.assignUnitsToCall(call.id, units, maxAssignmentsToCalls); await this.linkOrUnlinkCallDepartmentsAndDivisions({ type: "connect", departments: (data.departments ?? []) as string[], @@ -440,7 +444,11 @@ export class Calls911Controller { ); } - protected async assignUnitsToCall(callId: string, units: string[]) { + protected async assignUnitsToCall( + callId: string, + units: string[], + maxAssignmentsToCalls: number, + ) { await Promise.all( units.map(async (id) => { const { unit, type } = await findUnit( @@ -461,6 +469,18 @@ export class Calls911Controller { "ems-fd": "emsFdDeputyId", }; + const assignmentCount = await prisma.assignedUnit.count({ + where: { + [types[type]]: unit.id, + call911: { ended: false }, + }, + }); + + if (assignmentCount >= maxAssignmentsToCalls) { + // skip this officer + return; + } + const status = await prisma.statusValue.findFirst({ where: { shouldDo: "SET_ASSIGNED" }, }); diff --git a/packages/api/src/controllers/leo/incidents/IncidentController.ts b/packages/api/src/controllers/leo/incidents/IncidentController.ts index 51d502b63..c6fcf1a0b 100644 --- a/packages/api/src/controllers/leo/incidents/IncidentController.ts +++ b/packages/api/src/controllers/leo/incidents/IncidentController.ts @@ -12,6 +12,7 @@ import { validateSchema } from "lib/validateSchema"; import { Socket } from "services/SocketService"; import type { z } from "zod"; import { UsePermissions, Permissions } from "middlewares/UsePermissions"; +import type { MiscCadSettings } from "@snailycad/types"; export const incidentInclude = { creator: { include: leoProperties }, @@ -70,10 +71,12 @@ export class IncidentController { }) async createIncident( @BodyParams() body: unknown, + @Context("cad") cad: { miscCadSettings: MiscCadSettings }, @Context("activeOfficer") officer: Officer | null, ) { const data = validateSchema(LEO_INCIDENT_SCHEMA, body); const officerId = officer?.id ?? null; + const maxAssignmentsToIncidents = cad.miscCadSettings.maxAssignmentsToIncidents ?? Infinity; const incident = await prisma.leoIncident.create({ data: { @@ -88,7 +91,7 @@ export class IncidentController { }, }); - await this.connectOfficersInvolved(incident.id, data); + await this.connectOfficersInvolved(incident.id, data, maxAssignmentsToIncidents); const updated = await prisma.leoIncident.findUnique({ where: { id: incident.id }, @@ -113,8 +116,13 @@ export class IncidentController { permissions: [Permissions.ManageIncidents], fallback: (u) => u.isDispatch || u.isLeo, }) - async updateIncident(@BodyParams() body: unknown, @PathParams("id") incidentId: string) { + async updateIncident( + @BodyParams() body: unknown, + @Context("cad") cad: { miscCadSettings: MiscCadSettings }, + @PathParams("id") incidentId: string, + ) { const data = validateSchema(LEO_INCIDENT_SCHEMA, body); + const maxAssignmentsToIncidents = cad.miscCadSettings.maxAssignmentsToIncidents ?? Infinity; const incident = await prisma.leoIncident.findUnique({ where: { id: incidentId }, @@ -154,7 +162,7 @@ export class IncidentController { }, }); - await this.connectOfficersInvolved(incident.id, data); + await this.connectOfficersInvolved(incident.id, data, maxAssignmentsToIncidents); const updated = await prisma.leoIncident.findUnique({ where: { id: incident.id }, @@ -196,9 +204,21 @@ export class IncidentController { protected async connectOfficersInvolved( incidentId: string, data: Pick, "involvedOfficers" | "isActive">, + maxAssignmentsToIncidents: number, ) { - await prisma.$transaction( - (data.involvedOfficers ?? []).map((id: string) => { + await Promise.all( + (data.involvedOfficers ?? []).map(async (id: string) => { + const count = await prisma.leoIncident.count({ + where: { + officersInvolved: { some: { id } }, + isActive: true, + }, + }); + + if (count >= maxAssignmentsToIncidents) { + return; + } + return prisma.leoIncident.update({ where: { id: incidentId }, data: { diff --git a/packages/client/src/components/admin/manage/cad-settings/MiscFeatures.tsx b/packages/client/src/components/admin/manage/cad-settings/MiscFeatures.tsx index 6ac7a3a89..b8496dc82 100644 --- a/packages/client/src/components/admin/manage/cad-settings/MiscFeatures.tsx +++ b/packages/client/src/components/admin/manage/cad-settings/MiscFeatures.tsx @@ -91,6 +91,8 @@ export function MiscFeatures() { maxPlateLength: miscSettings.maxPlateLength, maxDivisionsPerOfficer: miscSettings.maxDivisionsPerOfficer ?? Infinity, maxDepartmentsEachPerUser: miscSettings.maxDepartmentsEachPerUser ?? Infinity, + maxAssignmentsToIncidents: miscSettings.maxAssignmentsToIncidents ?? Infinity, + maxAssignmentsToCalls: miscSettings.maxAssignmentsToCalls ?? Infinity, maxOfficersPerUser: miscSettings.maxOfficersPerUser ?? Infinity, callsignTemplate: miscSettings.callsignTemplate ?? "", pairedUnitTemplate: miscSettings.pairedUnitTemplate ?? "", @@ -210,6 +212,36 @@ export function MiscFeatures() { /> + + + + + + + +