Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
175
src/hooks/use-paginated-query.ts
Normal file
175
src/hooks/use-paginated-query.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useQuery, useQueryClient, type QueryKey } from '@tanstack/react-query';
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import type { PaginatedResponse } from '@/types/api';
|
||||
import {
|
||||
serializeFiltersToParams,
|
||||
deserializeFiltersFromParams,
|
||||
type FilterDefinition,
|
||||
type FilterValues,
|
||||
} from '@/components/shared/filter-bar';
|
||||
|
||||
interface UsePaginatedQueryOptions<T> {
|
||||
queryKey: QueryKey;
|
||||
endpoint: string;
|
||||
initialPage?: number;
|
||||
initialPageSize?: number;
|
||||
filterDefinitions?: FilterDefinition[];
|
||||
}
|
||||
|
||||
export function usePaginatedQuery<T>({
|
||||
queryKey,
|
||||
endpoint,
|
||||
initialPage = 1,
|
||||
initialPageSize = 25,
|
||||
filterDefinitions = [],
|
||||
}: UsePaginatedQueryOptions<T>) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Read initial state from URL
|
||||
const pageFromUrl = Number(searchParams.get('page')) || initialPage;
|
||||
const pageSizeFromUrl = Number(searchParams.get('limit')) || initialPageSize;
|
||||
const sortFieldFromUrl = searchParams.get('sort') ?? undefined;
|
||||
const sortOrderFromUrl = (searchParams.get('order') as 'asc' | 'desc') ?? 'desc';
|
||||
|
||||
const [page, setPageState] = useState(pageFromUrl);
|
||||
const [pageSize, setPageSizeState] = useState(pageSizeFromUrl);
|
||||
const [sort, setSortState] = useState<{ field: string; direction: 'asc' | 'desc' } | undefined>(
|
||||
sortFieldFromUrl ? { field: sortFieldFromUrl, direction: sortOrderFromUrl } : undefined,
|
||||
);
|
||||
const [filters, setFiltersState] = useState<FilterValues>(() =>
|
||||
deserializeFiltersFromParams(searchParams, filterDefinitions),
|
||||
);
|
||||
|
||||
// Sync state to URL
|
||||
const syncUrl = useCallback(
|
||||
(p: number, ps: number, s?: typeof sort, f?: FilterValues) => {
|
||||
const params = new URLSearchParams();
|
||||
if (p !== 1) params.set('page', String(p));
|
||||
if (ps !== initialPageSize) params.set('limit', String(ps));
|
||||
if (s) {
|
||||
params.set('sort', s.field);
|
||||
params.set('order', s.direction);
|
||||
}
|
||||
if (f) {
|
||||
const filterParams = serializeFiltersToParams(f);
|
||||
filterParams.forEach((value, key) => params.set(key, value));
|
||||
}
|
||||
// Keep existing tab param
|
||||
const tab = searchParams.get('tab');
|
||||
if (tab) params.set('tab', tab);
|
||||
|
||||
const qs = params.toString();
|
||||
router.replace(`${pathname}${qs ? `?${qs}` : ''}` as any, { scroll: false });
|
||||
},
|
||||
[pathname, router, searchParams, initialPageSize],
|
||||
);
|
||||
|
||||
function setPage(p: number) {
|
||||
setPageState(p);
|
||||
syncUrl(p, pageSize, sort, filters);
|
||||
}
|
||||
|
||||
function setPageSize(ps: number) {
|
||||
setPageSizeState(ps);
|
||||
setPageState(1);
|
||||
syncUrl(1, ps, sort, filters);
|
||||
}
|
||||
|
||||
function setSort(field: string, direction: 'asc' | 'desc') {
|
||||
const newSort = { field, direction };
|
||||
setSortState(newSort);
|
||||
setPageState(1);
|
||||
syncUrl(1, pageSize, newSort, filters);
|
||||
}
|
||||
|
||||
function setFilter(key: string, value: unknown) {
|
||||
const newFilters = { ...filters };
|
||||
if (value === undefined || value === null) {
|
||||
delete newFilters[key];
|
||||
} else {
|
||||
newFilters[key] = value;
|
||||
}
|
||||
setFiltersState(newFilters);
|
||||
setPageState(1);
|
||||
syncUrl(1, pageSize, sort, newFilters);
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
setFiltersState({});
|
||||
setPageState(1);
|
||||
syncUrl(1, pageSize, sort, {});
|
||||
}
|
||||
|
||||
// Build query string for API
|
||||
const apiParams = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('limit', String(pageSize));
|
||||
if (sort) {
|
||||
params.set('sort', sort.field);
|
||||
params.set('order', sort.direction);
|
||||
}
|
||||
const filterParams = serializeFiltersToParams(filters);
|
||||
filterParams.forEach((value, key) => params.set(key, value));
|
||||
return params.toString();
|
||||
}, [page, pageSize, sort, filters]);
|
||||
|
||||
const fullQueryKey = [...queryKey, apiParams];
|
||||
|
||||
const { data, isLoading, isFetching } = useQuery<PaginatedResponse<T>>({
|
||||
queryKey: fullQueryKey,
|
||||
queryFn: () =>
|
||||
apiFetch<PaginatedResponse<T>>(`${endpoint}?${apiParams}`),
|
||||
});
|
||||
|
||||
const pagination = data?.pagination
|
||||
? {
|
||||
page: data.pagination.page,
|
||||
pageSize: data.pagination.pageSize,
|
||||
total: data.pagination.total,
|
||||
totalPages: data.pagination.totalPages,
|
||||
}
|
||||
: { page, pageSize, total: 0, totalPages: 0 };
|
||||
|
||||
/**
|
||||
* Optimistically removes an item from the cached list (e.g., after archive).
|
||||
*/
|
||||
function optimisticRemove(id: string) {
|
||||
queryClient.setQueryData<PaginatedResponse<T>>(fullQueryKey, (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
data: old.data.filter((item: any) => item.id !== id),
|
||||
pagination: {
|
||||
...old.pagination,
|
||||
total: old.pagination.total - 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: data?.data ?? [],
|
||||
pagination,
|
||||
isLoading,
|
||||
isFetching,
|
||||
sort,
|
||||
setSort,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
filters,
|
||||
setFilter,
|
||||
clearFilters,
|
||||
optimisticRemove,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user