From 7836fe75ebf9be9b22bb65f7de2a3ffeb19a06c2 Mon Sep 17 00:00:00 2001 From: tugamars Date: Mon, 16 Sep 2024 11:26:06 +0100 Subject: [PATCH] Manage unit rank role (#1956) --- .../manage/units/manage-units-controller.ts | 86 +++++++++++++------ apps/api/src/controllers/leo/LeoController.ts | 2 +- apps/client/locales/en/cad-settings.json | 1 + .../src/components/admin/Sidebar/routes.ts | 1 + .../manage-unit-callsign-modal.tsx | 84 ++++++++++++++---- .../src/pages/admin/manage/units/index.tsx | 7 +- packages/permissions/src/defaults/admin.ts | 1 + packages/permissions/src/permissions.ts | 1 + packages/schemas/src/leo.ts | 2 + 9 files changed, 138 insertions(+), 47 deletions(-) diff --git a/apps/api/src/controllers/admin/manage/units/manage-units-controller.ts b/apps/api/src/controllers/admin/manage/units/manage-units-controller.ts index b3b696587..f952dbcd7 100644 --- a/apps/api/src/controllers/admin/manage/units/manage-units-controller.ts +++ b/apps/api/src/controllers/admin/manage/units/manage-units-controller.ts @@ -50,8 +50,9 @@ import { import { getTranslator } from "~/utils/get-translator"; import { type APIEmbed } from "discord-api-types/v10"; import { sendRawWebhook, sendDiscordWebhook } from "~/lib/discord/webhooks"; -import { type Citizen, type EmsFdDeputy, type LeoWhitelistStatus } from "@snailycad/types"; +import { type Citizen, type EmsFdDeputy, type LeoWhitelistStatus, User } from "@snailycad/types"; import { generateCallsign } from "@snailycad/utils"; +import { hasPermission } from "@snailycad/permissions"; const ACTIONS = ["SET_DEPARTMENT_DEFAULT", "SET_DEPARTMENT_NULL", "DELETE_UNIT"] as const; type Action = (typeof ACTIONS)[number]; @@ -370,11 +371,16 @@ export class AdminManageUnitsController { @Put("/callsign/:unitId") @UsePermissions({ - permissions: [Permissions.ManageUnitCallsigns, Permissions.ManageUnits], + permissions: [ + Permissions.ManageUnitCallsigns, + Permissions.ManageUnits, + Permissions.ManageUnitRank, + ], }) @Description("Update a unit's callsign by its id") async updateCallsignUnit( @Context("sessionUserId") sessionUserId: string, + @Context("user") user: User, @PathParams("unitId") unitId: string, @BodyParams() body: unknown, @Context("cad") cad: cad & { features?: Record }, @@ -396,38 +402,66 @@ export class AdminManageUnitsController { } as const; const t = prismaNames[type]; - if (data.callsign && data.callsign2 && unit.departmentId) { - const allowMultipleOfficersWithSameDeptPerUser = isFeatureEnabled({ - feature: Feature.ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER, - defaultReturn: false, - features: cad.features, - }); + const hasManageUnitCallsignPermission = hasPermission({ + permissionsToCheck: [Permissions.ManageUnitCallsigns], + userToCheck: user, + }); + const hasManageRankPermission = hasPermission({ + permissionsToCheck: [Permissions.ManageUnitRank], + userToCheck: user, + }); - await validateDuplicateCallsigns({ - departmentId: unit.departmentId, - callsign1: data.callsign, - callsign2: data.callsign2, - unitId: unit.id, - type, - userId: allowMultipleOfficersWithSameDeptPerUser && unit.userId ? unit.userId : undefined, - }); + const dataSubmit: { + callsign: string | undefined; + callsign2: string | undefined; + rankId: string | null | undefined; + position: string | null | undefined; + } = { + callsign2: undefined, + callsign: undefined, + rankId: undefined, + position: undefined, + }; + + if (hasManageUnitCallsignPermission) { + if (data.callsign && data.callsign2 && unit.departmentId) { + const allowMultipleOfficersWithSameDeptPerUser = isFeatureEnabled({ + feature: Feature.ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER, + defaultReturn: false, + features: cad.features, + }); + + await validateDuplicateCallsigns({ + departmentId: unit.departmentId, + callsign1: data.callsign, + callsign2: data.callsign2, + unitId: unit.id, + type, + userId: allowMultipleOfficersWithSameDeptPerUser && unit.userId ? unit.userId : undefined, + }); + } + + if (type === "leo") { + await updateOfficerDivisionsCallsigns({ + officerId: unit.id, + disconnectConnectArr: [], + callsigns: data.callsigns, + }); + } + + dataSubmit.callsign2 = data.callsign2; + dataSubmit.callsign = data.callsign; } - if (type === "leo") { - await updateOfficerDivisionsCallsigns({ - officerId: unit.id, - disconnectConnectArr: [], - callsigns: data.callsigns, - }); + if (hasManageRankPermission) { + dataSubmit.rankId = data.rank; + dataSubmit.position = data.position; } // @ts-expect-error ignore const updated = await prisma[t].update({ where: { id: unit.id }, - data: { - callsign2: data.callsign2, - callsign: data.callsign, - }, + data: dataSubmit, include: type === "leo" ? leoProperties : unitProperties, }); diff --git a/apps/api/src/controllers/leo/LeoController.ts b/apps/api/src/controllers/leo/LeoController.ts index ace03ed55..339f0f627 100644 --- a/apps/api/src/controllers/leo/LeoController.ts +++ b/apps/api/src/controllers/leo/LeoController.ts @@ -312,7 +312,7 @@ export class LeoController { @Put("/callsign/:officerId") @Description("Update the officer's activeDivisionCallsign") @UsePermissions({ - permissions: [Permissions.Leo, Permissions.ManageUnitCallsigns], + permissions: [Permissions.Leo, Permissions.ManageUnitCallsigns, Permissions.ManageUnitRank], }) async updateOfficerDivisionCallsign( @BodyParams() body: unknown, diff --git a/apps/client/locales/en/cad-settings.json b/apps/client/locales/en/cad-settings.json index 5057a8d9a..52eb6334e 100644 --- a/apps/client/locales/en/cad-settings.json +++ b/apps/client/locales/en/cad-settings.json @@ -421,6 +421,7 @@ "ManageUnits": "Manage Units", "DeleteUnits": "Delete Units", "ManageUnitCallsigns": "Manage Unit Callsigns", + "ManageUnitRank": "Manage Unit Ranks and Division", "ViewBusinesses": "View Businesses", "ManageBusinesses": "Manage Businesses", "DeleteBusinesses": "Delete Businesses", diff --git a/apps/client/src/components/admin/Sidebar/routes.ts b/apps/client/src/components/admin/Sidebar/routes.ts index 7a5e09145..7696defd8 100644 --- a/apps/client/src/components/admin/Sidebar/routes.ts +++ b/apps/client/src/components/admin/Sidebar/routes.ts @@ -29,6 +29,7 @@ export const managementRoutes: SidebarRoute[] = [ Permissions.DeleteUnits, Permissions.ManageUnitCallsigns, Permissions.ManageAwardsAndQualifications, + Permissions.ManageUnitRank, ], }, { diff --git a/apps/client/src/components/admin/manage/units/tabs/callsigns-tab/manage-unit-callsign-modal.tsx b/apps/client/src/components/admin/manage/units/tabs/callsigns-tab/manage-unit-callsign-modal.tsx index 29a0a3b71..3a0e24651 100644 --- a/apps/client/src/components/admin/manage/units/tabs/callsigns-tab/manage-unit-callsign-modal.tsx +++ b/apps/client/src/components/admin/manage/units/tabs/callsigns-tab/manage-unit-callsign-modal.tsx @@ -1,8 +1,8 @@ import { UPDATE_UNIT_CALLSIGN_SCHEMA } from "@snailycad/schemas"; -import type { DivisionValue } from "@snailycad/types"; +import { type DivisionValue, WhitelistStatus, ValueType } from "@snailycad/types"; import type { PutManageUnitCallsignData } from "@snailycad/types/api"; import { isUnitOfficer } from "@snailycad/utils"; -import { Button, Loader, TextField } from "@snailycad/ui"; +import { Button, FormRow, Loader, TextField } from "@snailycad/ui"; import { CallSignPreview } from "components/leo/CallsignPreview"; import { AdvancedSettings } from "components/leo/modals/AdvancedSettings"; import { makeDivisionsObjectMap } from "components/leo/modals/ManageOfficerModal"; @@ -14,6 +14,10 @@ import { useTranslations } from "next-intl"; import type { Unit } from "src/pages/admin/manage/units"; import { useModal } from "state/modalState"; import { ModalIds } from "types/modal-ids"; +import { Permissions, usePermission } from "hooks/usePermission"; +import { ValueSelectField } from "components/form/inputs/value-select-field"; +import { useValues } from "context/ValuesContext"; +import { getUnitDepartment } from "lib/utils"; interface Props { unit: Unit; @@ -24,6 +28,11 @@ export function ManageUnitCallsignModal({ onUpdate, unit }: Props) { const t = useTranslations(); const modalState = useModal(); const { state, execute } = useFetch(); + const { hasPermissions } = usePermission(); + const { officerRank } = useValues(); + + const hasManageCallsignPermissions = hasPermissions([Permissions.ManageUnitCallsigns]); + const hasManageRankPermissions = hasPermissions([Permissions.ManageUnitRank]); async function handleSubmit(values: typeof INITIAL_VALUES) { const { json } = await execute({ @@ -48,8 +57,13 @@ export function ManageUnitCallsignModal({ onUpdate, unit }: Props) { callsign2: unit.callsign2, callsigns: isUnitOfficer(unit) ? makeDivisionsObjectMap(unit) : {}, divisions: divisions.map((d) => ({ value: d.id, label: d.value.value })), + rank: unit.rankId, + position: unit.position ?? "", + department: getUnitDepartment(unit)?.id ?? "", }; + const areFormFieldsDisabled = unit.whitelistStatus?.status === WhitelistStatus.PENDING; + return ( {({ setFieldValue, errors, values, isValid }) => (
- setFieldValue("callsign", value)} - value={values.callsign} - /> + {hasManageCallsignPermissions ? ( + <> + setFieldValue("callsign", value)} + value={values.callsign} + /> + setFieldValue("callsign2", value)} + value={values.callsign2} + /> + + + ) : null} + + {hasManageRankPermissions ? ( + + { + // has no departments set - allows all departments + if (!value.officerRankDepartments || value.officerRankDepartments.length <= 0) { + return true; + } - setFieldValue("callsign2", value)} - value={values.callsign2} - /> + return values.department + ? value.officerRankDepartments.some((v) => v.id === values.department) + : true; + }} + /> - + setFieldValue("position", value)} + value={values.position} + /> + + ) : null} {isUnitOfficer(unit) ? : null} diff --git a/apps/client/src/pages/admin/manage/units/index.tsx b/apps/client/src/pages/admin/manage/units/index.tsx index 446682169..721caf135 100644 --- a/apps/client/src/pages/admin/manage/units/index.tsx +++ b/apps/client/src/pages/admin/manage/units/index.tsx @@ -60,6 +60,7 @@ export default function SupervisorPanelPage(props: Props) { ]); const hasManagePermissions = hasPermissions([Permissions.ManageUnits]); const hasManageCallsignPermissions = hasPermissions([Permissions.ManageUnitCallsigns]); + const hasManageRankPermissions = hasPermissions([Permissions.ManageUnitRank]); const TABS = []; @@ -67,7 +68,7 @@ export default function SupervisorPanelPage(props: Props) { TABS.push({ name: t("Management.allUnits"), value: "allUnits" }); } - if (hasManageCallsignPermissions) { + if (hasManageCallsignPermissions || hasManageRankPermissions) { TABS.push({ name: t("Management.callsignManagement"), value: "callsignManagement", @@ -104,7 +105,9 @@ export default function SupervisorPanelPage(props: Props) { - {hasManageCallsignPermissions ? : null} + {hasManageCallsignPermissions || hasManageRankPermissions ? ( + + ) : null} {props.pendingUnits.totalCount > 0 && hasManagePermissions ? ( ) : null} diff --git a/packages/permissions/src/defaults/admin.ts b/packages/permissions/src/defaults/admin.ts index 730111d2e..9236fccfc 100644 --- a/packages/permissions/src/defaults/admin.ts +++ b/packages/permissions/src/defaults/admin.ts @@ -24,6 +24,7 @@ export const defaultManagementPermissions = [ Permissions.ManageCustomFields, Permissions.ManageCustomRoles, Permissions.ViewCustomRoles, + Permissions.ManageUnitRank, ]; export const defaultImportPermissions = [ diff --git a/packages/permissions/src/permissions.ts b/packages/permissions/src/permissions.ts index a60b17ad4..fd015edf0 100644 --- a/packages/permissions/src/permissions.ts +++ b/packages/permissions/src/permissions.ts @@ -68,6 +68,7 @@ export enum Permissions { ManageUnits = "ManageUnits", DeleteUnits = "DeleteUnits", ManageUnitCallsigns = "ManageUnitCallsigns", + ManageUnitRank = "ManageUnitRank", ViewBusinesses = "ViewBusinesses", ManageBusinesses = "ManageBusinesses", diff --git a/packages/schemas/src/leo.ts b/packages/schemas/src/leo.ts index 8372eaad4..363799eff 100644 --- a/packages/schemas/src/leo.ts +++ b/packages/schemas/src/leo.ts @@ -59,6 +59,8 @@ export const UPDATE_UNIT_CALLSIGN_SCHEMA = z.object({ callsign: z.string().min(1).max(255), callsign2: z.string().min(1).max(255), callsigns: z.record(INDIVIDUAL_CALLSIGN_SCHEMA).nullish(), + rank: z.string().max(255).nullable(), + position: z.string().nullish(), }); export const UPDATE_OFFICER_STATUS_SCHEMA = z.object({