39948-vm/frontend/src/hooks/useEntityTable.ts
2026-03-19 07:12:29 +04:00

297 lines
7.9 KiB
TypeScript

/**
* useEntityTable Hook
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import type { RootState } from '../stores/store';
import type { AsyncThunk } from '@reduxjs/toolkit';
import type { GridColDef, GridSortModel } from '@mui/x-data-grid';
import type { BaseEntity } from '../types/entities';
import type { FetchParams } from '../types/api';
import type { NotificationState } from '../types/redux';
import type { Filter, FilterItem } from '../types/filters';
interface EntitySliceState<T> {
loading: boolean;
count: number;
refetch: boolean;
notify: NotificationState;
[entityName: string]: T[] | boolean | number | NotificationState | unknown;
}
interface UseEntityTableOptions<T extends BaseEntity> {
entityName: string;
sliceSelector: (state: RootState) => EntitySliceState<T>;
fetchAction: AsyncThunk<unknown, FetchParams, { rejectValue: unknown }>;
updateAction?: AsyncThunk<
unknown,
{ id: string; data: Partial<T> },
{ rejectValue: unknown }
>;
deleteAction: AsyncThunk<unknown, string, { rejectValue: unknown }>;
deleteByIdsAction?: AsyncThunk<unknown, string[], { rejectValue: unknown }>;
setRefetchAction: (value: boolean) => { type: string; payload: boolean };
loadColumnsFunction: (
handleDelete: (id: string) => void,
entityPath: string,
currentUser: unknown,
) => Promise<GridColDef[]>;
filters: Filter[];
perPage?: number;
}
interface UseEntityTableReturn<T> {
// Data
data: T[];
columns: GridColDef[];
loading: boolean;
count: number;
// Pagination
currentPage: number;
setCurrentPage: (page: number) => void;
numPages: number;
// Sorting
sortModel: GridSortModel;
setSortModel: (model: GridSortModel) => void;
// Selection
selectedRows: string[];
setSelectedRows: (ids: string[]) => void;
// Filters
filterItems: FilterItem[];
setFilterItems: (items: FilterItem[]) => void;
filterRequest: string;
handleFilterSubmit: () => void;
handleFilterReset: () => void;
// Delete modal
isDeleteModalActive: boolean;
deleteTargetId: string | null;
handleDeleteClick: (id: string) => void;
handleDeleteConfirm: () => Promise<void>;
handleDeleteCancel: () => void;
handleDeleteSelected: () => Promise<void>;
// Table update
handleRowUpdate: (id: string, data: Partial<T>) => Promise<void>;
// Refresh
loadData: (page?: number, request?: string) => void;
}
/**
* Hook for managing entity table state and operations
*
* @param options - Configuration options
* @returns Table state and management functions
*/
export function useEntityTable<T extends BaseEntity>(
options: UseEntityTableOptions<T>,
): UseEntityTableReturn<T> {
const {
entityName,
sliceSelector,
fetchAction,
updateAction,
deleteAction,
deleteByIdsAction,
setRefetchAction,
loadColumnsFunction,
filters,
perPage = 10,
} = options;
const dispatch = useAppDispatch();
const entityState = sliceSelector(
useAppSelector((state) => state) as RootState,
);
const { currentUser } = useAppSelector((state) => state.auth);
// Extract data from state
const data = (entityState[entityName] as T[]) || [];
const { loading, count, refetch, notify } = entityState;
// Local state
const [currentPage, setCurrentPage] = useState(0);
const [columns, setColumns] = useState<GridColDef[]>([]);
const [filterRequest, setFilterRequest] = useState('');
const [filterItems, setFilterItems] = useState<FilterItem[]>([]);
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const [sortModel, setSortModel] = useState<GridSortModel>([
{ field: '', sort: 'desc' },
]);
// Delete modal state
const [isDeleteModalActive, setIsDeleteModalActive] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
// Calculate number of pages
const numPages = useMemo(() => {
return count === 0 ? 1 : Math.ceil(count / perPage);
}, [count, perPage]);
// Load data function
const loadData = useCallback(
(page = currentPage, request = filterRequest) => {
if (page !== currentPage) setCurrentPage(page);
if (request !== filterRequest) setFilterRequest(request);
const { sort, field } = sortModel[0] || { sort: 'desc', field: '' };
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
dispatch(fetchAction({ query }));
},
[currentPage, filterRequest, sortModel, perPage, dispatch, fetchAction],
);
// Load columns when user is available
useEffect(() => {
if (!currentUser) return;
loadColumnsFunction(
(id: string) => handleDeleteClick(id),
entityName,
currentUser,
).then(setColumns);
}, [currentUser, entityName, loadColumnsFunction]);
// Load data when sort model changes
useEffect(() => {
if (!currentUser) return;
loadData();
}, [sortModel, currentUser]);
// Handle refetch flag
useEffect(() => {
if (refetch) {
loadData(0);
dispatch(setRefetchAction(false));
}
}, [refetch, dispatch, setRefetchAction, loadData]);
// Generate filter request
const generateFilterRequest = useCallback((): string => {
let request = '&';
filterItems.forEach((item) => {
const filter = filters.find((f) => f.title === item.fields.selectedField);
if (!filter) return;
const isRangeFilter = filter.number || filter.date;
if (isRangeFilter) {
const from = item.fields.filterValueFrom;
const to = item.fields.filterValueTo;
if (from) {
request += `${item.fields.selectedField}Range=${encodeURIComponent(from)}&`;
}
if (to) {
request += `${item.fields.selectedField}Range=${encodeURIComponent(to)}&`;
}
} else {
const value = item.fields.filterValue;
if (value) {
request += `${item.fields.selectedField}=${encodeURIComponent(value)}&`;
}
}
});
return request;
}, [filterItems, filters]);
// Filter handlers
const handleFilterSubmit = useCallback(() => {
loadData(0, generateFilterRequest());
}, [loadData, generateFilterRequest]);
const handleFilterReset = useCallback(() => {
setFilterItems([]);
loadData(0, '');
}, [loadData]);
// Delete handlers
const handleDeleteClick = useCallback((id: string) => {
setDeleteTargetId(id);
setIsDeleteModalActive(true);
}, []);
const handleDeleteConfirm = useCallback(async () => {
if (!deleteTargetId) return;
await dispatch(deleteAction(deleteTargetId));
await loadData(0);
setIsDeleteModalActive(false);
setDeleteTargetId(null);
}, [deleteTargetId, dispatch, deleteAction, loadData]);
const handleDeleteCancel = useCallback(() => {
setIsDeleteModalActive(false);
setDeleteTargetId(null);
}, []);
const handleDeleteSelected = useCallback(async () => {
if (!deleteByIdsAction || selectedRows.length === 0) return;
await dispatch(deleteByIdsAction(selectedRows));
await loadData(0);
setSelectedRows([]);
}, [deleteByIdsAction, selectedRows, dispatch, loadData]);
// Row update handler
const handleRowUpdate = useCallback(
async (id: string, rowData: Partial<T>) => {
if (!updateAction) return;
await dispatch(updateAction({ id, data: rowData })).unwrap();
},
[updateAction, dispatch],
);
return {
// Data
data,
columns,
loading,
count,
// Pagination
currentPage,
setCurrentPage,
numPages,
// Sorting
sortModel,
setSortModel,
// Selection
selectedRows,
setSelectedRows,
// Filters
filterItems,
setFilterItems,
filterRequest,
handleFilterSubmit,
handleFilterReset,
// Delete modal
isDeleteModalActive,
deleteTargetId,
handleDeleteClick,
handleDeleteConfirm,
handleDeleteCancel,
handleDeleteSelected,
// Table update
handleRowUpdate,
// Refresh
loadData,
};
}
export default useEntityTable;