'use client'; import ActionPopMenu from '@/components/common/ActionPopMenu'; import { type ColumnData, MyTable } from '@/components/common/MyTable'; import BaseButton from '@/components/ui-kit/BaseButton'; import BaseInput from '@/components/ui-kit/BaseInput'; import { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect'; import { useAuthContext } from '@/context/auth-context'; import type { BoxStatus, IBox, PrintStatus } from '@/data/box/box.model'; import { box_requests } from '@/data/box/box.requests'; import type { Product, UpdateProductBodyType } from '@/data/item/item.mode'; import { item_requests } from '@/data/item/item.requests'; import { party_requests } from '@/data/party/party.requests'; import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants'; import useInput from '@/hooks/useInput'; import { useMyNavigation } from '@/hooks/useMyNavigation'; import { useMyTranslation } from '@/hooks/useMyTranslation'; import useRequest from '@/hooks/useRequest'; import { useModalStore } from '@/modalStorage/modalSlice'; import { useBoxIdStore } from '@/modalStorage/partyId'; import { notifyUnknownError } from '@/services/notification'; import { CheckCircle, FilterListOff, Print, RemoveRedEye, Search } from '@mui/icons-material'; import { Box, Button, CircularProgress, FormControl, MenuItem, Select, Stack, Typography } from '@mui/material'; import { CloseIcon } from 'next/dist/client/components/react-dev-overlay/internal/icons/CloseIcon'; import { useRouter, useSearchParams } from 'next/navigation'; import type React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import AsyncSelect from 'react-select/async'; import { useReactToPrint } from 'react-to-print'; import BoxesPrintList from '../boxes-print/BoxesPrintList'; import CustomModal from '../customModal/CustomModal'; type Props = {}; const style = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, display: 'flex', flexDirection: 'column', gap: '10px', p: 4, }; const DashboardAcceptancePage = () => { // Redux for modal state const { isOpen: isModalOpen, openModal, closeModal } = useModalStore(); const t = useMyTranslation(); const navigation = useMyNavigation(); const { isAdmin } = useAuthContext(); const router = useRouter(); // State management const [page, setPage] = useState(1); const [pageSize] = useState(DEFAULT_PAGE_SIZE); const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput(''); const [boxStatusFilter, setBoxStatusFilter] = useState(undefined); const [trackId, setTrackId] = useState(); const [partyFilter, setPartyFilter] = useState<{ label: string; value: number } | undefined>(undefined); const [boxFilter, setBoxFilter] = useState<{ label: string; value: number } | undefined>(undefined); const [deleteIds, setDeleteIds] = useState([]); const [downloadIds, setDownloadIds] = useState([]); const [changeStatusIds, setChangeStatusIds] = useState([]); const [boxAmounts, setBoxAmounts] = useState>({}); const [printStatuses, setPrintStatuses] = useState>({}); const [hasMore, setHasMore] = useState(true); const [isFetching, setIsFetching] = useState(false); const [allData, setAllData] = useState([]); const searchParams = useSearchParams(); const boxId = searchParams.get('boxId'); const { boxesIdAccepted, setBoxIdAccepted } = useBoxIdStore(); // Print related state const [selectedBoxForPrint, setSelectedBoxForPrint] = useState(null); const [selectedBoxDetails, setSelectedBoxDetails] = useState(null); const printRef = useRef(null); const tableContainerRef = useRef(null); // Print functionality const handlePrint = useReactToPrint({ contentRef: printRef, onAfterPrint: () => { setSelectedBoxForPrint(null); setSelectedBoxDetails(null); }, }); // Fetch party options const { data: defaultPartyOptions } = useRequest(() => party_requests.getAll({}), { enabled: true, selectData(data) { return data.data.data.data.map(p => ({ value: p.id, label: p.name })); }, placeholderData: [], }); const partyOptions = (inputValue: string) => { return party_requests.getAll({ partyName: inputValue }).then(res => { return res.data.data.data.map(p => ({ label: p.name, value: p.id })); }); }; // Print box handler const onPrintBox = async (boxData: IBox) => { try { const response = await box_requests.find({ packetId: boxData.id }); const boxOne = response.data.data; const detailedBoxData = { id: +boxData.id, box_name: boxOne.packet.name, net_weight: +boxOne.packet.brutto, box_weight: +boxOne.packet.boxWeight, box_type: boxOne.packet.boxType, box_size: boxOne.packet.volume, passportName: boxOne.packet.passportName, status: boxOne.packet.status, packetId: boxData.id, partyId: +boxOne.packet.partyId, partyName: boxOne.packet.partyName, passportId: boxOne.client?.passportId, client_id: boxOne.packet?.cargoId, clientName: boxOne.client?.passportName, products_list: boxOne.items.map((item: any) => ({ id: item.id, price: item.price, cargoId: item.cargoId, trekId: item.trekId, name: item.name, nameRu: item.nameRu, amount: +item.amount, acceptedNumber: item.acceptedNumber, weight: +item.weight, })), }; setSelectedBoxDetails(detailedBoxData); setTimeout(() => { handlePrint(); }, 100); } catch (error) {} }; // Memoized options const boxStatusOptions = useMemo(() => { const p = ['READY_TO_INVOICE'] as BoxStatus[]; if (isAdmin) { p.push('READY'); } return p; }, [isAdmin]); const printOptions = useMemo(() => { const p = ['false'] as PrintStatus[]; if (isAdmin) { p.push('false'); } return p; }, [isAdmin]); // Main data fetching query with optimized refresh handling const getBoxesQuery = useRequest( () => box_requests.getAll({ page: page, cargoId: keyword, partyId: partyFilter?.value, status: boxStatusFilter, direction: 'desc', sort: 'id', }), { dependencies: [page, boxStatusFilter, keyword, partyFilter], selectData(data) { return data.data.data; }, onSuccess: data => { // Only update data if the page or filters have changed if (page === 1) { setAllData(data.data.data.data); } else { // Prevent duplicate data setAllData(prev => { const existingIds = new Set(prev.map(box => box.id)); const newData = data.data.data.data.filter(box => !existingIds.has(box.id)); return [...prev, ...newData]; }); } setHasMore(data.data.data.data.length === pageSize); setIsFetching(false); }, } ); // Secondary list query const getListQuery = useRequest( () => item_requests.getAll({ page: page, trekId: trackId, packetId: boxFilter?.value, partyId: partyFilter?.value, }), { dependencies: [page, trackId, boxFilter?.value, partyFilter?.value], selectData(data) { return data.data.data; }, } ); // Form state for item amounts const [values, setValues] = useState<{ [trackId: string]: number | '' }>({}); const handleAmountChange = (event: React.ChangeEvent, max: number, trackId: string) => { const val = Number(event.target.value); if (val >= 1 && val <= max) { setValues(prev => ({ ...prev, [trackId]: val })); } else if (event.target.value === '') { setValues(prev => ({ ...prev, [trackId]: '' })); } }; const loading = getBoxesQuery.loading; // Page change handler const handleChange = useCallback( (newPage: number) => { if (!isFetching && hasMore) { setIsFetching(true); setPage(newPage); } }, [isFetching, hasMore] ); // Reset all filters const resetFilter = useCallback(() => { setPage(1); setKeyword(''); setBoxStatusFilter(undefined); setPartyFilter(undefined); setAllData([]); }, []); // Box options for select const { data: defaultBoxOptions, refetch } = useRequest( () => box_requests.getAll({ partyId: partyFilter?.value, }), { enabled: !!partyFilter, selectData(data) { return data.data.data.data.map(p => ({ value: p.id, label: p.name })); }, placeholderData: [], dependencies: [partyFilter], } ); useEffect(() => { if (boxId && defaultPartyOptions && defaultPartyOptions.length > 0) { const selected = defaultPartyOptions.find(p => p.value === Number(boxId)); if (selected) { setPartyFilter(selected); } } }, [boxId, defaultPartyOptions]); useEffect(() => { if (boxesIdAccepted) { navigation.push(`${pageLinks.dashboard.acceptance.index}?boxId=${boxesIdAccepted}`); } }, [boxesIdAccepted]); const onChangePrint = async (id: number, newStatus: PrintStatus) => { if (changeStatusIds.includes(id)) return; try { setChangeStatusIds(p => [...p, id]); const res = await box_requests.find({ packetId: id }); await box_requests.update({ cargoId: String(res.data.data.packet.cargoId), items: res.data.data.items, print: newStatus === 'true' ? true : false, packetId: String(res.data.data.packet.id), passportId: res.data.data.client.passportId, status: res.data.data.packet.status, }); setPrintStatuses(prev => ({ ...prev, [id]: newStatus, })); } catch (error) { } finally { setChangeStatusIds(prev => prev.filter(i => i !== id)); } }; // Filter changes with debouncing useEffect(() => { const debounceTimer = setTimeout(() => { if (page === 1) { getBoxesQuery.refetch(); } else { setPage(1); // This will trigger the refetch automatically } }, 350); return () => clearTimeout(debounceTimer); }, [keyword, partyFilter?.value, boxFilter?.value]); // Fetch box amounts only when needed useEffect(() => { if (allData.length === 0 || loading) return; const controller = new AbortController(); const fetchAmounts = async () => { const result: Record = {}; try { await Promise.all( allData.map(async box => { if (boxAmounts[box.id]) return; try { const res = await box_requests.find({ packetId: box.id }); const boxData = res.data.data; const total = boxData.items.reduce( (acc, item) => { acc.totalAmount = boxData.packet.totalItems ?? 0; if (item.acceptedNumber && item.acceptedNumber > 0) { acc.totalAccepted += 1; } return acc; }, { totalAmount: 0, totalAccepted: 0 } ); result[box.id] = total; } catch (e) { if (!controller.signal.aborted) { console.error(e); } } }) ); if (Object.keys(result).length > 0) { setBoxAmounts(prev => ({ ...prev, ...result })); } } catch (error) { console.error(error); } }; fetchAmounts(); return () => { controller.abort(); }; }, [allData, loading]); // Optimized scroll handler with throttling const handleScroll = useCallback(() => { if (!tableContainerRef.current || isFetching || !hasMore) return; const { scrollTop, scrollHeight, clientHeight } = tableContainerRef.current; const scrollThreshold = 100; // pixels from bottom to trigger load if (scrollHeight - (scrollTop + clientHeight) < scrollThreshold) { handleChange(page + 1); } }, [page, isFetching, hasMore, handleChange]); // Scroll event listener with cleanup useEffect(() => { const container = tableContainerRef.current; if (!container) return; const throttledScroll = throttle(handleScroll, 200); container.addEventListener('scroll', throttledScroll); return () => { container.removeEventListener('scroll', throttledScroll); }; }, [handleScroll]); // Box options for select const boxOptions = (inputValue: string) => { return box_requests .getAll({ cargoId: inputValue, partyId: partyFilter?.value, }) .then(res => { return res.data.data.data.map(p => ({ label: p.name, value: p.id })); }); }; // Reset box filter when party changes useEffect(() => { setBoxFilter(undefined); }, [partyFilter]); // Table columns definition const columns: ColumnData[] = useMemo( () => [ { label: t('No'), width: 100, renderCell(data, rowIndex) { return rowIndex + 1; }, }, { dataKey: 'partyName', label: t('party_name'), width: 120, }, { dataKey: 'name', label: t('name'), width: 120, }, { dataKey: 'packetNetWeight', label: t('weight'), width: 120, }, { dataKey: 'totalItems', label: t('count_of_items'), width: 120, renderCell: data => { const total = boxAmounts[data.id]; if (!total) return ...; const isCompleted = total.totalAmount === total.totalAccepted && total.totalAmount > 0; return ( {data.totalItems} / {total.totalAccepted} {isCompleted && } ); }, }, { dataKey: 'totalNetWeight', label: t('party_weight'), width: 120, }, { dataKey: 'cargoId', label: t('cargo_id'), width: 120, }, { dataKey: 'passportName', label: t('client'), width: 120, }, { dataKey: 'id', label: t('print'), width: 120, renderHeaderCell() { return ( {t('print')} ); }, renderCell(data) { const total = boxAmounts[data.id]; const isCompleted = total?.totalAccepted === total?.totalAmount && total?.totalAmount > 0; return ( ); }, }, { dataKey: 'status', label: t('status'), width: 240, renderHeaderCell() { return ( {t('print_status')} ); }, renderCell(data) { const currentValue = printStatuses[data.id] || (data.print ? 'true' : 'false'); return ( ); }, }, { label: '', width: 100, numeric: true, renderCell(data) { return ( , label: t('view_packet'), onClick: () => { navigation.push(pageLinks.dashboard.boxes.detail(data.id)); }, }, ]} /> ); }, }, ], [t, boxAmounts, printStatuses, navigation, onPrintBox, onChangePrint] ); // Form handling for items const [items, setItems] = useState(); const [loaer, setLoading] = useState(false); const { register, control, handleSubmit, watch, setValue, formState: { errors }, } = useForm({ defaultValues: { trekId: items?.trekId, name: items?.name, nameRu: items?.nameRu, amount: items?.amount, weight: items?.weight, acceptedNumber: Number(values), }, }); const updateItems = async (item: Product, acceptedNumber: number) => { try { setLoading(true); const updateBody: UpdateProductBodyType = { itemId: item.id, acceptedNumber: item.amount, amount: item.amount, name: item.name, nameRu: item.nameRu, trekId: item.trekId, weight: item.weight, }; await item_requests.update(updateBody); getListQuery.refetch(); setValues(prev => ({ ...prev, [item.trekId]: '' })); setTrackId(''); } catch (error) { notifyUnknownError(error); } finally { setLoading(false); } }; return ( {t('product_inspection')} {t('enter_product')} { setPartyFilter(newValue); setPage(1); }} styles={selectDefaultStyles} noOptionsMessage={() => t('not_found')} loadingMessage={() => t('loading')} defaultOptions={defaultPartyOptions!} loadOptions={partyOptions} placeholder={t('filter_party_name')} /> { setBoxFilter(newValue); setPage(1); navigation.push(pageLinks.dashboard.boxes.detail(newValue.value)); }} styles={selectDefaultStyles} noOptionsMessage={() => t('enter_box_name_to_find')} loadingMessage={() => t('loading')} defaultOptions={defaultBoxOptions!} loadOptions={boxOptions} placeholder={t('filter_box_name')} /> {t('packet')} , }} placeholder={t('filter_packet_name')} value={keyword} onChange={e => { setKeyword(e.target.value); }} /> { setPartyFilter(newValue); if (newValue) { setBoxIdAccepted(newValue.value); navigation.push(`${pageLinks.dashboard.acceptance.index}?boxId=${newValue.value}`); } else { setBoxIdAccepted(undefined); navigation.push(`${pageLinks.dashboard.acceptance.index}`); } setPage(1); }} styles={selectDefaultStyles} noOptionsMessage={() => t('not_found')} loadingMessage={() => t('loading')} defaultOptions={defaultPartyOptions!} loadOptions={partyOptions} placeholder={t('filter_party_name')} /> } size='small' onClick={resetFilter}> {t('reset_filter')} {isFetching && page > 1 && ( )} {selectedBoxDetails && (
)}
); }; // Simple throttle implementation function throttle any>(func: T, limit: number): T { let inThrottle: boolean; return function (this: any, ...args: any[]) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } } as T; } export default DashboardAcceptancePage;