Skip to content

Commit

Permalink
🎉 feat: unit qualifications (#662)
Browse files Browse the repository at this point in the history
* 🎉 feat: unit qualifications

* 🎉 feat: add types

* fix: add id to type

* 🔨 chore: add more types

* 🎉 feat: able to create cad values

* 🔨 chore: import correct func.

* 🎉 feat: able to view qualifications

* 🎉 feat: able to add qualifications to units

* 🔨 chore: update state

* 🔨 chore: fix tests

* 🎉 feat: able to delete qualifications

* 🎉 feat: able to suspend qualifications

* 🔨 chore: `department` -> `departments`

* 🔨 chore: more improvements

* 🎉 add assignedAt to table

* 🎉 feat: add image support

* 🔨 chore: bug fixes

* 🔨 chore: remove unused code

* 🔨 chore: remove todo comment

* 1 migration
  • Loading branch information
casperiv0 authored Apr 26, 2022
1 parent 612c0df commit 90ec53d
Show file tree
Hide file tree
Showing 33 changed files with 809 additions and 35 deletions.
55 changes: 55 additions & 0 deletions packages/api/prisma/migrations/20220425183208_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- AlterEnum
ALTER TYPE "ValueType" ADD VALUE 'QUALIFICATION';

-- CreateTable
CREATE TABLE "UnitQualification" (
"id" TEXT NOT NULL,
"qualificationId" TEXT NOT NULL,
"suspendedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"officerId" TEXT,
"emsFdDeputyId" TEXT,

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

-- CreateTable
CREATE TABLE "QualificationValue" (
"id" TEXT NOT NULL,
"imageId" TEXT,
"valueId" TEXT NOT NULL,
"departmentId" TEXT,

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

-- CreateTable
CREATE TABLE "_DepartmentValueToQualificationValue" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "_DepartmentValueToQualificationValue_AB_unique" ON "_DepartmentValueToQualificationValue"("A", "B");

-- CreateIndex
CREATE INDEX "_DepartmentValueToQualificationValue_B_index" ON "_DepartmentValueToQualificationValue"("B");

-- AddForeignKey
ALTER TABLE "UnitQualification" ADD CONSTRAINT "UnitQualification_officerId_fkey" FOREIGN KEY ("officerId") REFERENCES "Officer"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "UnitQualification" ADD CONSTRAINT "UnitQualification_qualificationId_fkey" FOREIGN KEY ("qualificationId") REFERENCES "QualificationValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "UnitQualification" ADD CONSTRAINT "UnitQualification_emsFdDeputyId_fkey" FOREIGN KEY ("emsFdDeputyId") REFERENCES "EmsFdDeputy"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "QualificationValue" ADD CONSTRAINT "QualificationValue_valueId_fkey" FOREIGN KEY ("valueId") REFERENCES "Value"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_DepartmentValueToQualificationValue" ADD FOREIGN KEY ("A") REFERENCES "DepartmentValue"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_DepartmentValueToQualificationValue" ADD FOREIGN KEY ("B") REFERENCES "QualificationValue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
31 changes: 31 additions & 0 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ model Value {
citizenFlags Citizen[] @relation("citizenFlags")
DLExam DLExam[] @relation("dlExamToLicense")
DepartmentValue DepartmentValue[] @relation("departmentValueToOfficerRank")
QualificationValue QualificationValue[]
}

model PenalCode {
Expand Down Expand Up @@ -489,6 +490,7 @@ model DepartmentValue {
defaultOfficerRank Value? @relation("departmentValueToOfficerRank", fields: [defaultOfficerRankId], references: [id])
defaultOfficerRankId String?
isConfidential Boolean @default(false)
Qualification QualificationValue[]
}

model DriversLicenseCategoryValue {
Expand Down Expand Up @@ -681,9 +683,36 @@ model Officer {
LeoIncidentInvolvedOfficers LeoIncident[] @relation("involvedOfficers")
combinedLeoUnit CombinedLeoUnit? @relation(fields: [combinedLeoUnitId], references: [id])
combinedLeoUnitId String?
qualifications UnitQualification[]
IncidentInvolvedUnit IncidentInvolvedUnit[]
}

model UnitQualification {
id String @id @default(uuid())
qualification QualificationValue @relation(fields: [qualificationId], references: [id])
qualificationId String
suspendedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
officerId String?
Officer Officer? @relation(fields: [officerId], references: [id], onDelete: Cascade)
emsFdDeputyId String?
emsFdDeputy EmsFdDeputy? @relation(fields: [emsFdDeputyId], references: [id], onDelete: Cascade)
}

model QualificationValue {
id String @id @default(uuid())
imageId String?
/// name of the qualification
valueId String
value Value @relation(fields: [valueId], references: [id], onDelete: Cascade)
departments DepartmentValue[]
departmentId String?
UnitQualification UnitQualification[]
}

model LeoWhitelistStatus {
id String @id @default(uuid())
status WhitelistStatus
Expand Down Expand Up @@ -976,6 +1005,7 @@ model EmsFdDeputy {
updatedAt DateTime @default(now()) @updatedAt
radioChannelId String?
AssignedUnit AssignedUnit[]
qualifications UnitQualification[]
IncidentInvolvedUnit IncidentInvolvedUnit[]
}

Expand Down Expand Up @@ -1096,6 +1126,7 @@ enum ValueType {
IMPOUND_LOT
VEHICLE_FLAG
CITIZEN_FLAG
QUALIFICATION
}

enum DriversLicenseCategoryType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { PathParams, BodyParams, Context } from "@tsed/common";
import { Controller } from "@tsed/di";
import { BadRequest, NotFound } from "@tsed/exceptions";
import { UseBeforeEach } from "@tsed/platform-middlewares";
import { Description, Get, Post, Put } from "@tsed/schema";
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 { prisma } from "lib/prisma";
import { validateSchema } from "lib/validateSchema";
import { IsAuth } from "middlewares/IsAuth";
Expand All @@ -18,6 +19,9 @@ import { manyToManyHelper } from "utils/manyToMany";
const ACTIONS = ["SET_DEPARTMENT_DEFAULT", "SET_DEPARTMENT_NULL", "DELETE_OFFICER"] as const;
type Action = typeof ACTIONS[number];

const SUSPEND_TYPE = ["suspend", "unsuspend"] as const;
type SuspendType = "suspend" | "unsuspend";

export const ACCEPT_DECLINE_TYPES = ["ACCEPT", "DECLINE"] as const;
export type AcceptDeclineType = typeof ACCEPT_DECLINE_TYPES[number];

Expand Down Expand Up @@ -55,15 +59,19 @@ export class AdminManageUnitsController {
permissions: [Permissions.ViewUnits, Permissions.DeleteUnits, Permissions.ManageUnits],
})
async getUnit(@PathParams("id") id: string) {
const extraInclude = {
qualifications: { include: { qualification: { include: { value: true } } } },
};

let unit: any = await prisma.officer.findUnique({
where: { id },
include: { ...leoProperties, logs: true },
include: { ...leoProperties, ...extraInclude, logs: true },
});

if (!unit) {
unit = await prisma.emsFdDeputy.findUnique({
where: { id },
include: unitProperties,
include: { ...unitProperties, ...extraInclude },
});
}

Expand Down Expand Up @@ -288,4 +296,105 @@ export class AdminManageUnitsController {
return null;
}
}

@Post("/:unitId/qualifications")
async addUnitQualification(
@PathParams("unitId") unitId: string,
@BodyParams("qualificationId") qualificationId: string,
) {
const unit = await findUnit(unitId);

if (unit.type === "combined") {
throw new BadRequest("Cannot add qualifications to combined units");
}

if (!unit.unit) {
throw new NotFound("unitNotFound");
}

const types = {
leo: "officerId",
"ems-fd": "emsFdDeputyId",
} as const;

const qualificationValue = await prisma.qualificationValue.findUnique({
where: { id: qualificationId },
});

if (!qualificationValue) {
throw new NotFound("qualificationNotFound");
}

const t = types[unit.type];
const qualification = await prisma.unitQualification.create({
data: {
[t]: unitId,
qualificationId: qualificationValue.id,
},
include: { qualification: { include: { value: true } } },
});

return qualification;
}

@Delete("/:unitId/qualifications/:qualificationId")
async deleteUnitQualification(
@PathParams("unitId") unitId: string,
@PathParams("qualificationId") qualificationId: string,
) {
const unit = await findUnit(unitId);

if (unit.type === "combined") {
throw new BadRequest("Cannot add qualifications to combined units");
}

if (!unit.unit) {
throw new NotFound("unitNotFound");
}

await prisma.unitQualification.delete({
where: { id: qualificationId },
});

return true;
}

@Put("/:unitId/qualifications/:qualificationId")
async suspendOrUnsuspendUnitQualification(
@PathParams("unitId") unitId: string,
@PathParams("qualificationId") qualificationId: string,
@BodyParams("type") suspendType: SuspendType,
) {
if (!SUSPEND_TYPE.includes(suspendType)) {
throw new BadRequest("invalidType");
}

const unit = await findUnit(unitId);

if (unit.type === "combined") {
throw new BadRequest("Cannot add qualifications to combined units");
}

if (!unit.unit) {
throw new NotFound("unitNotFound");
}

const qualification = await prisma.unitQualification.findUnique({
where: { id: qualificationId },
});

if (!qualification) {
throw new NotFound("qualificationNotFound");
}

const updated = await prisma.unitQualification.update({
where: { id: qualification.id },
data: {
suspendedAt: suspendType === "suspend" ? new Date() : null,
},
include: { qualification: { include: { value: true } } },
});

return updated;
}
}
47 changes: 40 additions & 7 deletions packages/api/src/controllers/admin/values/Import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CODES_10_ARR,
DIVISION_ARR,
PENAL_CODE_ARR,
QUALIFICATION_ARR,
} from "@snailycad/schemas";
import {
type DepartmentType,
Expand All @@ -37,6 +38,7 @@ import { upsertWarningApplicable } from "lib/records/penal-code";
import { getLastOfArray, manyToManyHelper } from "utils/manyToMany";
import { getPermissionsForValuesRequest } from "lib/values/utils";
import { UsePermissions } from "middlewares/UsePermissions";
import { validateImgurURL } from "utils/image";

@Controller("/admin/values/import/:path")
@UseBeforeEach(IsAuth, IsValidPath)
Expand Down Expand Up @@ -193,11 +195,6 @@ export const typeHandlers = {
return handlePromiseAll(data, async (item) => {
const whatPages = (item.whatPages?.length ?? 0) <= 0 ? DEFAULT_WHAT_PAGES : item.whatPages;

const value = await prisma.statusValue.findUnique({
where: { id: String(id) },
select: { id: true, departments: true },
});

const updatedValue = await prisma.statusValue.upsert({
where: { id: String(id) },
...makePrismaData(ValueType.CODES_10, {
Expand All @@ -207,11 +204,11 @@ export const typeHandlers = {
whatPages: whatPages as WhatPages[],
value: item.value,
}),
include: { value: true },
include: { value: true, departments: { include: { value: true } } },
});

const disconnectConnectArr = manyToManyHelper(
value?.departments?.map((v) => v.id) ?? [],
updatedValue.departments.map((v) => v.id),
item.departments ?? [],
);

Expand Down Expand Up @@ -262,6 +259,42 @@ export const typeHandlers = {
});
});
},
QUALIFICATION: async (body: unknown, id?: string) => {
const data = validateSchema(QUALIFICATION_ARR, body);

return handlePromiseAll(data, async (item) => {
const updatedValue = await prisma.qualificationValue.upsert({
where: { id: String(id) },
...makePrismaData(ValueType.QUALIFICATION, {
imageId: validateImgurURL(item.image),
value: item.value,
}),
include: { value: true, departments: { include: { value: true } } },
});

const disconnectConnectArr = manyToManyHelper(
updatedValue.departments.map((v) => v.id),
item.departments ?? [],
);

const updated = getLastOfArray(
await prisma.$transaction(
disconnectConnectArr.map((v, idx) =>
prisma.qualificationValue.update({
where: { id: updatedValue.id },
data: { departments: v },
include:
idx + 1 === disconnectConnectArr.length
? { value: true, departments: { include: { value: true } } }
: undefined,
}),
),
),
);

return updated || updatedValue;
});
},

GENDER: async (body: unknown, id?: string) => typeHandlers.GENERIC(body, "GENDER", id),
ETHNICITY: async (body: unknown, id?: string) => typeHandlers.GENERIC(body, "ETHNICITY", id),
Expand Down
Loading

0 comments on commit 90ec53d

Please sign in to comment.