diff --git a/package.json b/package.json index 5ac2043..72d956b 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "cross-env NEXT_PUBLIC_API_URL=https://cpost.felixits.uz next dev --port=3080", + "dev": "cross-env NEXT_PUBLIC_API_URL=https://cpcargo.felixits.uz next dev --port=3080", "build": "cross-env NEXT_PUBLIC_API_URL=https://api.cpcargo.uz next build", - "build:dev": "cross-env NEXT_PUBLIC_API_URL=https://cpost.felixits.uz next build", + "build:dev": "cross-env NEXT_PUBLIC_API_URL=https://cpcargo.felixits.uz next build", "build:prod": "cross-env NEXT_PUBLIC_API_URL=https://api.cpcargo.uz next build", "start": "next start", "lint": "next lint", diff --git a/src/app/[locale]/dashboard/acceptance/page.tsx b/src/app/[locale]/dashboard/acceptance/page.tsx new file mode 100644 index 0000000..621e731 --- /dev/null +++ b/src/app/[locale]/dashboard/acceptance/page.tsx @@ -0,0 +1,5 @@ +import DashboardAcceptancePage from '@/routes/private/acceptance/DashboardAcceptancePage'; + +export default function Home() { + return ; +} diff --git a/src/app/[locale]/globals.css b/src/app/[locale]/globals.css index d27f194..c18bbb9 100644 --- a/src/app/[locale]/globals.css +++ b/src/app/[locale]/globals.css @@ -17,7 +17,7 @@ body { background-color: #fff; color: #555351; font-family: 'Inter', 'Arial', sans-serif !important; - overflow-x: hidden; + /* overflow-x: hidden; */ } .dashboard-layout { @@ -43,7 +43,7 @@ body { border: 0; padding: 0; clip: rect(0 0 0 0); - overflow: hidden; + /* overflow: hidden; */ } .no-select { diff --git a/src/components/common/MyTable/MyTable.tsx b/src/components/common/MyTable/MyTable.tsx index 9b3b55b..adaf075 100644 --- a/src/components/common/MyTable/MyTable.tsx +++ b/src/components/common/MyTable/MyTable.tsx @@ -169,7 +169,6 @@ const MyTable = (props: Props) => { ) : ( sortedData.map((row: any, rowIndex) => { const isCompleted = boxStatuses[row.id]; - console.log(row, 'rows'); return ( + + + + + ), + roles: [UserRoleEnum.ADMIN, UserRoleEnum.CHINA_WORKER], + }, ]; diff --git a/src/components/ui-kit/BasePagination/index.tsx b/src/components/ui-kit/BasePagination/index.tsx index 51e942d..8b6af1b 100644 --- a/src/components/ui-kit/BasePagination/index.tsx +++ b/src/components/ui-kit/BasePagination/index.tsx @@ -45,6 +45,8 @@ export default function BasePagination({ page, pageSize, totalCount, onChange }: onChange={(_, newPage) => onChange(newPage)} variant='outlined' shape='rounded' + siblingCount={11} + boundaryCount={1} color='primary' sx={{ '.Mui-selected': { diff --git a/src/data/box/box.model.ts b/src/data/box/box.model.ts index 6037198..e9d59d4 100644 --- a/src/data/box/box.model.ts +++ b/src/data/box/box.model.ts @@ -36,6 +36,7 @@ export interface IBoxDetail { volume: string; boxWeight: number; brutto: number; + print: PrintStatus; hasInvoice: boolean; status: BoxStatus; }; diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 51579d3..b64302c 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -24,6 +24,11 @@ export const pageLinks = { create: '/dashboard/boxes/create', edit: (slug: string | number) => '/dashboard/boxes/edit/' + slug, }, + acceptance: { + index: '/dashboard/acceptance', + // create: '/dashboard/boxes/create', + // edit: (slug: string | number) => '/dashboard/boxes/edit/' + slug, + }, items: { index: '/dashboard/items', create: '/dashboard/items/create', diff --git a/src/modalStorage/modalSlice.ts b/src/modalStorage/modalSlice.ts new file mode 100644 index 0000000..0e214c0 --- /dev/null +++ b/src/modalStorage/modalSlice.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +interface ModalState { + isOpen: boolean; + openModal: () => void; + closeModal: () => void; +} + +export const useModalStore = create(set => ({ + isOpen: false, + openModal: () => set({ isOpen: true }), + closeModal: () => set({ isOpen: false }), +})); diff --git a/src/modalStorage/partyId.ts b/src/modalStorage/partyId.ts new file mode 100644 index 0000000..e580ca3 --- /dev/null +++ b/src/modalStorage/partyId.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; + +interface BoxIDState { + boxesId: number | undefined; + setBoxId: (boxId: number | undefined) => void; + boxesIdAccepted: number | undefined; + setBoxIdAccepted: (boxId: number | undefined) => void; +} + +export const useBoxIdStore = create(set => ({ + boxesId: undefined, + boxesIdAccepted: undefined, + setBoxId: boxId => set({ boxesId: boxId }), + setBoxIdAccepted: boxId => set({ boxesIdAccepted: boxId }), +})); diff --git a/src/routes/private/acceptance/DashboardAcceptancePage.tsx b/src/routes/private/acceptance/DashboardAcceptancePage.tsx new file mode 100644 index 0000000..3889cde --- /dev/null +++ b/src/routes/private/acceptance/DashboardAcceptancePage.tsx @@ -0,0 +1,746 @@ +'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; diff --git a/src/routes/private/boxes-create/DashboardEditBox.tsx b/src/routes/private/boxes-create/DashboardEditBox.tsx index c3269f7..0fbc733 100644 --- a/src/routes/private/boxes-create/DashboardEditBox.tsx +++ b/src/routes/private/boxes-create/DashboardEditBox.tsx @@ -70,8 +70,6 @@ const DashboardEditBoxPage = (props: Props) => { } ); - console.log(getOneBox, 'pkets'); - if (getOneBox.loading || !getOneBox.data) { return ; } diff --git a/src/routes/private/boxes-one/BoxesOne.tsx b/src/routes/private/boxes-one/BoxesOne.tsx index 94802f5..58e4fe3 100644 --- a/src/routes/private/boxes-one/BoxesOne.tsx +++ b/src/routes/private/boxes-one/BoxesOne.tsx @@ -3,15 +3,23 @@ import Loader from '@/components/common/Loader'; import BaseInput from '@/components/ui-kit/BaseInput'; import { useAuthContext } from '@/context/auth-context'; -import { BoxStatus } from '@/data/box/box.model'; +import { BoxStatus, PrintStatus } from '@/data/box/box.model'; import { box_requests } from '@/data/box/box.requests'; +import { Product, UpdateProductBodyType } from '@/data/item/item.mode'; +import { item_requests } from '@/data/item/item.requests'; import { useMyTranslation } from '@/hooks/useMyTranslation'; import useRequest from '@/hooks/useRequest'; +import { useModalStore } from '@/modalStorage/modalSlice'; +import { notifyUnknownError } from '@/services/notification'; +import { Print, Search } from '@mui/icons-material'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import DangerousIcon from '@mui/icons-material/Dangerous'; -import { Box, Divider, Grid, Stack, Typography, styled } from '@mui/material'; +import { Box, Button, Card, CardContent, Divider, FormControl, Grid, MenuItem, Select, Stack, Typography, styled } from '@mui/material'; +import { CloseIcon } from 'next/dist/client/components/react-dev-overlay/internal/icons/CloseIcon'; import { useParams } from 'next/navigation'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useReactToPrint } from 'react-to-print'; +import CustomModal from '../customModal/CustomModal'; const StyledViewBox = styled(Box)` .item-row { @@ -30,15 +38,52 @@ const StyledViewBox = styled(Box)` } `; +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 DashboardBoxesOnePage = () => { const params = useParams(); + const { isOpen: isModalOpen, openModal, closeModal } = useModalStore(); const box_id = params.box_id as string; const { isAdmin: isAdminUser } = useAuthContext(); const t = useMyTranslation(); + const inputRef = useRef(null); + const [trackId, setTrackId] = useState(); + const [values, setValues] = useState<{ [trackId: string]: number | '' }>({}); + const [selectedBoxDetails, setSelectedBoxDetails] = useState(null); + const printRef = useRef(null); + const [printStatus, setPrintStatus] = useState('false'); + const [changeStatusIds, setChangeStatusIds] = useState([]); - const { data: boxData, loading } = useRequest(() => box_requests.find({ packetId: box_id }), { + const handlePrint = useReactToPrint({ + contentRef: printRef, + onAfterPrint: () => { + setSelectedBoxDetails(null); + }, + }); + + const { + data: boxData, + loading, + refetch, + } = useRequest(() => box_requests.find({ packetId: box_id }), { selectData(data) { const boxData = data.data.data; + const currentPrintStatus = boxData.packet.print ? 'true' : 'false'; + + setPrintStatus(currentPrintStatus); return { id: +box_id, @@ -49,6 +94,7 @@ const DashboardBoxesOnePage = () => { box_size: boxData.packet.volume, passportName: boxData.packet.passportName, status: boxData.packet.status, + print: boxData.packet.print, packetId: box_id, partyId: +boxData.packet.partyId, partyName: boxData.packet.partyName, @@ -88,6 +134,132 @@ const DashboardBoxesOnePage = () => { return statuses; }, [isAdminUser, t]); + const [page, setPage] = useState(1); + const getListQuery = useRequest( + () => + item_requests.getAll({ + page: page, + trekId: trackId, + packetId: boxData?.packetId, + partyId: boxData?.partyId, + }), + { + dependencies: [page, trackId, boxData], + selectData(data) { + return data.data.data; + }, + } + ); + + const onPrintBox = async (boxData: any) => { + 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) { + console.error('Failed to fetch box details:', error); + } + }; + + const onChangePrint = async (newStatus: PrintStatus) => { + if (!boxData || changeStatusIds.includes(boxData.id)) return; + + try { + setChangeStatusIds(p => [...p, boxData.id]); + const res = await box_requests.find({ packetId: boxData.id }); + await box_requests.update({ + cargoId: String(res.data.data.packet.cargoId), + items: res.data.data.items, + print: newStatus === 'true', + packetId: String(res.data.data.packet.id), + passportId: res.data.data.client.passportId, + status: res.data.data.packet.status, + }); + setPrintStatus(newStatus); + await refetch(); + } catch (error) { + console.error('Print status update failed:', error); + } finally { + setChangeStatusIds(prev => prev.filter(i => i !== boxData.id)); + } + }; + + const [loaer, setLoading] = useState(false); + 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(); + await Promise.all([getListQuery.refetch(), refetch()]); + setValues(prev => ({ ...prev, [item.trekId]: '' })); + setTrackId(''); + } catch (error) { + notifyUnknownError(error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!trackId) return; + + const delayDebounce = setTimeout(() => { + const item = getListQuery.data?.data?.[0]; + if (!item) return; + + const accepted = item.acceptedNumber === item.amount; + if (!accepted) { + updateItems(item, item.amount).then(() => { + inputRef.current?.focus(); + }); + } else { + setTrackId(''); + inputRef.current?.focus(); + } + }, 1000); + + return () => clearTimeout(delayDebounce); + }, [trackId, getListQuery.data]); + if (loading || !boxData) { return ; } @@ -102,6 +274,73 @@ const DashboardBoxesOnePage = () => { backgroundColor: '#fff', }} > + + + + + + + {t('product_inspection')} + + + {t('enter_product')} + + , + }} + value={trackId} + onChange={e => { + setTrackId(e.target.value); + }} + placeholder={t('filter_item_name')} + /> + {trackId && trackId.length > 0 && ( + <> + {getListQuery.loading ? ( + {t('loading')}... + ) : getListQuery.data?.data && getListQuery.data?.data.length > 0 ? ( + getListQuery.data?.data.map(e => ( + + + + + {t('track_id')}: {e.trekId} + + Nomi: {e.name || e.nameRu} + Mahsulot soni: {e.amount} + Paket nomi: {e?.packetName} + + + + + + )) + ) : ( + <> + {t('not_found') || 'Mahsulot topilmadi'} + + + )} + + )} + + + + {t('view_packet')} @@ -134,7 +373,12 @@ const DashboardBoxesOnePage = () => { {boxData.passportName || '-'} - + + + {boxData.products_list.length} / + {boxData.products_list.filter(product => product.acceptedNumber && product.acceptedNumber > 0).length} + + { }} > {boxData.products_list.map((product, index: number) => { - // - - // let totalPrice = 0; try { @@ -237,10 +478,90 @@ const DashboardBoxesOnePage = () => { ); })} + + + + + + + + + + {/* Hidden print component */} + {selectedBoxDetails && ( +
+
+ + + Box Details + + + Box Name: {selectedBoxDetails.box_name} + + + Party: {selectedBoxDetails.partyName} + + + Client: {selectedBoxDetails.clientName} + + + + + + + + + + + + + + + {selectedBoxDetails.products_list.map((product: any) => ( + + + + + + + + ))} + +
IDNameAmountAcceptedWeight
{product.trekId} + {product.name || product.nameRu} + {product.amount}{product.acceptedNumber}{product.weight}
+
+
+
+
+ )} ); }; diff --git a/src/routes/private/boxes/DashboardBoxesPage.tsx b/src/routes/private/boxes/DashboardBoxesPage.tsx index 9381bac..c37e23e 100644 --- a/src/routes/private/boxes/DashboardBoxesPage.tsx +++ b/src/routes/private/boxes/DashboardBoxesPage.tsx @@ -20,6 +20,7 @@ import useInput from '@/hooks/useInput'; import { useMyNavigation } from '@/hooks/useMyNavigation'; import { useMyTranslation } from '@/hooks/useMyTranslation'; import useRequest from '@/hooks/useRequest'; +import { useBoxIdStore } from '@/modalStorage/partyId'; import { file_service } from '@/services/file-service'; import { notifyUnknownError } from '@/services/notification'; import { getStatusColor } from '@/theme/getStatusBoxStyles'; @@ -37,6 +38,7 @@ import { Search, } from '@mui/icons-material'; import { Box, Button, Card, CardContent, FormControl, MenuItem, Modal, Select, Stack, Typography } from '@mui/material'; +import { useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import AsyncSelect from 'react-select/async'; @@ -80,6 +82,9 @@ const DashboardBoxesPage = (props: Props) => { const [changeStatusIds, setChangeStatusIds] = useState([]); const [boxAmounts, setBoxAmounts] = useState>({}); const [printStatuses, setPrintStatuses] = useState>({}); + const searchParams = useSearchParams(); + const boxId = searchParams.get('boxId'); + const { boxesId, setBoxId } = useBoxIdStore(); // Print uchun state const [selectedBoxForPrint, setSelectedBoxForPrint] = useState(null); @@ -95,7 +100,6 @@ const DashboardBoxesPage = (props: Props) => { setSelectedBoxDetails(null); }, }); - const { data: defaultPartyOptions } = useRequest(() => party_requests.getAll({}), { enabled: true, selectData(data) { @@ -114,7 +118,6 @@ const DashboardBoxesPage = (props: Props) => { 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, @@ -142,7 +145,6 @@ const DashboardBoxesPage = (props: Props) => { weight: +item.weight, })), }; - setSelectedBoxDetails(detailedBoxData); setTimeout(() => { handlePrint(); @@ -151,7 +153,6 @@ const DashboardBoxesPage = (props: Props) => { console.error('Failed to fetch box details:', error); } }; - const handleOpen = (data: any) => { setOpen(true); }; @@ -222,6 +223,22 @@ const DashboardBoxesPage = (props: Props) => { } }; + useEffect(() => { + if (boxId && defaultPartyOptions && defaultPartyOptions.length > 0) { + const selected = defaultPartyOptions.find(p => p.value === Number(boxId)); + if (selected) { + setPartyFilter(selected); + } + } + }, [boxId, defaultPartyOptions]); + + useEffect(() => { + if (boxesId) { + navigation.push(`${pageLinks.dashboard.boxes.index}?boxId=${boxesId}`); + console.log(boxesId, 'useeffect'); + } + }, [boxesId]); + const { data: list, totalElements, @@ -346,9 +363,11 @@ const DashboardBoxesPage = (props: Props) => { const boxData = res.data.data; const total = boxData.items.reduce( - (acc: { totalAmount: number; totalAccepted: number }, item: any) => { - acc.totalAmount += +item.amount || 0; - acc.totalAccepted += +item.acceptedNumber || 0; + (acc, item) => { + acc.totalAmount = boxData.packet.totalItems ?? 0; + if (item.acceptedNumber && item.acceptedNumber > 0) { + acc.totalAccepted += 1; + } return acc; }, { totalAmount: 0, totalAccepted: 0 } @@ -434,7 +453,7 @@ const DashboardBoxesPage = (props: Props) => { return ( - {total.totalAmount} | {total.totalAccepted} + {data.totalItems} | {total.totalAccepted} {isCompleted && } @@ -828,6 +847,13 @@ const DashboardBoxesPage = (props: Props) => { value={partyFilter} onChange={(newValue: any) => { setPartyFilter(newValue); + if (newValue) { + setBoxId(newValue.value); + navigation.push(`${pageLinks.dashboard.boxes.index}?boxId=${newValue.value}`); + } else { + setBoxId(undefined); + navigation.push(`${pageLinks.dashboard.boxes.index}`); + } setPage(1); }} styles={selectDefaultStyles} diff --git a/src/routes/private/customModal/CustomModal.tsx b/src/routes/private/customModal/CustomModal.tsx new file mode 100644 index 0000000..738cfb5 --- /dev/null +++ b/src/routes/private/customModal/CustomModal.tsx @@ -0,0 +1,44 @@ +import { Close as CloseIcon } from '@mui/icons-material'; +import { Box, Button, Typography } from '@mui/material'; +import { ReactNode } from 'react'; + +type CustomModalProps = { + open: boolean; + onClose: () => void; + children: ReactNode; + title?: string; +}; + +export default function CustomModal({ open, onClose, children, title }: CustomModalProps) { + if (!open) return null; + + return ( + <> + {/* */} + + + + {title} + + + + {children} + + + ); +} diff --git a/src/routes/private/real-boxes-create/DashboardCreateRealBox.tsx b/src/routes/private/real-boxes-create/DashboardCreateRealBox.tsx index 9d243e4..2fdcb29 100644 --- a/src/routes/private/real-boxes-create/DashboardCreateRealBox.tsx +++ b/src/routes/private/real-boxes-create/DashboardCreateRealBox.tsx @@ -86,7 +86,6 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => { const [allPackets, setAllPackets] = useState([]); // Barcha mahsulotlarni barcha sahifadan yuklash uchun map const [allItemsMap, setAllItemsMap] = useState<{ [packetId: number]: any[] }>({}); - console.log(initialValues?.partyName); const { control, diff --git a/src/routes/private/real-boxes/DashboardRealBoxesPage.tsx b/src/routes/private/real-boxes/DashboardRealBoxesPage.tsx index 316e844..124111a 100644 --- a/src/routes/private/real-boxes/DashboardRealBoxesPage.tsx +++ b/src/routes/private/real-boxes/DashboardRealBoxesPage.tsx @@ -129,12 +129,10 @@ const DashboardRealBoxesPage = (props: Props) => { try { setDownloadIds(p => [...p, id]); const response = await real_box_requests.downloadQrCode({ boxId: id }); - console.log(response, 'rres'); const file = new File([response.data], 'qr.png', { type: response.data.type }); file_service.download(file); } catch (error) { notifyUnknownError(error); - console.log(error); } finally { setDownloadIds(prev => prev.filter(i => i !== id)); }