All files / src/viewmodels/admin useAdminListViewModel.ts

89.02% Statements 73/82
73.33% Branches 11/15
100% Functions 1/1
89.02% Lines 73/82

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 1081x 1x     1x                         1x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x   60x 60x 60x 60x   60x 14x 14x 14x   14x 14x 14x 14x 14x 14x         14x 14x 14x 60x   60x 11x 60x   60x 2x 60x   60x 2x 2x 60x   60x       60x   60x 1x 60x   60x 1x 60x   60x     60x   60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x 60x  
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiError } from '@/services/api';
import type { AdminListResponse, AdminPageParams } from '@/models/admin';
 
const DEFAULT_LIMIT = 25;
 
export type AdminFilters = object;
 
interface UseAdminListOptions<TItem, TFilters extends AdminFilters> {
  token: string | null;
  initialFilters: TFilters;
  fetchPage: (
    token: string,
    params: AdminPageParams & TFilters,
  ) => Promise<AdminListResponse<TItem>>;
}
 
export function useAdminListViewModel<TItem, TFilters extends AdminFilters>({
  token,
  initialFilters,
  fetchPage,
}: UseAdminListOptions<TItem, TFilters>) {
  const [filters, setFilters] = useState<TFilters>(initialFilters);
  const [appliedFilters, setAppliedFilters] = useState<TFilters>(initialFilters);
  const [limit, setLimit] = useState(DEFAULT_LIMIT);
  const [offset, setOffset] = useState(0);
  const [items, setItems] = useState<TItem[]>([]);
  const [totalCount, setTotalCount] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
 
  const params = useMemo(
    () => ({ ...appliedFilters, limit, offset }) as AdminPageParams & TFilters,
    [appliedFilters, limit, offset],
  );
 
  const load = useCallback(async () => {
    if (!token) return;
    setIsLoading(true);
    setError(null);
 
    try {
      const data = await fetchPage(token, params);
      setItems(data.items ?? []);
      setTotalCount(data.total_count ?? 0);
      setHasNext(Boolean(data.has_next));
    } catch (err) {
      setItems([]);
      setTotalCount(0);
      setHasNext(false);
      setError(err instanceof ApiError ? err.message : 'Failed to load admin data.');
    } finally {
      setIsLoading(false);
    }
  }, [fetchPage, params, token]);
 
  useEffect(() => {
    load();
  }, [load]);
 
  const setFilter = useCallback(<K extends keyof TFilters>(key: K, value: TFilters[K]) => {
    setFilters((current) => ({ ...current, [key]: value }));
  }, []);
 
  const applyFilters = useCallback(() => {
    setOffset(0);
    setAppliedFilters(filters);
  }, [filters]);
 
  const clearFilters = useCallback(() => {
    setFilters(initialFilters);
    setAppliedFilters(initialFilters);
    setOffset(0);
  }, [initialFilters]);
 
  const nextPage = useCallback(() => {
    if (hasNext) setOffset((current) => current + limit);
  }, [hasNext, limit]);
 
  const previousPage = useCallback(() => {
    setOffset((current) => Math.max(0, current - limit));
  }, [limit]);
 
  const changeLimit = useCallback((nextLimit: number) => {
    setLimit(nextLimit);
    setOffset(0);
  }, []);
 
  return {
    filters,
    items,
    totalCount,
    hasNext,
    isLoading,
    error,
    limit,
    offset,
    setFilter,
    applyFilters,
    clearFilters,
    nextPage,
    previousPage,
    changeLimit,
    retry: load,
  };
}