From bea35c760ecd74e0b349099cbf1976758475eebd Mon Sep 17 00:00:00 2001 From: Casper Iversen <53900565+casperiv0@users.noreply.github.com> Date: Fri, 8 Sep 2023 17:29:32 +0200 Subject: [PATCH] feat: able to add department links (#1786) --- .../migration.sql | 12 ++ apps/api/prisma/schema.prisma | 9 ++ .../admin/values/import-values-controller.ts | 28 +++- .../admin/values/values-controller.ts | 2 +- apps/api/src/utils/leo/includes.ts | 4 +- apps/client/locales/en/leo.json | 5 +- .../admin/values/ManageValueModal.tsx | 2 + .../values/manage-modal/department-fields.tsx | 3 + .../manage-modal/department-links-section.tsx | 123 ++++++++++++++++++ .../src/components/ems-fd/ModalButtons.tsx | 4 + .../src/components/leo/ModalButtons.tsx | 1 + .../leo/modals/department-info-modal.tsx | 83 ++++++++++++ .../src/components/modal-buttons/buttons.ts | 5 + apps/client/src/pages/ems-fd/index.tsx | 5 + apps/client/src/pages/officer/index.tsx | 4 + apps/client/src/types/modal-ids.ts | 1 + packages/schemas/src/admin/values/import.ts | 3 + packages/types/src/index.ts | 7 +- 18 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 apps/api/prisma/migrations/20230908142856_department_links/migration.sql create mode 100644 apps/client/src/components/admin/values/manage-modal/department-links-section.tsx create mode 100644 apps/client/src/components/leo/modals/department-info-modal.tsx diff --git a/apps/api/prisma/migrations/20230908142856_department_links/migration.sql b/apps/api/prisma/migrations/20230908142856_department_links/migration.sql new file mode 100644 index 000000000..bc0daba9b --- /dev/null +++ b/apps/api/prisma/migrations/20230908142856_department_links/migration.sql @@ -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; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index ad1c370d5..da2038587 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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 { diff --git a/apps/api/src/controllers/admin/values/import-values-controller.ts b/apps/api/src/controllers/admin/values/import-values-controller.ts index 7e5b92bc7..6a7f64836 100644 --- a/apps/api/src/controllers/admin/values/import-values-controller.ts +++ b/apps/api/src/controllers/admin/values/import-values-controller.ts @@ -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, @@ -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 }; }), ); }, diff --git a/apps/api/src/controllers/admin/values/values-controller.ts b/apps/api/src/controllers/admin/values/values-controller.ts index d8417badd..086e02fed 100644 --- a/apps/api/src/controllers/admin/values/values-controller.ts +++ b/apps/api/src/controllers/admin/values/values-controller.ts @@ -39,7 +39,7 @@ export const GET_VALUES: Partial> = { 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 } } }, diff --git a/apps/api/src/utils/leo/includes.ts b/apps/api/src/utils/leo/includes.ts index fe4c0362a..ea567cc8c 100644 --- a/apps/api/src/utils/leo/includes.ts +++ b/apps/api/src/utils/leo/includes.ts @@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client"; import { userProperties } from "lib/auth/getSessionUser"; export const unitProperties = Prisma.validator()({ - 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 } }, @@ -14,7 +14,7 @@ export const unitProperties = Prisma.validator()({ }); export const _leoProperties = Prisma.validator()({ - 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 } }, diff --git a/apps/client/locales/en/leo.json b/apps/client/locales/en/leo.json index f79981101..48d0c84d5 100644 --- a/apps/client/locales/en/leo.json +++ b/apps/client/locales/en/leo.json @@ -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", diff --git a/apps/client/src/components/admin/values/ManageValueModal.tsx b/apps/client/src/components/admin/values/ManageValueModal.tsx index 5ee860097..89ef94951 100644 --- a/apps/client/src/components/admin/values/ManageValueModal.tsx +++ b/apps/client/src/components/admin/values/ManageValueModal.tsx @@ -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) { diff --git a/apps/client/src/components/admin/values/manage-modal/department-fields.tsx b/apps/client/src/components/admin/values/manage-modal/department-fields.tsx index da9e76f5a..c0f29480a 100644 --- a/apps/client/src/components/admin/values/manage-modal/department-fields.tsx +++ b/apps/client/src/components/admin/values/manage-modal/department-fields.tsx @@ -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", @@ -105,6 +106,8 @@ export function DepartmentFields() { value={values.extraFields} placeholder="JSON" /> + + ); } diff --git a/apps/client/src/components/admin/values/manage-modal/department-links-section.tsx b/apps/client/src/components/admin/values/manage-modal/department-links-section.tsx new file mode 100644 index 000000000..32cb86646 --- /dev/null +++ b/apps/client/src/components/admin/values/manage-modal/department-links-section.tsx @@ -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 ( +
+
+

Links

+ + setOpenPopover(v ? "new" : null)} + trigger={ + + } + /> +
+ + {values.departmentLinks.length <= 0 ? ( +

+ No links added yet. Click the Add Link button to add a link. +

+ ) : ( + { + return { + id: `${url.url}-${url.id}`, + name: url.title, + url: url.url, + actions: ( + + ), + }; + })} + columns={[ + { header: common("name"), accessorKey: "name" }, + { header: common("url"), accessorKey: "url" }, + { header: common("actions"), accessorKey: "actions" }, + ]} + /> + )} + + ); +} + +interface LinkPopoverProps { + trigger: React.ReactNode; + isPopoverOpen: boolean; + setIsPopoverOpen: React.Dispatch>; + + 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 ( + + + {props.trigger} + + + +

Add Link

+ +
+ setName(value)} /> + setUrl(value)} + /> + + +
+ + +
+
+ ); +} diff --git a/apps/client/src/components/ems-fd/ModalButtons.tsx b/apps/client/src/components/ems-fd/ModalButtons.tsx index 035d0be41..612742eb5 100644 --- a/apps/client/src/components/ems-fd/ModalButtons.tsx +++ b/apps/client/src/components/ems-fd/ModalButtons.tsx @@ -49,6 +49,10 @@ const buttons: MButton[] = [ nameKey: ["Leo", "notepad"], modalId: ModalIds.Notepad, }, + { + nameKey: ["Leo", "departmentInformation"], + modalId: ModalIds.DepartmentInfo, + }, ]; export function ModalButtons({ diff --git a/apps/client/src/components/leo/ModalButtons.tsx b/apps/client/src/components/leo/ModalButtons.tsx index 2966c5ccf..0d6813770 100644 --- a/apps/client/src/components/leo/ModalButtons.tsx +++ b/apps/client/src/components/leo/ModalButtons.tsx @@ -43,6 +43,7 @@ const buttons: modalButtons.ModalButton[] = [ modalButtons.createWarrantBtn, modalButtons.createBoloBtn, modalButtons.notepadBtn, + modalButtons.departmentInformationBtn, ]; export function ModalButtons({ initialActiveOfficer }: { initialActiveOfficer: ActiveOfficer }) { diff --git a/apps/client/src/components/leo/modals/department-info-modal.tsx b/apps/client/src/components/leo/modals/department-info-modal.tsx new file mode 100644 index 000000000..30f588fe6 --- /dev/null +++ b/apps/client/src/components/leo/modals/department-info-modal.tsx @@ -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 ( + modalState.closeModal(ModalIds.DepartmentInfo)} + isOpen={modalState.isOpen(ModalIds.DepartmentInfo)} + className="w-[600px]" + > +

+ {t("departmentInformationDesc")} +

+ + {links.length <= 0 ? ( +

+ {t("noDepartmentLinks")} +

+ ) : ( +
{ + return { + id: `${url.url}-${url.id}`, + name: url.title, + url: ( + + {url.url} + + ), + }; + })} + columns={[ + { header: common("name"), accessorKey: "name" }, + { header: common("url"), accessorKey: "url" }, + ]} + tableState={tableState} + /> + )} + +
+ +
+ + ); +} diff --git a/apps/client/src/components/modal-buttons/buttons.ts b/apps/client/src/components/modal-buttons/buttons.ts index d09860e06..76957661b 100644 --- a/apps/client/src/components/modal-buttons/buttons.ts +++ b/apps/client/src/components/modal-buttons/buttons.ts @@ -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"], diff --git a/apps/client/src/pages/ems-fd/index.tsx b/apps/client/src/pages/ems-fd/index.tsx index 758f1871a..dca7b7613 100644 --- a/apps/client/src/pages/ems-fd/index.tsx +++ b/apps/client/src/pages/ems-fd/index.tsx @@ -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: [ @@ -134,6 +138,7 @@ export default function EmsFDDashboard({ activeDeputy, calls, activeDeputies }: {isAdmin || state.activeDeputy ? ( <> + {modalState.isOpen(ModalIds.SearchMedicalRecord) ? null : ( diff --git a/apps/client/src/pages/officer/index.tsx b/apps/client/src/pages/officer/index.tsx index 29bb72399..e8f98b945 100644 --- a/apps/client/src/pages/officer/index.tsx +++ b/apps/client/src/pages/officer/index.tsx @@ -101,6 +101,9 @@ const Modals = { return (await import("components/leo/modals/SwitchDivisionCallsignModal")) .SwitchDivisionCallsignModal; }), + DepartmentInfoModal: dynamic(async () => { + return (await import("components/leo/modals/department-info-modal")).DepartmentInformationModal; + }), }; interface Props { @@ -207,6 +210,7 @@ export default function OfficerDashboard({ <> + {/* name search have their own vehicle/weapon search modal */} {modalState.isOpen(ModalIds.NameSearch) ? null : ( diff --git a/apps/client/src/types/modal-ids.ts b/apps/client/src/types/modal-ids.ts index 66d7e1879..714b427a2 100644 --- a/apps/client/src/types/modal-ids.ts +++ b/apps/client/src/types/modal-ids.ts @@ -94,6 +94,7 @@ export const enum ModalIds { BusinessSearch = "BusinessSearchModal", AlertPurgeIncidents = "AlertPurgeIncidents", PrivateMessage = "PrivateMessage", + DepartmentInfo = "DepartmentInfoModal", SearchMedicalRecord = "SearchMedicalRecordModal", CreateMedicalRecord = "CreateMedicalRecordModal", diff --git a/packages/schemas/src/admin/values/import.ts b/packages/schemas/src/admin/values/import.ts index 1548a5e82..b021b39f0 100644 --- a/packages/schemas/src/admin/values/import.ts +++ b/packages/schemas/src/admin/values/import.ts @@ -80,6 +80,9 @@ export const DEPARTMENT_SCHEMA = BASE_VALUE_SCHEMA.extend({ isConfidential: z.boolean().nullish(), extraFields: z.any().nullish(), customTemplate: z.string().nullish(), + departmentLinks: z + .array(z.object({ title: z.string().max(255), url: z.string().url() })) + .nullish(), }); export const DEPARTMENT_ARR = z.array(DEPARTMENT_SCHEMA).min(1); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 030184c78..53e9a3d73 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -230,7 +230,12 @@ export type DivisionValue = Prisma.DivisionValue & { value: Value }; export type CallTypeValue = Prisma.CallTypeValue & { value: Value }; -export type DepartmentValue = Prisma.DepartmentValue & { value: Value }; +export type DepartmentValueLink = Prisma.DepartmentValueLink; + +export type DepartmentValue = Prisma.DepartmentValue & { + value: Value; + links?: DepartmentValueLink[]; +}; export type DriversLicenseCategoryValue = Prisma.DriversLicenseCategoryValue & { value: Value;