From 647b4c746ff989d65f2f80cc8e406dfee0507efc Mon Sep 17 00:00:00 2001 From: Dev-CasperTheGhost <53900565+Dev-CasperTheGhost@users.noreply.github.com> Date: Thu, 4 Nov 2021 11:49:36 +0100 Subject: [PATCH 1/5] :tada: initial impl. for Quick Call Events --- .../components/modals/Manage911CallModal.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/client/src/components/modals/Manage911CallModal.tsx b/packages/client/src/components/modals/Manage911CallModal.tsx index 5202f382e..c05c6b0c9 100644 --- a/packages/client/src/components/modals/Manage911CallModal.tsx +++ b/packages/client/src/components/modals/Manage911CallModal.tsx @@ -20,6 +20,7 @@ import { SocketEvents } from "@snailycad/config"; import { CallEventsArea } from "./911Call/EventsArea"; import { useGenerateCallsign } from "hooks/useGenerateCallsign"; import { makeUnitName } from "lib/utils"; +import { ContextMenu } from "components/context-menu/ContextMenu"; interface Props { call: Full911Call | null; @@ -273,6 +274,29 @@ export const Manage911CallModal = ({ setCall, call, onClose }: Props) => { {call ? : null} +
+

Assigned Units

+ + {call?.assignedUnits.map((unit) => ( +
+ +

+ {generateCallsign(unit.unit)} + {makeUnitName(unit.unit)} +

+
+
+ ))} +
+ {call ? ( Date: Fri, 5 Nov 2021 07:41:46 +0100 Subject: [PATCH 2/5] :tada: start on quick-events --- .../migrations/20211105062536_/migration.sql | 5 +++++ packages/api/prisma/schema.prisma | 12 +++++++--- packages/api/src/controllers/admin/Values.ts | 2 ++ .../admin/values/ManageValueModal.tsx | 22 +++++++++++++++++++ .../src/components/ems-fd/StatusesArea.tsx | 2 +- .../client/src/components/form/FormField.tsx | 14 +++++++++--- .../src/components/leo/StatusesArea.tsx | 2 +- packages/client/src/types/prisma.ts | 8 +++++++ 8 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 packages/api/prisma/migrations/20211105062536_/migration.sql diff --git a/packages/api/prisma/migrations/20211105062536_/migration.sql b/packages/api/prisma/migrations/20211105062536_/migration.sql new file mode 100644 index 000000000..194b0880a --- /dev/null +++ b/packages/api/prisma/migrations/20211105062536_/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "StatusValueType" AS ENUM ('STATUS_CODE', 'SITUATION_CODE'); + +-- AlterTable +ALTER TABLE "StatusValue" ADD COLUMN "type" "StatusValueType" NOT NULL DEFAULT E'STATUS_CODE'; diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index be750b109..17a81112d 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -494,13 +494,14 @@ model Officer { } model StatusValue { - id String @id @default(uuid()) - value Value @relation("StatusValueToValue", fields: [valueId], references: [id], onDelete: Cascade) + id String @id @default(uuid()) + value Value @relation("StatusValueToValue", fields: [valueId], references: [id], onDelete: Cascade) valueId String - shouldDo ShouldDoType @default(SET_STATUS) + shouldDo ShouldDoType @default(SET_STATUS) position Int? whatPages WhatPages[] color String? + type StatusValueType @default(STATUS_CODE) officerStatusToValue Officer[] @relation("officerStatusToValue") emsFdStatusToValue EmsFdDeputy[] @relation("emsFdStatusToValue") @@ -542,6 +543,11 @@ model LeoIncident { updatedAt DateTime @default(now()) @updatedAt } +enum StatusValueType { + STATUS_CODE + SITUATION_CODE +} + enum StatusEnum { ON_DUTY OFF_DUTY diff --git a/packages/api/src/controllers/admin/Values.ts b/packages/api/src/controllers/admin/Values.ts index 5b10e569e..2b6d9d8c3 100644 --- a/packages/api/src/controllers/admin/Values.ts +++ b/packages/api/src/controllers/admin/Values.ts @@ -144,6 +144,7 @@ export class ValuesController { valueId: value.id, position: Number(body.get("position")), color: body.get("color") || null, + type: body.get("type") || "STATUS_CODE", }, include: { value: true, @@ -302,6 +303,7 @@ export class ValuesController { shouldDo: body.get("shouldDo"), position: Number(body.get("position")), color: body.get("color") || null, + type: body.get("type") || "STATUS_CODE", }, include: { value: true, diff --git a/packages/client/src/components/admin/values/ManageValueModal.tsx b/packages/client/src/components/admin/values/ManageValueModal.tsx index b1d8e5b39..9a70590e5 100644 --- a/packages/client/src/components/admin/values/ManageValueModal.tsx +++ b/packages/client/src/components/admin/values/ManageValueModal.tsx @@ -262,6 +262,28 @@ export const ManageValueModal = ({ onCreate, onUpdate, clType: dlType, type, val {errors.color} + + + setFieldValue("type", "STATUS_CODE")} + checked={values.type === "STATUS_CODE"} + /> + + + + setFieldValue("type", "SITUATION_CODE")} + checked={values.type === "SITUATION_CODE"} + /> + ) : null} diff --git a/packages/client/src/components/ems-fd/StatusesArea.tsx b/packages/client/src/components/ems-fd/StatusesArea.tsx index c7e38efcc..63d08902b 100644 --- a/packages/client/src/components/ems-fd/StatusesArea.tsx +++ b/packages/client/src/components/ems-fd/StatusesArea.tsx @@ -74,7 +74,7 @@ export const StatusesArea = () => { {codes10.values - .filter((v) => v.shouldDo !== ShouldDoType.SET_ON_DUTY) + .filter((v) => v.shouldDo !== ShouldDoType.SET_ON_DUTY && v.type === "STATUS_CODE") .sort((a, b) => Number(a.position) - Number(b.position)) .map((code) => { const isActive = code.id === activeDeputy?.statusId; diff --git a/packages/client/src/components/form/FormField.tsx b/packages/client/src/components/form/FormField.tsx index 86499b389..9bb178b11 100644 --- a/packages/client/src/components/form/FormField.tsx +++ b/packages/client/src/components/form/FormField.tsx @@ -17,11 +17,19 @@ export const FormField = ({ checkbox, children, label, className, fieldId }: Pro className, )} > - + {!checkbox ? ( + + ) : null} {children} + + {checkbox ? ( + + ) : null} ); }; diff --git a/packages/client/src/components/leo/StatusesArea.tsx b/packages/client/src/components/leo/StatusesArea.tsx index d72c060b1..65fa32e1d 100644 --- a/packages/client/src/components/leo/StatusesArea.tsx +++ b/packages/client/src/components/leo/StatusesArea.tsx @@ -74,7 +74,7 @@ export const StatusesArea = () => { {codes10.values - .filter((v) => v.shouldDo !== ShouldDoType.SET_ON_DUTY) + .filter((v) => v.shouldDo !== ShouldDoType.SET_ON_DUTY && v.type === "STATUS_CODE") .sort((a, b) => Number(a.position) - Number(b.position)) .map((code) => { const isActive = code.id === activeOfficer?.statusId; diff --git a/packages/client/src/types/prisma.ts b/packages/client/src/types/prisma.ts index 9a19ae07d..74a223df1 100644 --- a/packages/client/src/types/prisma.ts +++ b/packages/client/src/types/prisma.ts @@ -310,6 +310,7 @@ export type StatusValue = { whatPages: WhatPages[]; departmentId: string; color?: string; + type: StatusValueType; }; /** @@ -647,3 +648,10 @@ export const DepartmentType = { } as const; export type DepartmentType = typeof DepartmentType[keyof typeof DepartmentType]; + +export const StatusValueType = { + STATUS_CODE: "STATUS_CODE", + SITUATION_CODE: "SITUATION_CODE", +} as const; + +export type StatusValueType = typeof StatusValueType[keyof typeof StatusValueType]; From 84eb0c185c2b19eb83f1134e6634ac2aced0edfb Mon Sep 17 00:00:00 2001 From: Dev-CasperTheGhost <53900565+Dev-CasperTheGhost@users.noreply.github.com> Date: Fri, 5 Nov 2021 08:21:34 +0100 Subject: [PATCH 3/5] :tada: progress on quick-events --- .../dispatch/Calls911Controller.ts | 72 +++--- .../controllers/dispatch/StatusController.ts | 225 ++++++++++++++++++ .../src/controllers/ems-fd/EmsFdController.ts | 159 +------------ .../api/src/controllers/leo/LeoController.ts | 174 +------------- .../components/context-menu/ContextMenu.tsx | 2 +- .../components/dispatch/modals/ManageUnit.tsx | 41 ++-- .../src/components/ems-fd/StatusesArea.tsx | 2 +- .../components/ems-fd/modals/SelectDeputy.tsx | 2 +- .../client/src/components/form/Select.tsx | 55 ++++- .../src/components/leo/StatusesArea.tsx | 2 +- .../leo/modals/SelectOfficerModal.tsx | 2 +- .../components/modals/Manage911CallModal.tsx | 24 -- packages/config/src/routes.ts | 5 +- 13 files changed, 344 insertions(+), 421 deletions(-) create mode 100644 packages/api/src/controllers/dispatch/StatusController.ts diff --git a/packages/api/src/controllers/dispatch/Calls911Controller.ts b/packages/api/src/controllers/dispatch/Calls911Controller.ts index 5a4c4c0d1..e823b9330 100644 --- a/packages/api/src/controllers/dispatch/Calls911Controller.ts +++ b/packages/api/src/controllers/dispatch/Calls911Controller.ts @@ -277,7 +277,7 @@ export class Calls911Controller { throw new BadRequest("unitIsRequired"); } - const { unit, type } = await this.findUnit(rawUnit, undefined, true); + const { unit, type } = await findUnit(rawUnit, undefined, true); if (!unit) { throw new NotFound("unitNotFound"); @@ -324,38 +324,6 @@ export class Calls911Controller { return this.officerOrDeputyToUnit(updated); } - private async findUnit( - id: string, - extraFind?: any, - withType?: false, - ): Promise; - private async findUnit( - id: string, - extraFind?: any, - withType?: true, - ): Promise<{ unit: Officer | EmsFdDeputy; type: "leo" | "ems-fd" }>; - private async findUnit(id: string, extraFind?: any, withType?: boolean) { - let type: "leo" | "ems-fd" = "leo"; - let unit = await prisma.officer.findFirst({ - where: { id, ...extraFind }, - }); - - if (!unit) { - type = "ems-fd"; - unit = await prisma.emsFdDeputy.findFirst({ where: { id, ...extraFind } }); - } - - if (!unit) { - return null; - } - - if (withType) { - return { type, unit }; - } - - return unit; - } - private officerOrDeputyToUnit(call: any & { assignedUnits: any[] }) { return { ...call, @@ -371,7 +339,7 @@ export class Calls911Controller { private async assignUnitsToCall(callId: string, units: string[]) { await Promise.all( units.map(async (id) => { - const { unit, type } = await this.findUnit( + const { unit, type } = await findUnit( id, { NOT: { status: { shouldDo: ShouldDoType.SET_OFF_DUTY } }, @@ -404,3 +372,39 @@ export class Calls911Controller { ); } } + +export async function findUnit( + id: string, + extraFind?: any, + withType?: false, +): Promise; +export async function findUnit( + id: string, + extraFind?: any, + withType?: true, +): Promise<{ unit: Officer | EmsFdDeputy | null; type: "leo" | "ems-fd" }>; +export async function findUnit(id: string, extraFind?: any, withType?: boolean) { + let type: "leo" | "ems-fd" = "leo"; + let unit = await prisma.officer.findFirst({ + where: { id, ...extraFind }, + }); + + if (!unit) { + type = "ems-fd"; + unit = await prisma.emsFdDeputy.findFirst({ where: { id, ...extraFind } }); + } + + if (!unit) { + if (withType) { + return { type, unit: null }; + } + + return null; + } + + if (withType) { + return { type, unit }; + } + + return unit; +} diff --git a/packages/api/src/controllers/dispatch/StatusController.ts b/packages/api/src/controllers/dispatch/StatusController.ts new file mode 100644 index 000000000..2403246d5 --- /dev/null +++ b/packages/api/src/controllers/dispatch/StatusController.ts @@ -0,0 +1,225 @@ +import { User, ShouldDoType, MiscCadSettings, cad } from ".prisma/client"; +import { UPDATE_OFFICER_STATUS_SCHEMA, validate } from "@snailycad/schemas"; +import { Req, Res, UseBeforeEach } from "@tsed/common"; +import { Controller } from "@tsed/di"; +import { BadRequest, NotFound } from "@tsed/exceptions"; +import { BodyParams, Context, PathParams } from "@tsed/platform-params"; +import { JsonRequestBody, Put } from "@tsed/schema"; +import { prisma } from "../../lib/prisma"; +import { findUnit } from "./Calls911Controller"; +import { unitProperties } from "../../lib/officer"; +import { setCookie } from "../../utils/setCookie"; +import { Cookie } from "@snailycad/config"; +import { signJWT } from "../../utils/jwt"; +import { getWebhookData, sendDiscordWebhook } from "../../lib/discord"; +import { Socket } from "../../services/SocketService"; +import { APIWebhook } from "discord-api-types"; +import { IsAuth } from "../../middlewares"; + +@Controller("/dispatch/status") +@UseBeforeEach(IsAuth) +export class StatusController { + private socket: Socket; + constructor(socket: Socket) { + this.socket = socket; + } + + @Put("/:unitId") + async updateUnitStatus( + @PathParams("unitId") unitId: string, + @Context("user") user: User, + @BodyParams() body: JsonRequestBody, + @Res() res: Res, + @Req() req: Req, + @Context("cad") cad: cad & { miscCadSettings: MiscCadSettings }, + ) { + const error = validate(UPDATE_OFFICER_STATUS_SCHEMA, body.toJSON(), true); + if (error) { + throw new BadRequest(error); + } + + const statusId = body.get("status"); + + const isFromDispatch = req.headers["is-from-dispatch"]?.toString() === "true"; + const isDispatch = isFromDispatch && user.isDispatch; + + const { type, unit } = await findUnit( + unitId, + { userId: isDispatch ? undefined : user.id }, + true, + ); + + if (!unit) { + throw new NotFound("unitNotFound"); + } + + if (unit.suspended) { + throw new BadRequest("unitSuspended"); + } + + const code = await prisma.statusValue.findFirst({ + where: { + id: statusId, + }, + include: { + value: true, + }, + }); + + if (!code) { + throw new NotFound("statusNotFound"); + } + + // reset all units for user + if (type === "leo") { + await prisma.officer.updateMany({ + where: { + userId: user.id, + }, + data: { + statusId: null, + }, + }); + } else { + await prisma.emsFdDeputy.updateMany({ + where: { + userId: user.id, + }, + data: { + statusId: null, + }, + }); + } + + let updatedUnit; + if (type === "leo") { + updatedUnit = await prisma.officer.update({ + where: { + id: unit.id, + }, + data: { + statusId: code.shouldDo === ShouldDoType.SET_OFF_DUTY ? null : code.id, + }, + include: unitProperties, + }); + } else { + updatedUnit = await prisma.emsFdDeputy.update({ + where: { + id: unit.id, + }, + data: { + statusId: code.shouldDo === ShouldDoType.SET_OFF_DUTY ? null : code.id, + }, + include: unitProperties, + }); + } + + if (type === "leo") { + const officerLog = await prisma.officerLog.findFirst({ + where: { + officerId: unit.id, + endedAt: null, + }, + }); + + if (code.shouldDo === ShouldDoType.SET_ON_DUTY) { + if (!officerLog) { + await prisma.officerLog.create({ + data: { + officerId: unit.id, + userId: user.id, + startedAt: new Date(), + }, + }); + } + } else { + if (code.shouldDo === ShouldDoType.SET_OFF_DUTY) { + // unassign officer from call + await prisma.assignedUnit.deleteMany({ + where: { + officerId: unit.id, + }, + }); + + if (officerLog) { + await prisma.officerLog.update({ + where: { + id: officerLog.id, + }, + data: { + endedAt: new Date(), + }, + }); + } + } + } + } else { + // unassign deputy from call + await prisma.assignedUnit.deleteMany({ + where: { + emsFdDeputyId: unit.id, + }, + }); + } + + if (code.shouldDo === ShouldDoType.SET_OFF_DUTY) { + setCookie({ + res, + name: Cookie.ActiveOfficer, + value: "", + expires: -1, + }); + } else { + const cookieName = type === "leo" ? Cookie.ActiveOfficer : Cookie.ActiveOfficer; + const cookiePayloadName = type === "leo" ? "officerId" : "deputyId"; + + // expires after 3 hours. + setCookie({ + res, + name: cookieName, + value: signJWT({ [cookiePayloadName]: updatedUnit.id }, 60 * 60 * 3), + expires: 60 * 60 * 1000 * 3, + }); + } + + if (cad.discordWebhookURL) { + const webhook = await getWebhookData(cad.discordWebhookURL); + if (!webhook) return; + const data = createWebhookData(webhook, updatedUnit); + + await sendDiscordWebhook(webhook, data); + } + + if (type === "leo") { + this.socket.emitUpdateOfficerStatus(); + } else { + this.socket.emitUpdateDeputyStatus(); + } + + return updatedUnit; + } +} + +function createWebhookData(webhook: APIWebhook, unit: any) { + const status = unit.status.value.value; + const department = unit.department.value.value; + const officerName = `${unit.badgeNumber} - ${unit.name} ${unit.callsign} (${department})`; + + return { + avatar_url: webhook.avatar, + embeds: [ + { + title: "Status Change", + type: "rich", + description: `Unit **${officerName}** has changed their status to ${status}`, + fields: [ + { + name: "Status", + value: status, + inline: true, + }, + ], + }, + ], + }; +} diff --git a/packages/api/src/controllers/ems-fd/EmsFdController.ts b/packages/api/src/controllers/ems-fd/EmsFdController.ts index 83a86add6..64230732e 100644 --- a/packages/api/src/controllers/ems-fd/EmsFdController.ts +++ b/packages/api/src/controllers/ems-fd/EmsFdController.ts @@ -1,30 +1,12 @@ -import { - Res, - Controller, - UseBeforeEach, - Use, - Req, - MultipartFile, - PlatformMulterFile, -} from "@tsed/common"; +import { Controller, UseBeforeEach, Use, MultipartFile, PlatformMulterFile } from "@tsed/common"; import { Delete, Get, JsonRequestBody, Post, Put } from "@tsed/schema"; -import { - CREATE_OFFICER_SCHEMA, - MEDICAL_RECORD_SCHEMA, - UPDATE_OFFICER_STATUS_SCHEMA, - validate, -} from "@snailycad/schemas"; +import { CREATE_OFFICER_SCHEMA, MEDICAL_RECORD_SCHEMA, validate } from "@snailycad/schemas"; import { BodyParams, Context, PathParams } from "@tsed/platform-params"; import { BadRequest, NotFound } from "@tsed/exceptions"; import { prisma } from "../../lib/prisma"; -import { cad, ShouldDoType, MiscCadSettings, User } from ".prisma/client"; -import { setCookie } from "../../utils/setCookie"; -import { AllowedFileExtension, allowedFileExtensions, Cookie } from "@snailycad/config"; +import { ShouldDoType, User } from ".prisma/client"; +import { AllowedFileExtension, allowedFileExtensions } from "@snailycad/config"; import { IsAuth } from "../../middlewares"; -import { signJWT } from "../../utils/jwt"; -import { Socket } from "../../services/SocketService"; -import { getWebhookData, sendDiscordWebhook } from "../../lib/discord"; -import { APIWebhook } from "discord-api-types/payloads/v9/webhook"; import { ActiveDeputy } from "../../middlewares/ActiveDeputy"; import fs from "node:fs"; import { unitProperties } from "../../lib/officer"; @@ -32,11 +14,6 @@ import { unitProperties } from "../../lib/officer"; @Controller("/ems-fd") @UseBeforeEach(IsAuth) export class EmsFdController { - private socket: Socket; - constructor(socket: Socket) { - this.socket = socket; - } - @Get("/") async getUserDeputies(@Context("user") user: User) { const deputies = await prisma.emsFdDeputy.findMany({ @@ -164,110 +141,6 @@ export class EmsFdController { return updated; } - @Put("/:id/status") - async setDeputyStatus( - @PathParams("id") deputyId: string, - @BodyParams() body: JsonRequestBody, - @Context("user") user: User, - @Context("cad") cad: cad & { miscCadSettings: MiscCadSettings }, - @Res() res: Res, - @Req() req: Req, - ) { - const error = validate(UPDATE_OFFICER_STATUS_SCHEMA, body.toJSON(), true); - - if (error) { - throw new BadRequest(error); - } - - const statusId = body.get("status"); - const isFromDispatch = req.headers["is-from-dispatch"]?.toString() === "true"; - const isDispatch = isFromDispatch && user.isDispatch; - - const deputy = await prisma.emsFdDeputy.findFirst({ - where: { - userId: isDispatch ? undefined : user.id, - id: deputyId, - }, - }); - - if (!deputy) { - throw new NotFound("deputyNotFound"); - } - - if (deputy.suspended) { - throw new BadRequest("deputySuspended"); - } - - const code = await prisma.statusValue.findFirst({ - where: { - id: statusId, - }, - include: { - value: true, - }, - }); - - if (!code) { - throw new NotFound("statusNotFound"); - } - - // reset all user - await prisma.emsFdDeputy.updateMany({ - where: { - userId: user.id, - }, - data: { - statusId: null, - }, - }); - - const updatedDeputy = await prisma.emsFdDeputy.update({ - where: { - id: deputy.id, - }, - data: { - statusId: code.shouldDo === ShouldDoType.SET_OFF_DUTY ? null : code.id, - }, - include: unitProperties, - }); - - if (code.shouldDo === ShouldDoType.SET_OFF_DUTY) { - setCookie({ - res, - name: Cookie.ActiveDeputy, - value: "", - expires: -1, - }); - - // unassign deputy from call - await prisma.assignedUnit.deleteMany({ - where: { - emsFdDeputyId: deputy.id, - }, - }); - } else { - // expires after 3 hours. - setCookie({ - res, - name: Cookie.ActiveDeputy, - value: signJWT({ deputyId: updatedDeputy.id }, 60 * 60 * 3), - expires: 60 * 60 * 1000 * 3, - }); - } - - if (cad.discordWebhookURL) { - const webhook = await getWebhookData(cad.discordWebhookURL); - if (!webhook) return; - const data = createWebhookData(webhook, updatedDeputy); - - await sendDiscordWebhook(webhook, data); - } - - this.socket.emitUpdateDeputyStatus(); - - return updatedDeputy; - } - @Delete("/:id") async deleteDeputy(@PathParams("id") id: string, @Context() ctx: Context) { const deputy = await prisma.emsFdDeputy.findFirst({ @@ -410,27 +283,3 @@ export class EmsFdController { return data; } } - -export function createWebhookData(webhook: APIWebhook, officer: any) { - const status = officer.status.value.value; - const department = officer.department.value.value; - const officerName = `${officer.badgeNumber} - ${officer.name} ${officer.callsign} (${department})`; - - return { - avatar_url: webhook.avatar, - embeds: [ - { - title: "Status Change", - type: "rich", - description: `Officer **${officerName}** has changed their status to ${status}`, - fields: [ - { - name: "Status", - value: status, - inline: true, - }, - ], - }, - ], - }; -} diff --git a/packages/api/src/controllers/leo/LeoController.ts b/packages/api/src/controllers/leo/LeoController.ts index cd88c5c7a..8298dc8f5 100644 --- a/packages/api/src/controllers/leo/LeoController.ts +++ b/packages/api/src/controllers/leo/LeoController.ts @@ -1,30 +1,23 @@ import { - Res, Controller, UseBeforeEach, - Req, PlatformMulterFile, MultipartFile, UseBefore, } from "@tsed/common"; import { Delete, Get, JsonRequestBody, Post, Put } from "@tsed/schema"; -import { CREATE_OFFICER_SCHEMA, UPDATE_OFFICER_STATUS_SCHEMA, validate } from "@snailycad/schemas"; +import { CREATE_OFFICER_SCHEMA, validate } from "@snailycad/schemas"; import { BodyParams, Context, PathParams } from "@tsed/platform-params"; import { BadRequest, NotFound } from "@tsed/exceptions"; import { prisma } from "../../lib/prisma"; -import { Officer, cad, ShouldDoType, MiscCadSettings, User } from ".prisma/client"; -import { setCookie } from "../../utils/setCookie"; -import { AllowedFileExtension, allowedFileExtensions, Cookie } from "@snailycad/config"; +import { Officer, ShouldDoType, User } from ".prisma/client"; +import { AllowedFileExtension, allowedFileExtensions } from "@snailycad/config"; import { IsAuth } from "../../middlewares"; -import { signJWT } from "../../utils/jwt"; import { ActiveOfficer } from "../../middlewares/ActiveOfficer"; import { Socket } from "../../services/SocketService"; -import { getWebhookData, sendDiscordWebhook } from "../../lib/discord"; -import { APIWebhook } from "discord-api-types/payloads/v9/webhook"; import fs from "node:fs"; import { unitProperties } from "../../lib/officer"; -// todo: check for leo permissions @Controller("/leo") @UseBeforeEach(IsAuth) export class LeoController { @@ -161,143 +154,6 @@ export class LeoController { return updated; } - @Put("/:id/status") - async setOfficerStatus( - @PathParams("id") officerId: string, - @BodyParams() body: JsonRequestBody, - @Context("user") user: User, - @Context("cad") cad: cad & { miscCadSettings: MiscCadSettings }, - @Res() res: Res, - @Req() req: Req, - ) { - const error = validate(UPDATE_OFFICER_STATUS_SCHEMA, body.toJSON(), true); - - if (error) { - throw new BadRequest(error); - } - - const statusId = body.get("status"); - - const isFromDispatch = req.headers["is-from-dispatch"]?.toString() === "true"; - const isDispatch = isFromDispatch && user.isDispatch; - - const officer = await prisma.officer.findFirst({ - where: { - userId: isDispatch ? undefined : user.id, - id: officerId, - }, - }); - - if (!officer) { - throw new NotFound("officerNotFound"); - } - - if (officer.suspended) { - throw new BadRequest("officerSuspended"); - } - - const code = await prisma.statusValue.findFirst({ - where: { - id: statusId, - }, - include: { - value: true, - }, - }); - - if (!code) { - throw new NotFound("statusNotFound"); - } - - // reset all officers for user - await prisma.officer.updateMany({ - where: { - userId: user.id, - }, - data: { - statusId: null, - }, - }); - - const updatedOfficer = await prisma.officer.update({ - where: { - id: officer.id, - }, - data: { - statusId: code.shouldDo === ShouldDoType.SET_OFF_DUTY ? null : code.id, - }, - include: unitProperties, - }); - - const officerLog = await prisma.officerLog.findFirst({ - where: { - officerId: officer.id, - endedAt: null, - }, - }); - - if (code.shouldDo === ShouldDoType.SET_ON_DUTY) { - if (!officerLog) { - await prisma.officerLog.create({ - data: { - officerId: officer.id, - userId: user.id, - startedAt: new Date(), - }, - }); - } - } else { - if (code.shouldDo === ShouldDoType.SET_OFF_DUTY) { - // unassign officer from call - await prisma.assignedUnit.deleteMany({ - where: { - officerId: officer.id, - }, - }); - - if (officerLog) { - await prisma.officerLog.update({ - where: { - id: officerLog.id, - }, - data: { - endedAt: new Date(), - }, - }); - } - } - } - - if (code.shouldDo === ShouldDoType.SET_OFF_DUTY) { - setCookie({ - res, - name: Cookie.ActiveOfficer, - value: "", - expires: -1, - }); - } else { - // expires after 3 hours. - setCookie({ - res, - name: Cookie.ActiveOfficer, - value: signJWT({ officerId: updatedOfficer.id }, 60 * 60 * 3), - expires: 60 * 60 * 1000 * 3, - }); - } - - if (cad.discordWebhookURL) { - const webhook = await getWebhookData(cad.discordWebhookURL); - if (!webhook) return; - const data = createWebhookData(webhook, updatedOfficer); - - await sendDiscordWebhook(webhook, data); - } - - this.socket.emitUpdateOfficerStatus(); - - return updatedOfficer; - } - @Delete("/:id") async deleteOfficer(@PathParams("id") officerId: string, @Context() ctx: Context) { const officer = await prisma.officer.findFirst({ @@ -475,27 +331,3 @@ export class LeoController { return true; } } - -export function createWebhookData(webhook: APIWebhook, officer: any) { - const status = officer.status.value.value; - const department = officer.department.value.value; - const officerName = `${officer.badgeNumber} - ${officer.name} ${officer.callsign} (${department})`; - - return { - avatar_url: webhook.avatar, - embeds: [ - { - title: "Status Change", - type: "rich", - description: `Officer **${officerName}** has changed their status to ${status}`, - fields: [ - { - name: "Status", - value: status, - inline: true, - }, - ], - }, - ], - }; -} diff --git a/packages/client/src/components/context-menu/ContextMenu.tsx b/packages/client/src/components/context-menu/ContextMenu.tsx index e361a6cf9..29b590071 100644 --- a/packages/client/src/components/context-menu/ContextMenu.tsx +++ b/packages/client/src/components/context-menu/ContextMenu.tsx @@ -46,7 +46,7 @@ export const ContextMenu = ({ items, children }: Props) => { "flex flex-col", "shadow-md", "bg-white dark:bg-dark-bright shadow-sm", - "p-1.5 rounded-md w-36", + "p-1.5 rounded-md min-w-[12rem] max-h-[25rem] overflow-auto", )} > {items.map((item) => { diff --git a/packages/client/src/components/dispatch/modals/ManageUnit.tsx b/packages/client/src/components/dispatch/modals/ManageUnit.tsx index b356db861..ecf2ef04a 100644 --- a/packages/client/src/components/dispatch/modals/ManageUnit.tsx +++ b/packages/client/src/components/dispatch/modals/ManageUnit.tsx @@ -37,33 +37,24 @@ export const ManageUnitModal = ({ type = "leo", unit, onClose }: Props) => { async function onSubmit(values: typeof INITIAL_VALUES) { if (!unit) return; - if (type === "leo") { - const { json } = await execute(`/leo/${unit.id}/status`, { - method: "PUT", - data: { ...values }, - }); + const { json } = await execute(`/dispatch/status/${unit.id}`, { + method: "PUT", + data: { ...values }, + }); - if (json.id) { - setActiveOfficers( - activeOfficers.map((officer) => { - if (officer.id === json.id) { - return { ...officer, ...json }; - } + if (type === "leo" && json.id) { + setActiveOfficers( + activeOfficers.map((officer) => { + if (officer.id === json.id) { + return { ...officer, ...json }; + } - return officer; - }), - ); - handleClose(); - } - } else { - const { json } = await execute(`/ems-fd/${unit.id}/status`, { - method: "PUT", - data: { ...values }, - }); - - if (json.id) { - handleClose(); - } + return officer; + }), + ); + handleClose(); + } else if (json.id) { + handleClose(); } } diff --git a/packages/client/src/components/ems-fd/StatusesArea.tsx b/packages/client/src/components/ems-fd/StatusesArea.tsx index 63d08902b..77c66e651 100644 --- a/packages/client/src/components/ems-fd/StatusesArea.tsx +++ b/packages/client/src/components/ems-fd/StatusesArea.tsx @@ -46,7 +46,7 @@ export const StatusesArea = () => { if (!activeDeputy) return; if (status.id === activeDeputy?.statusId) return; - const { json } = await execute(`/ems-fd/${activeDeputy.id}/status`, { + const { json } = await execute(`/dispatch/status/${activeDeputy.id}`, { method: "PUT", data: { status: status.id, diff --git a/packages/client/src/components/ems-fd/modals/SelectDeputy.tsx b/packages/client/src/components/ems-fd/modals/SelectDeputy.tsx index a6ca854c1..52f1f628e 100644 --- a/packages/client/src/components/ems-fd/modals/SelectDeputy.tsx +++ b/packages/client/src/components/ems-fd/modals/SelectDeputy.tsx @@ -32,7 +32,7 @@ export const SelectDeputyModal = () => { async function onSubmit(values: typeof INITIAL_VALUES) { if (!onDutyCode) return; - const { json } = await execute(`/ems-fd/${values.deputy}/status`, { + const { json } = await execute(`/dispatch/status/${values.deputy}`, { method: "PUT", data: { ...values, diff --git a/packages/client/src/components/form/Select.tsx b/packages/client/src/components/form/Select.tsx index 8347fadb1..41fabcd45 100644 --- a/packages/client/src/components/form/Select.tsx +++ b/packages/client/src/components/form/Select.tsx @@ -1,7 +1,17 @@ import * as React from "react"; import { useTranslations } from "use-intl"; -import ReactSelect, { Props as SelectProps, GroupBase, StylesConfig } from "react-select"; +import ReactSelect, { + Props as SelectProps, + GroupBase, + StylesConfig, + components, + MultiValueGenericProps, +} from "react-select"; import { useAuth } from "context/AuthContext"; +import { ContextMenu } from "components/context-menu/ContextMenu"; +import { useValues } from "context/ValuesContext"; +import useFetch from "lib/useFetch"; +import { StatusValue } from "types/prisma"; export interface SelectValue { label: string; @@ -17,6 +27,41 @@ interface Props extends Exclude { disabled?: boolean; } +const MultiValueContainer = (props: MultiValueGenericProps) => { + const { codes10 } = useValues(); + const { execute } = useFetch(); + + const unitId = props.data.value; + + async function setCode(status: StatusValue) { + const { json } = await execute(`/dispatch/status/${unitId}`, { + method: "PUT", + data: { status: status.id }, + }); + + console.log({ json }); + } + + return ( + ({ + name: v.value.value, + onClick: () => setCode(v), + "aria-label": + v.type === "STATUS_CODE" + ? `Set status to ${v.value.value}` + : `Add code to event: ${v.value.value} `, + title: + v.type === "STATUS_CODE" + ? `Set status to ${v.value.value}` + : `Add code to event: ${v.value.value} `, + }))} + > + + + ); +}; + export const Select = ({ name, onChange, ...rest }: Props) => { const { user } = useAuth(); const common = useTranslations("Common"); @@ -45,6 +90,7 @@ export const Select = ({ name, onChange, ...rest }: Props) => { styles={styles(theme)} className="border-gray-500" menuPortalTarget={(typeof document !== "undefined" && document.body) || undefined} + components={{ MultiValueContainer }} /> ); }; @@ -91,7 +137,8 @@ export const styles = ({ multiValue: (base) => ({ ...base, color: "#000", - borderColor: "rgba(229, 231, 235, 0.5)", + borderColor: backgroundColor === "white" ? "#cccccc" : "#2f2f2f", + backgroundColor: backgroundColor === "white" ? "#cccccc" : "#2f2f2f", }), noOptionsMessage: (base) => ({ ...base, @@ -99,14 +146,14 @@ export const styles = ({ }), multiValueLabel: (base) => ({ ...base, - backgroundColor: "#cccccc", + backgroundColor: backgroundColor === "white" ? "#cccccc" : "#2f2f2f", color, padding: "0.2rem", borderRadius: "2px 0 0 2px", }), multiValueRemove: (base) => ({ ...base, - backgroundColor: "#cccccc", + backgroundColor: backgroundColor === "white" ? "#cccccc" : "#2f2f2f", color, borderRadius: "0 2px 2px 0", cursor: "pointer", diff --git a/packages/client/src/components/leo/StatusesArea.tsx b/packages/client/src/components/leo/StatusesArea.tsx index 65fa32e1d..6e43ba174 100644 --- a/packages/client/src/components/leo/StatusesArea.tsx +++ b/packages/client/src/components/leo/StatusesArea.tsx @@ -46,7 +46,7 @@ export const StatusesArea = () => { if (!activeOfficer) return; if (status.id === activeOfficer.statusId) return; - const { json } = await execute(`/leo/${activeOfficer.id}/status`, { + const { json } = await execute(`/dispatch/status/${activeOfficer.id}`, { method: "PUT", data: { status: status.id, diff --git a/packages/client/src/components/leo/modals/SelectOfficerModal.tsx b/packages/client/src/components/leo/modals/SelectOfficerModal.tsx index f542339f4..34a373fd3 100644 --- a/packages/client/src/components/leo/modals/SelectOfficerModal.tsx +++ b/packages/client/src/components/leo/modals/SelectOfficerModal.tsx @@ -31,7 +31,7 @@ export const SelectOfficerModal = () => { async function onSubmit(values: typeof INITIAL_VALUES) { if (!onDutyCode) return; - const { json } = await execute(`/leo/${values.officer}/status`, { + const { json } = await execute(`/dispatch/status/${values.officer}`, { method: "PUT", data: { ...values, diff --git a/packages/client/src/components/modals/Manage911CallModal.tsx b/packages/client/src/components/modals/Manage911CallModal.tsx index c05c6b0c9..5202f382e 100644 --- a/packages/client/src/components/modals/Manage911CallModal.tsx +++ b/packages/client/src/components/modals/Manage911CallModal.tsx @@ -20,7 +20,6 @@ import { SocketEvents } from "@snailycad/config"; import { CallEventsArea } from "./911Call/EventsArea"; import { useGenerateCallsign } from "hooks/useGenerateCallsign"; import { makeUnitName } from "lib/utils"; -import { ContextMenu } from "components/context-menu/ContextMenu"; interface Props { call: Full911Call | null; @@ -274,29 +273,6 @@ export const Manage911CallModal = ({ setCall, call, onClose }: Props) => { {call ? : null} -
-

Assigned Units

- - {call?.assignedUnits.map((unit) => ( -
- -

- {generateCallsign(unit.unit)} - {makeUnitName(unit.unit)} -

-
-
- ))} -
- {call ? ( export const PERMISSION_ROUTES: PermissionRoute[] = [ [ - ["PUT"], - // /v1/leo/:officerId/status - /\/v1\/(leo|ems-fd)\/[A-Z0-9]+\/status/i, + "*", + /\/v1\/dispatch\/status\/\w+/i, (u) => u.isLeo || u.isSupervisor || u.isDispatch || u.isEmsFd, ], [["GET"], /\/v1\/leo\/active-(officers|officer)/, (u) => u.isLeo || u.isDispatch], From 6648f1f802d16ea84de4083801c32b68b4dc8e49 Mon Sep 17 00:00:00 2001 From: Dev-CasperTheGhost <53900565+Dev-CasperTheGhost@users.noreply.github.com> Date: Fri, 5 Nov 2021 08:30:38 +0100 Subject: [PATCH 4/5] :tada: progress on quick-events --- .../dispatch/Calls911Controller.ts | 2 +- .../client/src/components/form/Select.tsx | 30 +++++++++++++++---- .../client/src/components/leo/ActiveCalls.tsx | 2 +- .../components/modals/911Call/EventsArea.tsx | 8 +++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/api/src/controllers/dispatch/Calls911Controller.ts b/packages/api/src/controllers/dispatch/Calls911Controller.ts index e823b9330..5959cd7da 100644 --- a/packages/api/src/controllers/dispatch/Calls911Controller.ts +++ b/packages/api/src/controllers/dispatch/Calls911Controller.ts @@ -327,7 +327,7 @@ export class Calls911Controller { private officerOrDeputyToUnit(call: any & { assignedUnits: any[] }) { return { ...call, - assignedUnits: call.assignedUnits.map((v: any) => ({ + assignedUnits: (call.assignedUnits ?? [])?.map((v: any) => ({ ...v, officer: undefined, deputy: undefined, diff --git a/packages/client/src/components/form/Select.tsx b/packages/client/src/components/form/Select.tsx index 41fabcd45..e99de7235 100644 --- a/packages/client/src/components/form/Select.tsx +++ b/packages/client/src/components/form/Select.tsx @@ -12,6 +12,11 @@ import { ContextMenu } from "components/context-menu/ContextMenu"; import { useValues } from "context/ValuesContext"; import useFetch from "lib/useFetch"; import { StatusValue } from "types/prisma"; +import { useGenerateCallsign } from "hooks/useGenerateCallsign"; +import { Full911Call, useDispatchState } from "state/dispatchState"; +import { makeUnitName } from "lib/utils"; +import { useModal } from "context/ModalContext"; +import { ModalIds } from "types/ModalIds"; export interface SelectValue { label: string; @@ -30,16 +35,31 @@ interface Props extends Exclude { const MultiValueContainer = (props: MultiValueGenericProps) => { const { codes10 } = useValues(); const { execute } = useFetch(); + const { getPayload } = useModal(); + const generateCallsign = useGenerateCallsign(); + const call = getPayload(ModalIds.Manage911Call); + const { allDeputies, allOfficers } = useDispatchState(); const unitId = props.data.value; + const unit = [...allDeputies, ...allOfficers].find((v) => v.id === unitId); async function setCode(status: StatusValue) { - const { json } = await execute(`/dispatch/status/${unitId}`, { - method: "PUT", - data: { status: status.id }, - }); + if (!unit) return; - console.log({ json }); + if (status.type === "STATUS_CODE") { + await execute(`/dispatch/status/${unitId}`, { + method: "PUT", + data: { status: status.id }, + }); + } else { + if (!call) return; + await execute(`/911-calls/events/${call.id}`, { + method: "POST", + data: { + description: `${generateCallsign(unit)} ${makeUnitName(unit)} / ${status.value.value}`, + }, + }); + } } return ( diff --git a/packages/client/src/components/leo/ActiveCalls.tsx b/packages/client/src/components/leo/ActiveCalls.tsx index f5d54cad3..93db9ac0f 100644 --- a/packages/client/src/components/leo/ActiveCalls.tsx +++ b/packages/client/src/components/leo/ActiveCalls.tsx @@ -85,7 +85,7 @@ export const ActiveCalls = () => { function handleManageClick(call: Full911Call) { setTempCall(call); - openModal(ModalIds.Manage911Call); + openModal(ModalIds.Manage911Call, call); } function handleCallTow(call: Full911Call) { diff --git a/packages/client/src/components/modals/911Call/EventsArea.tsx b/packages/client/src/components/modals/911Call/EventsArea.tsx index f7d97228f..6f2d57ed9 100644 --- a/packages/client/src/components/modals/911Call/EventsArea.tsx +++ b/packages/client/src/components/modals/911Call/EventsArea.tsx @@ -131,20 +131,22 @@ const EventItem = ({ event, setTempEvent }: { event: Call911Event; setTempEvent: return (
  • - {formatted}: + + {formatted}: + {event.description}
    -
    From 792b9b4f5e0534dc965300e6f89b211c45a1960f Mon Sep 17 00:00:00 2001 From: Dev-CasperTheGhost <53900565+Dev-CasperTheGhost@users.noreply.github.com> Date: Fri, 5 Nov 2021 09:39:24 +0100 Subject: [PATCH 5/5] :tada: minor improvements for quick-events --- .../components/context-menu/ContextMenu.tsx | 31 ++++++++++--- .../client/src/components/form/Select.tsx | 45 +++++++++++-------- .../components/modals/911Call/EventsArea.tsx | 2 +- .../components/modals/Manage911CallModal.tsx | 3 +- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/packages/client/src/components/context-menu/ContextMenu.tsx b/packages/client/src/components/context-menu/ContextMenu.tsx index 29b590071..bac957217 100644 --- a/packages/client/src/components/context-menu/ContextMenu.tsx +++ b/packages/client/src/components/context-menu/ContextMenu.tsx @@ -16,7 +16,8 @@ type ButtonProps = React.DetailedHTMLProps< interface ContextItem extends ButtonProps { name: string; - component?: string; + // eslint-disable-next-line @typescript-eslint/ban-types + component?: keyof typeof components | (string & {}); } export const ContextMenu = ({ items, children }: Props) => { @@ -46,17 +47,23 @@ export const ContextMenu = ({ items, children }: Props) => { "flex flex-col", "shadow-md", "bg-white dark:bg-dark-bright shadow-sm", - "p-1.5 rounded-md min-w-[12rem] max-h-[25rem] overflow-auto", + "p-1.5 rounded-md min-w-[15rem] max-h-[25rem] overflow-auto", )} + style={{ scrollbarWidth: "thin" }} > {items.map((item) => { const { component = "Item", ...rest } = typeof item === "object" ? item : {}; + // @ts-expect-error ignore const Component = components[component] ?? components.Item; return typeof item === "boolean" ? ( ) : Component ? ( - + {item.name} ) : null; @@ -90,13 +97,13 @@ export const ContextMenu = ({ items, children }: Props) => { ); }; -const components: Record any> = { +const components = { Item: ({ children, ...rest }: any) => ( any> = { {children} ), + Label: ({ children, ...rest }: any) => ( + + {children} + + ), }; diff --git a/packages/client/src/components/form/Select.tsx b/packages/client/src/components/form/Select.tsx index e99de7235..cc7965933 100644 --- a/packages/client/src/components/form/Select.tsx +++ b/packages/client/src/components/form/Select.tsx @@ -30,6 +30,7 @@ interface Props extends Exclude { hasError?: boolean; isClearable?: boolean; disabled?: boolean; + showContextMenuForUnits?: boolean; } const MultiValueContainer = (props: MultiValueGenericProps) => { @@ -38,10 +39,10 @@ const MultiValueContainer = (props: MultiValueGenericProps) => { const { getPayload } = useModal(); const generateCallsign = useGenerateCallsign(); const call = getPayload(ModalIds.Manage911Call); - const { allDeputies, allOfficers } = useDispatchState(); + const { activeDeputies, activeOfficers } = useDispatchState(); const unitId = props.data.value; - const unit = [...allDeputies, ...allOfficers].find((v) => v.id === unitId); + const unit = [...activeDeputies, ...activeOfficers].find((v) => v.id === unitId); async function setCode(status: StatusValue) { if (!unit) return; @@ -62,21 +63,28 @@ const MultiValueContainer = (props: MultiValueGenericProps) => { } } + const codesMapped: any[] = codes10.values.map((v) => ({ + name: v.value.value, + onClick: () => setCode(v), + "aria-label": + v.type === "STATUS_CODE" + ? `Set status to ${v.value.value}` + : `Add code to event: ${v.value.value} `, + title: + v.type === "STATUS_CODE" + ? `Set status to ${v.value.value}` + : `Add code to event: ${v.value.value} `, + })); + + if (unit) { + codesMapped.unshift({ + name: `${generateCallsign(unit)} ${makeUnitName(unit)}`, + component: "Label", + }); + } + return ( - ({ - name: v.value.value, - onClick: () => setCode(v), - "aria-label": - v.type === "STATUS_CODE" - ? `Set status to ${v.value.value}` - : `Add code to event: ${v.value.value} `, - title: - v.type === "STATUS_CODE" - ? `Set status to ${v.value.value}` - : `Add code to event: ${v.value.value} `, - }))} - > + ); @@ -87,6 +95,7 @@ export const Select = ({ name, onChange, ...rest }: Props) => { const common = useTranslations("Common"); const value = typeof rest.value === "string" ? rest.values.find((v) => v.value === rest.value) : rest.value; + const { canBeClosed } = useModal(); const useDarkTheme = user?.isDarkTheme && @@ -102,7 +111,7 @@ export const Select = ({ name, onChange, ...rest }: Props) => { return ( handleChange(v)} @@ -110,7 +119,7 @@ export const Select = ({ name, onChange, ...rest }: Props) => { styles={styles(theme)} className="border-gray-500" menuPortalTarget={(typeof document !== "undefined" && document.body) || undefined} - components={{ MultiValueContainer }} + components={rest.showContextMenuForUnits ? { MultiValueContainer } : undefined} /> ); }; diff --git a/packages/client/src/components/modals/911Call/EventsArea.tsx b/packages/client/src/components/modals/911Call/EventsArea.tsx index 6f2d57ed9..fc2215df9 100644 --- a/packages/client/src/components/modals/911Call/EventsArea.tsx +++ b/packages/client/src/components/modals/911Call/EventsArea.tsx @@ -47,7 +47,7 @@ export const CallEventsArea = ({ call }: Props) => { } return ( -
    +

    {common("events")}

      diff --git a/packages/client/src/components/modals/Manage911CallModal.tsx b/packages/client/src/components/modals/Manage911CallModal.tsx index 5202f382e..91a6e9a28 100644 --- a/packages/client/src/components/modals/Manage911CallModal.tsx +++ b/packages/client/src/components/modals/Manage911CallModal.tsx @@ -199,7 +199,7 @@ export const Manage911CallModal = ({ setCall, call, onClose }: Props) => { isOpen={isOpen(ModalIds.Manage911Call)} onClose={handleClose} title={call ? "Manage 911 Call" : t("create911Call")} - className={call ? "w-[1000px]" : "w-[650px]"} + className={call ? "w-[1200px]" : "w-[650px]"} >
      @@ -228,6 +228,7 @@ export const Manage911CallModal = ({ setCall, call, onClose }: Props) => { {isDispatch ? (