Skip to content

Commit

Permalink
feat: active incidents for LEO dashboard (#1805)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Sep 27, 2023
1 parent 694b10e commit 9ca646e
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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],
})
Expand Down
6 changes: 4 additions & 2 deletions apps/client/locales/en/leo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -385,4 +387,4 @@
"bureauOfFirearms": "Bureau of Firearms",
"noWeaponsPendingBof": "There are no weapons pending approval."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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");
Expand All @@ -104,16 +114,18 @@ export function ActiveIncidents() {
<header className="flex items-center justify-between p-2 px-4 bg-gray-200 dark:bg-secondary">
<h1 className="text-xl font-semibold">{t("activeIncidents")}</h1>

<div>
<Button
variant={null}
className="bg-gray-500 hover:bg-gray-600 dark:border dark:border-quinary dark:bg-tertiary dark:hover:brightness-125 text-white"
onPress={handleCreateIncident}
disabled={!hasActiveDispatchers}
>
{t("createIncident")}
</Button>
</div>
{isDispatch ? (
<div>
<Button
variant={null}
className="bg-gray-500 hover:bg-gray-600 dark:border dark:border-quinary dark:bg-tertiary dark:hover:brightness-125 text-white"
onPress={handleCreateIncident}
disabled={!hasActiveDispatchers}
>
{t("createIncident")}
</Button>
</div>
) : null}
</header>

{asyncTable.noItemsAvailable ? (
Expand All @@ -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: (
<InvolvedUnitsColumn
isDispatch={isDispatch}
handleAssignUnassignToIncident={handleAssignUnassignToIncident}
incident={incident}
/>
Expand All @@ -143,26 +163,13 @@ export function ActiveIncidents() {
situationCode: incident.situationCode?.value.value ?? common("none"),
description: <CallDescription data={incident} />,
actions: (
<>
<Button
onPress={() => onEditClick(incident)}
disabled={!hasActiveDispatchers}
size="xs"
variant="success"
>
{common("manage")}
</Button>

<Button
onPress={() => onEndClick(incident)}
disabled={!hasActiveDispatchers}
size="xs"
variant="danger"
className="ml-2"
>
{t("end")}
</Button>
</>
<ActiveIncidentsActionsColumn
handleAssignUnassignToIncident={handleAssignUnassignToIncident}
setTempIncident={setTempIncident}
unit={activeUnitForRoute}
isUnitAssigned={isUnitAssigned}
incident={incident}
/>
),
};
})}
Expand All @@ -180,24 +187,26 @@ export function ActiveIncidents() {
/>
)}

<Droppable<{ incident: LeoIncident; unit: IncidentInvolvedUnit }>
onDrop={({ incident, unit }) => {
if (!unit.unit?.id) return;
handleAssignUnassignToIncident(incident, unit.unit.id, "unassign");
}}
accepts={[DndActions.UnassignUnitFromIncident]}
>
<div
className={classNames(
"grid place-items-center z-50 border-2 border-slate-500 dark:bg-quinary fixed bottom-3 left-3 right-4 h-60 shadow-sm rounded-md transition-opacity",
draggingUnit === "incident"
? "pointer-events-all opacity-100"
: "pointer-events-none opacity-0",
)}
{isDispatch ? (
<Droppable<{ incident: LeoIncident; unit: IncidentInvolvedUnit }>
onDrop={({ incident, unit }) => {
if (!unit.unit?.id) return;
handleAssignUnassignToIncident(incident, unit.unit.id, "unassign");
}}
accepts={[DndActions.UnassignUnitFromIncident]}
>
<p>{t("dropToUnassignFromIncident")}</p>
</div>
</Droppable>
<div
className={classNames(
"grid place-items-center z-50 border-2 border-slate-500 dark:bg-quinary fixed bottom-3 left-3 right-4 h-60 shadow-sm rounded-md transition-opacity",
draggingUnit === "incident"
? "pointer-events-all opacity-100"
: "pointer-events-none opacity-0",
)}
>
<p>{t("dropToUnassignFromIncident")}</p>
</div>
</Droppable>
) : null}

{tempIncident === "hide" ? null : (
<ManageIncidentModal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Button } from "@snailycad/ui";
import { useActiveDispatchers } from "hooks/realtime/use-active-dispatchers";
import { useModal } from "state/modalState";
import { usePermission } from "hooks/usePermission";
import { useRouter } from "next/router";
import { defaultPermissions } from "@snailycad/permissions";
import type { ActiveOfficer } from "state/leo-state";
import type { ActiveDeputy } from "state/ems-fd-state";
import { LeoIncident, ShouldDoType } from "@snailycad/types";
import { ModalIds } from "types/modal-ids";
import { useTranslations } from "next-intl";

interface Props {
incident: LeoIncident;
unit: ActiveOfficer | ActiveDeputy | null;
isUnitAssigned: boolean;
setTempIncident(incident: LeoIncident): void;
handleAssignUnassignToIncident(
incident: LeoIncident,
unitId: string,
type: "assign" | "unassign",
): void;
}

export function ActiveIncidentsActionsColumn({
setTempIncident,
handleAssignUnassignToIncident,
isUnitAssigned,
unit,
incident,
}: Props) {
const modalState = useModal();
const { hasActiveDispatchers } = useActiveDispatchers();
const { hasPermissions } = usePermission();
const router = useRouter();

const t = useTranslations("Leo");
const common = useTranslations("Common");

const hasDispatchPermissions = hasPermissions(defaultPermissions.defaultDispatchPermissions);
const isDispatch = router.pathname === "/dispatch" && hasDispatchPermissions;

const isUnitActive = unit?.status && unit.status.shouldDo !== ShouldDoType.SET_OFF_DUTY;

function onEditClick(incident: LeoIncident) {
modalState.openModal(ModalIds.ManageIncident);
setTempIncident(incident);
}

function onEndClick(incident: LeoIncident) {
modalState.openModal(ModalIds.AlertDeleteIncident);
setTempIncident(incident);
}

return (
<>
<Button
isDisabled={isDispatch ? !hasActiveDispatchers : !isUnitActive}
size="xs"
variant="success"
onPress={() => onEditClick(incident)}
>
{isDispatch ? common("manage") : common("view")}
</Button>

{isDispatch ? (
<Button
onPress={() => onEndClick(incident)}
isDisabled={!hasActiveDispatchers}
size="xs"
variant="danger"
className="ml-2"
>
{t("end")}
</Button>
) : (
<Button
className="ml-2"
isDisabled={!isUnitActive}
size="xs"
onPress={() =>
unit &&
handleAssignUnassignToIncident(
incident,
unit.id,
isUnitAssigned ? "unassign" : "assign",
)
}
>
{isUnitAssigned ? t("unassignFromIncident") : t("assignToIncident")}
</Button>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,26 @@ import { DndActions } from "types/dnd-actions";

interface Props {
incident: LeoIncident;
isDispatch: boolean;
handleAssignUnassignToIncident(
incident: LeoIncident,
unitId: string,
type: "assign" | "unassign",
): Promise<void>;
}

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";
Expand All @@ -54,6 +59,7 @@ export function InvolvedUnitsColumn({ handleAssignUnassignToIncident, incident }
<Draggable
canDrag={canDrag}
onDrag={(isDragging) => {
if (!canDrag) return;
setDraggingUnit(isDragging ? "incident" : null);
}}
key={unit.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export function IncidentsTable<T extends EmsFdIncident | LeoIncident>(
injuriesOrFatalities: common(yesOrNoText(incident.injuriesOrFatalities)),
arrestsMade: common(yesOrNoText(incident.arrestsMade)),
situationCode: incident.situationCode?.value.value ?? common("none"),
description: <CallDescription data={incident} />,
description: <CallDescription nonCard data={incident} />,
createdAt: <FullDate>{incident.createdAt}</FullDate>,
actions: (
<>
Expand Down
Loading

0 comments on commit 9ca646e

Please sign in to comment.