Skip to content

Commit

Permalink
Manage unit rank role (#1956)
Browse files Browse the repository at this point in the history
  • Loading branch information
tugamars authored Sep 16, 2024
1 parent 6491543 commit 7836fe7
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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<Feature, boolean> },
Expand All @@ -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,
});

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/controllers/leo/LeoController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/client/locales/en/cad-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions apps/client/src/components/admin/Sidebar/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const managementRoutes: SidebarRoute[] = [
Permissions.DeleteUnits,
Permissions.ManageUnitCallsigns,
Permissions.ManageAwardsAndQualifications,
Permissions.ManageUnitRank,
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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<PutManageUnitCallsignData>({
Expand All @@ -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 (
<Modal
title={t("Common.manage")}
Expand All @@ -60,24 +74,58 @@ export function ManageUnitCallsignModal({ onUpdate, unit }: Props) {
<Formik validate={validate} initialValues={INITIAL_VALUES} onSubmit={handleSubmit}>
{({ setFieldValue, errors, values, isValid }) => (
<Form>
<TextField
errorMessage={errors.callsign}
label={t("Leo.callsign1")}
autoFocus
name="callsign"
onChange={(value) => setFieldValue("callsign", value)}
value={values.callsign}
/>
{hasManageCallsignPermissions ? (
<>
<TextField
errorMessage={errors.callsign}
label={t("Leo.callsign1")}
autoFocus
name="callsign"
onChange={(value) => setFieldValue("callsign", value)}
value={values.callsign}
/>
<TextField
errorMessage={errors.callsign2}
label={t("Leo.callsign2")}
name="callsign2"
onChange={(value) => setFieldValue("callsign2", value)}
value={values.callsign2}
/>
<CallSignPreview department={unit.department} divisions={divisions} />
</>
) : null}

{hasManageRankPermissions ? (
<FormRow>
<ValueSelectField
isDisabled={areFormFieldsDisabled}
label={t("Leo.rank")}
fieldName="rank"
values={officerRank.values}
valueType={ValueType.OFFICER_RANK}
isClearable
filterFn={(value) => {
// has no departments set - allows all departments
if (!value.officerRankDepartments || value.officerRankDepartments.length <= 0) {
return true;
}

<TextField
errorMessage={errors.callsign2}
label={t("Leo.callsign2")}
name="callsign2"
onChange={(value) => setFieldValue("callsign2", value)}
value={values.callsign2}
/>
return values.department
? value.officerRankDepartments.some((v) => v.id === values.department)
: true;
}}
/>

<CallSignPreview department={unit.department} divisions={divisions} />
<TextField
isDisabled={areFormFieldsDisabled}
errorMessage={errors.position}
label={t("Leo.position")}
name="position"
onChange={(value) => setFieldValue("position", value)}
value={values.position}
/>
</FormRow>
) : null}

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

Expand Down
7 changes: 5 additions & 2 deletions apps/client/src/pages/admin/manage/units/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ export default function SupervisorPanelPage(props: Props) {
]);
const hasManagePermissions = hasPermissions([Permissions.ManageUnits]);
const hasManageCallsignPermissions = hasPermissions([Permissions.ManageUnitCallsigns]);
const hasManageRankPermissions = hasPermissions([Permissions.ManageUnitRank]);

const TABS = [];

if (hasViewPermissions) {
TABS.push({ name: t("Management.allUnits"), value: "allUnits" });
}

if (hasManageCallsignPermissions) {
if (hasManageCallsignPermissions || hasManageRankPermissions) {
TABS.push({
name: t("Management.callsignManagement"),
value: "callsignManagement",
Expand Down Expand Up @@ -104,7 +105,9 @@ export default function SupervisorPanelPage(props: Props) {

<TabList tabs={TABS}>
<AllUnitsTab units={props.units} />
{hasManageCallsignPermissions ? <CallsignsTab units={props.units} /> : null}
{hasManageCallsignPermissions || hasManageRankPermissions ? (
<CallsignsTab units={props.units} />
) : null}
{props.pendingUnits.totalCount > 0 && hasManagePermissions ? (
<DepartmentWhitelistingTab pendingUnits={props.pendingUnits} />
) : null}
Expand Down
1 change: 1 addition & 0 deletions packages/permissions/src/defaults/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const defaultManagementPermissions = [
Permissions.ManageCustomFields,
Permissions.ManageCustomRoles,
Permissions.ViewCustomRoles,
Permissions.ManageUnitRank,
];

export const defaultImportPermissions = [
Expand Down
1 change: 1 addition & 0 deletions packages/permissions/src/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export enum Permissions {
ManageUnits = "ManageUnits",
DeleteUnits = "DeleteUnits",
ManageUnitCallsigns = "ManageUnitCallsigns",
ManageUnitRank = "ManageUnitRank",

ViewBusinesses = "ViewBusinesses",
ManageBusinesses = "ManageBusinesses",
Expand Down
2 changes: 2 additions & 0 deletions packages/schemas/src/leo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 7836fe7

Please sign in to comment.