Skip to content

Commit

Permalink
feat: able to edit dashboard layout
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 committed Sep 17, 2023
1 parent 7634271 commit 7f4ae04
Show file tree
Hide file tree
Showing 14 changed files with 346 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- CreateEnum
CREATE TYPE "DashboardLayoutCardType" AS ENUM ('ACTIVE_CALLS', 'ACTIVE_BOLOS', 'ACTIVE_WARRANTS', 'ACTIVE_OFFICERS', 'ACTIVE_DEPUTIES', 'ACTIVE_INCIDENTS');

-- AlterTable
ALTER TABLE "User" ADD COLUMN "dispatchLayoutOrder" "DashboardLayoutCardType"[],
ADD COLUMN "emsFdLayoutOrder" "DashboardLayoutCardType"[],
ADD COLUMN "officerLayoutOrder" "DashboardLayoutCardType"[];
12 changes: 12 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ model User {
toAddDefaultPermissions ToAddDefaultPermissions[]
lastSeen DateTime @default(now())
developerMode Boolean @default(false)
dispatchLayoutOrder DashboardLayoutCardType[]
emsFdLayoutOrder DashboardLayoutCardType[]
officerLayoutOrder DashboardLayoutCardType[]
// relational data
citizens Citizen[]
Expand Down Expand Up @@ -1954,3 +1957,12 @@ enum Feature {
LEO_EDITABLE_CITIZEN_PROFILE // see #1698
ALLOW_MULTIPLE_UNITS_DEPARTMENTS_PER_USER // see #1722
}

enum DashboardLayoutCardType {
ACTIVE_CALLS
ACTIVE_BOLOS
ACTIVE_WARRANTS
ACTIVE_OFFICERS
ACTIVE_DEPUTIES
ACTIVE_INCIDENTS
}
23 changes: 21 additions & 2 deletions apps/api/src/controllers/auth/user/user-controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Context, Res, BodyParams, QueryParams } from "@tsed/common";
import { Controller } from "@tsed/di";
import { UseBefore } from "@tsed/platform-middlewares";
import { ContentType, Delete, Description, Patch, Post } from "@tsed/schema";
import { ContentType, Delete, Description, Patch, Post, Put } from "@tsed/schema";
import { Cookie } from "@snailycad/config";
import { prisma } from "lib/data/prisma";
import { IsAuth } from "middlewares/auth/is-auth";
import { setCookie } from "utils/set-cookie";
import { cad, Rank, ShouldDoType, StatusViewMode, TableActionsAlignment } from "@prisma/client";
import { NotFound } from "@tsed/exceptions";
import { CHANGE_PASSWORD_SCHEMA, CHANGE_USER_SCHEMA } from "@snailycad/schemas";
import {
CHANGE_PASSWORD_SCHEMA,
CHANGE_USER_SCHEMA,
DASHBOARD_LAYOUT_SCHEMA,
} from "@snailycad/schemas";
import { compareSync, genSaltSync, hashSync } from "bcrypt";
import { userProperties } from "lib/auth/getSessionUser";
import { validateSchema } from "lib/data/validate-schema";
Expand Down Expand Up @@ -280,4 +284,19 @@ export class UserController {

return true;
}

@Put("/dashboard-layout")
async editDashboardLayout(@BodyParams() body: unknown, @Context("user") user: User) {
const data = validateSchema(DASHBOARD_LAYOUT_SCHEMA, body);

const updatedUser = await prisma.user.update({
where: { id: user.id },
data: {
[data.type]: data.layout,
},
select: userProperties,
});

return updatedUser;
}
}
3 changes: 3 additions & 0 deletions apps/api/src/lib/auth/getSessionUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const userProperties = Prisma.validator<Prisma.UserSelect>()({
updatedAt: true,
lastSeen: true,
developerMode: true,
dispatchLayoutOrder: true,
emsFdLayoutOrder: true,
officerLayoutOrder: true,
});

interface GetSessionUserOptions<ReturnNullOnError extends boolean> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import * as React from "react";
import { DashboardLayoutCardType } from "@snailycad/types";
import { Modal } from "components/modal/Modal";
import { useRouter } from "next/router";
import { useModal } from "state/modalState";
import { ModalIds } from "types/modal-ids";
import { ReactSortable } from "react-sortablejs";
import { ArrowsMove } from "react-bootstrap-icons";
import { useAuth } from "context/AuthContext";
import { Button, Loader } from "@snailycad/ui";
import { useTranslations } from "use-intl";
import useFetch from "lib/useFetch";
import { toastMessage } from "lib/toastMessage";

const cardTypes: Record<"ems-fd" | "officer" | "dispatch", DashboardLayoutCardType[]> = {
"ems-fd": [
DashboardLayoutCardType.ACTIVE_CALLS,
DashboardLayoutCardType.ACTIVE_DEPUTIES,
DashboardLayoutCardType.ACTIVE_OFFICERS,
],
officer: [
DashboardLayoutCardType.ACTIVE_CALLS,
DashboardLayoutCardType.ACTIVE_BOLOS,
DashboardLayoutCardType.ACTIVE_WARRANTS,
DashboardLayoutCardType.ACTIVE_OFFICERS,
DashboardLayoutCardType.ACTIVE_DEPUTIES,
],
dispatch: [
DashboardLayoutCardType.ACTIVE_OFFICERS,
DashboardLayoutCardType.ACTIVE_DEPUTIES,
DashboardLayoutCardType.ACTIVE_CALLS,
DashboardLayoutCardType.ACTIVE_INCIDENTS,
DashboardLayoutCardType.ACTIVE_BOLOS,
],
};

export function EditDashboardLayoutModal() {
const common = useTranslations("Common");

const { setUser, user } = useAuth();
const [sortedList, setSortedList] = React.useState<DashboardLayoutCardType[]>([]);

const modalState = useModal();
const router = useRouter();
const type = getCardsType(router.pathname);
const columnName = getColumnName(type);
const { execute, state } = useFetch();

const cardsForType = cardTypes[type];

function handleListChange(list: { id: DashboardLayoutCardType }[]) {
setSortedList(list.map((l) => l.id));
}

async function handleSave() {
if (sortedList.length <= 0) return;
if (!user || !columnName) return;

const { json } = await execute({
method: "PUT",
path: "/user/dashboard-layout",
data: {
type: columnName,
layout: sortedList,
},
});

if (json) {
setUser({ ...user, [columnName]: sortedList });
toastMessage({
icon: "success",
title: "Layout Saved",
message: "The layout has been saved",
});
}
}

React.useEffect(() => {
const userSortedList = columnName ? user?.[columnName] ?? [] : [];

const list = cardsForType.sort((a, b) => {
return userSortedList.indexOf(a) - userSortedList.indexOf(b);
});

setSortedList(list);
}, [cardsForType, columnName, user]);

return (
<Modal
title="Edit Dashboard Layout"
isOpen={modalState.isOpen(ModalIds.EditDashboardLayout)}
onClose={() => modalState.closeModal(ModalIds.EditDashboardLayout)}
className="w-[650px]"
>
<ReactSortable
animation={200}
tag="ul"
setList={handleListChange}
list={sortedList.map((type) => ({ id: type }))}
className="flex flex-col gap-y-2 mt-5"
>
{sortedList.map((type) => (
<li
className="card border-2 rounded-md p-4 cursor-pointer flex items-center justify-between"
key={type}
>
{type}

<ArrowsMove />
</li>
))}
</ReactSortable>

<footer className="flex items-center justify-end mt-3">
<Button
type="button"
className="flex items-center gap-2"
isDisabled={state === "loading"}
onPress={handleSave}
>
{state === "loading" ? <Loader /> : null}
{common("save")}
</Button>
</footer>
</Modal>
);
}

function getCardsType(pathname: string) {
if (pathname.includes("/dispatch")) {
return "dispatch";
}

if (pathname.includes("/officer")) {
return "officer";
}

return "ems-fd";
}

function getColumnName(type: "ems-fd" | "officer" | "dispatch") {
switch (type) {
case "ems-fd": {
return "emsFdLayoutOrder";
}
case "officer": {
return "officerLayoutOrder";
}
case "dispatch": {
return "dispatchLayoutOrder";
}
default: {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { useTime } from "hooks/shared/useTime";
import { useFeatureEnabled } from "hooks/useFeatureEnabled";
import { classNames } from "lib/classNames";
import { useTranslations } from "next-intl";
import { Wifi } from "react-bootstrap-icons";
import { Grid1x2Fill, Wifi } from "react-bootstrap-icons";
import dynamic from "next/dynamic";
import { Button } from "@snailycad/ui";
import { useModal } from "state/modalState";
import { ModalIds } from "types/modal-ids";
import { EditDashboardLayoutModal } from "./edit-dashboard-layout-modal";

const DispatchAreaOfPlay = dynamic(async () => {
return (await import("components/dispatch/dispatch-area-of-play")).DispatchAreaOfPlay;
Expand All @@ -22,6 +26,7 @@ export function UtilityPanel({ children, isDispatch }: Props) {
const t = useTranslations("Leo");
const { activeDispatchersCount, hasActiveDispatchers } = useActiveDispatchers();
const { ACTIVE_DISPATCHERS } = useFeatureEnabled();
const modalState = useModal();

return (
<div className="w-full mb-3 card overflow-y-hidden">
Expand Down Expand Up @@ -53,6 +58,19 @@ export function UtilityPanel({ children, isDispatch }: Props) {
</header>

{children}

<footer className="border-t-[1.5px] border-neutral-800 dark:border-secondary status-buttons-grid mt-2 px-4 py-2">
<Button
className="flex items-center gap-2"
size="xs"
onClick={() => modalState.openModal(ModalIds.EditDashboardLayout)}
>
<Grid1x2Fill />
Edit Dashboard Layout
</Button>

<EditDashboardLayoutModal />
</footer>
</div>
);
}
23 changes: 20 additions & 3 deletions apps/client/src/pages/dispatch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { requestAll } from "lib/utils";
import { useSignal100 } from "hooks/shared/useSignal100";
import { usePanicButton } from "hooks/shared/usePanicButton";
import { Title } from "components/shared/Title";
import { ValueType } from "@snailycad/types";
import { DashboardLayoutCardType, ValueType } from "@snailycad/types";
import { useFeatureEnabled } from "hooks/useFeatureEnabled";
import { ModalIds } from "types/modal-ids";
import { useModal } from "state/modalState";
Expand All @@ -25,13 +25,15 @@ import type {
GetBolosData,
GetDispatchData,
GetEmsFdActiveDeputies,
GetUserData,
} from "@snailycad/types/api";
import { UtilityPanel } from "components/shared/UtilityPanel";
import { UtilityPanel } from "components/shared/utility-panel/utility-panel";
import { useCall911State } from "state/dispatch/call-911-state";
import { useActiveDispatcherState } from "state/dispatch/active-dispatcher-state";
import { Infofield } from "@snailycad/ui";
import { ActiveOfficers } from "components/dispatch/active-units/officers/active-officers";
import { ActiveDeputies } from "components/dispatch/active-units/deputies/active-deputies";
import { useAuth } from "context/AuthContext";

const ActiveIncidents = dynamic(async () => {
return (await import("components/dispatch/active-incidents/active-incidents")).ActiveIncidents;
Expand Down Expand Up @@ -64,6 +66,7 @@ export interface DispatchPageProps extends GetDispatchData {
activeOfficers: GetActiveOfficersData;
calls: Get911CallsData;
bolos: GetBolosData;
session: GetUserData | null;
}

export default function DispatchDashboard(props: DispatchPageProps) {
Expand All @@ -90,6 +93,8 @@ export default function DispatchDashboard(props: DispatchPageProps) {
const set911Calls = useCall911State((state) => state.setCalls);
const t = useTranslations("Leo");
const { CALLS_911, ACTIVE_INCIDENTS } = useFeatureEnabled();
const { user } = useAuth();
const session = user ?? props.session;

React.useEffect(() => {
set911Calls(props.calls.calls);
Expand All @@ -105,35 +110,47 @@ export default function DispatchDashboard(props: DispatchPageProps) {

const cards = [
{
type: DashboardLayoutCardType.ACTIVE_OFFICERS,
isEnabled: true,
children: <ActiveOfficers initialOfficers={props.activeOfficers} />,
},
{
type: DashboardLayoutCardType.ACTIVE_DEPUTIES,
isEnabled: true,
children: <ActiveDeputies initialDeputies={props.activeDeputies} />,
},
{
type: DashboardLayoutCardType.ACTIVE_CALLS,
isEnabled: CALLS_911,
children: <ActiveCalls initialData={props.calls} />,
},

{
type: DashboardLayoutCardType.ACTIVE_INCIDENTS,
isEnabled: ACTIVE_INCIDENTS,
children: <ActiveIncidents />,
},
{
type: DashboardLayoutCardType.ACTIVE_BOLOS,
isEnabled: true,
children: <ActiveBolos initialBolos={props.bolos} />,
},
];

const layoutOrder = session?.dispatchLayoutOrder ?? [];
const sortedCards = cards.sort((a, b) => {
return layoutOrder.indexOf(a.type) - layoutOrder.indexOf(b.type);
});

return (
<Layout permissions={{ permissions: [Permissions.Dispatch] }} className="dark:text-white">
<Title renderLayoutTitle={false}>{t("dispatch")}</Title>

<DispatchHeader userActiveDispatcher={props.userActiveDispatcher} />

{cards.map((card) => (card.isEnabled ? card.children : null))}
{sortedCards.map((card) =>
card.isEnabled ? <React.Fragment key={card.type}>{card.children}</React.Fragment> : null,
)}

<DispatchModals />
</Layout>
Expand Down
Loading

0 comments on commit 7f4ae04

Please sign in to comment.