Skip to content

Commit

Permalink
feat: able to add department links (#1786)
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 authored Sep 8, 2023
1 parent afd9842 commit bea35c7
Show file tree
Hide file tree
Showing 18 changed files with 293 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "DepartmentValueLink" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"url" TEXT NOT NULL,
"departmentId" TEXT NOT NULL,

CONSTRAINT "DepartmentValueLink_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "DepartmentValueLink" ADD CONSTRAINT "DepartmentValueLink_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "DepartmentValue"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
9 changes: 9 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,15 @@ model DepartmentValue {
EmergencyVehicleValue EmergencyVehicleValue[]
mCombinedEmsFdUnit CombinedEmsFdUnit[]
ActiveDispatchers ActiveDispatchers[]
links DepartmentValueLink[]
}

model DepartmentValueLink {
id String @id @default(cuid())
title String
url String
department DepartmentValue @relation(fields: [departmentId], references: [id])
departmentId String
}

model EmergencyVehicleValue {
Expand Down
28 changes: 25 additions & 3 deletions apps/api/src/controllers/admin/values/import-values-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,17 @@ export const typeHandlers = {
DEPARTMENT: async ({ body, id }: HandlerOptions) => {
const data = validateSchema(DEPARTMENT_ARR, body);

return prisma.$transaction(
data.map((item) => {
return prisma.departmentValue.upsert({
return Promise.all(
data.map(async (item) => {
if (id) {
await prisma.departmentValueLink.deleteMany({
where: {
departmentId: id,
},
});
}

const departmentValue = await prisma.departmentValue.upsert({
where: { id: String(id) },
...makePrismaData(ValueType.DEPARTMENT, {
type: item.type as DepartmentType,
Expand All @@ -237,6 +245,20 @@ export const typeHandlers = {
}),
include: { value: true, defaultOfficerRank: true },
});

const links = await prisma.$transaction(
(item.departmentLinks ?? []).map((link) =>
prisma.departmentValueLink.create({
data: {
title: link.title,
url: link.url,
department: { connect: { id: departmentValue.id } },
},
}),
),
);

return { ...departmentValue, departmentLinks: links };
}),
);
},
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/controllers/admin/values/values-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const GET_VALUES: Partial<Record<ValueType, ValuesSelect>> = {
BUSINESS_ROLE: { name: "employeeValue" },
CODES_10: { name: "statusValue", include: { departments: { include: { value: true } } } },
DRIVERSLICENSE_CATEGORY: { name: "driversLicenseCategoryValue" },
DEPARTMENT: { name: "departmentValue", include: { defaultOfficerRank: true } },
DEPARTMENT: { name: "departmentValue", include: { defaultOfficerRank: true, links: true } },
DIVISION: {
name: "divisionValue",
include: { department: { include: { value: true } } },
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/utils/leo/includes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client";
import { userProperties } from "lib/auth/getSessionUser";

export const unitProperties = Prisma.validator<Prisma.EmsFdDeputySelect>()({
department: { include: { value: true } },
department: { include: { value: true, links: true } },
division: { include: { value: true, department: true } },
status: { include: { value: true } },
citizen: { select: { name: true, surname: true, id: true } },
Expand All @@ -14,7 +14,7 @@ export const unitProperties = Prisma.validator<Prisma.EmsFdDeputySelect>()({
});

export const _leoProperties = Prisma.validator<Prisma.OfficerSelect>()({
department: { include: { value: true } },
department: { include: { value: true, links: true } },
divisions: { include: { value: true, department: true } },
status: { include: { value: true } },
citizen: { select: { name: true, surname: true, id: true } },
Expand Down
5 changes: 4 additions & 1 deletion apps/client/locales/en/leo.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,10 @@
"hideSmartSigns": "Hide Smart Signs",
"showSmartSigns": "Show Smart Signs",
"smartSignUpdated": "SmartSign Updated",
"smartSignUpdatedMessage": "We sent a request to the FXServer to update the SmartSign."
"smartSignUpdatedMessage": "We sent a request to the FXServer to update the SmartSign.",
"departmentInformation": "Department Info",
"departmentInformationDesc": "Here you can view further external links and information for your department.",
"noDepartmentLinks": "This department doesn't have any extra information yet."
},
"Bolos": {
"activeBolos": "Active Bolos",
Expand Down
2 changes: 2 additions & 0 deletions apps/client/src/components/admin/values/ManageValueModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ export function ManageValueModal({ onCreate, onUpdate, type, value }: Props) {
value && (isDivisionValue(value) || isDepartmentValue(value))
? JSON.stringify(value.extraFields)
: "null",

departmentLinks: value && isDepartmentValue(value) ? value.links ?? [] : [],
};

function validate(values: typeof INITIAL_VALUES) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useValues } from "context/ValuesContext";
import { useTranslations } from "use-intl";
import { ValueSelectField } from "components/form/inputs/value-select-field";
import { CALLSIGN_TEMPLATE_VARIABLES } from "components/admin/manage/cad-settings/misc-features/template-tab";
import { DepartmentLinksSection } from "./department-links-section";

export const DEPARTMENT_LABELS = {
[DepartmentType.LEO]: "LEO",
Expand Down Expand Up @@ -105,6 +106,8 @@ export function DepartmentFields() {
value={values.extraFields}
placeholder="JSON"
/>

<DepartmentLinksSection />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Button, TextField } from "@snailycad/ui";
import * as Popover from "@radix-ui/react-popover";
import * as React from "react";
import { useTranslations } from "use-intl";
import { useFormikContext } from "formik";
import { DepartmentValueLink } from "@snailycad/types";
import { Table, useTableState } from "components/shared/Table";
import { v4 } from "uuid";

export function DepartmentLinksSection() {
const [openPopover, setOpenPopover] = React.useState<"new" | null>(null);
const tableState = useTableState();
const { values, setFieldValue } = useFormikContext<{ departmentLinks: DepartmentValueLink[] }>();
const common = useTranslations("Common");

console.log({
links: values.departmentLinks,
});

return (
<section className="mt-6">
<header className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Links</h2>

<LinkPopover
isPopoverOpen={openPopover === "new"}
setIsPopoverOpen={(v) => setOpenPopover(v ? "new" : null)}
trigger={
<Button onPress={() => setOpenPopover("new")} size="xs">
Add Link
</Button>
}
/>
</header>

{values.departmentLinks.length <= 0 ? (
<p>
No links added yet. Click the <b>Add Link</b> button to add a link.
</p>
) : (
<Table
features={{ isWithinCardOrModal: true }}
tableState={tableState}
data={values.departmentLinks.map((url) => {
return {
id: `${url.url}-${url.id}`,
name: url.title,
url: url.url,
actions: (
<Button
onPress={() => {
setFieldValue(
"departmentLinks",
values.departmentLinks.filter((v) => v.id !== url.id),
);
}}
size="xs"
variant="danger"
>
{common("delete")}
</Button>
),
};
})}
columns={[
{ header: common("name"), accessorKey: "name" },
{ header: common("url"), accessorKey: "url" },
{ header: common("actions"), accessorKey: "actions" },
]}
/>
)}
</section>
);
}

interface LinkPopoverProps {
trigger: React.ReactNode;
isPopoverOpen: boolean;
setIsPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>;

url?: { name: string; url: string };
}

function LinkPopover(props: LinkPopoverProps) {
const common = useTranslations("Common");
const { values, setFieldValue } = useFormikContext<{ departmentLinks: DepartmentValueLink[] }>();

const [name, setName] = React.useState(props.url?.name ?? "");
const [url, setUrl] = React.useState(props.url?.url ?? "");

function handleSubmit() {
setFieldValue("departmentLinks", [...values.departmentLinks, { id: v4(), title: name, url }]);
props.setIsPopoverOpen(false);
}

return (
<Popover.Root open={props.isPopoverOpen} onOpenChange={props.setIsPopoverOpen}>
<Popover.Trigger asChild>
<span>{props.trigger}</span>
</Popover.Trigger>

<Popover.Content className="z-[999] p-4 bg-gray-200 rounded-md shadow-md dropdown-fade w-96 dark:bg-primary dark:border dark:border-secondary text-base font-normal">
<h3 className="text-xl font-semibold mb-3">Add Link</h3>

<div>
<TextField label={common("name")} value={name} onChange={(value) => setName(value)} />
<TextField
type="url"
label={common("url")}
value={url}
onChange={(value) => setUrl(value)}
/>

<Button type="button" onPress={handleSubmit} size="xs">
{common("save")}
</Button>
</div>

<Popover.Arrow className="fill-primary" />
</Popover.Content>
</Popover.Root>
);
}
4 changes: 4 additions & 0 deletions apps/client/src/components/ems-fd/ModalButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const buttons: MButton[] = [
nameKey: ["Leo", "notepad"],
modalId: ModalIds.Notepad,
},
{
nameKey: ["Leo", "departmentInformation"],
modalId: ModalIds.DepartmentInfo,
},
];

export function ModalButtons({
Expand Down
1 change: 1 addition & 0 deletions apps/client/src/components/leo/ModalButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const buttons: modalButtons.ModalButton[] = [
modalButtons.createWarrantBtn,
modalButtons.createBoloBtn,
modalButtons.notepadBtn,
modalButtons.departmentInformationBtn,
];

export function ModalButtons({ initialActiveOfficer }: { initialActiveOfficer: ActiveOfficer }) {
Expand Down
83 changes: 83 additions & 0 deletions apps/client/src/components/leo/modals/department-info-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Button } from "@snailycad/ui";
import { Modal } from "components/modal/Modal";
import { useModal } from "state/modalState";
import { ModalIds } from "types/modal-ids";
import { useTranslations } from "use-intl";
import { useLeoState } from "state/leo-state";

import { useEmsFdState } from "state/ems-fd-state";
import { useRouter } from "next/router";
import { Table, useTableState } from "components/shared/Table";

export function DepartmentInformationModal() {
const activeOfficer = useLeoState((state) => state.activeOfficer);
const activeEmsFdDeputy = useEmsFdState((state) => state.activeDeputy);

const modalState = useModal();
const common = useTranslations("Common");
const t = useTranslations("Leo");
const router = useRouter();
const isLeo = router.pathname.startsWith("/officer");
const activeUnit = isLeo ? activeOfficer : activeEmsFdDeputy;
const links = activeUnit?.department?.links ?? [];
const tableState = useTableState();

console.log({
activeUnit,
links,
});

return (
<Modal
title={t("departmentInformation")}
onClose={() => modalState.closeModal(ModalIds.DepartmentInfo)}
isOpen={modalState.isOpen(ModalIds.DepartmentInfo)}
className="w-[600px]"
>
<p className="text-[17px] max-w-lg dark:text-gray-400 text-neutral-800">
{t("departmentInformationDesc")}
</p>

{links.length <= 0 ? (
<p className="mt-3 dark:text-gray-400 text-neutral-800 max-w-sm">
{t("noDepartmentLinks")}
</p>
) : (
<Table
features={{ isWithinCardOrModal: true }}
data={links.map((url) => {
return {
id: `${url.url}-${url.id}`,
name: url.title,
url: (
<a
className="underline text-blue-400"
rel="noreferrer"
target="_blank"
href={url.url}
>
{url.url}
</a>
),
};
})}
columns={[
{ header: common("name"), accessorKey: "name" },
{ header: common("url"), accessorKey: "url" },
]}
tableState={tableState}
/>
)}

<footer className="flex justify-end mt-5">
<Button
type="reset"
onPress={() => modalState.closeModal(ModalIds.DepartmentInfo)}
variant="cancel"
>
{common("cancel")}
</Button>
</footer>
</Modal>
);
}
5 changes: 5 additions & 0 deletions apps/client/src/components/modal-buttons/buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export const createWarrantBtn: ModalButton = ({ user }) => ({
isEnabled: hasPermission({ userToCheck: user, permissionsToCheck: [Permissions.ManageWarrants] }),
});

export const departmentInformationBtn: ModalButton = () => ({
modalId: ModalIds.DepartmentInfo,
nameKey: ["Leo", "departmentInformation"],
});

export const notepadBtn: ModalButton = () => ({
modalId: ModalIds.Notepad,
nameKey: ["Leo", "notepad"],
Expand Down
5 changes: 5 additions & 0 deletions apps/client/src/pages/ems-fd/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const SearchMedicalRecordModal = dynamic(
{ ssr: false },
);

const DepartmentInfoModal = dynamic(async () => {
return (await import("components/leo/modals/department-info-modal")).DepartmentInformationModal;
});

export default function EmsFDDashboard({ activeDeputy, calls, activeDeputies }: Props) {
useLoadValuesClientSide({
valueTypes: [
Expand Down Expand Up @@ -134,6 +138,7 @@ export default function EmsFDDashboard({ activeDeputy, calls, activeDeputies }:
{isAdmin || state.activeDeputy ? (
<>
<NotepadModal />
<DepartmentInfoModal />

<SearchMedicalRecordModal />
{modalState.isOpen(ModalIds.SearchMedicalRecord) ? null : (
Expand Down
Loading

0 comments on commit bea35c7

Please sign in to comment.