From 4d408cc8c68ff1a1136db4d8d03a765067b0389d Mon Sep 17 00:00:00 2001 From: Dev-CasperTheGhost <53900565+Dev-CasperTheGhost@users.noreply.github.com> Date: Sun, 24 Oct 2021 09:24:50 +0200 Subject: [PATCH] :tada: implement taxi --- .../migrations/20211024071230_/migration.sql | 21 +++ packages/api/prisma/schema.prisma | 17 ++ .../src/controllers/calls/TaxiController.ts | 155 +++++++++++++++++ .../{tow => calls}/TowController.ts | 0 packages/api/src/services/SocketService.ts | 14 +- packages/client/locales/en/calls.json | 2 + .../citizen/tow/AssignToTowCall.tsx | 9 +- .../components/citizen/tow/ManageTowCall.tsx | 25 ++- packages/client/src/pages/citizen/index.tsx | 22 ++- packages/client/src/pages/taxi.tsx | 163 ++++++++++++++++++ packages/client/src/pages/tow.tsx | 4 +- packages/client/src/types/prisma.ts | 14 ++ 12 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 packages/api/prisma/migrations/20211024071230_/migration.sql create mode 100644 packages/api/src/controllers/calls/TaxiController.ts rename packages/api/src/controllers/{tow => calls}/TowController.ts (100%) create mode 100644 packages/client/src/pages/taxi.tsx diff --git a/packages/api/prisma/migrations/20211024071230_/migration.sql b/packages/api/prisma/migrations/20211024071230_/migration.sql new file mode 100644 index 000000000..1d7c4f8de --- /dev/null +++ b/packages/api/prisma/migrations/20211024071230_/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "TaxiCall" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "assignedUnitId" TEXT, + "location" VARCHAR(255) NOT NULL, + "description" TEXT NOT NULL, + "creatorId" TEXT, + + CONSTRAINT "TaxiCall_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "TaxiCall" ADD CONSTRAINT "TaxiCall_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TaxiCall" ADD CONSTRAINT "TaxiCall_assignedUnitId_fkey" FOREIGN KEY ("assignedUnitId") REFERENCES "Citizen"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TaxiCall" ADD CONSTRAINT "TaxiCall_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "Citizen"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 24d4ab989..ffe197859 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -78,6 +78,7 @@ model User { Call911 Call911[] OfficerLog OfficerLog[] emsFdDeputies EmsFdDeputy[] + TaxiCall TaxiCall[] } model Citizen { @@ -119,6 +120,8 @@ model Citizen { warrants Warrant[] Record Record[] emsFdDeputies EmsFdDeputy[] + TaxiCall TaxiCall[] + createdTaxiCalls TaxiCall[] @relation("taxiCallCreator") } enum Rank { @@ -277,6 +280,20 @@ model TowCall { creatorId String? } +// taxi +model TaxiCall { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id]) + userId String + assignedUnit Citizen? @relation(fields: [assignedUnitId], references: [id]) + assignedUnitId String? + location String @db.VarChar(255) + description String @db.Text + creator Citizen? @relation("taxiCallCreator", fields: [creatorId], references: [id]) + creatorId String? +} + // businesses model Business { id String @id @default(cuid()) diff --git a/packages/api/src/controllers/calls/TaxiController.ts b/packages/api/src/controllers/calls/TaxiController.ts new file mode 100644 index 000000000..267e946be --- /dev/null +++ b/packages/api/src/controllers/calls/TaxiController.ts @@ -0,0 +1,155 @@ +import { Controller, BodyParams, Context, UseBefore, PathParams } from "@tsed/common"; +import { Delete, Get, JsonRequestBody, Post, Put } from "@tsed/schema"; +import { prisma } from "../../lib/prisma"; +import { validate, TOW_SCHEMA, UPDATE_TOW_SCHEMA } from "@snailycad/schemas"; +import { BadRequest, NotFound } from "@tsed/exceptions"; +import { IsAuth } from "../../middlewares"; +import { Socket } from "../../services/SocketService"; + +const CITIZEN_SELECTS = { + name: true, + surname: true, + id: true, +}; + +@Controller("/taxi") +export class TowController { + private socket: Socket; + constructor(socket: Socket) { + this.socket = socket; + } + + @Get("/") + async getTaxiCalls() { + const calls = await prisma.taxiCall.findMany({ + include: { + assignedUnit: { + select: CITIZEN_SELECTS, + }, + creator: { + select: CITIZEN_SELECTS, + }, + }, + }); + + return calls; + } + + @UseBefore(IsAuth) + @Post("/") + async createTaxiCall(@BodyParams() body: JsonRequestBody, @Context() ctx: Context) { + const error = validate(TOW_SCHEMA, body.toJSON(), true); + if (error) { + throw new BadRequest(error); + } + + const citizen = await prisma.citizen.findUnique({ + where: { + id: body.get("creatorId"), + }, + }); + + if (!citizen || citizen.userId !== ctx.get("user").id) { + throw new NotFound("notFound"); + } + + const call = await prisma.taxiCall.create({ + data: { + creatorId: body.get("creatorId"), + userId: ctx.get("user").id, + description: body.get("description"), + location: body.get("location"), + }, + include: { + assignedUnit: { + select: CITIZEN_SELECTS, + }, + creator: { + select: CITIZEN_SELECTS, + }, + }, + }); + + this.socket.emitCreateTaxiCall(call); + + return call; + } + + @UseBefore(IsAuth) + @Put("/:id") + async updateTaxiCall(@PathParams("id") callId: string, @BodyParams() body: JsonRequestBody) { + const error = validate(UPDATE_TOW_SCHEMA, body.toJSON(), true); + if (error) { + throw new BadRequest(error); + } + + console.log({ callId }); + + const call = await prisma.taxiCall.findUnique({ + where: { + id: callId, + }, + }); + + if (!call) { + throw new NotFound("notFound"); + } + + const rawAssignedUnitId = body.get("assignedUnitId"); + const assignedUnitId = + rawAssignedUnitId === null + ? { + disconnect: true, + } + : body.get("assignedUnitId") + ? { connect: { id: body.get("assignedUnitId") } } + : undefined; + + const updated = await prisma.taxiCall.update({ + where: { + id: callId, + }, + data: { + description: body.get("description"), + location: body.get("location"), + assignedUnit: assignedUnitId, + }, + include: { + assignedUnit: { + select: CITIZEN_SELECTS, + }, + creator: { + select: CITIZEN_SELECTS, + }, + }, + }); + + this.socket.emitUpdateTaxiCall(updated); + + return updated; + } + + @UseBefore(IsAuth) + @Delete("/:id") + async deleteTowCall(@PathParams("id") callId: string) { + const call = await prisma.taxiCall.findUnique({ + where: { + id: callId, + }, + }); + + if (!call) { + throw new NotFound("notFound"); + } + + await prisma.taxiCall.delete({ + where: { + id: call.id, + }, + }); + + this.socket.emitDeleteTaxiCall(call); + + return true; + } +} diff --git a/packages/api/src/controllers/tow/TowController.ts b/packages/api/src/controllers/calls/TowController.ts similarity index 100% rename from packages/api/src/controllers/tow/TowController.ts rename to packages/api/src/controllers/calls/TowController.ts diff --git a/packages/api/src/services/SocketService.ts b/packages/api/src/services/SocketService.ts index ff1ee8049..60a07af97 100644 --- a/packages/api/src/services/SocketService.ts +++ b/packages/api/src/services/SocketService.ts @@ -1,7 +1,7 @@ import { Nsp, SocketService } from "@tsed/socketio"; import * as SocketIO from "socket.io"; import { SocketEvents } from "@snailycad/config"; -import { Call911, TowCall, Bolo, Call911Event } from ".prisma/client"; +import { Call911, TowCall, Bolo, Call911Event, TaxiCall } from ".prisma/client"; @SocketService("/") export class Socket { @@ -67,4 +67,16 @@ export class Socket { emitAddCallEvent(event: Call911Event) { this.io.sockets.emit(SocketEvents.AddCallEvent, event); } + + emitCreateTaxiCall(call: TaxiCall) { + this.io.sockets.emit(SocketEvents.CreateTaxiCall, call); + } + + emitUpdateTaxiCall(call: TaxiCall) { + this.io.sockets.emit(SocketEvents.UpdateTaxiCall, call); + } + + emitDeleteTaxiCall(call: TaxiCall) { + this.io.sockets.emit(SocketEvents.EndTaxiCall, call); + } } diff --git a/packages/client/locales/en/calls.json b/packages/client/locales/en/calls.json index a339a25a7..c3aa54cd4 100644 --- a/packages/client/locales/en/calls.json +++ b/packages/client/locales/en/calls.json @@ -14,11 +14,13 @@ "createTaxiCall": "Create taxi call", "create911Call": "Create 911 call", "editTowCall": "Edit tow call", + "editTaxiCall": "Edit taxi call", "assignedUnits": "Assigned Units", "selectCitizen": "Select citizen", "endCall": "End Call", "end911Call": "End 911 Call", "addEvent": "Add Event", + "selectUnit": "Select Unit", "noEvents": "This call does not have any events", "alert_end911Call": "Are you sure you want to end this call?", "alert_endTowCall": "Are you sure you want to end this call?" diff --git a/packages/client/src/components/citizen/tow/AssignToTowCall.tsx b/packages/client/src/components/citizen/tow/AssignToTowCall.tsx index 48bc9c8cf..fa79048ad 100644 --- a/packages/client/src/components/citizen/tow/AssignToTowCall.tsx +++ b/packages/client/src/components/citizen/tow/AssignToTowCall.tsx @@ -12,6 +12,7 @@ import { ModalIds } from "types/ModalIds"; import { TowCall } from "types/prisma"; import { useCitizen } from "context/CitizenContext"; import { FullTowCall } from "src/pages/tow"; +import { useRouter } from "next/router"; interface Props { call: FullTowCall | null; @@ -24,6 +25,9 @@ export const AssignToCallModal = ({ call, onSuccess }: Props) => { const common = useTranslations("Common"); const t = useTranslations("Calls"); const { citizens } = useCitizen(); + const router = useRouter(); + + const isTow = router.pathname === "/tow"; const INITIAL_VALUES = { assignedUnitId: call?.assignedUnitId ?? "", @@ -40,7 +44,8 @@ export const AssignToCallModal = ({ call, onSuccess }: Props) => { return; } - const { json } = await execute(`/tow/${call.id}`, { + const path = isTow ? `/tow/${call.id}` : `/taxi/${call.id}`; + const { json } = await execute(path, { method: "PUT", data: { ...call, ...values }, }); @@ -53,7 +58,7 @@ export const AssignToCallModal = ({ call, onSuccess }: Props) => { return ( closeModal(ModalIds.AssignToTowCall)} className="min-w-[500px]" diff --git a/packages/client/src/components/citizen/tow/ManageTowCall.tsx b/packages/client/src/components/citizen/tow/ManageTowCall.tsx index d042f04a7..e803362a1 100644 --- a/packages/client/src/components/citizen/tow/ManageTowCall.tsx +++ b/packages/client/src/components/citizen/tow/ManageTowCall.tsx @@ -14,6 +14,7 @@ import { useModal } from "context/ModalContext"; import { Formik } from "formik"; import { handleValidate } from "lib/handleValidate"; import useFetch from "lib/useFetch"; +import { useRouter } from "next/router"; import toast from "react-hot-toast"; import { ModalIds } from "types/ModalIds"; import { TowCall } from "types/prisma"; @@ -21,21 +22,34 @@ import { useTranslations } from "use-intl"; interface Props { call: TowCall | null; + isTow?: boolean; onUpdate?: (old: TowCall, newC: TowCall) => void; onDelete?: (call: TowCall) => void; } -export const ManageTowCallModal = ({ onDelete, onUpdate, call }: Props) => { +export const ManageCallModal = ({ onDelete, onUpdate, isTow: tow, call }: Props) => { const common = useTranslations("Common"); const t = useTranslations("Calls"); const { isOpen, closeModal, openModal } = useModal(); const { state, execute } = useFetch(); const { citizens } = useCitizen(); + const router = useRouter(); + + const isTowPath = router.pathname === "/tow"; + const isTow = typeof tow === "undefined" ? isTowPath : tow; + const title = isTow + ? call + ? t("editTowCall") + : t("createTowCall") + : call + ? t("editTaxiCall") + : t("createTaxiCall"); async function handleEndCall() { if (!call) return; - const { json } = await execute(`/tow/${call.id}`, { + const path = isTow ? `/tow/${call.id}` : `/taxi/${call.id}`; + const { json } = await execute(path, { method: "DELETE", }); @@ -47,7 +61,8 @@ export const ManageTowCallModal = ({ onDelete, onUpdate, call }: Props) => { async function onSubmit(values: typeof INITIAL_VALUES) { if (call) { - const { json } = await execute(`/tow/${call.id}`, { + const path = isTow ? `/tow/${call.id}` : `/taxi/${call.id}`; + const { json } = await execute(path, { method: "PUT", data: { ...call, ...values, assignedUnitId: (call as any).assignedUnit?.id ?? "" }, }); @@ -56,7 +71,7 @@ export const ManageTowCallModal = ({ onDelete, onUpdate, call }: Props) => { onUpdate?.(call, json); } } else { - const { json } = await execute("/tow", { + const { json } = await execute(isTow ? "/tow" : "/taxi", { method: "POST", data: values, }); @@ -81,7 +96,7 @@ export const ManageTowCallModal = ({ onDelete, onUpdate, call }: Props) => { return ( closeModal(ModalIds.ManageTowCall)} - title={call ? t("editTowCall") : t("createTowCall")} + title={title} isOpen={isOpen(ModalIds.ManageTowCall)} className="min-w-[700px]" > diff --git a/packages/client/src/pages/citizen/index.tsx b/packages/client/src/pages/citizen/index.tsx index ad6d449ae..7a0625c4b 100644 --- a/packages/client/src/pages/citizen/index.tsx +++ b/packages/client/src/pages/citizen/index.tsx @@ -1,3 +1,4 @@ +import * as React from "react"; import type { GetServerSideProps } from "next"; import type { Citizen } from "types/prisma"; import Link from "next/link"; @@ -14,7 +15,7 @@ import { RegisterVehicleModal } from "components/citizen/modals/RegisterVehicleM import { RegisterWeaponModal } from "components/citizen/modals/RegisterWeaponModal"; import { PersonFill } from "react-bootstrap-icons"; import { makeImageUrl } from "lib/utils"; -import { ManageTowCallModal } from "components/citizen/tow/ManageTowCall"; +import { ManageCallModal } from "components/citizen/tow/ManageTowCall"; import { Manage911CallModal } from "components/modals/Manage911CallModal"; interface Props { @@ -24,6 +25,7 @@ interface Props { export default function CitizenPage({ citizens }: Props) { const t = useTranslations("Citizen"); const { openModal, closeModal } = useModal(); + const [modal, setModal] = React.useState(null); return ( @@ -48,10 +50,22 @@ export default function CitizenPage({ citizens }: Props) {
    - - + + + {calls.length <= 0 ? ( +

    {t("noTaxiCalls")}

    + ) : ( +
    + + + + + + + + + + + + {calls.map((call) => ( + + + + + + + + ))} + +
    {t("location")}{common("description")}{t("caller")}{t("assignedUnit")}{common("actions")}
    {call.location}{call.description} + {call.creator.name} {call.creator.surname} + {assignedUnit(call)} + + +
    +
    + )} + + + + + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ locale, req }) => { + const [data, citizens] = await requestAll(req, [ + ["/taxi", []], + ["/citizen", []], + ]); + + return { + props: { + calls: data, + citizens, + session: await getSessionUser(req.headers), + messages: { + ...(await getTranslations(["calls", "common"], locale)), + }, + }, + }; +}; diff --git a/packages/client/src/pages/tow.tsx b/packages/client/src/pages/tow.tsx index f94ce1380..13e67ef28 100644 --- a/packages/client/src/pages/tow.tsx +++ b/packages/client/src/pages/tow.tsx @@ -12,7 +12,7 @@ import { SocketEvents } from "@snailycad/config"; import { useModal } from "context/ModalContext"; import { ModalIds } from "types/ModalIds"; import { AssignToCallModal } from "components/citizen/tow/AssignToTowCall"; -import { ManageTowCallModal } from "components/citizen/tow/ManageTowCall"; +import { ManageCallModal } from "components/citizen/tow/ManageTowCall"; import { requestAll } from "lib/utils"; export type FullTowCall = TowCall & { assignedUnit: Citizen | null; creator: Citizen }; @@ -139,7 +139,7 @@ export default function Tow(props: Props) { )} - + ); } diff --git a/packages/client/src/types/prisma.ts b/packages/client/src/types/prisma.ts index f40c27644..90b4c03e9 100644 --- a/packages/client/src/types/prisma.ts +++ b/packages/client/src/types/prisma.ts @@ -170,6 +170,20 @@ export type TowCall = { creatorId: string; }; +/** + * Model TaxiCall + */ + +export type TaxiCall = { + id: string; + createdAt: Date; + userId: string; + assignedUnitId: string | null; + location: string; + description: string; + creatorId: string; +}; + /** * Model Business */