Skip to content

Commit

Permalink
Merge branch 'main' of github.com:SnailyCAD/snaily-cadv4 into main
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 committed May 19, 2022
2 parents 6d4af1b + f7d7b21 commit 4c1427b
Show file tree
Hide file tree
Showing 21 changed files with 602 additions and 136 deletions.
22 changes: 22 additions & 0 deletions packages/api/prisma/migrations/20220519150926_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- AlterTable
ALTER TABLE "Officer" ADD COLUMN "activeDivisionCallsignId" TEXT;

-- CreateTable
CREATE TABLE "IndividualDivisionCallsign" (
"id" TEXT NOT NULL,
"divisionId" TEXT NOT NULL,
"callsign" TEXT NOT NULL,
"callsign2" TEXT NOT NULL,
"officerId" TEXT NOT NULL,

CONSTRAINT "IndividualDivisionCallsign_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Officer" ADD CONSTRAINT "Officer_activeDivisionCallsignId_fkey" FOREIGN KEY ("activeDivisionCallsignId") REFERENCES "IndividualDivisionCallsign"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "IndividualDivisionCallsign" ADD CONSTRAINT "IndividualDivisionCallsign_divisionId_fkey" FOREIGN KEY ("divisionId") REFERENCES "DivisionValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "IndividualDivisionCallsign" ADD CONSTRAINT "IndividualDivisionCallsign_officerId_fkey" FOREIGN KEY ("officerId") REFERENCES "Officer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
69 changes: 42 additions & 27 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -513,17 +513,18 @@ model SeizedItem {
}

model DivisionValue {
id String @id @default(cuid())
value Value @relation("divisionToValue", fields: [valueId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
value Value @relation("divisionToValue", fields: [valueId], references: [id], onDelete: Cascade)
valueId String
department DepartmentValue? @relation("divisionDepartmentToValue", fields: [departmentId], references: [id], onDelete: Cascade)
department DepartmentValue? @relation("divisionDepartmentToValue", fields: [departmentId], references: [id], onDelete: Cascade)
departmentId String?
callsign String?
officers Officer[] @relation("officerDivisionToDivision")
officerDivisionsToDivision Officer[] @relation("officerDivisionsToDivision")
deputies EmsFdDeputy[] @relation("emsFdDivisionToDivision")
officers Officer[] @relation("officerDivisionToDivision")
officerDivisionsToDivision Officer[] @relation("officerDivisionsToDivision")
deputies EmsFdDeputy[] @relation("emsFdDivisionToDivision")
pairedUnitTemplate String?
Call911 Call911[]
IndividualDivisionCallsign IndividualDivisionCallsign[]
}

model DepartmentValue {
Expand Down Expand Up @@ -703,50 +704,64 @@ model EmployeeValue {

// leo
model Officer {
id String @id @default(cuid())
department DepartmentValue? @relation("officerDepartmentToDepartment", fields: [departmentId], references: [id])
id String @id @default(cuid())
department DepartmentValue? @relation("officerDepartmentToDepartment", fields: [departmentId], references: [id])
departmentId String?
callsign String @db.VarChar(255)
callsign2 String @db.VarChar(255)
callsign String @db.VarChar(255)
callsign2 String @db.VarChar(255)
activeDivisionCallsign IndividualDivisionCallsign? @relation("divisionCallsign", fields: [activeDivisionCallsignId], references: [id])
activeDivisionCallsignId String?
incremental Int?
// `division` is deprecated. Use `divisions` instead.
division DivisionValue? @relation("officerDivisionToDivision", fields: [divisionId], references: [id])
division DivisionValue? @relation("officerDivisionToDivision", fields: [divisionId], references: [id])
divisionId String?
divisions DivisionValue[] @relation("officerDivisionsToDivision")
rank Value? @relation("officerRankToValue", fields: [rankId], references: [id])
divisions DivisionValue[] @relation("officerDivisionsToDivision")
rank Value? @relation("officerRankToValue", fields: [rankId], references: [id])
rankId String?
position String? @db.Text
status StatusValue? @relation("officerStatusToValue", fields: [statusId], references: [id])
position String? @db.Text
status StatusValue? @relation("officerStatusToValue", fields: [statusId], references: [id])
statusId String?
suspended Boolean @default(false)
suspended Boolean @default(false)
badgeNumber Int?
imageId String? @db.VarChar(255)
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
imageId String? @db.VarChar(255)
citizen Citizen @relation(fields: [citizenId], references: [id], onDelete: Cascade)
citizenId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
whitelistStatus LeoWhitelistStatus? @relation(fields: [whitelistStatusId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
whitelistStatus LeoWhitelistStatus? @relation(fields: [whitelistStatusId], references: [id])
whitelistStatusId String?
radioChannelId String?
bolos Bolo[] @relation("bolosToOfficer")
bolos Bolo[] @relation("bolosToOfficer")
warrants Warrant[]
logs OfficerLog[]
Record Record[]
assignedUnit AssignedUnit[]
activeIncident LeoIncident? @relation("activeIncident", fields: [activeIncidentId], references: [id])
activeIncident LeoIncident? @relation("activeIncident", fields: [activeIncidentId], references: [id])
activeIncidentId String?
activeCall Call911? @relation("activeCall", fields: [activeCallId], references: [id])
activeCall Call911? @relation("activeCall", fields: [activeCallId], references: [id])
activeCallId String?
LeoIncident LeoIncident[]
LeoIncidentInvolvedOfficers LeoIncident[] @relation("involvedOfficers")
combinedLeoUnit CombinedLeoUnit? @relation(fields: [combinedLeoUnitId], references: [id])
LeoIncidentInvolvedOfficers LeoIncident[] @relation("involvedOfficers")
combinedLeoUnit CombinedLeoUnit? @relation(fields: [combinedLeoUnitId], references: [id])
combinedLeoUnitId String?
lastStatusChangeTimestamp DateTime?
qualifications UnitQualification[]
IncidentInvolvedUnit IncidentInvolvedUnit[]
Note Note[]
callsigns IndividualDivisionCallsign[]
}

model IndividualDivisionCallsign {
id String @id @default(uuid())
divisionId String
callsign String
callsign2 String
officerId String
officer Officer @relation(fields: [officerId], references: [id], onDelete: Cascade)
division DivisionValue @relation(fields: [divisionId], references: [id])
Officer Officer[] @relation("divisionCallsign")
}

model UnitQualification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Delete, Description, Get, Post, Put } from "@tsed/schema";
import { validateMaxDivisionsPerOfficer } from "controllers/leo/LeoController";
import { leoProperties, unitProperties } from "lib/leo/activeOfficer";
import { findUnit } from "lib/leo/findUnit";
import { updateOfficerDivisionsCallsigns } from "lib/leo/utils";
import { validateDuplicateCallsigns } from "lib/leo/validateDuplicateCallsigns";
import { prisma } from "lib/prisma";
import { validateSchema } from "lib/validateSchema";
Expand Down Expand Up @@ -162,6 +163,14 @@ export class AdminManageUnitsController {
type,
});

if (type === "leo") {
await updateOfficerDivisionsCallsigns({
officerId: unit.id,
disconnectConnectArr: [],
callsigns: data.callsigns,
});
}

// @ts-expect-error ignore
const updated = await prisma[t].update({
where: { id: unit.id },
Expand Down
67 changes: 65 additions & 2 deletions packages/api/src/controllers/leo/LeoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
UseBefore,
} from "@tsed/common";
import { Delete, Description, Get, Post, Put } from "@tsed/schema";
import { CREATE_OFFICER_SCHEMA } from "@snailycad/schemas";
import { CREATE_OFFICER_SCHEMA, SWITCH_CALLSIGN_SCHEMA } from "@snailycad/schemas";
import { BodyParams, Context, PathParams } from "@tsed/platform-params";
import { BadRequest, NotFound } from "@tsed/exceptions";
import { prisma } from "lib/prisma";
Expand All @@ -25,7 +25,11 @@ import { handleWhitelistStatus } from "lib/leo/handleWhitelistStatus";
import type { CombinedLeoUnit } from "@snailycad/types";
import { getLastOfArray, manyToManyHelper } from "utils/manyToMany";
import { Permissions, UsePermissions } from "middlewares/UsePermissions";
import { getInactivityFilter, validateMaxDepartmentsEachPerUser } from "lib/leo/utils";
import {
getInactivityFilter,
updateOfficerDivisionsCallsigns,
validateMaxDepartmentsEachPerUser,
} from "lib/leo/utils";
import { isFeatureEnabled } from "lib/cad";
import { findUnit } from "lib/leo/findUnit";
import { validateDuplicateCallsigns } from "lib/leo/validateDuplicateCallsigns";
Expand Down Expand Up @@ -138,6 +142,12 @@ export class LeoController {

const disconnectConnectArr = manyToManyHelper([], data.divisions as string[]);

await updateOfficerDivisionsCallsigns({
officerId: officer.id,
disconnectConnectArr,
callsigns: data.callsigns,
});

const updated = getLastOfArray(
await prisma.$transaction(
disconnectConnectArr.map((v, idx) =>
Expand Down Expand Up @@ -242,6 +252,12 @@ export class LeoController {
? undefined
: await findNextAvailableIncremental({ type: "leo" });

await updateOfficerDivisionsCallsigns({
officerId: officer.id,
disconnectConnectArr,
callsigns: data.callsigns,
});

const updatedOfficer = await prisma.officer.update({
where: {
id: officer.id,
Expand Down Expand Up @@ -575,6 +591,53 @@ export class LeoController {

return data;
}

@Put("/callsign/:officerId")
@Description("Update the officer's activeDivisionCallsign")
@UsePermissions({
fallback: (u) => u.isLeo || u.rank !== "USER",
permissions: [Permissions.Leo, Permissions.ManageUnitCallsigns],
})
async updateOfficerDivisionCallsign(
@BodyParams() body: unknown,
@PathParams("officerId") officerId: string,
) {
const officer = await prisma.officer.findUnique({
where: { id: officerId },
});

if (!officer) {
throw new NotFound("officerNotFound");
}

const data = validateSchema(SWITCH_CALLSIGN_SCHEMA, body);

let callsignId = null;
/**
* yes, !== "null" can be here, in the UI its handled that way. A bit weird I know, but it does the job!
*/
if (data.callsign && data.callsign !== "null") {
const callsign = await prisma.individualDivisionCallsign.findFirst({
where: { id: data.callsign, officerId: officer.id },
});

if (!callsign) {
throw new NotFound("callsignNotFound");
}

callsignId = callsign.id;
}

const updated = await prisma.officer.update({
where: { id: officer.id },
data: { activeDivisionCallsignId: callsignId },
include: leoProperties,
});

await this.socket.emitUpdateOfficerStatus();

return updated;
}
}

export async function validateMaxDivisionsPerOfficer(
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/lib/leo/activeOfficer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const _leoProperties = {
assignedUnit: { where: { call911: { ended: false } } },
IncidentInvolvedUnit: { where: { incident: { isActive: true } }, select: { id: true } },
rank: true,
callsigns: true,
activeDivisionCallsign: true,
};

export const leoProperties = {
Expand Down
50 changes: 50 additions & 0 deletions packages/api/src/lib/leo/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MiscCadSettings, JailTimeScale } from "@prisma/client";
import type { INDIVIDUAL_CALLSIGN_SCHEMA } from "@snailycad/schemas";
import { prisma } from "lib/prisma";
import { ExtendedBadRequest } from "src/exceptions/ExtendedBadRequest";

Expand Down Expand Up @@ -72,3 +73,52 @@ export function convertToJailTimeScale(total: number, scale: JailTimeScale) {

return total * 1000;
}

export async function updateOfficerDivisionsCallsigns({
officerId,
disconnectConnectArr,
callsigns,
}: {
officerId: string;
disconnectConnectArr: any[];
callsigns?: Record<string, Zod.infer<typeof INDIVIDUAL_CALLSIGN_SCHEMA>> | null;
}) {
if (!callsigns) return;

const _callsigns = Object.values(callsigns);

if (_callsigns.length <= 0) {
await prisma.individualDivisionCallsign.deleteMany({
where: { officerId },
});
}

await Promise.all(
_callsigns.map(async (callsign) => {
const existing = await prisma.individualDivisionCallsign.findFirst({
where: { officerId, divisionId: callsign.divisionId },
});

const doCallsignHaveValues =
callsign.callsign.trim() !== "" && callsign.callsign2.trim() !== "";

const shouldDelete =
!doCallsignHaveValues ||
disconnectConnectArr.find(
(v) => "disconnect" in v && v.disconnect?.id === existing?.divisionId,
);

if (shouldDelete) {
await prisma.individualDivisionCallsign.deleteMany({
where: { id: String(existing?.id) },
});
} else {
await prisma.individualDivisionCallsign.upsert({
where: { id: String(existing?.id) },
create: { ...callsign, officerId },
update: { ...callsign, officerId },
});
}
}),
);
}
1 change: 1 addition & 0 deletions packages/client/locales/en/leo.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
"text": "Text",
"deleteNote": "Delete note",
"dropToUnassignFromIncident": "Drop to unassign from active incident",
"switchDivisionCallsign": "Switch callsign",
"alert_deleteNote": "Are you sure you want to delete this note? This action cannot be undone.",
"alert_deleteQualification": "Are you sure you want to delete this qualification? This action cannot be undone.",
"alert_deleteDLExam": "Are you sure you want to delete this driver's license exam? This action cannot be undone.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Button } from "components/Button";
import { FormField } from "components/form/FormField";
import { Input } from "components/form/inputs/Input";
import { CallSignPreview } from "components/leo/CallsignPreview";
import { AdvancedSettings } from "components/leo/modals/AdvancedSettings";
import { makeDivisionsObjectMap } from "components/leo/modals/ManageOfficerModal";
import { Loader } from "components/Loader";
import { Modal } from "components/modal/Modal";
import { Form, Formik } from "formik";
Expand Down Expand Up @@ -46,6 +48,8 @@ export function ManageUnitCallsignModal({ unit }: Props) {
citizenId: unit.citizenId,
callsign: unit.callsign,
callsign2: unit.callsign2,
callsigns: isUnitOfficer(unit) ? makeDivisionsObjectMap(unit) : {},
divisions: divisions.map((d) => ({ value: d.id, label: d.value.value })),
};

return (
Expand All @@ -68,6 +72,8 @@ export function ManageUnitCallsignModal({ unit }: Props) {

<CallSignPreview department={unit.department} divisions={divisions} />

{isUnitOfficer(unit) ? <AdvancedSettings /> : null}

<footer className="flex justify-end mt-5">
<Button
type="reset"
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/components/leo/CallsignPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function CallSignPreview({ divisions, department }: Props) {
callsign2: values.callsign2,
divisions,
incremental: null,
activeDivisionCallsign: null,
};

return (
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/components/leo/ModalButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as modalButtons from "components/modal-buttons/buttons";
import { ModalButton } from "components/modal-buttons/ModalButton";

const buttons: modalButtons.ModalButton[] = [
modalButtons.switchDivision,
modalButtons.nameSearchBtn,
modalButtons.plateSearchBtn,
modalButtons.weaponSearchBtn,
Expand Down Expand Up @@ -67,6 +68,7 @@ export function ModalButtons() {
title={isButtonDisabled ? "Go on-duty before continuing" : undefined}
key={idx}
button={button}
unit={activeOfficer}
/>
);
})}
Expand Down
Loading

0 comments on commit 4c1427b

Please sign in to comment.