diff --git a/apps/api/src/controllers/admin/manage/citizens/records-logs-controller.ts b/apps/api/src/controllers/admin/manage/citizens/records-logs-controller.ts index fb00f8425..0129b7c33 100644 --- a/apps/api/src/controllers/admin/manage/citizens/records-logs-controller.ts +++ b/apps/api/src/controllers/admin/manage/citizens/records-logs-controller.ts @@ -14,7 +14,7 @@ import type * as APITypes from "@snailycad/types/api"; import { AcceptDeclineType, ACCEPT_DECLINE_TYPES } from "../units/manage-units-controller"; import { BadRequest, NotFound } from "@tsed/exceptions"; -const recordsInclude = { +export const recordsLogsInclude = { officer: { include: leoProperties }, violations: { include: { @@ -100,7 +100,7 @@ export class AdminManageCitizensController { skip: includeAll ? undefined : skip, include: { warrant: { include: { officer: { include: leoProperties } } }, - records: { include: recordsInclude }, + records: { include: recordsLogsInclude }, business: { include: { employees: { where: { role: { as: "OWNER" } } } } }, citizen: { include: { user: { select: userProperties }, gender: true, ethnicity: true }, @@ -129,7 +129,7 @@ export class AdminManageCitizensController { orderBy: { createdAt: "desc" }, include: { warrant: { include: { officer: { include: leoProperties } } }, - records: { include: recordsInclude }, + records: { include: recordsLogsInclude }, citizen: { include: { user: { select: userProperties }, gender: true, ethnicity: true }, }, @@ -166,7 +166,7 @@ export class AdminManageCitizensController { data: { status: type === "ACCEPT" ? WhitelistStatus.ACCEPTED : WhitelistStatus.DECLINED, }, - include: recordsInclude, + include: recordsLogsInclude, }); return updated; diff --git a/apps/api/src/controllers/leo/LeoController.ts b/apps/api/src/controllers/leo/LeoController.ts index 3f3655a25..859a64d65 100644 --- a/apps/api/src/controllers/leo/LeoController.ts +++ b/apps/api/src/controllers/leo/LeoController.ts @@ -18,6 +18,8 @@ import type * as APITypes from "@snailycad/types/api"; import { IsFeatureEnabled } from "middlewares/is-enabled"; import { handlePanicButtonPressed } from "lib/leo/send-panic-button-webhook"; import { HandleInactivity } from "middlewares/handle-inactivity"; +import { userProperties } from "~/lib/auth/getSessionUser"; +import { recordsLogsInclude } from "../admin/manage/citizens/records-logs-controller"; @Controller("/leo") @UseBeforeEach(IsAuth) @@ -327,6 +329,41 @@ export class LeoController { return updated; } + + @Get("/my-record-reports") + async getMyRecordReports( + @Context("sessionUserId") sessionUserId: string, + @QueryParams("skip", Number) skip = 0, + @QueryParams("includeAll", Boolean) includeAll = false, + ): Promise { + const where = { + OR: [ + { records: { officer: { userId: sessionUserId } } }, + { warrant: { officer: { userId: sessionUserId } } }, + ], + } satisfies Prisma.RecordLogWhereInput; + + const [totalCount, reports] = await prisma.$transaction([ + prisma.recordLog.count({ + where, + }), + prisma.recordLog.findMany({ + take: includeAll ? undefined : 35, + skip: includeAll ? undefined : skip, + where, + orderBy: { createdAt: "desc" }, + include: { + warrant: { include: { officer: { include: leoProperties } } }, + records: { include: recordsLogsInclude }, + citizen: { + include: { user: { select: userProperties }, gender: true, ethnicity: true }, + }, + }, + }), + ]); + + return { reports, totalCount }; + } } function activeOfficersWhereInput(query: string) { diff --git a/apps/client/locales/en/common.json b/apps/client/locales/en/common.json index 2b6beb7fa..9f07eba9d 100644 --- a/apps/client/locales/en/common.json +++ b/apps/client/locales/en/common.json @@ -113,7 +113,8 @@ "emsFdIncidents": "EMS/FD Incidents", "hospitalServices": "Hospital Services", "pets": "Pets", - "lawBook": "Law Book" + "lawBook": "Law Book", + "myRecordReports": "My Record Reports" }, "Errors": { "unknown": "An unexpected error occurred", diff --git a/apps/client/locales/en/leo.json b/apps/client/locales/en/leo.json index 428fe38fe..cc5f7f13e 100644 --- a/apps/client/locales/en/leo.json +++ b/apps/client/locales/en/leo.json @@ -355,7 +355,10 @@ "LASER": "Laser", "PACE": "Pace", "unassignFromIncident": "Unassign from incident", - "assignToIncident": "Assign to incident" + "assignToIncident": "Assign to incident", + "myRecordReports": "My Record Reports", + "myRecordReportsDescription": "Here you can view all the tickets, arrest reports, written warnings and warrants that officers associated to your account have created.", + "noReportsCreated": "You have not created any reports yet." }, "Bolos": { "activeBolos": "Active Bolos", @@ -387,4 +390,4 @@ "bureauOfFirearms": "Bureau of Firearms", "noWeaponsPendingBof": "There are no weapons pending approval." } -} \ No newline at end of file +} diff --git a/apps/client/src/components/shared/nav/dropdowns/officer-dropdown.tsx b/apps/client/src/components/shared/nav/dropdowns/officer-dropdown.tsx index b92022327..8c3d8bc00 100644 --- a/apps/client/src/components/shared/nav/dropdowns/officer-dropdown.tsx +++ b/apps/client/src/components/shared/nav/dropdowns/officer-dropdown.tsx @@ -55,6 +55,11 @@ export function OfficerDropdown() { href: "/officer/my-officer-logs", show: hasPermissions([Permissions.Leo]), }, + { + name: t("myRecordReports"), + href: "/officer/my-record-reports", + show: hasPermissions([Permissions.Leo]), + }, { name: t("penalCodes"), href: "/officer/penal-codes", diff --git a/apps/client/src/pages/officer/my-record-reports.tsx b/apps/client/src/pages/officer/my-record-reports.tsx new file mode 100644 index 000000000..3ba8c95d5 --- /dev/null +++ b/apps/client/src/pages/officer/my-record-reports.tsx @@ -0,0 +1,131 @@ +import { useTranslations } from "use-intl"; +import { Layout } from "components/Layout"; +import { getSessionUser } from "lib/auth"; +import { getTranslations } from "lib/getTranslation"; +import type { GetServerSideProps } from "next"; +import { RecordType, type Officer, type OfficerLog } from "@snailycad/types"; +import { makeUnitName, requestAll } from "lib/utils"; +import { Title } from "components/shared/Title"; +import { Permissions } from "@snailycad/permissions"; +import type { GetMyRecordReports } from "@snailycad/types/api"; +import { Table, useAsyncTable, useTableState } from "components/shared/Table"; +import { useGenerateCallsign } from "hooks/useGenerateCallsign"; +import { FullDate, Status } from "@snailycad/ui"; +import { ViolationsColumn } from "components/leo/ViolationsColumn"; +import { RecordsCaseNumberColumn } from "components/leo/records-case-number-column"; + +export interface OfficerLogWithOfficer extends Omit { + officer: Officer; +} + +interface Props { + reports: GetMyRecordReports; +} + +export default function MyOfficersLogs({ reports: data }: Props) { + const t = useTranslations("Leo"); + const common = useTranslations("Common"); + + const { generateCallsign } = useGenerateCallsign(); + + const TYPE_LABELS = { + [RecordType.TICKET]: t("ticket"), + [RecordType.ARREST_REPORT]: t("arrestReport"), + [RecordType.WRITTEN_WARNING]: t("writtenWarning"), + }; + + const asyncTable = useAsyncTable({ + fetchOptions: { + pageSize: 25, + onResponse: (json: GetMyRecordReports) => ({ + data: json.reports, + totalCount: json.totalCount, + }), + path: "/leo/my-record-reports", + }, + totalCount: data.totalCount, + initialData: data.reports, + }); + const tableState = useTableState(asyncTable); + + return ( + +
+ {t("myRecordReports")} +

{t("myRecordReportsDescription")}

+
+ + {data.totalCount <= 0 ? ( +

{t("noReportsCreated")}

+ ) : ( + { + const type = item.records !== null ? TYPE_LABELS[item.records.type] : t("warrant"); + const createdAt = item.warrant?.createdAt ?? item.records?.createdAt; + const officer = item.warrant?.officer ?? item.records?.officer; + const officerName = officer && makeUnitName(officer); + const callsign = officer && generateCallsign(officer); + + const extra = item.records + ? { + caseNumber: , + status: {item.records.status}, + postal: item.records.postal || common("none"), + notes: item.records.notes || common("none"), + violations: , + paymentStatus: {item.records.paymentStatus}, + } + : { + caseNumber: "—", + status: item.warrant?.status, + postal: "—", + notes: "—", + violations: "—", + }; + + return { + id: item.id, + type, + citizen: `${item.citizen?.name} ${item.citizen?.surname}`, + officer: callsign && officerName ? `${callsign} ${officerName}` : "—", + ...extra, + createdAt: createdAt ? {createdAt} : "—", + }; + })} + columns={[ + { header: t("caseNumber"), accessorKey: "caseNumber" }, + { header: common("type"), accessorKey: "type" }, + { header: t("citizen"), accessorKey: "citizen" }, + { header: t("officer"), accessorKey: "officer" }, + { header: t("postal"), accessorKey: "postal" }, + { header: t("status"), accessorKey: "status" }, + { header: t("paymentStatus"), accessorKey: "paymentStatus" }, + { header: t("notes"), accessorKey: "notes" }, + { header: t("violations"), accessorKey: "violations" }, + { header: common("createdAt"), accessorKey: "createdAt" }, + ]} + tableState={tableState} + /> + )} + + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ req, locale }) => { + const user = await getSessionUser(req); + const [reports] = await requestAll(req, [ + ["/leo/my-record-reports", { reports: [], totalCount: 0 }], + ]); + + console.log(reports); + + return { + props: { + session: user, + reports, + messages: { + ...(await getTranslations(["leo", "common"], user?.locale ?? locale)), + }, + }, + }; +}; diff --git a/packages/types/src/api/leo.ts b/packages/types/src/api/leo.ts index 07c418cf8..acd7db578 100644 --- a/packages/types/src/api/leo.ts +++ b/packages/types/src/api/leo.ts @@ -1,4 +1,5 @@ import type * as Types from "../index.js"; +import type * as Prisma from "../prisma/index"; /** * @method GET @@ -167,3 +168,23 @@ export type GetUnitQualificationsByUnitIdData = Types.UnitQualification[]; * @route /leo/callsign/:officerId */ export type PutLeoCallsignData = Types.Officer; + +/** + * @method GET + * @route /leo/my-record-reports + */ +export interface GetMyRecordReports { + totalCount: number; + reports: (Prisma.RecordLog & { + business?: Prisma.Business | null; + citizen?: + | (Prisma.Citizen & { + user: Types.User | null; + ethnicity?: Prisma.Value | null; + gender?: Prisma.Value | null; + }) + | null; + warrant: Types.Warrant | null; + records: Types.Record | null; + })[]; +}