From 450a06efa6cf38bf4de9b4bb1ea3897659a56551 Mon Sep 17 00:00:00 2001 From: casperiv0 <53900565+casperiv0@users.noreply.github.com> Date: Sat, 9 Sep 2023 08:12:23 +0200 Subject: [PATCH] feat: improved active units search --- .../controllers/ems-fd/ems-fd-controller.ts | 47 ++++ apps/api/src/controllers/leo/LeoController.ts | 63 +++-- apps/client/locales/en/leo.json | 4 +- .../manage-modal/department-links-section.tsx | 4 - .../components/dispatch/active-deputies.tsx | 230 +++++++++-------- .../components/dispatch/active-officers.tsx | 242 ++++++++++-------- .../active-units/ActiveUnitsSearch.tsx | 36 --- .../active-units/active-units-search.tsx | 69 +++++ .../CustomFieldSearch/CustomFieldSearch.tsx | 1 + .../leo/modals/department-info-modal.tsx | 5 - .../src/hooks/shared/useActiveUnitsFilter.ts | 63 ----- apps/client/src/pages/officer/index.tsx | 101 ++++---- 12 files changed, 470 insertions(+), 395 deletions(-) delete mode 100644 apps/client/src/components/dispatch/active-units/ActiveUnitsSearch.tsx create mode 100644 apps/client/src/components/dispatch/active-units/active-units-search.tsx delete mode 100644 apps/client/src/hooks/shared/useActiveUnitsFilter.ts diff --git a/apps/api/src/controllers/ems-fd/ems-fd-controller.ts b/apps/api/src/controllers/ems-fd/ems-fd-controller.ts index f66418f87..03d33ff5f 100644 --- a/apps/api/src/controllers/ems-fd/ems-fd-controller.ts +++ b/apps/api/src/controllers/ems-fd/ems-fd-controller.ts @@ -17,6 +17,7 @@ import { ShouldDoType, type User, Feature, + Prisma, } from "@prisma/client"; import type { EmsFdDeputy } from "@snailycad/types"; import { AllowedFileExtension, allowedFileExtensions } from "@snailycad/config"; @@ -193,6 +194,9 @@ export class EmsFdController { async getActiveDeputies( @Context("cad") cad: { miscCadSettings: MiscCadSettings }, @Context("user") user: User, + @QueryParams("includeAll", Boolean) includeAll = false, + @QueryParams("skip", Number) skip = 0, + @QueryParams("query", String) query?: string, ): Promise { const unitsInactivityFilter = getInactivityFilter( cad, @@ -205,13 +209,17 @@ export class EmsFdController { select: { departmentId: true }, }); + const emsFdWhere = query ? activeEmsFdDeputiesWhereInput(query) : undefined; const [deputies, combinedEmsFdDeputies] = await prisma.$transaction([ prisma.emsFdDeputy.findMany({ orderBy: { updatedAt: "desc" }, + take: includeAll ? undefined : 12, + skip: includeAll ? undefined : skip, where: { status: { NOT: { shouldDo: ShouldDoType.SET_OFF_DUTY } }, departmentId: activeDispatcher?.departmentId || undefined, ...(unitsInactivityFilter?.filter ?? {}), + ...(emsFdWhere ?? {}), }, include: unitProperties, }), @@ -591,3 +599,42 @@ export class EmsFdController { } } } + +function activeEmsFdDeputiesWhereInput(query: string) { + const [name, surname] = query.toString().toLowerCase().split(/ +/g); + + return { + OR: [ + { callsign: { contains: query, mode: "insensitive" } }, + { callsign2: { contains: query, mode: "insensitive" } }, + { division: { value: { value: { contains: query, mode: "insensitive" } } } }, + { department: { value: { value: { contains: query, mode: "insensitive" } } } }, + { badgeNumberString: { contains: query, mode: "insensitive" } }, + { rank: { value: { contains: query, mode: "insensitive" } } }, + { activeVehicle: { value: { value: { contains: query, mode: "insensitive" } } } }, + { radioChannelId: { contains: query, mode: "insensitive" } }, + { + status: { + AND: [ + { value: { value: { contains: query, mode: "insensitive" } } }, + { NOT: { shouldDo: ShouldDoType.SET_OFF_DUTY } }, + ], + }, + }, + { + citizen: { + OR: [ + { + name: { contains: name, mode: "insensitive" }, + surname: { contains: surname, mode: "insensitive" }, + }, + { + name: { contains: surname, mode: "insensitive" }, + surname: { contains: name, mode: "insensitive" }, + }, + ], + }, + }, + ], + } satisfies Prisma.EmsFdDeputyWhereInput; +} diff --git a/apps/api/src/controllers/leo/LeoController.ts b/apps/api/src/controllers/leo/LeoController.ts index fb5af1fad..3f3655a25 100644 --- a/apps/api/src/controllers/leo/LeoController.ts +++ b/apps/api/src/controllers/leo/LeoController.ts @@ -48,9 +48,9 @@ export class LeoController { async getActiveOfficers( @Context("user") user: User, @Context("cad") cad: { miscCadSettings: MiscCadSettings }, - @QueryParams("includeAll", Boolean) includeAll = true, + @QueryParams("includeAll", Boolean) includeAll = false, @QueryParams("skip", Number) skip = 0, - @QueryParams("search", String) search?: string, + @QueryParams("query", String) query?: string, ): Promise { const unitsInactivityFilter = getInactivityFilter( cad, @@ -63,25 +63,7 @@ export class LeoController { select: { departmentId: true }, }); - // todo: async-table for active officers/deputies. - // i have yet to find a good way to do this, but i will find a way. - const officerWhere: Prisma.OfficerWhereInput | undefined = search - ? { - callsign: { contains: search, mode: "insensitive" }, - callsign2: { contains: search, mode: "insensitive" }, - divisions: { some: { value: { value: { contains: search, mode: "insensitive" } } } }, - department: { value: { value: { contains: search, mode: "insensitive" } } }, - badgeNumberString: { contains: search, mode: "insensitive" }, - rank: { value: { contains: search, mode: "insensitive" } }, - activeVehicle: { value: { value: { contains: search, mode: "insensitive" } } }, - radioChannelId: { contains: search, mode: "insensitive" }, - AND: [ - { status: { value: { value: { contains: search, mode: "insensitive" } } } }, - { status: { NOT: { shouldDo: ShouldDoType.SET_OFF_DUTY } } }, - ], - } - : undefined; - + const officerWhere = query ? activeOfficersWhereInput(query) : undefined; const [officers, combinedUnits] = await prisma.$transaction([ prisma.officer.findMany({ take: includeAll ? undefined : 12, @@ -346,3 +328,42 @@ export class LeoController { return updated; } } + +function activeOfficersWhereInput(query: string) { + const [name, surname] = query.toString().toLowerCase().split(/ +/g); + + return { + OR: [ + { callsign: { contains: query, mode: "insensitive" } }, + { callsign2: { contains: query, mode: "insensitive" } }, + { divisions: { some: { value: { value: { contains: query, mode: "insensitive" } } } } }, + { department: { value: { value: { contains: query, mode: "insensitive" } } } }, + { badgeNumberString: { contains: query, mode: "insensitive" } }, + { rank: { value: { contains: query, mode: "insensitive" } } }, + { activeVehicle: { value: { value: { contains: query, mode: "insensitive" } } } }, + { radioChannelId: { contains: query, mode: "insensitive" } }, + { + status: { + AND: [ + { value: { value: { contains: query, mode: "insensitive" } } }, + { NOT: { shouldDo: ShouldDoType.SET_OFF_DUTY } }, + ], + }, + }, + { + citizen: { + OR: [ + { + name: { contains: name, mode: "insensitive" }, + surname: { contains: surname, mode: "insensitive" }, + }, + { + name: { contains: surname, mode: "insensitive" }, + surname: { contains: name, mode: "insensitive" }, + }, + ], + }, + }, + ], + } satisfies Prisma.OfficerWhereInput; +} diff --git a/apps/client/locales/en/leo.json b/apps/client/locales/en/leo.json index 48d0c84d5..85714cb53 100644 --- a/apps/client/locales/en/leo.json +++ b/apps/client/locales/en/leo.json @@ -333,7 +333,9 @@ "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." + "noDepartmentLinks": "This department doesn't have any extra information yet.", + "showingOnlyLatest12Units": "Showing only 12 latest units.", + "showingOnlyLatest12UnitsDescription": "Showing only the 12 latest units in this list. The latest units are located at the top of the list. This is to avoid page slowdowns. Use the filters the refine your search." }, "Bolos": { "activeBolos": "Active Bolos", 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 index 32cb86646..c720d3fd5 100644 --- 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 @@ -13,10 +13,6 @@ export function DepartmentLinksSection() { const { values, setFieldValue } = useFormikContext<{ departmentLinks: DepartmentValueLink[] }>(); const common = useTranslations("Common"); - console.log({ - links: values.departmentLinks, - }); - return (
diff --git a/apps/client/src/components/dispatch/active-deputies.tsx b/apps/client/src/components/dispatch/active-deputies.tsx index 4c0dfeac4..fe628544c 100644 --- a/apps/client/src/components/dispatch/active-deputies.tsx +++ b/apps/client/src/components/dispatch/active-deputies.tsx @@ -12,15 +12,14 @@ import { useGenerateCallsign } from "hooks/useGenerateCallsign"; import { CombinedEmsFdUnit, EmsFdDeputy, StatusViewMode } from "@snailycad/types"; import { useAuth } from "context/AuthContext"; -import { Table, useTableState } from "components/shared/Table"; +import { Table, useAsyncTable, useTableState } from "components/shared/Table"; import { useActiveDispatchers } from "hooks/realtime/use-active-dispatchers"; import { useFeatureEnabled } from "hooks/useFeatureEnabled"; import { UnitRadioChannelModal } from "./active-units/UnitRadioChannelModal"; import { useActiveUnitsState } from "state/active-unit-state"; import { classNames } from "lib/classNames"; import { Filter } from "react-bootstrap-icons"; -import { ActiveUnitsSearch } from "./active-units/ActiveUnitsSearch"; -import { useActiveUnitsFilter } from "hooks/shared/useActiveUnitsFilter"; +import { ActiveUnitsSearch } from "./active-units/active-units-search"; import { ActiveCallColumn } from "./active-units/officers/active-call-column"; import { ActiveIncidentColumn } from "./active-units/officers/active-incident-column"; import { DeputyColumn } from "./active-units/deputies/DeputyColumn"; @@ -30,6 +29,7 @@ import { generateContrastColor } from "lib/table/get-contrasting-text-color"; import { Permissions, usePermission } from "hooks/usePermission"; import { isUnitCombinedEmsFd } from "@snailycad/utils"; import { MergeUnitModal } from "./active-units/merge-unit-modal"; +import { GetEmsFdActiveDeputies } from "@snailycad/types/api"; interface Props { initialDeputies: (EmsFdDeputy | CombinedEmsFdUnit)[]; @@ -39,19 +39,45 @@ function ActiveDeputies({ initialDeputies }: Props) { const t = useTranslations(); const common = useTranslations("Common"); + const { emsSearch, showEmsFilters, setShowFilters } = useActiveUnitsState((state) => ({ + emsSearch: state.emsSearch, + showEmsFilters: state.showEmsFilters, + setShowFilters: state.setShowFilters, + })); + + const asyncTable = useAsyncTable({ + search: emsSearch, + fetchOptions: { + refetchOnWindowFocus: false, + pageSize: 12, + requireFilterText: true, + path: "/ems-fd/active-deputies", + onResponse: (json: GetEmsFdActiveDeputies) => ({ + data: json, + totalCount: json.length, + }), + }, + initialData: initialDeputies, + totalCount: initialDeputies.length, + scrollToTopOnDataChange: false, + }); + + const tableState = useTableState({ + tableId: "active-deputies", + pagination: asyncTable.pagination, + }); + const { activeDeputies: _activeDeputies, setActiveDeputies } = useActiveDeputies(); const { hasPermissions } = usePermission(); const modalState = useModal(); const { generateCallsign } = useGenerateCallsign(); const { user } = useAuth(); const { hasActiveDispatchers } = useActiveDispatchers(); - const { handleFilter } = useActiveUnitsFilter(); const { DIVISIONS, BADGE_NUMBERS, RADIO_CHANNEL_MANAGEMENT, ACTIVE_INCIDENTS } = useFeatureEnabled(); const isMounted = useMounted(); const router = useRouter(); - const tableState = useTableState({ tableId: "active-deputies", pagination: { pageSize: 12 } }); const activeDeputies = isMounted ? _activeDeputies : initialDeputies; const isDispatch = router.pathname === "/dispatch"; @@ -63,11 +89,6 @@ function ActiveDeputies({ initialDeputies }: Props) { activeDeputy: state.activeDeputy, setActiveDeputy: state.setActiveDeputy, })); - const { emsSearch, showEmsFilters, setShowFilters } = useActiveUnitsState((state) => ({ - emsSearch: state.emsSearch, - showEmsFilters: state.showEmsFilters, - setShowFilters: state.setShowFilters, - })); const [tempDeputy, deputyState] = useTemporaryItem(activeDeputies); @@ -76,6 +97,10 @@ function ActiveDeputies({ initialDeputies }: Props) { modalState.openModal(ModalIds.ManageUnit); } + React.useEffect(() => { + setActiveDeputies(asyncTable.items); + }, [asyncTable.items]); // eslint-disable-line react-hooks/exhaustive-deps + return (
@@ -112,107 +137,102 @@ function ActiveDeputies({ initialDeputies }: Props) {
+ + {activeDeputies.length <= 0 ? (

{t("Ems.noActiveDeputies")}

) : ( - <> - - - handleFilter(deputy, emsSearch)) - .map((deputy) => { - const color = deputy.status?.color; - const useDot = user?.statusViewMode === StatusViewMode.DOT_COLOR; - - const nameAndCallsign = `${generateCallsign(deputy)} ${makeUnitName(deputy)}`; - - return { - id: deputy.id, - rowProps: { - style: { - background: !useDot && color ? color : undefined, - color: !useDot && color ? generateContrastColor(color) : undefined, - }, - }, - name: nameAndCallsign, - deputy: ( - - ), - badgeNumberString: !isUnitCombinedEmsFd(deputy) && deputy.badgeNumberString, - department: - (!isUnitCombinedEmsFd(deputy) && deputy.department?.value.value) ?? - common("none"), - division: !isUnitCombinedEmsFd(deputy) && formatUnitDivisions(deputy), - rank: (!isUnitCombinedEmsFd(deputy) && deputy.rank?.value) ?? common("none"), - status: ( - - {useDot && color ? ( - - ) : null} - {deputy.status?.value?.value} - - ), - vehicle: deputy.activeVehicle?.value.value ?? common("none"), - incident: ( - - ), - activeCall: ( - { + const color = deputy.status?.color; + const useDot = user?.statusViewMode === StatusViewMode.DOT_COLOR; + + const nameAndCallsign = `${generateCallsign(deputy)} ${makeUnitName(deputy)}`; + + return { + id: deputy.id, + rowProps: { + style: { + background: !useDot && color ? color : undefined, + color: !useDot && color ? generateContrastColor(color) : undefined, + }, + }, + name: nameAndCallsign, + deputy: ( + + ), + badgeNumberString: !isUnitCombinedEmsFd(deputy) && deputy.badgeNumberString, + department: + (!isUnitCombinedEmsFd(deputy) && deputy.department?.value.value) ?? common("none"), + division: !isUnitCombinedEmsFd(deputy) && formatUnitDivisions(deputy), + rank: (!isUnitCombinedEmsFd(deputy) && deputy.rank?.value) ?? common("none"), + status: ( + + {useDot && color ? ( + - ), - radioChannel: , - actions: isDispatch ? ( - - ) : null, - }; - })} - columns={[ - { header: t("Ems.deputy"), accessorKey: "deputy" }, - BADGE_NUMBERS - ? { header: t("Leo.badgeNumber"), accessorKey: "badgeNumberString" } - : null, - { header: t("Leo.department"), accessorKey: "department" }, - DIVISIONS ? { header: t("Leo.division"), accessorKey: "division" } : null, - { header: t("Leo.rank"), accessorKey: "rank" }, - { header: t("Leo.status"), accessorKey: "status" }, - { header: t("Ems.emergencyVehicle"), accessorKey: "vehicle" }, - ACTIVE_INCIDENTS ? { header: t("Leo.incident"), accessorKey: "incident" } : null, - { header: t("Leo.activeCall"), accessorKey: "activeCall" }, - RADIO_CHANNEL_MANAGEMENT - ? { header: t("Leo.radioChannel"), accessorKey: "radioChannel" } - : null, - isDispatch ? { header: common("actions"), accessorKey: "actions" } : null, - ]} - /> - + ) : null} + {deputy.status?.value?.value} + + ), + vehicle: deputy.activeVehicle?.value.value ?? common("none"), + incident: ( + + ), + activeCall: ( + + ), + radioChannel: , + actions: isDispatch ? ( + + ) : null, + }; + })} + columns={[ + { header: t("Ems.deputy"), accessorKey: "deputy" }, + BADGE_NUMBERS + ? { header: t("Leo.badgeNumber"), accessorKey: "badgeNumberString" } + : null, + { header: t("Leo.department"), accessorKey: "department" }, + DIVISIONS ? { header: t("Leo.division"), accessorKey: "division" } : null, + { header: t("Leo.rank"), accessorKey: "rank" }, + { header: t("Leo.status"), accessorKey: "status" }, + { header: t("Ems.emergencyVehicle"), accessorKey: "vehicle" }, + ACTIVE_INCIDENTS ? { header: t("Leo.incident"), accessorKey: "incident" } : null, + { header: t("Leo.activeCall"), accessorKey: "activeCall" }, + RADIO_CHANNEL_MANAGEMENT + ? { header: t("Leo.radioChannel"), accessorKey: "radioChannel" } + : null, + isDispatch ? { header: common("actions"), accessorKey: "actions" } : null, + ]} + /> )} {tempDeputy ? ( diff --git a/apps/client/src/components/dispatch/active-officers.tsx b/apps/client/src/components/dispatch/active-officers.tsx index 71bb68546..9899078ea 100644 --- a/apps/client/src/components/dispatch/active-officers.tsx +++ b/apps/client/src/components/dispatch/active-officers.tsx @@ -12,13 +12,12 @@ import { useAuth } from "context/AuthContext"; import { CombinedLeoUnit, StatusViewMode, Officer } from "@snailycad/types"; import { Filter } from "react-bootstrap-icons"; import { useActiveDispatchers } from "hooks/realtime/use-active-dispatchers"; -import { useTableState, Table } from "components/shared/Table"; +import { useTableState, Table, useAsyncTable } from "components/shared/Table"; import { useFeatureEnabled } from "hooks/useFeatureEnabled"; import { UnitRadioChannelModal } from "./active-units/UnitRadioChannelModal"; -import { ActiveUnitsSearch } from "./active-units/ActiveUnitsSearch"; +import { ActiveUnitsSearch } from "./active-units/active-units-search"; import { classNames } from "lib/classNames"; import { useActiveUnitsState } from "state/active-unit-state"; -import { useActiveUnitsFilter } from "hooks/shared/useActiveUnitsFilter"; import { OfficerColumn } from "./active-units/officers/officer-column"; import { isUnitCombined, isUnitOfficer } from "@snailycad/utils/typeguards"; import { ActiveIncidentColumn } from "./active-units/officers/active-incident-column"; @@ -30,6 +29,7 @@ import dynamic from "next/dynamic"; import { Permissions } from "@snailycad/permissions"; import { usePermission } from "hooks/usePermission"; import { PrivateMessagesModal } from "./active-units/private-messages/private-messages-modal"; +import { GetActiveOfficersData } from "@snailycad/types/api"; const CreateTemporaryUnitModal = dynamic( async () => @@ -55,9 +55,32 @@ function ActiveOfficers({ initialOfficers }: Props) { const t = useTranslations("Leo"); const common = useTranslations("Common"); + const { leoSearch, showLeoFilters, setShowFilters } = useActiveUnitsState((state) => ({ + leoSearch: state.leoSearch, + showLeoFilters: state.showLeoFilters, + setShowFilters: state.setShowFilters, + })); + + const asyncTable = useAsyncTable({ + search: leoSearch, + fetchOptions: { + pageSize: 12, + refetchOnWindowFocus: false, + requireFilterText: true, + path: "/leo/active-officers", + onResponse: (json: GetActiveOfficersData) => ({ + data: json, + totalCount: json.length, + }), + }, + initialData: initialOfficers, + totalCount: initialOfficers.length, + scrollToTopOnDataChange: false, + }); + const tableState = useTableState({ tableId: "active-officers", - pagination: { pageSize: 12, totalDataCount: initialOfficers.length }, + pagination: asyncTable.pagination, }); const { activeOfficers: _activeOfficers, setActiveOfficers } = useActiveOfficers(); @@ -66,7 +89,6 @@ function ActiveOfficers({ initialOfficers }: Props) { const { user } = useAuth(); const { hasPermissions } = usePermission(); const { hasActiveDispatchers } = useActiveDispatchers(); - const { handleFilter } = useActiveUnitsFilter(); const { BADGE_NUMBERS, ACTIVE_INCIDENTS, RADIO_CHANNEL_MANAGEMENT, DIVISIONS } = useFeatureEnabled(); @@ -79,12 +101,6 @@ function ActiveOfficers({ initialOfficers }: Props) { const hasDispatchPerms = hasPermissions([Permissions.Dispatch]); const showCreateTemporaryUnitButton = isDispatch && hasDispatchPerms; - const { leoSearch, showLeoFilters, setShowFilters } = useActiveUnitsState((state) => ({ - leoSearch: state.leoSearch, - showLeoFilters: state.showLeoFilters, - setShowFilters: state.setShowFilters, - })); - const { activeOfficer, setActiveOfficer } = useLeoState((state) => ({ activeOfficer: state.activeOfficer, setActiveOfficer: state.setActiveOfficer, @@ -97,6 +113,10 @@ function ActiveOfficers({ initialOfficers }: Props) { modalState.openModal(ModalIds.ManageUnit); } + React.useEffect(() => { + setActiveOfficers(asyncTable.items); + }, [asyncTable.items]); // eslint-disable-line react-hooks/exhaustive-deps + return (
@@ -134,115 +154,111 @@ function ActiveOfficers({ initialOfficers }: Props) {
+ + {activeOfficers.length <= 0 ? (

{t("noActiveOfficers")}

) : ( - <> - - -
handleFilter(officer, leoSearch)) - .map((officer) => { - const color = officer.status?.color; +
{ + const color = officer.status?.color; - const useDot = user?.statusViewMode === StatusViewMode.DOT_COLOR; - const nameAndCallsign = `${generateCallsign(officer)} ${makeUnitName(officer)}`; + const useDot = user?.statusViewMode === StatusViewMode.DOT_COLOR; + const nameAndCallsign = `${generateCallsign(officer)} ${makeUnitName(officer)}`; - return { - id: officer.id, - rowProps: { - style: { - background: !useDot && color ? color : undefined, - color: !useDot && color ? generateContrastColor(color) : undefined, - }, - }, - name: nameAndCallsign, - officer: ( - - ), - badgeNumberString: isUnitOfficer(officer) && officer.badgeNumberString, - department: - ((isUnitCombined(officer) && officer.officers[0]?.department?.value.value) || - (isUnitOfficer(officer) && officer.department?.value.value)) ?? - common("none"), - division: ( - - -

- {isUnitOfficer(officer) && formatUnitDivisions(officer)} -

-
+ return { + id: officer.id, + rowProps: { + style: { + background: !useDot && color ? color : undefined, + color: !useDot && color ? generateContrastColor(color) : undefined, + }, + }, + name: nameAndCallsign, + officer: ( + + ), + badgeNumberString: isUnitOfficer(officer) && officer.badgeNumberString, + department: + ((isUnitCombined(officer) && officer.officers[0]?.department?.value.value) || + (isUnitOfficer(officer) && officer.department?.value.value)) ?? + common("none"), + division: ( + + +

+ {isUnitOfficer(officer) && formatUnitDivisions(officer)} +

+
- - {isUnitOfficer(officer) && formatUnitDivisions(officer)} - -
- ), - rank: (isUnitOfficer(officer) && officer.rank?.value) ?? common("none"), - status: ( - - {useDot && color ? ( - - ) : null} - {officer.status?.value?.value} - - ), - vehicle: officer.activeVehicle?.value.value ?? common("none"), - incident: ( - + {isUnitOfficer(officer) && formatUnitDivisions(officer)} + +
+ ), + rank: (isUnitOfficer(officer) && officer.rank?.value) ?? common("none"), + status: ( + + {useDot && color ? ( + - ), - activeCall: ( - - ), - radioChannel: , - actions: isDispatch ? ( - - ) : null, - }; - })} - columns={[ - { header: t("officer"), accessorKey: "officer" }, - BADGE_NUMBERS ? { header: t("badgeNumber"), accessorKey: "badgeNumberString" } : null, - { header: t("department"), accessorKey: "department" }, - DIVISIONS ? { header: t("division"), accessorKey: "division" } : null, - { header: t("rank"), accessorKey: "rank" }, - { header: t("status"), accessorKey: "status" }, - { header: t("patrolVehicle"), accessorKey: "vehicle" }, - ACTIVE_INCIDENTS ? { header: t("incident"), accessorKey: "incident" } : null, - { header: t("activeCall"), accessorKey: "activeCall" }, - RADIO_CHANNEL_MANAGEMENT - ? { header: t("radioChannel"), accessorKey: "radioChannel" } - : null, - isDispatch ? { header: common("actions"), accessorKey: "actions" } : null, - ]} - /> - + ) : null} + {officer.status?.value?.value} + + ), + vehicle: officer.activeVehicle?.value.value ?? common("none"), + incident: ( + + ), + activeCall: ( + + ), + radioChannel: , + actions: isDispatch ? ( + + ) : null, + }; + })} + columns={[ + { header: t("officer"), accessorKey: "officer" }, + BADGE_NUMBERS ? { header: t("badgeNumber"), accessorKey: "badgeNumberString" } : null, + { header: t("department"), accessorKey: "department" }, + DIVISIONS ? { header: t("division"), accessorKey: "division" } : null, + { header: t("rank"), accessorKey: "rank" }, + { header: t("status"), accessorKey: "status" }, + { header: t("patrolVehicle"), accessorKey: "vehicle" }, + ACTIVE_INCIDENTS ? { header: t("incident"), accessorKey: "incident" } : null, + { header: t("activeCall"), accessorKey: "activeCall" }, + RADIO_CHANNEL_MANAGEMENT + ? { header: t("radioChannel"), accessorKey: "radioChannel" } + : null, + isDispatch ? { header: common("actions"), accessorKey: "actions" } : null, + ]} + /> )} {tempOfficer ? ( diff --git a/apps/client/src/components/dispatch/active-units/ActiveUnitsSearch.tsx b/apps/client/src/components/dispatch/active-units/ActiveUnitsSearch.tsx deleted file mode 100644 index 979e18f0a..000000000 --- a/apps/client/src/components/dispatch/active-units/ActiveUnitsSearch.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { TextField } from "@snailycad/ui"; -import { useTranslations } from "next-intl"; -import { useActiveUnitsState } from "state/active-unit-state"; - -interface Props { - type: "leo" | "ems-fd"; -} - -export function ActiveUnitsSearch({ type }: Props) { - const setSearchType = type === "leo" ? "leoSearch" : "emsSearch"; - const showFiltersType: "showLeoFilters" | "showEmsFilters" = - type === "leo" ? "showLeoFilters" : "showEmsFilters"; - - const common = useTranslations("Common"); - const { - [showFiltersType]: showFilters, - [setSearchType]: search, - setSearch, - } = useActiveUnitsState((state) => ({ - [showFiltersType]: state[showFiltersType], - [setSearchType]: state[setSearchType], - setSearch: state.setSearch, - })); - - return (showFilters as boolean) ? ( -
- setSearch(setSearchType, value)} - /> -
- ) : null; -} diff --git a/apps/client/src/components/dispatch/active-units/active-units-search.tsx b/apps/client/src/components/dispatch/active-units/active-units-search.tsx new file mode 100644 index 000000000..950334994 --- /dev/null +++ b/apps/client/src/components/dispatch/active-units/active-units-search.tsx @@ -0,0 +1,69 @@ +import * as Tooltip from "@radix-ui/react-tooltip"; +import { Loader, TextField } from "@snailycad/ui"; +import { useTranslations } from "next-intl"; +import { InfoCircleFill } from "react-bootstrap-icons"; +import { useActiveUnitsState } from "state/active-unit-state"; + +interface Props { + type: "leo" | "ems-fd"; + isLoading: boolean; +} + +export function ActiveUnitsSearch({ isLoading, type }: Props) { + const t = useTranslations("Leo"); + const setSearchType = type === "leo" ? "leoSearch" : "emsSearch"; + const showFiltersType: "showLeoFilters" | "showEmsFilters" = + type === "leo" ? "showLeoFilters" : "showEmsFilters"; + + const common = useTranslations("Common"); + const { + [showFiltersType]: showFilters, + [setSearchType]: search, + setSearch, + } = useActiveUnitsState((state) => ({ + [showFiltersType]: state[showFiltersType], + [setSearchType]: state[setSearchType], + setSearch: state.setSearch, + })); + + return ( +
+ + + +

+ {t("showingOnlyLatest12Units")} + +

+
+ + +

+ {t("showingOnlyLatest12UnitsDescription")} +

+
+
+
+ + {(showFilters as boolean) ? ( + setSearch(setSearchType, value)} + placeholder="Name, Badge Number, Status, ..." + > + {isLoading ? ( + + + + ) : null} + + ) : null} +
+ ); +} diff --git a/apps/client/src/components/leo/modals/CustomFieldSearch/CustomFieldSearch.tsx b/apps/client/src/components/leo/modals/CustomFieldSearch/CustomFieldSearch.tsx index ee0606ce1..0d37614ef 100644 --- a/apps/client/src/components/leo/modals/CustomFieldSearch/CustomFieldSearch.tsx +++ b/apps/client/src/components/leo/modals/CustomFieldSearch/CustomFieldSearch.tsx @@ -22,6 +22,7 @@ export function CustomFieldSearch() { const [results, setResults] = React.useState(null); const { data: customFieldsData, isLoading } = useQuery({ + refetchOnWindowFocus: false, initialData: { customFields: [], totalCount: 0 }, queryKey: ["custom-fields"], queryFn: async () => { diff --git a/apps/client/src/components/leo/modals/department-info-modal.tsx b/apps/client/src/components/leo/modals/department-info-modal.tsx index 30f588fe6..4c6a0f49a 100644 --- a/apps/client/src/components/leo/modals/department-info-modal.tsx +++ b/apps/client/src/components/leo/modals/department-info-modal.tsx @@ -22,11 +22,6 @@ export function DepartmentInformationModal() { const links = activeUnit?.department?.links ?? []; const tableState = useTableState(); - console.log({ - activeUnit, - links, - }); - return ( `${generateCallsign(v)} ${makeUnitName(v)}`).join(", "); - const deputies = - isUnitCombinedEmsFd(unit) && - unit.deputies.map((v) => `${generateCallsign(v)} ${makeUnitName(v)}`).join(", "); - - const department = !isCombined && (unit.department?.value.value ?? common("none")); - const rank = !isCombined && (unit.rank?.value ?? common("none")); - const status = unit.status?.value.value ?? common("none"); - const radioChannel = unit.radioChannelId ?? common("none"); - const badgeNumberString = !isCombined && unit.badgeNumberString; - - const divisions = ( - "divisions" in unit ? unit.divisions : "division" in unit ? [unit.division] : [] - ).filter(Number) as DivisionValue[]; - - const divisionString = divisions.map((v) => v.value.value).join(","); - - const searchableArr = [ - nameAndCallsign, - department, - rank, - status, - radioChannel, - badgeNumberString, - divisionString, - officers, - deputies, - ]; - - const searchableString = searchableArr.filter(Boolean).join(" ").toLowerCase(); - return searchableString.includes(search.trim().toLowerCase()); - } - - return { handleFilter }; -} diff --git a/apps/client/src/pages/officer/index.tsx b/apps/client/src/pages/officer/index.tsx index e8f98b945..bf8f5c17a 100644 --- a/apps/client/src/pages/officer/index.tsx +++ b/apps/client/src/pages/officer/index.tsx @@ -143,24 +143,7 @@ export default function OfficerDashboard({ const signal100 = useSignal100(); const tones = useTones(ActiveToneType.LEO); const panic = usePanicButton(); - const modalState = useModal(); - const { LEO_TICKETS, ACTIVE_WARRANTS, CALLS_911 } = useFeatureEnabled(); - const { hasPermissions } = usePermission(); - const isAdmin = hasPermissions(defaultPermissions.allDefaultAdminPermissions); - - const { currentResult, setCurrentResult } = useNameSearch((state) => ({ - currentResult: state.currentResult, - setCurrentResult: state.setCurrentResult, - })); - - function handleRecordCreate(data: Record) { - if (!currentResult || currentResult.isConfidential) return; - - setCurrentResult({ - ...currentResult, - Record: [data, ...currentResult.Record], - }); - } + const { ACTIVE_WARRANTS, CALLS_911 } = useFeatureEnabled(); React.useEffect(() => { leoState.setActiveOfficer(activeOfficer); @@ -205,39 +188,63 @@ export default function OfficerDashboard({ + + + ); +} - {isAdmin || leoState.activeOfficer ? ( - <> - - - +function OfficerModals() { + const leoState = useLeoState(); + const modalState = useModal(); + const { LEO_TICKETS, ACTIVE_WARRANTS } = useFeatureEnabled(); + const { hasPermissions } = usePermission(); + const isAdmin = hasPermissions(defaultPermissions.allDefaultAdminPermissions); + + const { currentResult, setCurrentResult } = useNameSearch((state) => ({ + currentResult: state.currentResult, + setCurrentResult: state.setCurrentResult, + })); + + function handleRecordCreate(data: Record) { + if (!currentResult || currentResult.isConfidential) return; + + setCurrentResult({ + ...currentResult, + Record: [data, ...currentResult.Record], + }); + } - {/* name search have their own vehicle/weapon search modal */} - {modalState.isOpen(ModalIds.NameSearch) ? null : ( - <> - - - + if (!isAdmin || !leoState.activeOfficer) { + return null; + } - {LEO_TICKETS ? ( - - ) : null} - - - - )} - - {!ACTIVE_WARRANTS ? : null} - + return ( + <> + + + + + {/* name search have their own vehicle/weapon search modal */} + {modalState.isOpen(ModalIds.NameSearch) ? null : ( + <> + + + + + {LEO_TICKETS ? ( + + ) : null} + + - ) : null} - + )} + + {!ACTIVE_WARRANTS ? : null} + + ); }