From ff9ab4d790fb6588a5dd728df18a13355cb5611c Mon Sep 17 00:00:00 2001 From: Dev-CasperTheGhost <53900565+Dev-CasperTheGhost@users.noreply.github.com> Date: Sun, 24 Oct 2021 10:47:06 +0200 Subject: [PATCH] :tada: add truck-logs --- .../migrations/20211024074740_/migration.sql | 20 ++ .../migrations/20211024083227_/migration.sql | 4 + packages/api/prisma/schema.prisma | 36 +++- .../citizen/TruckLogsController.ts | 187 ++++++++++++++++++ packages/client/locales/en/truck-logs.json | 13 ++ .../nav-dropdowns/CitizenDropdown.tsx | 4 +- .../components/truck-logs/ManageTruckLog.tsx | 147 ++++++++++++++ packages/client/src/pages/citizen/index.tsx | 6 +- packages/client/src/pages/truck-logs.tsx | 154 +++++++++++++++ packages/client/src/types/ModalIds.ts | 3 + packages/client/src/types/prisma.ts | 13 ++ packages/schemas/src/index.ts | 1 + packages/schemas/src/truck-log.ts | 8 + 13 files changed, 583 insertions(+), 13 deletions(-) create mode 100644 packages/api/prisma/migrations/20211024074740_/migration.sql create mode 100644 packages/api/prisma/migrations/20211024083227_/migration.sql create mode 100644 packages/api/src/controllers/citizen/TruckLogsController.ts create mode 100644 packages/client/locales/en/truck-logs.json create mode 100644 packages/client/src/components/truck-logs/ManageTruckLog.tsx create mode 100644 packages/client/src/pages/truck-logs.tsx create mode 100644 packages/schemas/src/truck-log.ts diff --git a/packages/api/prisma/migrations/20211024074740_/migration.sql b/packages/api/prisma/migrations/20211024074740_/migration.sql new file mode 100644 index 000000000..0fcd4f2b4 --- /dev/null +++ b/packages/api/prisma/migrations/20211024074740_/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "TruckLog" ( + "id" TEXT NOT NULL, + "citizenId" TEXT, + "userId" TEXT NOT NULL, + "vehicleId" TEXT, + "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "endedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TruckLog_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "TruckLog" ADD CONSTRAINT "TruckLog_citizenId_fkey" FOREIGN KEY ("citizenId") REFERENCES "Citizen"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TruckLog" ADD CONSTRAINT "TruckLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TruckLog" ADD CONSTRAINT "TruckLog_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "RegisteredVehicle"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/api/prisma/migrations/20211024083227_/migration.sql b/packages/api/prisma/migrations/20211024083227_/migration.sql new file mode 100644 index 000000000..ecb0ef392 --- /dev/null +++ b/packages/api/prisma/migrations/20211024083227_/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "TruckLog" ALTER COLUMN "startedAt" DROP DEFAULT, +ALTER COLUMN "startedAt" SET DATA TYPE VARCHAR(255), +ALTER COLUMN "endedAt" SET DATA TYPE VARCHAR(255); diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index ffe197859..0e75dbe28 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -79,6 +79,7 @@ model User { OfficerLog OfficerLog[] emsFdDeputies EmsFdDeputy[] TaxiCall TaxiCall[] + TruckLog TruckLog[] } model Citizen { @@ -122,6 +123,7 @@ model Citizen { emsFdDeputies EmsFdDeputy[] TaxiCall TaxiCall[] createdTaxiCalls TaxiCall[] @relation("taxiCallCreator") + truckLogs TruckLog[] } enum Rank { @@ -138,20 +140,21 @@ enum WhitelistStatus { } model RegisteredVehicle { - id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id]) + id String @id @default(cuid()) + user User @relation(fields: [userId], references: [id]) userId String - citizen Citizen @relation(fields: [citizenId], references: [id]) + citizen Citizen @relation(fields: [citizenId], references: [id]) citizenId String - vinNumber String @unique @db.VarChar(255) - plate String @unique @db.VarChar(255) - model Value @relation("modelToValue", fields: [modelId], references: [id]) + vinNumber String @unique @db.VarChar(255) + plate String @unique @db.VarChar(255) + model Value @relation("modelToValue", fields: [modelId], references: [id]) modelId String - color String @db.VarChar(255) - createdAt DateTime @default(now()) - registrationStatus Value @relation("registrationStatusToValue", fields: [registrationStatusId], references: [id]) + color String @db.VarChar(255) + createdAt DateTime @default(now()) + registrationStatus Value @relation("registrationStatusToValue", fields: [registrationStatusId], references: [id]) registrationStatusId String - insuranceStatus String @db.VarChar(255) + insuranceStatus String @db.VarChar(255) + TruckLog TruckLog[] } model Weapon { @@ -523,6 +526,19 @@ model EmsFdDeputy { // call911Id String? } +// truck logs +model TruckLog { + id String @id @default(uuid()) + citizen Citizen? @relation(fields: [citizenId], references: [id], onDelete: Cascade) + citizenId String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + vehicle RegisteredVehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull) + vehicleId String? + startedAt String @db.VarChar(255) + endedAt String @db.VarChar(255) +} + // other enum Feature { BLEETER diff --git a/packages/api/src/controllers/citizen/TruckLogsController.ts b/packages/api/src/controllers/citizen/TruckLogsController.ts new file mode 100644 index 000000000..fc0dd5d99 --- /dev/null +++ b/packages/api/src/controllers/citizen/TruckLogsController.ts @@ -0,0 +1,187 @@ +import { User } from ".prisma/client"; +import { validate } from "@snailycad/schemas"; +import { Controller } from "@tsed/di"; +import { BadRequest, NotFound } from "@tsed/exceptions"; +import { BodyParams, Context, PathParams } from "@tsed/platform-params"; +import { Delete, Get, JsonRequestBody, Post, Put } from "@tsed/schema"; +import { prisma } from "../../lib/prisma"; +import { CREATE_TRUCK_LOG_SCHEMA } from "@snailycad/schemas"; +import { IsAuth } from "../../middlewares"; +import { UseBeforeEach } from "@tsed/platform-middlewares"; + +@Controller("/truck-logs") +@UseBeforeEach(IsAuth) +export class TruckLogsController { + @Get("/") + async getTruckLogs(@Context("user") user: User) { + const logs = await prisma.truckLog.findMany({ + where: { + userId: user.id, + }, + include: { + citizen: true, + vehicle: { + include: { + model: true, + registrationStatus: true, + }, + }, + }, + }); + + const registeredVehicles = await prisma.registeredVehicle.findMany({ + where: { + userId: user.id, + }, + include: { + model: true, + }, + }); + + console.log({ registeredVehicles }); + + return { logs, registeredVehicles }; + } + + @Post("/") + async createTruckLog(@Context("user") user: User, @BodyParams() body: JsonRequestBody) { + const error = validate(CREATE_TRUCK_LOG_SCHEMA, body.toJSON(), true); + if (error) { + throw new BadRequest(error); + } + + const citizen = await prisma.citizen.findFirst({ + where: { + id: body.get("citizenId"), + userId: user.id, + }, + }); + + if (!citizen) { + throw new NotFound("citizenNotFound"); + } + + const vehicle = await prisma.registeredVehicle.findFirst({ + where: { + id: body.get("vehicleId"), + userId: user.id, + citizenId: citizen.id, + }, + }); + + if (!vehicle) { + throw new NotFound("vehicleNotFound"); + } + + const log = await prisma.truckLog.create({ + data: { + userId: user.id, + citizenId: body.get("citizenId"), + endedAt: body.get("endedAt"), + startedAt: body.get("startedAt"), + vehicleId: body.get("vehicleId"), + }, + include: { + citizen: true, + vehicle: { + include: { + model: true, + registrationStatus: true, + }, + }, + }, + }); + + return log; + } + + @Put("/:id") + async updateTruckLog( + @Context("user") user: User, + @BodyParams() body: JsonRequestBody, + @PathParams("id") id: string, + ) { + const error = validate(CREATE_TRUCK_LOG_SCHEMA, body.toJSON(), true); + if (error) { + throw new BadRequest(error); + } + + const log = await prisma.truckLog.findFirst({ + where: { + id, + userId: user.id, + }, + }); + + if (!log) { + throw new NotFound("notFound"); + } + + const citizen = await prisma.citizen.findFirst({ + where: { + id: body.get("citizenId"), + userId: user.id, + }, + }); + + if (!citizen) { + throw new NotFound("citizenNotFound"); + } + + const vehicle = await prisma.registeredVehicle.findFirst({ + where: { + id: body.get("vehicleId"), + userId: user.id, + citizenId: citizen.id, + }, + }); + + if (!vehicle) { + throw new NotFound("vehicleNotFound"); + } + + const updated = await prisma.truckLog.update({ + where: { + id, + }, + data: { + citizenId: body.get("citizenId"), + endedAt: body.get("endedAt"), + vehicleId: body.get("vehicleId"), + }, + include: { + citizen: true, + vehicle: { + include: { + model: true, + registrationStatus: true, + }, + }, + }, + }); + + return updated; + } + + @Delete("/:id") + async deleteTruckLog(@Context("user") user: User, @PathParams("id") id: string) { + const log = await prisma.truckLog.findFirst({ + where: { + id, + userId: user.id, + }, + }); + + if (!log) { + throw new NotFound("notFound"); + } + + await prisma.truckLog.delete({ + where: { + id, + }, + }); + + return true; + } +} diff --git a/packages/client/locales/en/truck-logs.json b/packages/client/locales/en/truck-logs.json new file mode 100644 index 000000000..b7950f375 --- /dev/null +++ b/packages/client/locales/en/truck-logs.json @@ -0,0 +1,13 @@ +{ + "TruckLogs": { + "truckLogs": "Truck Logs", + "createTruckLog": "Create Truck Log", + "editTruckLog": "Edit Truck Log", + "deleteTruckLog": "Delete Truck Log", + "driver": "Driver", + "vehicle": "Vehicle", + "startedAt": "Started At", + "endedAt": "Ended At", + "alert_deleteTruckLog": "Are you sure you want to delete this truck log? This action cannot be undone." + } +} diff --git a/packages/client/src/components/nav-dropdowns/CitizenDropdown.tsx b/packages/client/src/components/nav-dropdowns/CitizenDropdown.tsx index 8a8625b73..1e4871adf 100644 --- a/packages/client/src/components/nav-dropdowns/CitizenDropdown.tsx +++ b/packages/client/src/components/nav-dropdowns/CitizenDropdown.tsx @@ -53,8 +53,8 @@ export const CitizenDropdown = () => { {items.map((item) => { - const upperCase = item.toUpperCase() as Feature; - const lower = item.toLowerCase(); + const upperCase = item.replace(/ +/g, "_").toUpperCase() as Feature; + const lower = item.replace(/ +/g, "-").toLowerCase(); if (!enabled[upperCase]) { return null; diff --git a/packages/client/src/components/truck-logs/ManageTruckLog.tsx b/packages/client/src/components/truck-logs/ManageTruckLog.tsx new file mode 100644 index 000000000..31d81b204 --- /dev/null +++ b/packages/client/src/components/truck-logs/ManageTruckLog.tsx @@ -0,0 +1,147 @@ +import { CREATE_TRUCK_LOG_SCHEMA } from "@snailycad/schemas"; +import { Button } from "components/Button"; +import { Error } from "components/form/Error"; +import { FormField } from "components/form/FormField"; +import { FormRow } from "components/form/FormRow"; +import { Input } from "components/form/Input"; +import { Select } from "components/form/Select"; +import { Loader } from "components/Loader"; +import { Modal } from "components/modal/Modal"; +import { useCitizen } from "context/CitizenContext"; +import { useModal } from "context/ModalContext"; +import { Formik } from "formik"; +import { handleValidate } from "lib/handleValidate"; +import useFetch from "lib/useFetch"; +import { FullTruckLog } from "src/pages/truck-logs"; +import { ModalIds } from "types/ModalIds"; +import { RegisteredVehicle } from "types/prisma"; +import { useTranslations } from "use-intl"; + +interface Props { + log: FullTruckLog | null; + registeredVehicles: RegisteredVehicle[]; + onUpdate?: (old: FullTruckLog, newLog: FullTruckLog) => void; + onCreate?: (log: FullTruckLog) => void; + onClose?: () => void; +} + +export const ManageTruckLogModal = ({ + onUpdate, + onCreate, + onClose, + registeredVehicles, + log, +}: Props) => { + const common = useTranslations("Common"); + const t = useTranslations("TruckLogs"); + const { isOpen, closeModal } = useModal(); + const { state, execute } = useFetch(); + const { citizens } = useCitizen(); + + function handleClose() { + onClose?.(); + closeModal(ModalIds.ManageTruckLog); + } + + async function onSubmit(values: typeof INITIAL_VALUES) { + if (log) { + const { json } = await execute(`/truck-logs/${log.id}`, { + method: "PUT", + data: values, + }); + + if (json.id) { + onUpdate?.(log, json); + closeModal(ModalIds.ManageTruckLog); + } + } else { + const { json } = await execute("/truck-logs", { + method: "POST", + data: values, + }); + + if (json.id) { + onCreate?.(json); + closeModal(ModalIds.ManageTruckLog); + } + } + } + + const INITIAL_VALUES = { + endedAt: log?.endedAt ?? "", + startedAt: log?.startedAt ?? "", + vehicleId: log?.vehicleId ?? "", + citizenId: log?.citizenId ?? "", + }; + + const validate = handleValidate(CREATE_TRUCK_LOG_SCHEMA); + + return ( + + + {({ handleSubmit, handleChange, values, isValid, errors }) => ( + + + + + {errors.startedAt} + + + + + {errors.endedAt} + + + + + ({ + label: `${citizen.name} ${citizen.surname}`, + value: citizen.id, + }))} + value={values.citizenId} + /> + {errors.citizenId} + + + + ({ + label: vehicle.model.value, + value: vehicle.id, + }))} + value={values.vehicleId} + /> + {errors.vehicleId} + + + + + )} + + + ); +}; diff --git a/packages/client/src/pages/citizen/index.tsx b/packages/client/src/pages/citizen/index.tsx index 3f241b179..e9261f234 100644 --- a/packages/client/src/pages/citizen/index.tsx +++ b/packages/client/src/pages/citizen/index.tsx @@ -18,6 +18,7 @@ import { makeImageUrl } from "lib/utils"; import { ManageCallModal } from "components/citizen/tow/ManageTowCall"; import { Manage911CallModal } from "components/modals/Manage911CallModal"; import { useFeatureEnabled } from "hooks/useFeatureEnabled"; +import { useAreaOfPlay } from "hooks/useAreaOfPlay"; interface Props { citizens: Citizen[]; @@ -28,6 +29,7 @@ export default function CitizenPage({ citizens }: Props) { const { openModal, closeModal } = useModal(); const [modal, setModal] = React.useState(null); const { TOW, TAXI } = useFeatureEnabled(); + const { showAop, areaOfPlay } = useAreaOfPlay(); return ( @@ -35,7 +37,9 @@ export default function CitizenPage({ citizens }: Props) { {t("citizens")} - SnailyCAD - Citizens + + Citizens{showAop ? - AOP: {areaOfPlay} : null} + diff --git a/packages/client/src/pages/truck-logs.tsx b/packages/client/src/pages/truck-logs.tsx new file mode 100644 index 000000000..ffbd9135e --- /dev/null +++ b/packages/client/src/pages/truck-logs.tsx @@ -0,0 +1,154 @@ +import * as React from "react"; +import Head from "next/head"; +import { Button } from "components/Button"; +import { Layout } from "components/Layout"; +import { ManageTruckLogModal } from "components/truck-logs/ManageTruckLog"; +import { useModal } from "context/ModalContext"; +import { getSessionUser } from "lib/auth"; +import { getTranslations } from "lib/getTranslation"; +import { requestAll } from "lib/utils"; +import type { GetServerSideProps } from "next"; +import { ModalIds } from "types/ModalIds"; +import { Citizen, RegisteredVehicle, TruckLog } from "types/prisma"; +import { useTranslations } from "use-intl"; +import { AlertModal } from "components/modal/AlertModal"; +import useFetch from "lib/useFetch"; + +export type FullTruckLog = TruckLog & { + citizen: Citizen; + vehicle: RegisteredVehicle | null; +}; + +interface Props { + truckLogs: FullTruckLog[]; + registeredVehicles: RegisteredVehicle[]; +} + +export default function TruckLogs({ registeredVehicles, truckLogs }: Props) { + const { openModal, closeModal } = useModal(); + const [logs, setLogs] = React.useState(truckLogs); + const [tempLog, setTempLog] = React.useState(null); + + const t = useTranslations("TruckLogs"); + const common = useTranslations("Common"); + const { execute, state } = useFetch(); + + async function handleDelete() { + if (!tempLog) return; + + const { json } = await execute(`/truck-logs/${tempLog.id}`, { method: "DELETE" }); + + if (json) { + setLogs((p) => p.filter((v) => v.id !== tempLog.id)); + setTempLog(null); + closeModal(ModalIds.AlertDeleteTruckLog); + } + } + + function handleEditClick(log: FullTruckLog) { + setTempLog(log); + openModal(ModalIds.ManageTruckLog); + } + + function handleDeleteClick(log: FullTruckLog) { + setTempLog(log); + openModal(ModalIds.AlertDeleteTruckLog); + } + + return ( + + + {t("truckLogs")} - SnailyCAD + + + + {t("truckLogs")} + + openModal(ModalIds.ManageTruckLog)}>{t("createTruckLog")} + + + + + + + {t("driver")} + {t("vehicle")} + {t("startedAt")} + {t("endedAt")} + {common("actions")} + + + + {logs.map((log) => ( + + + {log.citizen.name} {log.citizen.surname} + + {log.vehicle?.model.value} + {log.startedAt} + {log.endedAt} + + handleEditClick(log)} small variant="success"> + {common("edit")} + + handleDeleteClick(log)} + className="ml-2" + small + variant="danger" + > + {common("delete")} + + + + ))} + + + + + { + setLogs((p) => [log, ...p]); + }} + onUpdate={(old, log) => { + setLogs((p) => { + const idx = p.indexOf(old); + p[idx] = log; + return p; + }); + }} + log={tempLog} + registeredVehicles={registeredVehicles} + onClose={() => setTempLog(null)} + /> + + setTempLog(null)} + /> + + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ locale, req }) => { + const [{ logs, registeredVehicles }, citizens] = await requestAll(req, [ + ["/truck-logs", { logs: [], registeredVehicles: [] }], + ["/citizen", []], + ]); + + return { + props: { + truckLogs: logs, + registeredVehicles, + citizens, + session: await getSessionUser(req.headers), + messages: { + ...(await getTranslations(["truck-logs", "common"], locale)), + }, + }, + }; +}; diff --git a/packages/client/src/types/ModalIds.ts b/packages/client/src/types/ModalIds.ts index 21497f6b1..04d876f3b 100644 --- a/packages/client/src/types/ModalIds.ts +++ b/packages/client/src/types/ModalIds.ts @@ -40,6 +40,8 @@ export const enum ModalIds { ManageDeputy = "ManageDeputyModal", ManageAOP = "ManageAOPModal", + ManageTruckLog = "ManageTruckLogModal", + AlertDeleteCitizen = "alert_DeleteCitizenModal", AlertDeleteVehicle = "alert_DeleteVehicleModal", AlertDeleteWeapon = "alert_DeleteWeaponModal", @@ -52,4 +54,5 @@ export const enum ModalIds { AlertDeleteBolo = "AlertDeleteBoloModal", AlertEnd911Call = "AlertEnd911CallModal", AlertDeleteDeputy = "AlertDeleteDeputyModal", + AlertDeleteTruckLog = "AlertDeleteTruckLogModal", } diff --git a/packages/client/src/types/prisma.ts b/packages/client/src/types/prisma.ts index 90b4c03e9..00c521ea1 100644 --- a/packages/client/src/types/prisma.ts +++ b/packages/client/src/types/prisma.ts @@ -382,6 +382,19 @@ export type Call911Event = { description: string; }; +/** + * Model TruckLog + */ + +export type TruckLog = { + id: string; + citizenId: string | null; + userId: string; + vehicleId: string | null; + startedAt: string; + endedAt: string; +}; + /** * Enums */ diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index 9cad19db1..63d2f7ed5 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -10,3 +10,4 @@ export * from "./dispatch"; export * from "./leo"; export * from "./records"; export * from "./ems-fd"; +export * from "./truck-log"; diff --git a/packages/schemas/src/truck-log.ts b/packages/schemas/src/truck-log.ts new file mode 100644 index 000000000..237468a57 --- /dev/null +++ b/packages/schemas/src/truck-log.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const CREATE_TRUCK_LOG_SCHEMA = z.object({ + endedAt: z.string().min(1).max(255), + startedAt: z.string().min(1).max(255), + citizenId: z.string().min(2).max(255), + vehicleId: z.string().min(2).max(255), +});