Skip to content

Commit

Permalink
🎉 quick events (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Nov 5, 2021
2 parents 43ba31e + 792b9b4 commit 82a9b62
Show file tree
Hide file tree
Showing 21 changed files with 468 additions and 417 deletions.
5 changes: 5 additions & 0 deletions packages/api/prisma/migrations/20211105062536_/migration.sql
Original file line number Diff line number Diff line change
@@ -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';
12 changes: 9 additions & 3 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -542,6 +543,11 @@ model LeoIncident {
updatedAt DateTime @default(now()) @updatedAt
}

enum StatusValueType {
STATUS_CODE
SITUATION_CODE
}

enum StatusEnum {
ON_DUTY
OFF_DUTY
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/controllers/admin/Values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
74 changes: 39 additions & 35 deletions packages/api/src/controllers/dispatch/Calls911Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -324,42 +324,10 @@ export class Calls911Controller {
return this.officerOrDeputyToUnit(updated);
}

private async findUnit(
id: string,
extraFind?: any,
withType?: false,
): Promise<Officer | EmsFdDeputy>;
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,
assignedUnits: call.assignedUnits.map((v: any) => ({
assignedUnits: (call.assignedUnits ?? [])?.map((v: any) => ({
...v,
officer: undefined,
deputy: undefined,
Expand All @@ -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 } },
Expand Down Expand Up @@ -404,3 +372,39 @@ export class Calls911Controller {
);
}
}

export async function findUnit(
id: string,
extraFind?: any,
withType?: false,
): Promise<Officer | EmsFdDeputy | null>;
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;
}
225 changes: 225 additions & 0 deletions packages/api/src/controllers/dispatch/StatusController.ts
Original file line number Diff line number Diff line change
@@ -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,
},
],
},
],
};
}
Loading

0 comments on commit 82a9b62

Please sign in to comment.