diff --git a/src/app/(modules)/clients/components/ClientColumns.tsx b/src/app/(modules)/clients/components/ClientColumns.tsx index 6202681..10ee639 100644 --- a/src/app/(modules)/clients/components/ClientColumns.tsx +++ b/src/app/(modules)/clients/components/ClientColumns.tsx @@ -81,6 +81,7 @@ export function useClientColumns({ columns.push({ id: 'actions', header: () =>
Actions
, + enableSorting: false, cell: ({ row }) => { const client = row.original; return ( diff --git a/src/app/(modules)/clients/components/ClientTable.tsx b/src/app/(modules)/clients/components/ClientTable.tsx index 454b1a8..a3499ab 100644 --- a/src/app/(modules)/clients/components/ClientTable.tsx +++ b/src/app/(modules)/clients/components/ClientTable.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ReactNode } from 'react'; -import type { ColumnDef } from '@tanstack/react-table'; +import type { ColumnDef, SortingState } from '@tanstack/react-table'; import { DataTable } from '@/components/data-table'; import type { Client } from '@/types'; @@ -14,8 +14,10 @@ interface ClientTableProps { skip: number; limit: number; total: number; + sorting: SortingState; onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; + onSortingChange: (sorting: SortingState) => void; } export function ClientTable({ @@ -26,8 +28,10 @@ export function ClientTable({ skip, limit, total, + sorting, onPageChange, onLimitChange, + onSortingChange, }: ClientTableProps) { return ( ); } diff --git a/src/app/(modules)/clients/hooks/useClientFilters.ts b/src/app/(modules)/clients/hooks/useClientFilters.ts index 84a5bbf..f9b6502 100644 --- a/src/app/(modules)/clients/hooks/useClientFilters.ts +++ b/src/app/(modules)/clients/hooks/useClientFilters.ts @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useState } from 'react'; +import type { SortingState } from '@tanstack/react-table'; import { useDebounce } from '@/hooks/useDebounce'; @@ -9,6 +10,7 @@ export type ClientStatusFilter = 'all' | 'active' | 'inactive'; export function useClientFilters() { const [skip, setSkip] = useState(0); const [limit, setLimitValue] = useState(10); + const [sorting, setSortingValue] = useState([]); const [searchTerm, setSearchTermValue] = useState(''); const [statusFilter, setStatusFilterValue] = useState('all'); const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400); @@ -28,11 +30,18 @@ export function useClientFilters() { setSkip(0); }, []); + const setSorting = useCallback((value: SortingState) => { + setSortingValue(value); + setSkip(0); + }, []); + return { skip, setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, diff --git a/src/app/(modules)/clients/hooks/useClientQueries.ts b/src/app/(modules)/clients/hooks/useClientQueries.ts index 5f53c41..37c83bd 100644 --- a/src/app/(modules)/clients/hooks/useClientQueries.ts +++ b/src/app/(modules)/clients/hooks/useClientQueries.ts @@ -2,7 +2,9 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; +import type { SortingState } from '@tanstack/react-table'; +import { getServerSortParams } from '@/components/data-table/sorting'; import { clientService } from '@/services/api'; import type { ClientListParams } from '@/types'; import { clientKeys } from '../queries/clientKeys'; @@ -13,6 +15,7 @@ interface UseClientsQueryParams { limit: number; searchTerm: string; statusFilter: ClientStatusFilter; + sorting: SortingState; } function buildClientListParams({ @@ -20,22 +23,22 @@ function buildClientListParams({ limit, searchTerm, statusFilter, + sorting, }: UseClientsQueryParams): ClientListParams { return { skip, limit, search_term: searchTerm || undefined, is_active: statusFilter === 'all' ? undefined : statusFilter === 'active', - sort_by: 'created_at', - sort_order: 'desc', + ...getServerSortParams(sorting), }; } export function useClientsQuery(params: UseClientsQueryParams) { - const { skip, limit, searchTerm, statusFilter } = params; + const { skip, limit, searchTerm, statusFilter, sorting } = params; const listParams = useMemo( - () => buildClientListParams({ skip, limit, searchTerm, statusFilter }), - [limit, searchTerm, skip, statusFilter], + () => buildClientListParams({ skip, limit, searchTerm, statusFilter, sorting }), + [limit, searchTerm, skip, sorting, statusFilter], ); return useQuery({ diff --git a/src/app/(modules)/clients/page.tsx b/src/app/(modules)/clients/page.tsx index a3288b8..356a341 100644 --- a/src/app/(modules)/clients/page.tsx +++ b/src/app/(modules)/clients/page.tsx @@ -26,6 +26,8 @@ export default function ClientsPage() { setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, @@ -38,6 +40,7 @@ export default function ClientsPage() { limit, searchTerm: debouncedSearchTerm, statusFilter, + sorting, }); const clientForm = useClientForm({ onSaved: () => setIsSheetOpen(false), @@ -112,8 +115,10 @@ export default function ClientsPage() { skip={skip} limit={limit} total={total} + sorting={sorting} onPageChange={setSkip} onLimitChange={setLimit} + onSortingChange={setSorting} /> diff --git a/src/app/(modules)/plans/components/PlanColumns.tsx b/src/app/(modules)/plans/components/PlanColumns.tsx index 086521e..49e120f 100644 --- a/src/app/(modules)/plans/components/PlanColumns.tsx +++ b/src/app/(modules)/plans/components/PlanColumns.tsx @@ -65,6 +65,7 @@ export function usePlanColumns({ { id: 'limits', header: 'Limits', + enableSorting: false, cell: ({ row }) => (

{row.original.max_projects} projects

@@ -96,6 +97,7 @@ export function usePlanColumns({ columns.push({ id: 'actions', header: () =>
Actions
, + enableSorting: false, cell: ({ row }) => { const plan = row.original; return ( diff --git a/src/app/(modules)/plans/components/PlanTable.tsx b/src/app/(modules)/plans/components/PlanTable.tsx index 209e3eb..a71c3c9 100644 --- a/src/app/(modules)/plans/components/PlanTable.tsx +++ b/src/app/(modules)/plans/components/PlanTable.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ReactNode } from 'react'; -import type { ColumnDef } from '@tanstack/react-table'; +import type { ColumnDef, SortingState } from '@tanstack/react-table'; import { DataTable } from '@/components/data-table'; import type { Plan } from '@/types'; @@ -14,8 +14,10 @@ interface PlanTableProps { skip: number; limit: number; total: number; + sorting: SortingState; onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; + onSortingChange: (sorting: SortingState) => void; } export function PlanTable({ @@ -26,8 +28,10 @@ export function PlanTable({ skip, limit, total, + sorting, onPageChange, onLimitChange, + onSortingChange, }: PlanTableProps) { return ( ); } diff --git a/src/app/(modules)/plans/hooks/usePlanFilters.ts b/src/app/(modules)/plans/hooks/usePlanFilters.ts index d3aea30..ab6001e 100644 --- a/src/app/(modules)/plans/hooks/usePlanFilters.ts +++ b/src/app/(modules)/plans/hooks/usePlanFilters.ts @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import type { SortingState } from '@tanstack/react-table'; import { useDebounce } from '@/hooks/useDebounce'; export type PlanStatusFilter = 'all' | 'active' | 'inactive'; @@ -8,6 +9,7 @@ export type PlanStatusFilter = 'all' | 'active' | 'inactive'; export function usePlanFilters() { const [skip, setSkip] = useState(0); const [limit, setLimitValue] = useState(10); + const [sorting, setSortingValue] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const debouncedSearchTerm = useDebounce(searchTerm, 400); @@ -27,11 +29,18 @@ export function usePlanFilters() { setSkip(0); }; + const setSorting = (value: SortingState) => { + setSortingValue(value); + setSkip(0); + }; + return { skip, setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm: updateSearchTerm, diff --git a/src/app/(modules)/plans/hooks/usePlanQueries.ts b/src/app/(modules)/plans/hooks/usePlanQueries.ts index 8d811c1..7848827 100644 --- a/src/app/(modules)/plans/hooks/usePlanQueries.ts +++ b/src/app/(modules)/plans/hooks/usePlanQueries.ts @@ -2,7 +2,9 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; +import type { SortingState } from '@tanstack/react-table'; +import { getServerSortParams } from '@/components/data-table/sorting'; import { mapPermissionTree } from '@/components/permission-tree'; import { permissionService, planService } from '@/services/api'; import type { PlanListParams } from '@/types'; @@ -15,6 +17,7 @@ interface UsePlansQueryParams { limit: number; searchTerm: string; statusFilter: PlanStatusFilter; + sorting: SortingState; } function buildPlanListParams({ @@ -22,14 +25,14 @@ function buildPlanListParams({ limit, searchTerm, statusFilter, + sorting, }: UsePlansQueryParams): PlanListParams { return { skip, limit, search_term: searchTerm || undefined, is_active: statusFilter === 'all' ? undefined : statusFilter === 'active', - sort_by: 'created_at', - sort_order: 'desc', + ...getServerSortParams(sorting), }; } diff --git a/src/app/(modules)/plans/page.tsx b/src/app/(modules)/plans/page.tsx index 5e2be83..e8626e9 100644 --- a/src/app/(modules)/plans/page.tsx +++ b/src/app/(modules)/plans/page.tsx @@ -28,6 +28,8 @@ export default function PlansPage() { setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, @@ -40,6 +42,7 @@ export default function PlansPage() { limit, searchTerm: debouncedSearchTerm, statusFilter, + sorting, }); const permissionsQuery = useOrganizationPermissionTreeQuery(isSheetOpen); @@ -166,8 +169,10 @@ export default function PlansPage() { skip={skip} limit={limit} total={total} + sorting={sorting} onPageChange={setSkip} onLimitChange={setLimit} + onSortingChange={setSorting} /> diff --git a/src/app/(modules)/roles/components/RoleColumns.tsx b/src/app/(modules)/roles/components/RoleColumns.tsx index d3a74f6..8c570ff 100644 --- a/src/app/(modules)/roles/components/RoleColumns.tsx +++ b/src/app/(modules)/roles/components/RoleColumns.tsx @@ -66,6 +66,7 @@ export function useRoleColumns({ columns.push({ id: 'actions', header: () =>
Actions
, + enableSorting: false, cell: ({ row }) => { const role = row.original; return ( diff --git a/src/app/(modules)/roles/components/RoleTable.tsx b/src/app/(modules)/roles/components/RoleTable.tsx index 2d9ecac..569396e 100644 --- a/src/app/(modules)/roles/components/RoleTable.tsx +++ b/src/app/(modules)/roles/components/RoleTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { ColumnDef } from '@tanstack/react-table'; +import type { ColumnDef, SortingState } from '@tanstack/react-table'; import type { ReactNode } from 'react'; import { DataTable } from '@/components/data-table'; @@ -14,8 +14,10 @@ interface RoleTableProps { skip: number; limit: number; total: number; + sorting: SortingState; onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; + onSortingChange: (sorting: SortingState) => void; } export function RoleTable({ @@ -26,8 +28,10 @@ export function RoleTable({ skip, limit, total, + sorting, onPageChange, onLimitChange, + onSortingChange, }: RoleTableProps) { return ( ); } diff --git a/src/app/(modules)/roles/hooks/useRoleFilters.ts b/src/app/(modules)/roles/hooks/useRoleFilters.ts index fe64690..78e5ec7 100644 --- a/src/app/(modules)/roles/hooks/useRoleFilters.ts +++ b/src/app/(modules)/roles/hooks/useRoleFilters.ts @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import type { SortingState } from '@tanstack/react-table'; export type StatusFilter = 'all' | 'active' | 'inactive'; @@ -9,6 +10,7 @@ const SEARCH_DEBOUNCE_MS = 400; export function useRoleFilters() { const [skip, setSkip] = useState(0); const [limit, setLimitValue] = useState(10); + const [sorting, setSortingValue] = useState([]); const [searchTerm, setSearchTermValue] = useState(''); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); const [statusFilter, setStatusFilterValue] = useState('all'); @@ -36,11 +38,18 @@ export function useRoleFilters() { setSkip(0); }; + const setSorting = (value: SortingState) => { + setSortingValue(value); + setSkip(0); + }; + return { skip, setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, diff --git a/src/app/(modules)/roles/hooks/useRoleQueries.ts b/src/app/(modules)/roles/hooks/useRoleQueries.ts index 2e6718c..ad8e050 100644 --- a/src/app/(modules)/roles/hooks/useRoleQueries.ts +++ b/src/app/(modules)/roles/hooks/useRoleQueries.ts @@ -1,9 +1,11 @@ 'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { SortingState } from '@tanstack/react-table'; import { useMemo } from 'react'; import { toast } from 'sonner'; +import { getServerSortParams } from '@/components/data-table/sorting'; import { mapPermissionTree } from '@/components/permission-tree'; import { permissionService, roleService } from '@/services/api'; import type { Role, RoleListParams } from '@/types'; @@ -16,6 +18,7 @@ interface UseRolesQueryParams { limit: number; searchTerm: string; statusFilter: StatusFilter; + sorting: SortingState; } function buildRoleListParams({ @@ -23,22 +26,22 @@ function buildRoleListParams({ limit, searchTerm, statusFilter, + sorting, }: UseRolesQueryParams): RoleListParams { return { skip, limit, search_term: searchTerm || undefined, effective_status: statusFilter === 'all' ? undefined : statusFilter === 'active', - sort_by: 'created_at', - sort_order: 'desc', + ...getServerSortParams(sorting), }; } export function useRolesQuery(params: UseRolesQueryParams) { - const { skip, limit, searchTerm, statusFilter } = params; + const { skip, limit, searchTerm, statusFilter, sorting } = params; const listParams = useMemo( - () => buildRoleListParams({ skip, limit, searchTerm, statusFilter }), - [limit, searchTerm, skip, statusFilter], + () => buildRoleListParams({ skip, limit, searchTerm, statusFilter, sorting }), + [limit, searchTerm, skip, sorting, statusFilter], ); return useQuery({ diff --git a/src/app/(modules)/roles/page.tsx b/src/app/(modules)/roles/page.tsx index 0b1ca86..10dd40e 100644 --- a/src/app/(modules)/roles/page.tsx +++ b/src/app/(modules)/roles/page.tsx @@ -31,6 +31,8 @@ export default function RolesPage() { setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, @@ -43,6 +45,7 @@ export default function RolesPage() { limit, searchTerm: debouncedSearchTerm, statusFilter, + sorting, }); const permissionsQuery = usePermissionsTreeQuery(isSheetOpen); const roleForm = useRoleForm({ @@ -125,8 +128,10 @@ export default function RolesPage() { skip={skip} limit={limit} total={total} + sorting={sorting} onPageChange={setSkip} onLimitChange={setLimit} + onSortingChange={setSorting} /> diff --git a/src/app/(modules)/tenants/components/TenantColumns.tsx b/src/app/(modules)/tenants/components/TenantColumns.tsx index 66394e6..5e09fa6 100644 --- a/src/app/(modules)/tenants/components/TenantColumns.tsx +++ b/src/app/(modules)/tenants/components/TenantColumns.tsx @@ -82,6 +82,7 @@ export function useTenantColumns({ columns.push({ id: 'actions', header: () =>
Actions
, + enableSorting: false, cell: ({ row }) => { const tenant = row.original; return ( diff --git a/src/app/(modules)/tenants/components/TenantTable.tsx b/src/app/(modules)/tenants/components/TenantTable.tsx index 5d18497..b7fb22f 100644 --- a/src/app/(modules)/tenants/components/TenantTable.tsx +++ b/src/app/(modules)/tenants/components/TenantTable.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ReactNode } from 'react'; -import type { ColumnDef } from '@tanstack/react-table'; +import type { ColumnDef, SortingState } from '@tanstack/react-table'; import { DataTable } from '@/components/data-table'; import type { Tenant } from '@/types'; @@ -14,8 +14,10 @@ interface TenantTableProps { skip: number; limit: number; total: number; + sorting: SortingState; onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; + onSortingChange: (sorting: SortingState) => void; } export function TenantTable({ @@ -26,8 +28,10 @@ export function TenantTable({ skip, limit, total, + sorting, onPageChange, onLimitChange, + onSortingChange, }: TenantTableProps) { return ( ); } diff --git a/src/app/(modules)/tenants/hooks/useTenantFilters.ts b/src/app/(modules)/tenants/hooks/useTenantFilters.ts index 2a87c8a..fd90854 100644 --- a/src/app/(modules)/tenants/hooks/useTenantFilters.ts +++ b/src/app/(modules)/tenants/hooks/useTenantFilters.ts @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useState } from 'react'; +import type { SortingState } from '@tanstack/react-table'; import { useDebounce } from '@/hooks/useDebounce'; @@ -9,6 +10,7 @@ export type TenantStatusFilter = 'all' | 'active' | 'inactive'; export function useTenantFilters() { const [skip, setSkip] = useState(0); const [limit, setLimitValue] = useState(10); + const [sorting, setSortingValue] = useState([]); const [searchTerm, setSearchTermValue] = useState(''); const [statusFilter, setStatusFilterValue] = useState('all'); const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400); @@ -28,11 +30,18 @@ export function useTenantFilters() { setSkip(0); }, []); + const setSorting = useCallback((value: SortingState) => { + setSortingValue(value); + setSkip(0); + }, []); + return { skip, setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, diff --git a/src/app/(modules)/tenants/hooks/useTenantQueries.ts b/src/app/(modules)/tenants/hooks/useTenantQueries.ts index 20335fc..625124f 100644 --- a/src/app/(modules)/tenants/hooks/useTenantQueries.ts +++ b/src/app/(modules)/tenants/hooks/useTenantQueries.ts @@ -2,7 +2,9 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; +import type { SortingState } from '@tanstack/react-table'; +import { getServerSortParams } from '@/components/data-table/sorting'; import { clientService, planService, tenantService } from '@/services/api'; import type { TenantListParams } from '@/types'; @@ -14,6 +16,7 @@ interface UseTenantsQueryParams { limit: number; searchTerm: string; statusFilter: TenantStatusFilter; + sorting: SortingState; } const clientLookupParams = { @@ -37,14 +40,14 @@ function buildTenantListParams({ limit, searchTerm, statusFilter, + sorting, }: UseTenantsQueryParams): TenantListParams { return { skip, limit, search_term: searchTerm || undefined, is_active: statusFilter === 'all' ? undefined : statusFilter === 'active', - sort_by: 'created_at', - sort_order: 'desc', + ...getServerSortParams(sorting), }; } diff --git a/src/app/(modules)/tenants/page.tsx b/src/app/(modules)/tenants/page.tsx index 9df07e6..abd8044 100644 --- a/src/app/(modules)/tenants/page.tsx +++ b/src/app/(modules)/tenants/page.tsx @@ -31,6 +31,8 @@ export default function TenantsPage() { setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, @@ -43,6 +45,7 @@ export default function TenantsPage() { limit, searchTerm: debouncedSearchTerm, statusFilter, + sorting, }); const clientsLookupQuery = useClientLookupQuery(isSheetOpen); const plansLookupQuery = usePlanLookupQuery(isSheetOpen); @@ -163,8 +166,10 @@ export default function TenantsPage() { skip={skip} limit={limit} total={total} + sorting={sorting} onPageChange={setSkip} onLimitChange={setLimit} + onSortingChange={setSorting} /> diff --git a/src/app/(modules)/users/components/UserColumns.tsx b/src/app/(modules)/users/components/UserColumns.tsx index ba80947..c5b7e9e 100644 --- a/src/app/(modules)/users/components/UserColumns.tsx +++ b/src/app/(modules)/users/components/UserColumns.tsx @@ -58,6 +58,7 @@ export function useUserColumns({ { accessorKey: 'roles', header: 'Roles', + enableSorting: false, cell: ({ row }) => (
{row.original.roles?.length ? ( @@ -93,6 +94,7 @@ export function useUserColumns({ columns.push({ id: 'actions', header: () =>
Actions
, + enableSorting: false, cell: ({ row }) => { const user = row.original; return ( diff --git a/src/app/(modules)/users/components/UserTable.tsx b/src/app/(modules)/users/components/UserTable.tsx index ea2bd5e..cfd1795 100644 --- a/src/app/(modules)/users/components/UserTable.tsx +++ b/src/app/(modules)/users/components/UserTable.tsx @@ -1,7 +1,7 @@ 'use client'; import type { ReactNode } from 'react'; -import type { ColumnDef } from '@tanstack/react-table'; +import type { ColumnDef, SortingState } from '@tanstack/react-table'; import { DataTable } from '@/components/data-table'; import type { AdministrationUser } from '@/types'; @@ -14,8 +14,10 @@ interface UserTableProps { skip: number; limit: number; total: number; + sorting: SortingState; onPageChange: (skip: number) => void; onLimitChange: (limit: number) => void; + onSortingChange: (sorting: SortingState) => void; } export function UserTable({ @@ -26,8 +28,10 @@ export function UserTable({ skip, limit, total, + sorting, onPageChange, onLimitChange, + onSortingChange, }: UserTableProps) { return ( ); } diff --git a/src/app/(modules)/users/hooks/useUserFilters.ts b/src/app/(modules)/users/hooks/useUserFilters.ts index b35613d..79db46b 100644 --- a/src/app/(modules)/users/hooks/useUserFilters.ts +++ b/src/app/(modules)/users/hooks/useUserFilters.ts @@ -2,6 +2,7 @@ import { useDebounce } from '@/hooks/useDebounce'; import type { UserStatus } from '@/types'; +import type { SortingState } from '@tanstack/react-table'; import { useCallback, useState } from 'react'; export type StatusFilter = 'all' | UserStatus; @@ -9,6 +10,7 @@ export type StatusFilter = 'all' | UserStatus; export function useUserFilters() { const [skip, setSkip] = useState(0); const [limit, setLimitValue] = useState(10); + const [sorting, setSortingValue] = useState([]); const [searchTerm, setSearchTermValue] = useState(''); const [statusFilter, setStatusFilterValue] = useState('all'); const debouncedSearchTerm = useDebounce(searchTerm.trim(), 400); @@ -28,11 +30,18 @@ export function useUserFilters() { setSkip(0); }, []); + const setSorting = useCallback((value: SortingState) => { + setSortingValue(value); + setSkip(0); + }, []); + return { skip, setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, diff --git a/src/app/(modules)/users/hooks/useUserQueries.ts b/src/app/(modules)/users/hooks/useUserQueries.ts index 2c6f589..8c23968 100644 --- a/src/app/(modules)/users/hooks/useUserQueries.ts +++ b/src/app/(modules)/users/hooks/useUserQueries.ts @@ -2,7 +2,9 @@ import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; +import type { SortingState } from '@tanstack/react-table'; +import { getServerSortParams } from '@/components/data-table/sorting'; import { userService } from '@/services/api'; import type { UserListParams } from '@/types'; import { userKeys } from '../queries/userKeys'; @@ -13,6 +15,7 @@ interface UseUsersQueryParams { limit: number; searchTerm: string; statusFilter: StatusFilter; + sorting: SortingState; } function buildUserListParams({ @@ -20,22 +23,22 @@ function buildUserListParams({ limit, searchTerm, statusFilter, + sorting, }: UseUsersQueryParams): UserListParams { return { skip, limit, search_term: searchTerm || undefined, effective_status: statusFilter === 'all' ? undefined : statusFilter, - sort_by: 'created_at', - sort_order: 'desc', + ...getServerSortParams(sorting), }; } export function useUsersQuery(params: UseUsersQueryParams) { - const { skip, limit, searchTerm, statusFilter } = params; + const { skip, limit, searchTerm, statusFilter, sorting } = params; const listParams = useMemo( - () => buildUserListParams({ skip, limit, searchTerm, statusFilter }), - [limit, searchTerm, skip, statusFilter], + () => buildUserListParams({ skip, limit, searchTerm, statusFilter, sorting }), + [limit, searchTerm, skip, sorting, statusFilter], ); return useQuery({ diff --git a/src/app/(modules)/users/page.tsx b/src/app/(modules)/users/page.tsx index 792df4d..7329d69 100644 --- a/src/app/(modules)/users/page.tsx +++ b/src/app/(modules)/users/page.tsx @@ -27,6 +27,8 @@ export default function UsersPage() { setSkip, limit, setLimit, + sorting, + setSorting, searchTerm, debouncedSearchTerm, setSearchTerm, @@ -39,6 +41,7 @@ export default function UsersPage() { limit, searchTerm: debouncedSearchTerm, statusFilter, + sorting, }); const userForm = useUserForm({ onSaved: () => setIsSheetOpen(false), @@ -128,8 +131,10 @@ export default function UsersPage() { skip={skip} limit={limit} total={total} + sorting={sorting} onPageChange={setSkip} onLimitChange={setLimit} + onSortingChange={setSorting} /> diff --git a/src/components/data-table/TableHeader.tsx b/src/components/data-table/TableHeader.tsx index 7f54bae..c504674 100644 --- a/src/components/data-table/TableHeader.tsx +++ b/src/components/data-table/TableHeader.tsx @@ -2,6 +2,8 @@ import { flexRender } from '@tanstack/react-table'; import { TableHead, TableHeader as ShadTableHeader, TableRow } from '@/components/ui/table'; import { Table } from '@tanstack/react-table'; +import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide } from 'lucide-react'; +import { cn } from '@/lib/utils'; const TableHeader = ({ table }: { table: Table }) => { return ( @@ -9,14 +11,38 @@ const TableHeader = ({ table }: { table: Table }) => { {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { + const canSort = header.column.getCanSort(); + const sortDirection = header.column.getIsSorted(); + const SortIcon = + sortDirection === 'asc' + ? ArrowUpNarrowWide + : sortDirection === 'desc' + ? ArrowDownWideNarrow + : ArrowUpDown; + return ( {header.isPlaceholder ? null : ( -
- {flexRender(header.column.columnDef.header, header.getContext())} +
+ + {flexRender(header.column.columnDef.header, header.getContext())} + + {canSort ? ( + + ) : null}
)} diff --git a/src/components/data-table/index.tsx b/src/components/data-table/index.tsx index f8fd277..c645998 100644 --- a/src/components/data-table/index.tsx +++ b/src/components/data-table/index.tsx @@ -38,6 +38,8 @@ export interface DataTableProps { onPageChange: (newSkip: number) => void; onLimitChange: (newLimit: number) => void; }; + sorting?: SortingState; + onSortingChange?: (sorting: SortingState) => void; } export function DataTable({ @@ -53,9 +55,24 @@ export function DataTable({ emptyTitle = 'No results found.', emptyDescription = 'Try adjusting your filters or search terms.', pagination, + sorting: controlledSorting, + onSortingChange, }: DataTableProps) { const [rowSelection, setRowSelection] = React.useState({}); - const [sorting, setSorting] = React.useState([]); + const [internalSorting, setInternalSorting] = React.useState([]); + const sorting = controlledSorting ?? internalSorting; + const isManualSorting = Boolean(onSortingChange); + const handleSortingChange = React.useCallback( + (updater: SortingState | ((old: SortingState) => SortingState)) => { + const nextSorting = typeof updater === 'function' ? updater(sorting) : updater; + if (onSortingChange) { + onSortingChange(nextSorting); + return; + } + setInternalSorting(nextSorting); + }, + [onSortingChange, sorting], + ); const columns = React.useMemo(() => { const cols: ColumnDef[] = [...initialColumns]; @@ -105,10 +122,11 @@ export function DataTable({ columns, getCoreRowModel: getCoreRowModel(), onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, + onSortingChange: handleSortingChange, getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), manualPagination: !!pagination, + manualSorting: isManualSorting, pageCount: pagination?.totalItems ? Math.ceil(pagination.totalItems / pagination.limit) : -1, state: { rowSelection, diff --git a/src/components/data-table/sorting.ts b/src/components/data-table/sorting.ts new file mode 100644 index 0000000..2b447e3 --- /dev/null +++ b/src/components/data-table/sorting.ts @@ -0,0 +1,14 @@ +import type { SortingState } from '@tanstack/react-table'; + +export function getServerSortParams(sorting: SortingState) { + const activeSort = sorting[0]; + + if (!activeSort) { + return {}; + } + + return { + sort_by: activeSort.id, + sort_order: activeSort.desc ? 'desc' : 'asc', + }; +}