diff --git a/apps/map/src/app/admin/areas/areas-table.tsx b/apps/map/src/app/admin/areas/areas-table.tsx index 956253c0..21c44bfc 100644 --- a/apps/map/src/app/admin/areas/areas-table.tsx +++ b/apps/map/src/app/admin/areas/areas-table.tsx @@ -5,6 +5,7 @@ import type { TableOptions } from "@tanstack/react-table"; import { useCallback, useMemo, useState } from "react"; import type { IsActiveStatus } from "@acme/shared/app/enums"; +import type { SortingSchema } from "@acme/validators"; import { Button } from "@acme/ui/button"; import { DropdownMenu, @@ -33,6 +34,7 @@ export const AreasTable = () => { ]); const [onlyMine, setOnlyMine] = useState(true); const [searchTerm, setSearchTerm] = useState(""); + const [sorting, setSorting] = useState([]); const { data: sectorsData } = useQuery( orpc.org.all.queryOptions({ @@ -62,6 +64,7 @@ export const AreasTable = () => { onlyMine: onlyMine || undefined, searchTerm: searchTerm || undefined, parentOrgIds: parentOrgIds.length > 0 ? parentOrgIds : undefined, + sorting, }, }), ); @@ -123,6 +126,8 @@ export const AreasTable = () => { totalCount={areasData?.total} pagination={pagination} setPagination={setPagination} + sorting={sorting} + setSorting={setSorting} searchTerm={searchTerm} setSearchTerm={setSearchTerm} filterComponent={ @@ -185,12 +190,14 @@ const columns: TableOptions< cell: (cell) => , }, { + id: "parentOrgName", accessorKey: "sector", meta: { name: "Sector" }, header: Header, cell: (cell) => , }, { + id: "status", accessorKey: "isActive", meta: { name: "Status" }, header: Header, @@ -245,6 +252,7 @@ const columns: TableOptions< { id: "id", enableHiding: false, + enableSorting: false, cell: ({ row }) => { return ( diff --git a/apps/map/src/app/admin/sectors/sectors-table.tsx b/apps/map/src/app/admin/sectors/sectors-table.tsx index c0762484..8c1e30b3 100644 --- a/apps/map/src/app/admin/sectors/sectors-table.tsx +++ b/apps/map/src/app/admin/sectors/sectors-table.tsx @@ -5,6 +5,7 @@ import type { TableOptions } from "@tanstack/react-table"; import { useState } from "react"; import type { IsActiveStatus } from "@acme/shared/app/enums"; +import type { SortingSchema } from "@acme/validators"; import { Button } from "@acme/ui/button"; import { DropdownMenu, @@ -31,6 +32,7 @@ export const SectorsTable = () => { ]); const [onlyMine, setOnlyMine] = useState(true); const [searchTerm, setSearchTerm] = useState(""); + const [sorting, setSorting] = useState([]); const { data: sectorsData } = useQuery( orpc.org.all.queryOptions({ @@ -41,6 +43,7 @@ export const SectorsTable = () => { statuses: selectedStatuses, onlyMine: onlyMine || undefined, searchTerm: searchTerm || undefined, + sorting, }, }), ); @@ -67,6 +70,8 @@ export const SectorsTable = () => { totalCount={sectorsData?.total} pagination={pagination} setPagination={setPagination} + sorting={sorting} + setSorting={setSorting} searchTerm={searchTerm} setSearchTerm={setSearchTerm} filterComponent={ @@ -116,6 +121,7 @@ const columns: TableOptions["columns"] = [ cell: (cell) => , }, { + id: "status", accessorKey: "isActive", meta: { name: "Status" }, header: Header, @@ -164,6 +170,7 @@ const columns: TableOptions["columns"] = [ { id: "id", enableHiding: false, + enableSorting: false, cell: ({ row }) => { return ( diff --git a/apps/map/src/app/admin/users/all/all-users-table.tsx b/apps/map/src/app/admin/users/all/all-users-table.tsx index 224666fa..5b49646a 100644 --- a/apps/map/src/app/admin/users/all/all-users-table.tsx +++ b/apps/map/src/app/admin/users/all/all-users-table.tsx @@ -34,6 +34,7 @@ import type { RouterOutputs } from "~/orpc/types"; import { useDebounce } from "~/utils/hooks/use-debounce"; import { DeleteType, ModalType, openModal } from "~/utils/store/modal"; import { OrgFilter } from "../org-filter"; +import type { SortingSchema } from "@acme/validators"; type Org = RouterOutputs["org"]["all"]["orgs"][number]; @@ -163,6 +164,7 @@ export const AllUsersTable = () => { const [selectedHomeRegions, setSelectedHomeRegions] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const debouncedSearchTerm = useDebounce(searchTerm, 500); + const [sorting, setSorting] = useState([]); const { pagination, setPagination } = usePagination({ pageSize: 20, }); @@ -216,6 +218,7 @@ export const AllUsersTable = () => { const activeFilterCount = selectedOrgs.length + + selectedHomeRegions.length + (selectedStatuses.length !== 1 || selectedStatuses[0] !== "active" ? selectedStatuses.length : 0) + @@ -231,6 +234,7 @@ export const AllUsersTable = () => { pageIndex: pagination.pageIndex, orgIds: selectedOrgs.map((org) => org.id), homeRegionIds: selectedHomeRegions.map((region) => region.id), + sorting: sorting, }, }), ); @@ -307,6 +311,8 @@ export const AllUsersTable = () => { } cellClassName="p-1" columns={columns} + sorting={sorting} + setSorting={setSorting} pagination={pagination} totalCount={data?.totalCount} setPagination={setPagination} diff --git a/apps/map/src/app/admin/users/mine/my-users-table.tsx b/apps/map/src/app/admin/users/mine/my-users-table.tsx index 4e371092..211fb787 100644 --- a/apps/map/src/app/admin/users/mine/my-users-table.tsx +++ b/apps/map/src/app/admin/users/mine/my-users-table.tsx @@ -34,6 +34,7 @@ import type { RouterOutputs } from "~/orpc/types"; import { useDebounce } from "~/utils/hooks/use-debounce"; import { DeleteType, ModalType, openModal } from "~/utils/store/modal"; import { OrgFilter } from "../org-filter"; +import type { SortingSchema } from "@acme/validators"; type Org = RouterOutputs["org"]["all"]["orgs"][number]; @@ -162,6 +163,7 @@ export const MyUsersTable = () => { const [selectedRoles, setSelectedRoles] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [selectedHomeRegions, setSelectedHomeRegions] = useState([]); + const [sorting, setSorting] = useState([]); const debouncedSearchTerm = useDebounce(searchTerm, 500); const { pagination, setPagination } = usePagination({ @@ -218,6 +220,7 @@ export const MyUsersTable = () => { orgIds: adminAndEditorOrgIds, includePii: true, homeRegionIds: selectedHomeRegions.map((region) => region.id), + sorting: sorting, }, enabled: adminAndEditorOrgIds.length > 0, }), @@ -302,6 +305,8 @@ export const MyUsersTable = () => { } cellClassName="p-1" columns={columns} + sorting={sorting} + setSorting={setSorting} pagination={pagination} totalCount={data?.totalCount} setPagination={setPagination} diff --git a/apps/map/src/app/admin/workouts/workouts-table.tsx b/apps/map/src/app/admin/workouts/workouts-table.tsx index af5cac82..ede30b58 100644 --- a/apps/map/src/app/admin/workouts/workouts-table.tsx +++ b/apps/map/src/app/admin/workouts/workouts-table.tsx @@ -183,7 +183,7 @@ const columns: TableOptions["columns"] = [ ), }, { - accessorKey: "ao", + accessorKey: "parent", meta: { name: "AO" }, header: Header, cell: (cell) => ( diff --git a/packages/api/src/get-sorting-columns.tsx b/packages/api/src/get-sorting-columns.tsx index 6e82a963..a544e87a 100644 --- a/packages/api/src/get-sorting-columns.tsx +++ b/packages/api/src/get-sorting-columns.tsx @@ -1,6 +1,6 @@ import type { SQL } from "drizzle-orm"; import type { PgColumn } from "drizzle-orm/pg-core"; -import { asc, desc } from "drizzle-orm"; +import { asc, desc, sql } from "drizzle-orm"; import type { SortingSchema } from "@acme/validators"; import { isTruthy } from "@acme/shared/common/functions"; @@ -9,6 +9,7 @@ export const getSortingColumns = ( sorting: SortingSchema | undefined, sortingIdToColumn: Record, defaultId: keyof T, + nullsLastColumns?: Set, ) => { return ( sorting @@ -18,7 +19,11 @@ export const getSortingColumns = ( if (!column) { return undefined; } - return direction(column); + const ordered = direction(column); + if (nullsLastColumns?.has(sorting.id as keyof T)) { + return sql`${ordered} NULLS LAST`; + } + return ordered; }) .filter(isTruthy) ?? [desc(sortingIdToColumn[defaultId])] ); diff --git a/packages/api/src/lib/user.ts b/packages/api/src/lib/user.ts index c2f18be4..30b4fea0 100644 --- a/packages/api/src/lib/user.ts +++ b/packages/api/src/lib/user.ts @@ -300,20 +300,24 @@ export const buildUserListQuery = async ({ : undefined, ); + const homeRegion = aliasedTable(schema.orgs, "homeRegion"); + const sortedColumns = getSortingColumns( input?.sorting, { id: schema.users.id, name: schema.users.firstName, f3Name: schema.users.f3Name, - roles: schema.roles.name, + roles: sql`MIN(${schema.roles.name})`, status: schema.users.status, + homeRegion: homeRegion.name, email: schema.users.email, - phone: schema.users.phone, - regions: schema.orgs.name, + phone: sql`NULLIF(${schema.users.phone}, '')`, + regions: sql`MIN(${schema.orgs.name})`, created: schema.users.created, }, "id", + new Set(["homeRegion", "regions", "roles"] as const), ); const userIdsQuery = ctx.db @@ -332,8 +336,6 @@ export const buildUserListQuery = async ({ .from(userIdsQuery.as("distinct_users")); const userCount = countResult[0]; - - const homeRegion = aliasedTable(schema.orgs, "homeRegion"); const select = buildUserSelect({ includePii, includeListFields: true, // Include list-specific fields diff --git a/packages/api/src/router/event-type.ts b/packages/api/src/router/event-type.ts index 4b467809..cd823552 100644 --- a/packages/api/src/router/event-type.ts +++ b/packages/api/src/router/event-type.ts @@ -13,6 +13,7 @@ import { isNull, or, schema, + sql, } from "@acme/db"; import { EventCategory, IsActiveStatus } from "@acme/shared/app/enums"; import { arrayOrSingle, parseSorting } from "@acme/shared/app/functions"; @@ -116,7 +117,7 @@ export const eventTypeRouter = { case "name": return direction(schema.eventTypes.name); case "description": - return direction(schema.eventTypes.description); + return direction(sql`NULLIF(${schema.eventTypes.description}, '')`); case "eventCategory": return direction(schema.eventTypes.eventCategory); case "specificOrgName": diff --git a/packages/api/src/router/map/event.ts b/packages/api/src/router/map/event.ts index cdb23df8..c268d24b 100644 --- a/packages/api/src/router/map/event.ts +++ b/packages/api/src/router/map/event.ts @@ -295,16 +295,20 @@ export const mapEventRouter = { const sortedColumns = input?.sorting?.map((sorting) => { const direction = sorting.desc ? desc : asc; switch (sorting.id) { + case "name": + return direction(schema.events.name); case "regions": return direction(regionOrg.name); case "parent": - return direction(parentOrg.name); + return direction(sql`NULLIF(${parentOrg.name}, '')`); case "status": return direction(schema.events.isActive); case "dayOfWeek": return direction(schema.events.dayOfWeek); case "created": return direction(schema.events.created); + case "location": + return direction(schema.locations.name); default: return direction(schema.events.id); } diff --git a/packages/api/src/router/org.ts b/packages/api/src/router/org.ts index b4f62452..14484604 100644 --- a/packages/api/src/router/org.ts +++ b/packages/api/src/router/org.ts @@ -305,10 +305,13 @@ export const orgRouter = { id: org.id, name: org.name, parentOrgName: parentOrg.name, + aoCount: org.aoCount, + lastAnnualReview: org.lastAnnualReview, status: org.isActive, created: org.created, }, "id", + new Set(["parentOrgName", "lastAnnualReview"] as const), ); const total = await getOrgCount({ db: ctx.db, where });