From 9ca646eed728e25287b34979c58512b3ebf837e5 Mon Sep 17 00:00:00 2001 From: Casper Iversen <53900565+casperiv0@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:13:02 +0200 Subject: [PATCH] feat: active incidents for LEO dashboard (#1805) --- .../leo/incidents/IncidentController.ts | 1 + apps/client/locales/en/leo.json | 6 +- .../active-calls/columns/actions-column.tsx | 4 +- .../active-incidents/active-incidents.tsx | 123 ++++++++++-------- .../columns/actions-column.tsx | 95 ++++++++++++++ .../columns/involved-units-column.tsx | 10 +- .../leo/incidents/incidents-table.tsx | 2 +- .../leo/incidents/manage-incident-modal.tsx | 22 +++- .../edit-dashboard-layout-modal.tsx | 1 + apps/client/src/pages/officer/index.tsx | 8 +- 10 files changed, 199 insertions(+), 73 deletions(-) create mode 100644 apps/client/src/components/dispatch/active-incidents/columns/actions-column.tsx diff --git a/apps/api/src/controllers/leo/incidents/IncidentController.ts b/apps/api/src/controllers/leo/incidents/IncidentController.ts index d0fb52266..281821295 100644 --- a/apps/api/src/controllers/leo/incidents/IncidentController.ts +++ b/apps/api/src/controllers/leo/incidents/IncidentController.ts @@ -168,6 +168,7 @@ export class IncidentController { } @Post("/:type/:incidentId") + @Description("Assign or unassign a unit from an Active Incident") @UsePermissions({ permissions: [Permissions.Dispatch, Permissions.Leo, Permissions.EmsFd], }) diff --git a/apps/client/locales/en/leo.json b/apps/client/locales/en/leo.json index b4ff5142e..428fe38fe 100644 --- a/apps/client/locales/en/leo.json +++ b/apps/client/locales/en/leo.json @@ -353,7 +353,9 @@ "OTHER": "Other", "RADAR": "Radar", "LASER": "Laser", - "PACE": "Pace" + "PACE": "Pace", + "unassignFromIncident": "Unassign from incident", + "assignToIncident": "Assign to incident" }, "Bolos": { "activeBolos": "Active Bolos", @@ -385,4 +387,4 @@ "bureauOfFirearms": "Bureau of Firearms", "noWeaponsPendingBof": "There are no weapons pending approval." } -} +} \ No newline at end of file diff --git a/apps/client/src/components/dispatch/active-calls/columns/actions-column.tsx b/apps/client/src/components/dispatch/active-calls/columns/actions-column.tsx index c792e924d..0e4f3398c 100644 --- a/apps/client/src/components/dispatch/active-calls/columns/actions-column.tsx +++ b/apps/client/src/components/dispatch/active-calls/columns/actions-column.tsx @@ -32,9 +32,7 @@ export function ActiveCallsActionsColumn({ const { hasActiveDispatchers } = useActiveDispatchers(); const { hasPermissions } = usePermission(); const router = useRouter(); - const { setCurrentlySelectedCall } = useCall911State((s) => ({ - setCurrentlySelectedCall: s.setCurrentlySelectedCall, - })); + const setCurrentlySelectedCall = useCall911State((s) => s.setCurrentlySelectedCall); const t = useTranslations("Calls"); const common = useTranslations("Common"); diff --git a/apps/client/src/components/dispatch/active-incidents/active-incidents.tsx b/apps/client/src/components/dispatch/active-incidents/active-incidents.tsx index f71436d3c..02e48edc9 100644 --- a/apps/client/src/components/dispatch/active-incidents/active-incidents.tsx +++ b/apps/client/src/components/dispatch/active-incidents/active-incidents.tsx @@ -20,6 +20,12 @@ import { CallDescription } from "../active-calls/CallDescription"; import dynamic from "next/dynamic"; import { useActiveIncidents } from "hooks/realtime/useActiveIncidents"; import compareDesc from "date-fns/compareDesc"; +import { usePermission } from "hooks/usePermission"; +import { defaultPermissions } from "@snailycad/permissions"; +import { useRouter } from "next/router"; +import { useEmsFdState } from "state/ems-fd-state"; +import { useLeoState } from "state/leo-state"; +import { ActiveIncidentsActionsColumn } from "./columns/actions-column"; const ManageIncidentModal = dynamic( async () => (await import("components/leo/incidents/manage-incident-modal")).ManageIncidentModal, @@ -40,7 +46,21 @@ export function ActiveIncidents() { const draggingUnit = useDispatchState((state) => state.draggingUnit); const asyncTable = useActiveIncidentsTable(); + const router = useRouter(); const { activeIncidents } = useActiveIncidents(); + const { hasPermissions } = usePermission(); + + const activeDeputy = useEmsFdState((state) => state.activeDeputy); + const activeOfficer = useLeoState((state) => state.activeOfficer); + + const hasDispatchPermissions = hasPermissions(defaultPermissions.defaultDispatchPermissions); + const isDispatch = router.pathname === "/dispatch" && hasDispatchPermissions; + const activeUnitForRoute = + router.pathname === "/officer" + ? activeOfficer + : router.pathname === "/ems-fd" + ? activeDeputy + : null; const tableState = useTableState({ tableId: "active-incidents", @@ -84,16 +104,6 @@ export function ActiveIncidents() { } } - function onEditClick(incident: LeoIncident) { - modalState.openModal(ModalIds.ManageIncident); - setTempIncident(incident); - } - - function onEndClick(incident: LeoIncident) { - modalState.openModal(ModalIds.AlertDeleteIncident); - setTempIncident(incident); - } - function handleCreateIncident() { modalState.openModal(ModalIds.ManageIncident); setTempIncident("create"); @@ -104,16 +114,18 @@ export function ActiveIncidents() {

{t("activeIncidents")}

-
- -
+ {isDispatch ? ( +
+ +
+ ) : null}
{asyncTable.noItemsAvailable ? ( @@ -127,11 +139,19 @@ export function ActiveIncidents() { data={activeIncidents .sort((a, b) => compareDesc(new Date(a.updatedAt), new Date(b.updatedAt))) .map((incident) => { + const isUnitAssigned = incident.unitsInvolved.some( + (v) => v.unit?.id === activeUnitForRoute?.id, + ); + return { + rowProps: { + className: classNames(isUnitAssigned && "bg-gray-200 dark:bg-amber-900"), + }, id: incident.id, caseNumber: `#${incident.caseNumber}`, unitsInvolved: ( @@ -143,26 +163,13 @@ export function ActiveIncidents() { situationCode: incident.situationCode?.value.value ?? common("none"), description: , actions: ( - <> - - - - + ), }; })} @@ -180,24 +187,26 @@ export function ActiveIncidents() { /> )} - - onDrop={({ incident, unit }) => { - if (!unit.unit?.id) return; - handleAssignUnassignToIncident(incident, unit.unit.id, "unassign"); - }} - accepts={[DndActions.UnassignUnitFromIncident]} - > -
+ onDrop={({ incident, unit }) => { + if (!unit.unit?.id) return; + handleAssignUnassignToIncident(incident, unit.unit.id, "unassign"); + }} + accepts={[DndActions.UnassignUnitFromIncident]} > -

{t("dropToUnassignFromIncident")}

-
- +
+

{t("dropToUnassignFromIncident")}

+
+ + ) : null} {tempIncident === "hide" ? null : ( + + + {isDispatch ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/client/src/components/dispatch/active-incidents/columns/involved-units-column.tsx b/apps/client/src/components/dispatch/active-incidents/columns/involved-units-column.tsx index c5c79a83c..e6e8c2a8c 100644 --- a/apps/client/src/components/dispatch/active-incidents/columns/involved-units-column.tsx +++ b/apps/client/src/components/dispatch/active-incidents/columns/involved-units-column.tsx @@ -17,6 +17,7 @@ import { DndActions } from "types/dnd-actions"; interface Props { incident: LeoIncident; + isDispatch: boolean; handleAssignUnassignToIncident( incident: LeoIncident, unitId: string, @@ -24,14 +25,18 @@ interface Props { ): Promise; } -export function InvolvedUnitsColumn({ handleAssignUnassignToIncident, incident }: Props) { +export function InvolvedUnitsColumn({ + isDispatch, + handleAssignUnassignToIncident, + incident, +}: Props) { const common = useTranslations("Common"); const setDraggingUnit = useDispatchState((state) => state.setDraggingUnit); const { generateCallsign } = useGenerateCallsign(); const { hasActiveDispatchers } = useActiveDispatchers(); - const canDrag = hasActiveDispatchers; + const canDrag = isDispatch && hasActiveDispatchers; function makeAssignedUnit(unit: IncidentInvolvedUnit) { if (!unit.unit) return "UNKNOWN"; @@ -54,6 +59,7 @@ export function InvolvedUnitsColumn({ handleAssignUnassignToIncident, incident } { + if (!canDrag) return; setDraggingUnit(isDragging ? "incident" : null); }} key={unit.id} diff --git a/apps/client/src/components/leo/incidents/incidents-table.tsx b/apps/client/src/components/leo/incidents/incidents-table.tsx index 5f3c87f80..a60cec2db 100644 --- a/apps/client/src/components/leo/incidents/incidents-table.tsx +++ b/apps/client/src/components/leo/incidents/incidents-table.tsx @@ -173,7 +173,7 @@ export function IncidentsTable( injuriesOrFatalities: common(yesOrNoText(incident.injuriesOrFatalities)), arrestsMade: common(yesOrNoText(incident.arrestsMade)), situationCode: incident.situationCode?.value.value ?? common("none"), - description: , + description: , createdAt: {incident.createdAt}, actions: ( <> diff --git a/apps/client/src/components/leo/incidents/manage-incident-modal.tsx b/apps/client/src/components/leo/incidents/manage-incident-modal.tsx index e67c5d8c6..3610c22d7 100644 --- a/apps/client/src/components/leo/incidents/manage-incident-modal.tsx +++ b/apps/client/src/components/leo/incidents/manage-incident-modal.tsx @@ -63,7 +63,7 @@ function areFormFieldsDisabled(options: AreFormFieldsDisabledOptions) { const isAssignedToIncident = options.incident.unitsInvolved.some( (u) => u.unit?.id === options.activeUnit?.id, ); - return !isAssignedToIncident; + return isAssignedToIncident; } // todo: make this an optional feature @@ -99,11 +99,15 @@ export function ManageIncidentModal({ const isDispatch = router.pathname.includes("/dispatch"); const isLeoIncidents = type === "leo"; - const isEmsFdIncidents = router.pathname.includes("/ems-fd"); - const isOfficerIncidents = router.pathname.includes("/officer"); + const isEmsFdIncidentsPage = router.pathname === "/ems-fd/incidents"; + const isOfficerIncidentsPage = router.pathname === "/officer/incidents"; - const activeUnit = isOfficerIncidents ? activeOfficer : isEmsFdIncidents ? activeDeputy : null; - const isReadOnly = isOfficerIncidents || isEmsFdIncidents; + const activeUnit = isOfficerIncidentsPage + ? activeOfficer + : isEmsFdIncidentsPage + ? activeDeputy + : null; + const isReadOnly = isOfficerIncidentsPage || isEmsFdIncidentsPage; const areFieldsDisabled = areFormFieldsDisabled({ isActiveIncidentsList: !isReadOnly, @@ -298,7 +302,11 @@ export function ManageIncidentModal({ {incident ? ( - + ) : null} @@ -332,7 +340,7 @@ export function ManageIncidentModal({ {incident ? ( ) : null} diff --git a/apps/client/src/components/shared/utility-panel/edit-dashboard-layout-modal.tsx b/apps/client/src/components/shared/utility-panel/edit-dashboard-layout-modal.tsx index 42929a3fb..66f173f5f 100644 --- a/apps/client/src/components/shared/utility-panel/edit-dashboard-layout-modal.tsx +++ b/apps/client/src/components/shared/utility-panel/edit-dashboard-layout-modal.tsx @@ -24,6 +24,7 @@ const cardTypes: Record<"ems-fd" | "officer" | "dispatch", DashboardLayoutCardTy DashboardLayoutCardType.ACTIVE_WARRANTS, DashboardLayoutCardType.ACTIVE_OFFICERS, DashboardLayoutCardType.ACTIVE_DEPUTIES, + DashboardLayoutCardType.ACTIVE_INCIDENTS, ], dispatch: [ DashboardLayoutCardType.ACTIVE_OFFICERS, diff --git a/apps/client/src/pages/officer/index.tsx b/apps/client/src/pages/officer/index.tsx index 2b35f4b1f..0850f4b7f 100644 --- a/apps/client/src/pages/officer/index.tsx +++ b/apps/client/src/pages/officer/index.tsx @@ -45,6 +45,7 @@ import { CreateWarrantModal } from "components/leo/modals/CreateWarrantModal"; import { useCall911State } from "state/dispatch/call-911-state"; import { usePermission } from "hooks/usePermission"; import { useAuth } from "context/AuthContext"; +import { ActiveIncidents } from "components/dispatch/active-incidents/active-incidents"; const Modals = { CreateWarrantModal: dynamic( @@ -154,7 +155,7 @@ export default function OfficerDashboard({ })); const set911Calls = useCall911State((state) => state.setCalls); const t = useTranslations("Leo"); - const { ACTIVE_WARRANTS, CALLS_911 } = useFeatureEnabled(); + const { ACTIVE_INCIDENTS, ACTIVE_WARRANTS, CALLS_911 } = useFeatureEnabled(); const { user } = useAuth(); const session = user ?? _session; @@ -196,6 +197,11 @@ export default function OfficerDashboard({ isEnabled: true, children: , }, + { + type: DashboardLayoutCardType.ACTIVE_INCIDENTS, + isEnabled: ACTIVE_INCIDENTS, + children: , + }, ]; const layoutOrder = session?.officerLayoutOrder ?? [];