diff --git a/apps/api/src/controllers/business/BusinessController.ts b/apps/api/src/controllers/business/BusinessController.ts index e51ef3dbd..230386228 100644 --- a/apps/api/src/controllers/business/BusinessController.ts +++ b/apps/api/src/controllers/business/BusinessController.ts @@ -11,7 +11,14 @@ import { } from "@snailycad/schemas"; import { BadRequest, NotFound } from "@tsed/exceptions"; import { prisma } from "lib/data/prisma"; -import { type User, EmployeeAsEnum, MiscCadSettings, WhitelistStatus, cad } from "@prisma/client"; +import { + type User, + EmployeeAsEnum, + MiscCadSettings, + WhitelistStatus, + cad, + Prisma, +} from "@prisma/client"; import { validateSchema } from "lib/data/validate-schema"; import { UsePermissions, Permissions } from "middlewares/use-permissions"; import type * as APITypes from "@snailycad/types/api"; @@ -72,6 +79,18 @@ export class BusinessController { return { ownedBusinesses, joinedBusinesses, joinableBusinesses }; } + @Get("/search") + async searchBusinesses(@QueryParams("query") query: string) { + const where: Prisma.BusinessWhereInput = query + ? { name: { contains: query, mode: "insensitive" } } + : {}; + const joinableBusinesses = await prisma.business.findMany({ + where, + }); + + return joinableBusinesses; + } + @Get("/business/:id") async getBusinessById( @Context("user") user: User, diff --git a/apps/api/src/controllers/citizen/vehicles-controller.ts b/apps/api/src/controllers/citizen/vehicles-controller.ts index 59b270e9b..3b78a27ef 100644 --- a/apps/api/src/controllers/citizen/vehicles-controller.ts +++ b/apps/api/src/controllers/citizen/vehicles-controller.ts @@ -39,6 +39,7 @@ import { getLastOfArray, manyToManyHelper } from "lib/data/many-to-many"; import { AllowedFileExtension, allowedFileExtensions } from "@snailycad/config"; import { getImageWebPPath } from "~/lib/images/get-image-webp-path"; import fs from "node:fs/promises"; +import { ExtendedNotFound } from "~/exceptions/extended-not-found"; @Controller("/vehicles") @UseBeforeEach(IsAuth) @@ -484,25 +485,51 @@ export class VehiclesController { throw new NotFound("vehicleNotFound"); } - const newOwner = await prisma.citizen.findFirst({ - where: { - AND: [{ id: data.ownerId }, { NOT: { id: String(vehicle.citizenId) } }], - }, - }); + if (data.businessId) { + const business = await prisma.business.findUnique({ + where: { id: data.businessId }, + }); - if (!newOwner) { - throw new NotFound("newOwnerNotFound"); + if (!business) { + throw new ExtendedNotFound({ business: "businessNotFound" }); + } + + const updatedVehicle = await prisma.registeredVehicle.update({ + where: { id: vehicle.id }, + data: { + Business: { connect: { id: data.businessId } }, + }, + }); + + return updatedVehicle; } - const updatedVehicle = await prisma.registeredVehicle.update({ - where: { id: vehicle.id }, - data: { - citizenId: newOwner.id, - userId: newOwner.userId, - }, - }); + if (data.ownerId) { + const newOwner = await prisma.citizen.findFirst({ + where: { + AND: [{ id: data.ownerId }, { NOT: { id: String(vehicle.citizenId) } }], + }, + }); - return updatedVehicle; + if (!newOwner) { + throw new NotFound("newOwnerNotFound"); + } + + const updatedVehicle = await prisma.registeredVehicle.update({ + where: { id: vehicle.id }, + data: { + citizenId: newOwner.id, + userId: newOwner.userId, + }, + }); + + return updatedVehicle; + } + + throw new ExtendedBadRequest({ + ownerId: "ownerIdOrBusinessIdRequired", + businessId: "ownerIdOrBusinessIdRequired", + }); } @Delete("/:id") diff --git a/apps/client/locales/en/citizen.json b/apps/client/locales/en/citizen.json index 8d1614470..fbb39c0b1 100644 --- a/apps/client/locales/en/citizen.json +++ b/apps/client/locales/en/citizen.json @@ -101,7 +101,8 @@ "trimLevels": "Trim Levels", "image": "Image", "transferVehicleInfo": "Transfer your {model} to a new owner. This means you will lose ownership of this vehicle and will be unable to manage it further.", - "alert_deleteVehicle": "Are you sure you want to delete this vehicle? This action cannot be undone." + "alert_deleteVehicle": "Are you sure you want to delete this vehicle? This action cannot be undone.", + "transferToBusiness": "Transfer to business" }, "Weapons": { "model": "Model", @@ -156,7 +157,7 @@ "LawBook": { "lawBook": "Law Book", "warningApplicable": "Warning Applicable", - "noPenalCodes": "No penal code has been registered", + "noPenalCodes": "No penal code has been registered", "warningNotApplicable": "Warning Not Applicable", "fines": "Fines", "bail": "Bail", @@ -164,4 +165,3 @@ "isPrimary": "Is Primary" } } - \ No newline at end of file diff --git a/apps/client/src/components/citizen/vehicles/modals/transfer-vehicle-modal.tsx b/apps/client/src/components/citizen/vehicles/modals/transfer-vehicle-modal.tsx index 146112cfb..5315595b6 100644 --- a/apps/client/src/components/citizen/vehicles/modals/transfer-vehicle-modal.tsx +++ b/apps/client/src/components/citizen/vehicles/modals/transfer-vehicle-modal.tsx @@ -1,10 +1,18 @@ -import { Loader, Button, TextField, FormRow } from "@snailycad/ui"; +import { + Loader, + Button, + TextField, + FormRow, + SwitchField, + AsyncListSearchField, + Item, +} from "@snailycad/ui"; import { Modal } from "components/modal/Modal"; import { useModal } from "state/modalState"; import { Form, Formik } from "formik"; import useFetch from "lib/useFetch"; import { useTranslations } from "use-intl"; -import type { RegisteredVehicle } from "@snailycad/types"; +import type { Business, RegisteredVehicle } from "@snailycad/types"; import { ModalIds } from "types/modal-ids"; import { handleValidate } from "lib/handleValidate"; import { TRANSFER_VEHICLE_SCHEMA } from "@snailycad/schemas"; @@ -37,6 +45,10 @@ export function TransferVehicleModal({ onTransfer, vehicle }: Props) { const validate = handleValidate(TRANSFER_VEHICLE_SCHEMA); const INITIAL_VALUES = { + transferType: "citizen" as "citizen" | "business", + businessName: "", + businessId: "", + ownerId: "", name: "", }; @@ -49,27 +61,65 @@ export function TransferVehicleModal({ onTransfer, vehicle }: Props) { className="w-[750px]" > - {({ isValid }) => ( + {({ isValid, values, errors, setValues, setFieldValue }) => (

- {t("transferVehicleInfo", { + {t.rich("transferVehicleInfo", { model: vehicle.model.value.value, + span: (children) => {children}, })}

+ + setFieldValue("transferType", isSelected ? "business" : "citizen") + } + > + {t("transferToBusiness")} + + - + {values.transferType === "business" ? ( + + className="w-full" + setValues={({ localValue, node }) => { + const labelValue = + typeof localValue !== "undefined" ? { businessName: localValue } : {}; + const valueField = node?.value ? { businessId: node.key as string } : {}; + + setValues({ ...values, ...labelValue, ...valueField }); + }} + localValue={values.businessName} + errorMessage={errors.businessId} + label="Business" + selectedKey={values.businessId} + fetchOptions={{ + apiPath: (query) => `/businesses/search?query=${query}`, + filterTextRequired: true, + }} + > + {(item) => { + return ( + + {item.name} + + ); + }} + + ) : ( + + )}