Skip to content

Commit

Permalink
🎉 feat: able to view officer qualifications (#687)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Apr 27, 2022
1 parent 89134c7 commit 2261940
Show file tree
Hide file tree
Showing 22 changed files with 436 additions and 173 deletions.
5 changes: 5 additions & 0 deletions packages/api/prisma/migrations/20220427153022_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "QualificationValueType" AS ENUM ('QUALIFICATION', 'AWARD');

-- AlterTable
ALTER TABLE "QualificationValue" ADD COLUMN "qualificationType" "QualificationValueType" NOT NULL DEFAULT E'QUALIFICATION';
12 changes: 9 additions & 3 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -706,14 +706,15 @@ model UnitQualification {
}

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

Expand Down Expand Up @@ -1062,6 +1063,11 @@ model CustomFieldValue {
Weapons Weapon[]
}

enum QualificationValueType {
QUALIFICATION
AWARD
}

enum CustomFieldCategory {
CITIZEN
WEAPON
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/controllers/admin/values/Import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
type ShouldDoType,
type StatusValueType,
type ValueLicenseType,
type QualificationValueType,
WhatPages,
ValueType,
Value,
Expand Down Expand Up @@ -269,6 +270,7 @@ export const typeHandlers = {
description: item.description,
imageId: validateImgurURL(item.image),
value: item.value,
qualificationType: item.qualificationType as QualificationValueType,
}),
include: { value: true, departments: { include: { value: true } } },
});
Expand Down
31 changes: 31 additions & 0 deletions packages/api/src/controllers/leo/LeoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getLastOfArray, manyToManyHelper } from "utils/manyToMany";
import { Permissions, UsePermissions } from "middlewares/UsePermissions";
import { validateMaxDepartmentsEachPerUser } from "lib/leo/utils";
import { isFeatureEnabled } from "lib/cad";
import { findUnit } from "lib/leo/findUnit";

@Controller("/leo")
@UseBeforeEach(IsAuth)
Expand Down Expand Up @@ -509,6 +510,36 @@ export class LeoController {

return true;
}

@Get("/qualifications/:unitId")
@Description("Get a unit's awards and qualifications")
@UsePermissions({
fallback: (u) => u.isLeo,
permissions: [Permissions.Leo],
})
async getUnitQualifications(@PathParams("unitId") unitId: string) {
const { type, unit } = await findUnit(unitId);

if (type === "combined") {
throw new BadRequest("combinedNotSupported");
}

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

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

const data = await prisma.unitQualification.findMany({
where: { [types[type]]: unit.id },
include: { qualification: { include: { value: true } } },
});

return data;
}
}

export async function validateMaxDivisionsPerOfficer(
Expand Down
4 changes: 4 additions & 0 deletions packages/client/locales/en/leo.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@
"deleteQualification": "Delete Qualification",
"assignedAt": "Assigned at",
"suspendedOn": "Suspended on: ",
"unitAwards": "Unit Awards",
"noAwards": "This unit has no awards yet.",
"addAward": "Add Award",
"award": "Award",
"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.",
"vehicleImpoundLocation": "The provided vehicle ({plate}) will be impounded at {impoundLocation}. You will be able to view this vehicle on the impound lot page.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EmsFdDeputy, Officer, UnitQualification } from "@snailycad/types";
import { EmsFdDeputy, Officer, QualificationValueType, UnitQualification } from "@snailycad/types";
import { Button } from "components/Button";
import { FormField } from "components/form/FormField";
import { Select } from "components/form/Select";
Expand All @@ -19,9 +19,10 @@ interface Props {
export function AddQualificationsModal({ unit, setUnit }: Props) {
const common = useTranslations("Common");
const t = useTranslations();
const { isOpen, closeModal } = useModal();
const { isOpen, closeModal, getPayload } = useModal();
const { state, execute } = useFetch();
const { qualification } = useValues();
const type = getPayload<QualificationValueType>(ModalIds.ManageUnitQualifications);

function handleClose() {
closeModal(ModalIds.ManageUnitQualifications);
Expand All @@ -45,24 +46,30 @@ export function AddQualificationsModal({ unit, setUnit }: Props) {

return (
<Modal
title={t("Leo.addQualification")}
title={type === QualificationValueType.AWARD ? t("Leo.addAward") : t("Leo.addQualification")}
onClose={() => closeModal(ModalIds.ManageUnitQualifications)}
isOpen={isOpen(ModalIds.ManageUnitQualifications)}
className="min-w-[600px]"
>
<Formik initialValues={INITIAL_VALUES} onSubmit={handleSubmit}>
{({ handleChange, errors, values, isValid }) => (
<Form>
<FormField errorMessage={errors.qualificationId} label={t("Leo.qualification")}>
<FormField
errorMessage={errors.qualificationId}
label={
type === QualificationValueType.AWARD ? t("Leo.award") : t("Leo.qualification")
}
>
<Select
value={values.qualificationId}
name="qualificationId"
onChange={handleChange}
values={qualification.values
.filter((v) => {
return !v.departments.length
? true
: v.departments.some((v) => unit.departmentId === v.id);
? v.qualificationType === type
: v.departments.some((v) => unit.departmentId === v.id) &&
v.qualificationType === type;
})
.map((q) => ({
value: q.id,
Expand All @@ -81,7 +88,9 @@ export function AddQualificationsModal({ unit, setUnit }: Props) {
type="submit"
>
{state === "loading" ? <Loader className="mr-2" /> : null}
{t("Leo.addQualification")}
{type === QualificationValueType.AWARD
? t("Leo.addAward")
: t("Leo.addQualification")}
</Button>
</footer>
</Form>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import type { EmsFdDeputy, Officer, UnitQualification } from "@snailycad/types";
import { EmsFdDeputy, Officer, QualificationValueType, UnitQualification } from "@snailycad/types";
import { Button } from "components/Button";
import { AlertModal } from "components/modal/AlertModal";
import { Table } from "components/shared/Table";
Expand All @@ -17,6 +17,68 @@ interface Props {
}

export function QualificationsTable({ setUnit, unit }: Props) {
const t = useTranslations("Leo");
const { openModal } = useModal();

const awards = unit.qualifications.filter(
(v) => v.qualification.qualificationType === QualificationValueType.AWARD,
);

const qualifications = unit.qualifications.filter(
(v) => v.qualification.qualificationType === QualificationValueType.QUALIFICATION,
);

return (
<div className="mt-10">
<div id="qualifications">
<header className="flex items-center justify-between">
<h2 className="text-xl font-semibold">{t("unitQualifications")}</h2>

<div>
<Button
onClick={() =>
openModal(ModalIds.ManageUnitQualifications, QualificationValueType.QUALIFICATION)
}
>
{t("addQualification")}
</Button>
</div>
</header>

{!qualifications.length ? (
<p className="my-2 text-gray-400">{t("noQualifications")}</p>
) : (
<QualificationAwardsTable setUnit={setUnit} unit={{ ...unit, qualifications }} />
)}
</div>

<div id="awards">
<header className="flex items-center justify-between">
<h2 className="text-xl font-semibold">{t("unitAwards")}</h2>
<div>
<Button
onClick={() =>
openModal(ModalIds.ManageUnitQualifications, QualificationValueType.AWARD)
}
>
{t("addAward")}
</Button>
</div>
</header>

{!awards.length ? (
<p className="my-2 text-gray-400">{t("noAwards")}</p>
) : (
<QualificationAwardsTable setUnit={setUnit} unit={{ ...unit, qualifications: awards }} />
)}
</div>

<AddQualificationsModal setUnit={setUnit} unit={unit} />
</div>
);
}

function QualificationAwardsTable({ unit, setUnit }: Props) {
const [tempQualification, setTempQualification] = React.useState<UnitQualification | null>(null);

const t = useTranslations("Leo");
Expand Down Expand Up @@ -71,70 +133,55 @@ export function QualificationsTable({ setUnit, unit }: Props) {
}

return (
<div className="mt-10">
<header className="flex items-center justify-between">
<h1 className="text-xl font-semibold">{t("unitQualifications")}</h1>

<div>
<Button onClick={() => openModal(ModalIds.ManageUnitQualifications)}>
{t("addQualification")}
</Button>
</div>
</header>

{unit.qualifications.length <= 0 ? (
<p className="my-2 text-gray-400">{t("noQualifications")}</p>
) : (
<Table
data={unit.qualifications.map((qa) => {
return {
image: <QualificationsHoverCard qualification={qa} />,
name: qa.qualification.value.value,
assignedAt: <FullDate>{qa.createdAt}</FullDate>,
actions: (
<>
{qa.suspendedAt ? (
<Button
onClick={() => handleSuspendOrUnsuspend("unsuspend", qa)}
disabled={state === "loading"}
small
variant="success"
>
{t("unsuspend")}
</Button>
) : (
<Button
disabled={state === "loading"}
onClick={() => handleSuspendOrUnsuspend("suspend", qa)}
small
variant="amber"
>
{t("suspend")}
</Button>
)}
<div>
<Table
data={unit.qualifications.map((qa) => {
return {
image: <QualificationsHoverCard qualification={qa} />,
name: qa.qualification.value.value,
assignedAt: <FullDate>{qa.createdAt}</FullDate>,
actions: (
<>
{qa.suspendedAt ? (
<Button
onClick={() => handleSuspendOrUnsuspend("unsuspend", qa)}
disabled={state === "loading"}
onClick={() => handleDeleteClick(qa)}
className="ml-2"
small
variant="danger"
variant="success"
>
{common("delete")}
{t("unsuspend")}
</Button>
</>
),
};
})}
columns={[
{ Header: common("image"), accessor: "image" },
{ Header: common("name"), accessor: "name" },
{ Header: t("assignedAt"), accessor: "assignedAt" },
{ Header: common("actions"), accessor: "actions" },
]}
/>
)}
) : (
<Button
disabled={state === "loading"}
onClick={() => handleSuspendOrUnsuspend("suspend", qa)}
small
variant="amber"
>
{t("suspend")}
</Button>
)}
<Button
disabled={state === "loading"}
onClick={() => handleDeleteClick(qa)}
className="ml-2"
small
variant="danger"
>
{common("delete")}
</Button>
</>
),
};
})}
columns={[
{ Header: common("image"), accessor: "image" },
{ Header: common("name"), accessor: "name" },
{ Header: t("assignedAt"), accessor: "assignedAt" },
{ Header: common("actions"), accessor: "actions" },
]}
/>

<AddQualificationsModal setUnit={setUnit} unit={unit} />
<AlertModal
title={t("deleteQualification")}
description={t("alert_deleteQualification")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ export function ManageValueModal({ onCreate, onUpdate, clType: dlType, type, val
value: value ? getValueStrFromValue(value) : "",

description: value && isUnitQualification(value) ? value.description : "",
qualificationType:
value && isUnitQualification(value) ? value.qualificationType : "qualification",

shouldDo: value && isStatusValue(value) ? value.shouldDo : "",
color: value && isStatusValue(value) ? value.color ?? "" : "",
type: value && (isStatusValue(value) || isDepartmentValue(value)) ? value.type : "STATUS_CODE",
Expand Down
Loading

0 comments on commit 2261940

Please sign in to comment.