297 lines
7.9 KiB
TypeScript
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;
|