Merge branch 'dev' into 'main'

Dev

See merge request azizziy/cpost!35
This commit is contained in:
Azizbek Usmonov
2025-06-24 09:57:49 +05:00
20 changed files with 953 additions and 341 deletions

View File

@@ -212,5 +212,10 @@
"qr_code": "二维码", "qr_code": "二维码",
"created_at": "添加日期", "created_at": "添加日期",
"download_all_items_exel": "将所有产品上传至Excel", "download_all_items_exel": "将所有产品上传至Excel",
"select_all": "全选," "select_all": "全选,",
"product_inspection": "产品检验",
"enter_product": "请输入产品的追溯ID",
"confirmation": "确认",
"view_packet": "查看包裹数据",
"accepted_number": "已接收"
} }

View File

@@ -212,5 +212,10 @@
"download_all_items_exel": "Upload all products in Excel", "download_all_items_exel": "Upload all products in Excel",
"select_all": "Select all", "select_all": "Select all",
"qr_code": "QR Code", "qr_code": "QR Code",
"created_at": "Date of joining" "created_at": "Date of joining",
"product_inspection": "Product Inspection",
"enter_product": "Enter your product tracking ID",
"confirmation": "Confirm",
"view_packet": "View package data",
"accepted_number": "Accepted"
} }

View File

@@ -224,7 +224,11 @@
"party_weight": "Вес партии", "party_weight": "Вес партии",
"download_all_items_exel": "Загрузить все товары в Excel", "download_all_items_exel": "Загрузить все товары в Excel",
"select_all": "Выделить все", "select_all": "Выделить все",
"product_inspection": "Проверка товаров",
"enter_product": "Enter your product tracking ID",
"confirmation": "Подтверждение",
"view_packet": "Просмотреть данные посылки",
"accepted_number": "Принято",
"qr_code": "QR код", "qr_code": "QR код",
"created_at": "Дата добавления" "created_at": "Дата добавления"
} }

View File

@@ -117,6 +117,8 @@
"create_packet": "Paket yaratish", "create_packet": "Paket yaratish",
"update_box": "Qutni yangilash", "update_box": "Qutni yangilash",
"update_package": "Paketni yangilash", "update_package": "Paketni yangilash",
"view_packet": "Paketni ma'lumotlarini ko'rish",
"accepted_number": "Qabul qilingan",
"update_item": "Mahsulotni yangilash", "update_item": "Mahsulotni yangilash",
"no": "Yo'q", "no": "Yo'q",
"id": "ID", "id": "ID",
@@ -223,6 +225,9 @@
"update_packet": "Paketni Tahrirlash", "update_packet": "Paketni Tahrirlash",
"party_weight": "Partiya og'irligi", "party_weight": "Partiya og'irligi",
"select_all": "Hammasini belgilash", "select_all": "Hammasini belgilash",
"product_inspection": "Mahsulotlarni tekshirish",
"enter_product": "Mahsulotning Trassirovka IDni kiriting",
"confirmation": "Tasdiqlash",
"qr_code": "QR kod", "qr_code": "QR kod",
"created_at": "Qoshilgan sana" "created_at": "Qoshilgan sana"

View File

@@ -1,75 +1,75 @@
{ {
"name": "c-post", "name": "c-post",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "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": "cross-env NEXT_PUBLIC_API_URL=https://api.cpost-express.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", "build:prod": "cross-env NEXT_PUBLIC_API_URL=https://api.cpost-express.uz next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"test": "vitest", "test": "vitest",
"check-types": "tsc --noemit", "check-types": "tsc --noemit",
"format-code": "prettier --write .", "format-code": "prettier --write .",
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@emotion/cache": "^11.11.0", "@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.3", "@mui/icons-material": "^5.15.3",
"@mui/material": "^5.15.3", "@mui/material": "^5.15.3",
"@mui/material-nextjs": "^5.15.3", "@mui/material-nextjs": "^5.15.3",
"@tanstack/react-query": "^4.35.0", "@tanstack/react-query": "^4.35.0",
"@tanstack/react-query-next-experimental": "^5.17.9", "@tanstack/react-query-next-experimental": "^5.17.9",
"aos": "^2.3.4", "aos": "^2.3.4",
"axios": "^1.6.5", "axios": "^1.6.5",
"cookies-next": "^4.1.0", "cookies-next": "^4.1.0",
"i18next": "^23.7.16", "i18next": "^23.7.16",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"next": "14.0.4", "next": "14.0.4",
"next-i18next": "^15.2.0", "next-i18next": "^15.2.0",
"next-intl": "^3.4.2", "next-intl": "^3.4.2",
"nextjs-progressbar": "^0.0.16", "nextjs-progressbar": "^0.0.16",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-i18next": "^14.0.0", "react-i18next": "^14.0.0",
"react-select": "^5.8.0", "react-select": "^5.8.0",
"simplebar-react": "^3.2.4", "simplebar-react": "^3.2.4",
"swiper": "^11.0.5", "swiper": "^11.0.5",
"use-dehydrated-state": "^0.1.0", "use-dehydrated-state": "^0.1.0",
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-query-devtools": "^4.35.3", "@tanstack/react-query-devtools": "^4.35.3",
"@types/aos": "^3.0.7", "@types/aos": "^3.0.7",
"@types/i18next": "^13.0.0", "@types/i18next": "^13.0.0",
"@types/lodash.clonedeep": "^4.5.9", "@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.debounce": "^4.0.9", "@types/lodash.debounce": "^4.0.9",
"@types/lodash.get": "^4.4.9", "@types/lodash.get": "^4.4.9",
"@types/lodash.omit": "^4.5.9", "@types/lodash.omit": "^4.5.9",
"@types/node": "^20", "@types/node": "^20",
"@types/parse-json": "^4.0.2", "@types/parse-json": "^4.0.2",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-i18next": "^8.1.0", "@types/react-i18next": "^8.1.0",
"@types/react-select": "^5.0.1", "@types/react-select": "^5.0.1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.0.4", "eslint-config-next": "14.0.4",
"husky": "^8.0.0", "husky": "^8.0.0",
"jsdom": "^23.2.0", "jsdom": "^23.2.0",
"lint-staged": "^15.2.0", "lint-staged": "^15.2.0",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"typescript": "^5", "typescript": "^5",
"vitest": "^1.1.3" "vitest": "^1.1.3"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@@ -0,0 +1,9 @@
import { CircularProgress, Stack } from '@mui/material';
export default function DashboardLoading() {
return (
<Stack justifyContent={'center'} alignItems={'center'} p={8}>
<CircularProgress size={'64px'} />
</Stack>
);
}

View File

@@ -0,0 +1,5 @@
import DashboardBoxesOnePage from '@/routes/private/boxes-one';
export default function Home() {
return <DashboardBoxesOnePage />;
}

View File

@@ -1,23 +1,9 @@
'use client';
import Loader from '@/components/common/Loader'; import Loader from '@/components/common/Loader';
import { Scrollbar } from '@/components/common/Scrollbar'; import { Scrollbar } from '@/components/common/Scrollbar';
import BaseButton from '@/components/ui-kit/BaseButton'; import { box_requests } from '@/data/box/box.requests';
import BaseIconButton from '@/components/ui-kit/BaseIconButton'; import { Box, styled, SxProps, Table, TableBody, TableCell, TableHead, TableRow, Theme } from '@mui/material';
import { FilterList, Search } from '@mui/icons-material';
import {
Box,
IconButton,
Stack,
SxProps,
Table,
TableBody,
TableHead,
TableRow,
Theme,
Tooltip,
Typography,
styled,
TableCell,
} from '@mui/material';
import React from 'react'; import React from 'react';
export interface ColumnData<Data, DataKey = keyof Data> { export interface ColumnData<Data, DataKey = keyof Data> {
@@ -65,6 +51,7 @@ const StyledTable = styled(Table)`
} }
} }
`; `;
const StyledTableRow = styled(TableRow)``; const StyledTableRow = styled(TableRow)``;
const StyledTableCell = styled(TableCell)` const StyledTableCell = styled(TableCell)`
flex-shrink: 0; flex-shrink: 0;
@@ -75,6 +62,7 @@ type Props<Data> = {
data: Data[]; data: Data[];
loading: boolean; loading: boolean;
onClickRow?: (data: Data) => void; onClickRow?: (data: Data) => void;
color?: string;
}; };
const MyTable = <Data extends { id: number | string }>(props: Props<Data>) => { const MyTable = <Data extends { id: number | string }>(props: Props<Data>) => {
@@ -82,6 +70,43 @@ const MyTable = <Data extends { id: number | string }>(props: Props<Data>) => {
const isEmpty = !data?.length && !loading; const isEmpty = !data?.length && !loading;
const [boxStatuses, setBoxStatuses] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
const fetchBoxStatuses = async () => {
const statuses: Record<string, boolean> = {};
await Promise.all(
data.map(async row => {
try {
const res = await box_requests.find({ packetId: row.id });
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;
return acc;
},
{ totalAmount: 0, totalAccepted: 0 }
);
statuses[row.id] = total.totalAmount === total.totalAccepted;
} catch (error) {
console.error('Error fetching box status:', error);
statuses[row.id] = false;
}
})
);
setBoxStatuses(statuses);
};
if (!loading && data.length > 0) {
fetchBoxStatuses();
}
}, [data, loading]);
return ( return (
<Box> <Box>
<Scrollbar> <Scrollbar>
@@ -90,10 +115,9 @@ const MyTable = <Data extends { id: number | string }>(props: Props<Data>) => {
<StyledTableRow> <StyledTableRow>
{columns.map((column, index) => ( {columns.map((column, index) => (
<StyledTableCell <StyledTableCell
// @ts-expect-error key={String(column.dataKey) + index}
key={column.dataKey + index}
variant='head' variant='head'
align={column.numeric || false ? 'right' : 'left'} align={column.numeric ? 'right' : 'left'}
sx={{ sx={{
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
width: column.width, width: column.width,
@@ -107,17 +131,24 @@ const MyTable = <Data extends { id: number | string }>(props: Props<Data>) => {
</TableHead> </TableHead>
<TableBody> <TableBody>
{isEmpty ? ( {isEmpty ? (
'Empty' <StyledTableRow>
<StyledTableCell colSpan={columns.length}>Empty</StyledTableCell>
</StyledTableRow>
) : loading ? ( ) : loading ? (
<StyledTableCell colSpan={columns.length}> <StyledTableRow>
<Loader p={4} size={96} /> <StyledTableCell colSpan={columns.length}>
</StyledTableCell> <Loader p={4} size={96} />
</StyledTableCell>
</StyledTableRow>
) : ( ) : (
data.map((row, rowIndex) => { data.map((row: any, rowIndex) => {
const status = boxStatuses[row.id];
return ( return (
<StyledTableRow <StyledTableRow
key={row.id} key={row.id}
sx={{ sx={{
background: !row.hasInvoice ? 'inherit' : status ? '#a3e635' : '#f87171',
...(onClickRow ...(onClickRow
? { ? {
cursor: 'pointer', cursor: 'pointer',
@@ -127,22 +158,18 @@ const MyTable = <Data extends { id: number | string }>(props: Props<Data>) => {
} }
: {}), : {}),
}} }}
onClick={() => { onClick={() => onClickRow?.(row)}
onClickRow?.(row);
}}
> >
{columns.map((column, index) => ( {columns.map((column, index) => (
<StyledTableCell <StyledTableCell
// @ts-expect-error key={String(column.dataKey) + index}
key={column.dataKey + index} align={column.numeric ? 'right' : 'left'}
align={column.numeric || false ? 'right' : 'left'}
sx={{ sx={{
...column.getSxStyles?.(row), ...column.getSxStyles?.(row),
width: column.width, width: column.width,
}} }}
> >
{/* @ts-expect-error */} {column.renderCell ? column.renderCell(row, rowIndex) : row[column.dataKey as keyof Data]}
{column.renderCell ? column.renderCell(row, rowIndex) : row[column.dataKey]}
</StyledTableCell> </StyledTableCell>
))} ))}
</StyledTableRow> </StyledTableRow>

View File

@@ -52,6 +52,7 @@ export interface IBoxDetail {
nameRu: string; nameRu: string;
amount: number; amount: number;
weight: number; weight: number;
acceptedNumber: number;
price: number; price: number;
totalPrice: number; totalPrice: number;
hasImage: boolean; hasImage: boolean;

View File

@@ -11,9 +11,10 @@ export type Product = {
amount: number; amount: number;
weight: number; weight: number;
price?: number; price?: number;
packetName: string;
totalPrice?: number; totalPrice?: number;
status: BoxStatus; status: BoxStatus;
acceptedNumber: number;
hasImage: boolean; hasImage: boolean;
}; };
@@ -22,11 +23,11 @@ export type CreateProductBodyType = {
}; };
export type UpdateProductBodyType = { export type UpdateProductBodyType = {
itemId: number; itemId: string | number;
cargoId: string;
trekId: string; trekId: string;
name: string; name: string;
nameRu: string;
amount: number; amount: number;
weight: number; weight: number;
acceptedNumber: number | null;
}; };

View File

@@ -8,7 +8,7 @@ export const item_requests = {
packetId?: number | string; packetId?: number | string;
partyId?: number | string; partyId?: number | string;
name?: string; name?: string;
trekId?: string; trekId?: string | number;
page?: number; page?: number;
sort?: string; sort?: string;
status?: BoxStatus; status?: BoxStatus;

View File

@@ -15,6 +15,7 @@ export const pageLinks = {
}, },
boxes: { boxes: {
index: '/dashboard/packets', index: '/dashboard/packets',
detail: (slug: string | number) => '/dashboard/packets/' + slug,
create: '/dashboard/packets/create', create: '/dashboard/packets/create',
edit: (slug: string | number) => '/dashboard/packets/edit/' + slug, edit: (slug: string | number) => '/dashboard/packets/edit/' + slug,
}, },

View File

@@ -1,34 +1,31 @@
'use client'; 'use client';
import BaseButton from '@/components/ui-kit/BaseButton'; import BaseButton from '@/components/ui-kit/BaseButton';
import BaseInput from '@/components/ui-kit/BaseInput';
import { party_requests } from '@/data/party/party.requests';
import { pageLinks } from '@/helpers/constants';
import { notifyError, notifyUnknownError } from '@/services/notification';
import { Box, Divider, FormHelperText, Grid, Stack, Typography, styled } from '@mui/material';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useLocale } from 'use-intl';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import BaseIconButton from '@/components/ui-kit/BaseIconButton'; import BaseIconButton from '@/components/ui-kit/BaseIconButton';
import { AddCircleRounded, Close } from '@mui/icons-material'; import BaseInput from '@/components/ui-kit/BaseInput';
import { box_requests } from '@/data/box/box.requests';
import useRequest from '@/hooks/useRequest';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { BoxStatus, CreateBoxBodyType, UpdateBoxBodyType } from '@/data/box/box.model';
import BaseReactSelect, { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect'; import BaseReactSelect, { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect';
import { customer_requests } from '@/data/customers/customer.requests';
import { useAuthContext } from '@/context/auth-context'; import { useAuthContext } from '@/context/auth-context';
import { useMyNavigation } from '@/hooks/useMyNavigation'; import { BoxStatus, CreateBoxBodyType, UpdateBoxBodyType } from '@/data/box/box.model';
import AsyncSelect from 'react-select/async'; import { box_requests } from '@/data/box/box.requests';
import { Party } from '@/data/party/party.model';
import cloneDeep from 'lodash.clonedeep';
import { item_requests } from '@/data/item/item.requests';
import get from 'lodash.get';
import Loader from '@/components/common/Loader';
import { Customer } from '@/data/customers/customer.model'; import { Customer } from '@/data/customers/customer.model';
import { customer_requests } from '@/data/customers/customer.requests';
import { item_requests } from '@/data/item/item.requests';
import { Party } from '@/data/party/party.model';
import { party_requests } from '@/data/party/party.requests';
import { Passport } from '@/data/passport/passport.model'; import { Passport } from '@/data/passport/passport.model';
import { passport_requests } from '@/data/passport/passport.request'; import { passport_requests } from '@/data/passport/passport.request';
import { pageLinks } from '@/helpers/constants';
import { useMyNavigation } from '@/hooks/useMyNavigation';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import useRequest from '@/hooks/useRequest';
import { notifyError, notifyUnknownError } from '@/services/notification';
import { AddCircleRounded, Close } from '@mui/icons-material';
import { Box, Divider, FormHelperText, Grid, Stack, Typography, styled } from '@mui/material';
import get from 'lodash.get';
import { useSearchParams } from 'next/navigation';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import AsyncSelect from 'react-select/async';
const StyledCreateBox = styled(Box)` const StyledCreateBox = styled(Box)`
.item-row { .item-row {
@@ -123,19 +120,19 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
...(editMode ...(editMode
? {} ? {}
: { : {
products_list: [ products_list: [
{ {
id: '', id: '',
cargoId: '', cargoId: '',
trekId: '', trekId: '',
name: '', name: '',
nameRu: '', nameRu: '',
amount: '', amount: '',
weight: '', weight: '',
price: '', price: '',
}, },
], ],
}), }),
...initialValues, ...initialValues,
}, },
}); });
@@ -160,8 +157,8 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
label: initialValues?.passportName, label: initialValues?.passportName,
}, },
]; ];
const n = "123ds" const n = '123ds';
n.toUpperCase() n.toUpperCase();
const { data: passportOptions } = useRequest(() => passport_requests.getAll({ cargoId: cargoId?.toUpperCase() }), { const { data: passportOptions } = useRequest(() => passport_requests.getAll({ cargoId: cargoId?.toUpperCase() }), {
enabled: !!cargoId, enabled: !!cargoId,
selectData: data => { selectData: data => {
@@ -185,7 +182,7 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
placeholderData: [], // Kerak emas, chunki server PageAble qaytarmayapti placeholderData: [], // Kerak emas, chunki server PageAble qaytarmayapti
onSuccess(data) { onSuccess(data) {
if (data?.data.data?.[0]?.id) { if (data?.data.data?.[0]?.id) {
setValue("passportId", initialValues?.passportId) setValue('passportId', initialValues?.passportId);
setValue('passport_id', data.data.data[0].id); setValue('passport_id', data.data.data[0].id);
setSelectedPassport(data.data.data[0]); // Birinchi elementni tanlash setSelectedPassport(data.data.data[0]); // Birinchi elementni tanlash
} }
@@ -251,7 +248,6 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
}; };
const onSubmit = handleSubmit(async values => { const onSubmit = handleSubmit(async values => {
try { try {
setLoading(true); setLoading(true);
@@ -377,11 +373,11 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
label: string; label: string;
value: BoxStatus; value: BoxStatus;
}[] = [ }[] = [
{ {
label: t('READY_TO_INVOICE'), label: t('READY_TO_INVOICE'),
value: 'READY_TO_INVOICE', value: 'READY_TO_INVOICE',
}, },
]; ];
if (isAdmin) { if (isAdmin) {
p.push({ p.push({
@@ -426,15 +422,15 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
defaultValue={ defaultValue={
editMode editMode
? { ? {
value: initialValues.partyId, value: initialValues.partyId,
label: initialValues.partyName, label: initialValues.partyName,
} }
: partiesData?.length : partiesData?.length
? { ? {
value: partiesData[0].value, value: partiesData[0].value,
label: partiesData[0].label, label: partiesData[0].label,
} }
: null : null
} }
styles={selectDefaultStyles} styles={selectDefaultStyles}
noOptionsMessage={() => t('not_found')} noOptionsMessage={() => t('not_found')}
@@ -552,7 +548,7 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
if (!Number.isNaN(p)) { if (!Number.isNaN(p)) {
totalPrice = p; totalPrice = p;
} }
} catch (error) { } } catch (error) {}
return ( return (
<Box key={product.key} mb={1.5}> <Box key={product.key} mb={1.5}>
@@ -707,7 +703,7 @@ const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
value={totalPrice} value={totalPrice}
mainBorderColor='#D8D8D8' mainBorderColor='#D8D8D8'
placeholder={t('total_price')} placeholder={t('total_price')}
// {...register(`products_list.${index}.totalPrice`, { required: requiredText })} // {...register(`products_list.${index}.totalPrice`, { required: requiredText })}
/> />
</Box> </Box>
</React.Fragment> </React.Fragment>

View File

@@ -5,7 +5,6 @@ import { box_requests } from '@/data/box/box.requests';
import useRequest from '@/hooks/useRequest'; import useRequest from '@/hooks/useRequest';
import DashboardCreateBoxPage from '@/routes/private/boxes-create/DashboardCreateBox'; import DashboardCreateBoxPage from '@/routes/private/boxes-create/DashboardCreateBox';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import React from 'react';
type Props = {}; type Props = {};
@@ -71,7 +70,7 @@ const DashboardEditBoxPage = (props: Props) => {
} }
); );
console.log(getOneBox, "pkets"); console.log(getOneBox, 'pkets');
if (getOneBox.loading || !getOneBox.data) { if (getOneBox.loading || !getOneBox.data) {
return <Loader p={8} size={96} />; return <Loader p={8} size={96} />;

View File

@@ -0,0 +1,322 @@
'use client';
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 { box_requests } from '@/data/box/box.requests';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import useRequest from '@/hooks/useRequest';
import { Box, Divider, Grid, Stack, Typography, styled } from '@mui/material';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
const StyledViewBox = styled(Box)`
.item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.item-row-field {
flex: 1;
}
& > * {
flex: 1 1 auto;
}
`;
const DashboardBoxesOnePage = () => {
const params = useParams();
const box_id = params.box_id as string;
const { isAdmin: isAdminUser } = useAuthContext();
const t = useMyTranslation();
const { data: boxData, loading } = useRequest(() => box_requests.find({ packetId: box_id }), {
selectData(data) {
const boxData = data.data.data;
return {
id: +box_id,
box_name: boxData.packet.name,
net_weight: +boxData.packet.brutto,
box_weight: +boxData.packet.boxWeight,
box_type: boxData.packet.boxType,
box_size: boxData.packet.volume,
passportName: boxData.packet.passportName,
status: boxData.packet.status,
packetId: box_id,
partyId: +boxData.packet.partyId,
partyName: boxData.packet.partyName,
passportId: boxData.client?.passportId,
client_id: boxData.packet?.cargoId,
clientName: boxData.client?.passportName,
products_list: boxData.items.map(item => ({
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,
})),
};
},
});
const boxStatuses = useMemo(() => {
const statuses: { label: string; value: BoxStatus }[] = [
{
label: t('READY_TO_INVOICE'),
value: 'READY_TO_INVOICE',
},
];
if (isAdminUser) {
statuses.push({
label: t('READY'),
value: 'READY',
});
}
return statuses;
}, [isAdminUser, t]);
if (loading || !boxData) {
return <Loader p={8} size={96} />;
}
return (
<StyledViewBox
width={1}
mb={3}
sx={{
padding: '28px',
borderRadius: '16px',
backgroundColor: '#fff',
}}
>
<Box>
<Typography variant='h5' mb={3.5}>
{t('view_packet')}
</Typography>
<Grid container columnSpacing={2.5} rowSpacing={3} mb={3.5}>
<Grid item xs={5}>
<Typography fontSize='18px' fontWeight={500} color='#5D5850' mb={2}>
{t('party_name')}
</Typography>
<Typography sx={{ width: '100%', border: '1px solid gray', padding: '8px', borderRadius: '12px' }}>
{boxData.partyName || '-'}
</Typography>
</Grid>
<Grid item xs={5}>
<Typography fontSize='18px' fontWeight={500} color='#5D5850' mb={2}>
{t('cargo_id')}
</Typography>
<Typography sx={{ width: '100%', border: '1px solid gray', padding: '8px', borderRadius: '12px' }}>
{boxData.client_id || '-'}
</Typography>
</Grid>
<Grid item xs={5}>
<Typography fontSize='18px' fontWeight={500} color='#5D5850' mb={2}>
{t('passport')}
</Typography>
<Typography sx={{ width: '100%', border: '1px solid gray', padding: '8px', borderRadius: '12px' }}>
{boxData.passportName || '-'}
</Typography>
</Grid>
<Grid item xs={12}>
<Stack
sx={{
borderRadius: '8px',
border: '2px solid #3489E4',
background: '#FFF',
padding: '24px',
}}
>
{boxData.products_list.map((product, index: number) => {
//
//
let totalPrice = 0;
try {
const p = +product.price * +product.amount;
if (!Number.isNaN(p)) {
totalPrice = p;
}
} catch (error) {}
return (
<Box key={index} mb={1.5}>
<Box className='item-row' mb={1.5}>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('track_id')}
</Typography>
<BaseInput
InputProps={{
startAdornment: (
<Box
sx={{
backgroundColor: '#EBEFF5',
color: '#929191',
alignSelf: 'stretch',
height: 'auto',
display: 'flex',
alignItems: 'center',
pl: '10px',
pr: '10px',
borderRadius: '8px 0 0 8px',
}}
>
<span>ID</span>
</Box>
),
}}
fullWidth
placeholder={t('id')}
disabled
defaultValue={product.trekId}
sx={{
'.MuiInputBase-root': {
paddingLeft: 0,
},
}}
/>
</Box>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('name')}
</Typography>
<BaseInput
fullWidth
mainBorderColor='#D8D8D8'
placeholder={t('name')}
defaultValue={product.name}
disabled
/>
</Box>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{'NAME_RU'}
</Typography>
<BaseInput
defaultValue={product.nameRu}
disabled
fullWidth
mainBorderColor='#D8D8D8'
placeholder={t('name')}
InputProps={{
sx: {
'&.Mui-disabled': {
color: '!important',
},
'& input.Mui-disabled': {
color: 'black !important',
},
},
}}
/>
</Box>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('quantity')}
</Typography>
<BaseInput
fullWidth
type='number'
mainBorderColor='#D8D8D8'
placeholder={t('quantity')}
defaultValue={product.amount}
disabled
/>
</Box>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('accepted_number')}
</Typography>
<BaseInput
fullWidth
type='number'
inputProps={{ step: 'any', min: 0, type: 'number' }}
mainBorderColor='#D8D8D8'
placeholder={t('accepted_number')}
defaultValue={product.acceptedNumber === null ? 0 : product.acceptedNumber}
disabled
/>
</Box>
<>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('weight')}
</Typography>
<Stack direction={'row'} alignItems={'center'} spacing={2.5}>
<BaseInput
fullWidth
type='text'
inputProps={{
step: 0.1,
}}
mainBorderColor='#D8D8D8'
placeholder={t('weight')}
defaultValue={product.weight}
disabled
/>
</Stack>
</Box>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('price')}
</Typography>
<BaseInput
fullWidth
type='text'
inputProps={{
step: 0.1,
}}
defaultValue={product.price}
disabled
mainBorderColor='#D8D8D8'
placeholder={t('price')}
/>
</Box>
<Box className='item-row-field'>
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
{t('total_price')}
</Typography>
<BaseInput
fullWidth
type='number'
inputProps={{
step: 0.001,
}}
disabled
value={totalPrice}
mainBorderColor='#D8D8D8'
placeholder={t('total_price')}
/>
</Box>
</>
</Box>
<Divider color='#EBEFF6' />
</Box>
);
})}
</Stack>
</Grid>
</Grid>
</Box>
</StyledViewBox>
);
};
export default DashboardBoxesOnePage;

View File

@@ -0,0 +1 @@
export { default } from './BoxesOne';

View File

@@ -9,6 +9,8 @@ import BasePagination from '@/components/ui-kit/BasePagination';
import { useAuthContext } from '@/context/auth-context'; import { useAuthContext } from '@/context/auth-context';
import { BoxStatus, BoxStatusList, IBox } from '@/data/box/box.model'; import { BoxStatus, BoxStatusList, IBox } from '@/data/box/box.model';
import { box_requests } from '@/data/box/box.requests'; 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 { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants'; import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants';
import useInput from '@/hooks/useInput'; import useInput from '@/hooks/useInput';
import { useMyNavigation } from '@/hooks/useMyNavigation'; import { useMyNavigation } from '@/hooks/useMyNavigation';
@@ -17,13 +19,32 @@ import useRequest from '@/hooks/useRequest';
import { file_service } from '@/services/file-service'; import { file_service } from '@/services/file-service';
import { notifyUnknownError } from '@/services/notification'; import { notifyUnknownError } from '@/services/notification';
import { getStatusColor } from '@/theme/getStatusBoxStyles'; import { getStatusColor } from '@/theme/getStatusBoxStyles';
import { Add, Circle, Delete, Download, Edit, FilterList, FilterListOff, Search } from '@mui/icons-material'; import { Add, Circle, Delete, Download, Edit, FilterList, FilterListOff, RemoveRedEye, Search } from '@mui/icons-material';
import { Box, Button, Stack, Typography } from '@mui/material'; import { Box, Button, Card, CardContent, Modal, Stack, TextField, Typography } from '@mui/material';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
type Props = {}; 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 DashboardBoxesPage = (props: Props) => { const DashboardBoxesPage = (props: Props) => {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const t = useMyTranslation(); const t = useMyTranslation();
const navigation = useMyNavigation(); const navigation = useMyNavigation();
const { isAdmin } = useAuthContext(); const { isAdmin } = useAuthContext();
@@ -31,10 +52,12 @@ const DashboardBoxesPage = (props: Props) => {
const [pageSize] = useState(DEFAULT_PAGE_SIZE); const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput(''); const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
const [boxStatusFilter, setBoxStatusFilter] = useState<BoxStatus | undefined>(undefined); const [boxStatusFilter, setBoxStatusFilter] = useState<BoxStatus | undefined>(undefined);
const [trackId, setTrackId] = useState<string>();
const [deleteIds, setDeleteIds] = useState<number[]>([]); const [deleteIds, setDeleteIds] = useState<number[]>([]);
const [downloadIds, setDownloadIds] = useState<number[]>([]); const [downloadIds, setDownloadIds] = useState<number[]>([]);
const [changeStatusIds, setChangeStatusIds] = useState<number[]>([]); const [changeStatusIds, setChangeStatusIds] = useState<number[]>([]);
const [boxAmounts, setBoxAmounts] = useState<Record<number, { totalAmount: number; totalAccepted: number }>>({});
const boxStatusOptions = useMemo(() => { const boxStatusOptions = useMemo(() => {
const p = ['READY_TO_INVOICE'] as BoxStatus[]; const p = ['READY_TO_INVOICE'] as BoxStatus[];
@@ -61,6 +84,31 @@ const DashboardBoxesPage = (props: Props) => {
} }
); );
const getListQuery = useRequest(
() =>
item_requests.getAll({
page: page,
trekId: trackId,
}),
{
dependencies: [page, trackId],
selectData(data) {
return data.data.data;
},
}
);
const [values, setValues] = useState<{ [trackId: string]: number | '' }>({});
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, 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 { const {
data: list, data: list,
totalElements, totalElements,
@@ -145,6 +193,40 @@ const DashboardBoxesPage = (props: Props) => {
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [keyword]); }, [keyword]);
useEffect(() => {
const fetchAmounts = async () => {
const result: Record<number, { totalAmount: number; totalAccepted: number }> = {};
await Promise.all(
list.map(async box => {
try {
const res = await box_requests.find({ packetId: box.id });
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;
return acc;
},
{ totalAmount: 0, totalAccepted: 0 }
);
result[box.id] = total;
} catch (e) {
console.error(`Failed to fetch box ${box.id}`, e);
}
})
);
setBoxAmounts(result);
};
if (list.length > 0 && !loading) {
fetchAmounts();
}
}, [list, loading]);
// No, PartyName, PacketName, PartyTozaOg'irlik, CountOfItems, WeightOfItems, CargoID, PassportNameFamily - PacketStatusForInvoice // No, PartyName, PacketName, PartyTozaOg'irlik, CountOfItems, WeightOfItems, CargoID, PassportNameFamily - PacketStatusForInvoice
const columns: ColumnData<IBox>[] = [ const columns: ColumnData<IBox>[] = [
{ {
@@ -185,6 +267,15 @@ const DashboardBoxesPage = (props: Props) => {
dataKey: 'totalItems', dataKey: 'totalItems',
label: t('count_of_items'), label: t('count_of_items'),
width: 120, width: 120,
renderCell: data => {
const total = boxAmounts[data.id];
if (!total) return <Typography>...</Typography>;
return (
<Typography>
{total.totalAmount} | {total.totalAccepted}
</Typography>
);
},
}, },
{ {
dataKey: 'totalNetWeight', dataKey: 'totalNetWeight',
@@ -272,6 +363,13 @@ const DashboardBoxesPage = (props: Props) => {
return ( return (
<ActionPopMenu <ActionPopMenu
buttons={[ buttons={[
{
icon: <RemoveRedEye sx={{ path: { color: '#3489E4' } }} />,
label: t('view_packet'),
onClick: () => {
navigation.push(pageLinks.dashboard.boxes.detail(data.id));
},
},
{ {
icon: <Edit sx={{ path: { color: '#3489E4' } }} />, icon: <Edit sx={{ path: { color: '#3489E4' } }} />,
label: t('edit'), label: t('edit'),
@@ -307,6 +405,53 @@ const DashboardBoxesPage = (props: Props) => {
}, },
}, },
]; ];
const [items, setItems] = useState<Product>();
const [loaer, setLoading] = useState(false);
const {
register,
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<Product>({
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,
amount: item.amount,
name: item.name,
nameRu: item.nameRu,
trekId: item.trekId,
weight: item.weight,
};
await item_requests.update(updateBody);
// Ma'lumotni yangilab olamiz
getListQuery.refetch();
getBoxesQuery.refetch();
setValues(prev => ({ ...prev, [item.trekId]: '' }));
} catch (error) {
notifyUnknownError(error);
} finally {
setLoading(false);
}
};
return ( return (
<Box> <Box>
@@ -314,6 +459,66 @@ const DashboardBoxesPage = (props: Props) => {
<BaseButton colorVariant='blue' startIcon={<Add />} href={pageLinks.dashboard.boxes.create}> <BaseButton colorVariant='blue' startIcon={<Add />} href={pageLinks.dashboard.boxes.create}>
{t('create_packet')} {t('create_packet')}
</BaseButton> </BaseButton>
<Button onClick={handleOpen}>{t('product_inspection')}</Button>
<Modal open={open} onClose={handleClose} aria-labelledby='modal-modal-title' aria-describedby='modal-modal-description'>
<Box sx={style}>
<Typography id='modal-modal-title' variant='h6' component='h2'>
{t('product_inspection')}
</Typography>
<Typography id='modal-modal-description' sx={{ mt: 2 }}>
{t('enter_product')}
</Typography>
<TextField
id='outlined-basic'
label={t('track_id')}
variant='outlined'
onChange={e => setTrackId(e.target.value)}
/>
{trackId && trackId.length > 0 && (
<>
{getListQuery.loading ? (
<Typography sx={{ mt: 2 }}>{t('loading')}...</Typography> // yoki <CircularProgress />
) : getListQuery.data?.data && getListQuery.data?.data.length > 0 ? (
getListQuery.data?.data.map(e => (
<Box key={e.id} sx={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
<Card sx={{ minWidth: 275, mb: 2 }}>
<CardContent>
<Typography sx={{ fontSize: 14 }}>
{t('track_id')}: {e.trekId}
</Typography>
<Typography sx={{ fontSize: 14 }}>Nomi: {e.name || e.nameRu}</Typography>
<Typography sx={{ fontSize: 14 }}>Mahsulot soni: {e.amount}</Typography>
<Typography sx={{ fontSize: 14 }}>Paket nomi: {e?.packetName}</Typography>
</CardContent>
</Card>
<TextField
id={`amount-${e.trekId}`}
label='Mahsulot soni'
type='number'
sx={{ width: '100%' }}
value={values[e.trekId] ?? ''}
onChange={change => handleAmountChange(change, e.amount, e.trekId)}
inputProps={{ min: 1, max: e.amount }}
/>
<Button
sx={{ mt: '10px' }}
onClick={() => {
if (values[e.trekId] !== '') {
updateItems(e, Number(values[e.trekId]));
}
}}
>
{t('confirmation')}
</Button>
</Box>
))
) : (
<Typography sx={{ mt: 2 }}>{t('no_products_found') || 'Mahsulot topilmadi'}</Typography>
)}
</>
)}
</Box>
</Modal>
</Stack> </Stack>
<Box <Box
width={1} width={1}

View File

@@ -29,6 +29,7 @@ const DashboardClientsPage = (props: Props) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize] = useState(DEFAULT_PAGE_SIZE); const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput(''); const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
const { value: name, onChange: handleName, setValue: setName } = useInput('');
const { value: aviaCargoIdValue, onChange: handleAviaCargoIdValue, setValue: setAviaCargoIdValue } = useInput(''); const { value: aviaCargoIdValue, onChange: handleAviaCargoIdValue, setValue: setAviaCargoIdValue } = useInput('');
const { value: autoCargoIdValue, onChange: handleAutoCargoIdValue, setValue: setAutoCargoIdValue } = useInput(''); const { value: autoCargoIdValue, onChange: handleAutoCargoIdValue, setValue: setAutoCargoIdValue } = useInput('');
@@ -42,7 +43,8 @@ const DashboardClientsPage = (props: Props) => {
() => () =>
customer_requests.getAll({ customer_requests.getAll({
page: page, page: page,
clientName: keyword, clientName: name,
autoCargoId: keyword,
}), }),
{ {
selectData(data) { selectData(data) {
@@ -112,6 +114,7 @@ const DashboardClientsPage = (props: Props) => {
setPage(1); setPage(1);
setKeyword(''); setKeyword('');
setAviaCargoIdValue(''); setAviaCargoIdValue('');
setName('');
setAutoCargoIdValue(''); setAutoCargoIdValue('');
}; };
@@ -136,7 +139,7 @@ const DashboardClientsPage = (props: Props) => {
getClientsQuery.refetch(); getClientsQuery.refetch();
}, 350); }, 350);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [keyword, aviaCargoIdValue, autoCargoIdValue]); }, [keyword, aviaCargoIdValue, autoCargoIdValue, name]);
const columns: ColumnData<Customer>[] = [ const columns: ColumnData<Customer>[] = [
{ {
@@ -332,13 +335,21 @@ const DashboardClientsPage = (props: Props) => {
{/* onChange={handleAutoCargoIdValue}*/} {/* onChange={handleAutoCargoIdValue}*/}
{/* placeholder={t('auto_cargo_id')}*/} {/* placeholder={t('auto_cargo_id')}*/}
{/*/>*/} {/*/>*/}
<BaseInput
InputProps={{
startAdornment: <Search color='primary' />,
}}
value={name}
onChange={handleName}
placeholder={t('name')}
/>
<BaseInput <BaseInput
InputProps={{ InputProps={{
startAdornment: <Search color='primary' />, startAdornment: <Search color='primary' />,
}} }}
value={keyword} value={keyword}
onChange={handleKeyword} onChange={handleKeyword}
placeholder={'Kargo ID'} placeholder={t('id')}
/> />
<BaseButton colorVariant='gray' startIcon={<FilterListOff />} size='small' onClick={resetFilter}> <BaseButton colorVariant='gray' startIcon={<FilterListOff />} size='small' onClick={resetFilter}>

View File

@@ -1,15 +1,13 @@
import BaseButton from '@/components/ui-kit/BaseButton'; import BaseButton from '@/components/ui-kit/BaseButton';
import BaseInput from '@/components/ui-kit/BaseInput'; import BaseInput from '@/components/ui-kit/BaseInput';
import BaseModal from '@/components/ui-kit/BaseModal'; import BaseModal from '@/components/ui-kit/BaseModal';
import BaseReactSelect from '@/components/ui-kit/BaseReactSelect';
import { Product } from '@/data/item/item.mode'; import { Product } from '@/data/item/item.mode';
import { item_requests } from '@/data/item/item.requests'; import { item_requests } from '@/data/item/item.requests';
import { staff_requests } from '@/data/staff/staff.requests';
import { useMyTranslation } from '@/hooks/useMyTranslation'; import { useMyTranslation } from '@/hooks/useMyTranslation';
import { notifyUnknownError } from '@/services/notification'; import { notifyUnknownError } from '@/services/notification';
import { Box, Grid, Stack, Typography, styled } from '@mui/material'; import { Box, Grid, Stack, Typography, styled } from '@mui/material';
import React, { useState } from 'react'; import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
const StyledBox = styled(Box)` const StyledBox = styled(Box)`
.title { .title {
@@ -52,6 +50,8 @@ const EditItemModal = ({ onClose, open, onSuccess, item }: Props) => {
name: string; name: string;
amount: number; amount: number;
weight: number; weight: number;
acceptedNumber: number | null;
nameRu: string;
}>({ }>({
defaultValues: { defaultValues: {
amount: item.amount, amount: item.amount,
@@ -59,6 +59,8 @@ const EditItemModal = ({ onClose, open, onSuccess, item }: Props) => {
name: item.name, name: item.name,
trekId: item.trekId, trekId: item.trekId,
weight: item.weight, weight: item.weight,
acceptedNumber: item.acceptedNumber,
nameRu: item.nameRu,
}, },
}); });

View File

@@ -34,7 +34,7 @@ import {
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import get from 'lodash.get'; import get from 'lodash.get';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { Controller, useFieldArray, useForm } from 'react-hook-form';
const StyledCreateBox = styled(Box)` const StyledCreateBox = styled(Box)`
@@ -67,9 +67,26 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
const t = useMyTranslation(); const t = useMyTranslation();
const params = useSearchParams(); const params = useSearchParams();
const { push } = useMyNavigation(); const { push } = useMyNavigation();
const [partyId, setPartyId] = useState<number | string>(''); const [partyId, setPartyId] = useState<number | string>(initialValues?.partyId || '');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const selectMenuProps = useMemo(
() => ({
PaperProps: { style: { maxHeight: 280 } },
autoFocus: false,
disableAutoFocus: true,
disableEnforceFocus: true,
disableRestoreFocus: true,
disableScrollLock: true,
}),
[]
);
// Barcha box/packetlarni barcha sahifadan yuklash uchun state
const [allPackets, setAllPackets] = useState<any[]>([]);
// Barcha mahsulotlarni barcha sahifadan yuklash uchun map
const [allItemsMap, setAllItemsMap] = useState<{ [packetId: number]: any[] }>({});
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -94,6 +111,65 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
}, },
}); });
// Paketlar va mahsulotlarni yuklash
const [isLoadingPackets, setIsLoadingPackets] = useState(false);
useEffect(() => {
const fetchAllPackets = async () => {
if (!partyId) {
setAllPackets([]);
return;
}
setIsLoadingPackets(true);
let packets: any[] = [];
let totalPages = 1;
try {
const firstRes = await box_requests.getAll({ partyId, page: 1 });
const firstData = firstRes?.data?.data;
packets = firstData?.data || [];
totalPages = firstData?.totalPages || 1;
if (totalPages > 1) {
const promises = [];
for (let page = 2; page <= totalPages; page++) {
promises.push(box_requests.getAll({ partyId, page }));
}
const results = await Promise.all(promises);
results.forEach(res => {
const data = res?.data?.data;
packets = [...packets, ...(data?.data || [])];
});
}
} catch (e) {}
setAllPackets(packets);
setIsLoadingPackets(false);
};
fetchAllPackets();
}, [partyId]);
const fetchAllItemsForPacket = async (packetId: number) => {
let items: any[] = [];
let totalPages = 1;
try {
const firstRes = await item_requests.getAll({ packetId, page: 1 });
const firstData = firstRes?.data?.data;
items = firstData?.data || [];
totalPages = firstData?.totalPages || 1;
if (totalPages > 1) {
const promises = [];
for (let page = 2; page <= totalPages; page++) {
promises.push(item_requests.getAll({ packetId, page }));
}
const results = await Promise.all(promises);
results.forEach(res => {
const data = res?.data?.data;
items = [...items, ...(data?.data || [])];
});
}
} catch (e) {}
setAllItemsMap(prev => ({ ...prev, [packetId]: items }));
return items;
};
useEffect(() => { useEffect(() => {
if (initialValues) { if (initialValues) {
reset({ reset({
@@ -111,9 +187,15 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
if (initialValues.partyId) { if (initialValues.partyId) {
setPartyId(initialValues.partyId); setPartyId(initialValues.partyId);
} }
if (initialValues.paketIds) {
initialValues.paketIds.forEach((paket: any) => {
fetchAllItemsForPacket(paket.id);
});
}
} }
}, [initialValues, reset]); }, [initialValues, reset]);
// useFieldArray keyName="key" orqali unique key
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
name: 'packetItemDtos', name: 'packetItemDtos',
@@ -165,21 +247,16 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
} }
}); });
const handlePacketChange = (index: number, value: number) => {
setValue(`packetItemDtos.${index}.packetId`, value);
setValue(`packetItemDtos.${index}.itemDtos`, []);
};
const appendPacket = () => { const appendPacket = () => {
append({ packetId: 0, itemDtos: [] }); append({ packetId: 0, itemDtos: [] });
setTimeout(() => {
document.activeElement instanceof HTMLElement && document.activeElement.blur();
}, 0);
}; };
const removePacket = (index: number) => { const removePacket = (index: number) => {
remove(index); remove(index);
}; };
const [packetSearchTerm, setPacketSearchTerm] = useState('');
const packetSearchInputRef = useRef<HTMLInputElement>(null);
const handlePartyChange = (event: any) => { const handlePartyChange = (event: any) => {
const selectedParty = parties.find(p => p.id === event.target.value); const selectedParty = parties.find(p => p.id === event.target.value);
@@ -190,113 +267,76 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
} }
}; };
const [paketName, setPaketName] = useState<string>('');
const PacketRow = ({ index, field }: { index: number; field: any }) => { const PacketRow = ({ index, field }: { index: number; field: any }) => {
const packetId = watch(`packetItemDtos.${index}.packetId`); const packetId = watch(`packetItemDtos.${index}.packetId`);
const [itemsPage, setItemsPage] = useState(1);
const [itemsList, setItemsList] = useState<any[]>([]);
const [itemsHasMore, setItemsHasMore] = useState(true);
const itemScrollRef = useRef<HTMLDivElement>(null);
const [packetsPage, setPacketsPage] = useState(1);
const [packetsList, setPacketsList] = useState<any[]>([]);
const [packetsHasMore, setPacketsHasMore] = useState(true);
const [selectedProductNames, setSelectedProductNames] = useState<{ [packetIndex: number]: { [productId: number]: string } }>({});
const [paketName, setPaketName] = useState<string>('');
const [lastPacketId, setLastPacketId] = useState<number | null>(null);
const { isLoading: isLoadingProducts } = useQuery({
queryKey: ['product-list', packetId, itemsPage],
queryFn: () => item_requests.getAll({ packetId, page: itemsPage }),
enabled: !!packetId,
onSuccess: data => {
const newItems = data?.data?.data?.data || [];
setItemsList(prev => (itemsPage === 1 ? newItems : [...prev, ...newItems]));
},
});
const packetScrollRef = useRef<HTMLDivElement>(null);
const [keyword, setKeyword] = useState<string>(''); const [keyword, setKeyword] = useState<string>('');
const { isFetching: isLoadingPackets } = useQuery({ const [selectedProductNames, setSelectedProductNames] = useState<{ [productId: number]: string }>({});
queryKey: ['packets-list', partyId, keyword, packetsPage], const packetsList = keyword ? allPackets.filter(p => (p.name || '').toLowerCase().includes(keyword.toLowerCase())) : allPackets;
queryFn: () => box_requests.getAll({ partyId, cargoId: keyword, page: packetsPage }), const itemsList = allItemsMap[packetId] || [];
enabled: !!partyId, const loadingItems = !allItemsMap[packetId] && !!packetId;
onSuccess: data => {
const newPackets = data?.data?.data?.data || [];
setPacketsList(prev => (packetsPage === 1 ? newPackets : [...prev, ...newPackets]));
const totalPages = data?.data?.data?.totalPages || 0;
setPacketsHasMore(packetsPage < totalPages);
},
});
const handlePacketScroll = (e: React.UIEvent<HTMLDivElement>) => { useEffect(() => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; if (packetId && !allItemsMap[packetId]) {
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; fetchAllItemsForPacket(packetId);
if (isNearBottom && !isLoadingPackets && packetsHasMore) {
setPacketsPage(prev => prev + 1);
} }
}; }, [packetId]);
const handleItemScroll = (e: React.UIEvent<HTMLDivElement>) => { useEffect(() => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; if (packetId && field?.itemDtos?.length && itemsList.length) {
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; const names: { [productId: number]: string } = {};
field.itemDtos.forEach((id: number) => {
if (isNearBottom && !isLoadingProducts && itemsHasMore) { const prod = itemsList.find(p => p.id === id);
setItemsPage(prev => prev + 1); names[id] = prod?.name || prod?.nameRu || String(id);
});
setSelectedProductNames(names);
} }
}; }, [field?.itemDtos, itemsList, packetId]);
const handleProductChange = (packetIndex: number, product: any, checked: boolean) => { const handleProductChange = (product: any, checked: boolean) => {
setSelectedProductNames(prev => { setSelectedProductNames(prev => {
const prevNames = prev[packetIndex] || {};
if (checked) { if (checked) {
return { ...prev, [packetIndex]: { ...prevNames, [product.id]: product.name || product.nameRu || String(product.id) } }; return { ...prev, [product.id]: product.name || product.nameRu || String(product.id) };
} else { } else {
const newNames = { ...prevNames }; const newNames = { ...prev };
delete newNames[product.id]; delete newNames[product.id];
return { ...prev, [packetIndex]: newNames }; return newNames;
} }
}); });
}; };
useEffect(() => {
const element = document.getElementById(`packet-select-${index}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [fields.length]);
const handleSelectAllProducts = async () => { const handleSelectAllProducts = async () => {
if (!packetId) return; if (!packetId) return;
let allProducts: any[] = []; let allProducts = allItemsMap[packetId] || [];
let page = 1; if (!allProducts.length) {
let totalPages = 1; allProducts = await fetchAllItemsForPacket(packetId);
try {
do {
const res = await item_requests.list({ packetId, page });
const data = res?.data?.data;
const products = data?.data || [];
totalPages = data?.totalPages || 1;
allProducts = [...allProducts, ...products];
page++;
} while (page <= totalPages);
} catch (e) {
// error silent
} }
if (allProducts.length > 0) { if (allProducts.length > 0) {
setValue( setValue(
`packetItemDtos.${index}.itemDtos`, `packetItemDtos.${index}.itemDtos`,
allProducts.map((p: any) => p.id) allProducts.map((p: any) => p.id)
); );
setSelectedProductNames((prev: any) => ({ setSelectedProductNames(
...prev, allProducts.reduce(
[index]: allProducts.reduce(
(acc, p) => ({ (acc, p) => ({
...acc, ...acc,
[p.id]: p.name || p.nameRu || String(p.id), [p.id]: p.name || p.nameRu || String(p.id),
}), }),
{} {}
), )
})); );
} }
}; };
const handleClearAll = () => { const handleClearAll = () => {
setValue(`packetItemDtos.${index}.itemDtos`, []); setValue(`packetItemDtos.${index}.itemDtos`, []);
setSelectedProductNames((prev: any) => ({ ...prev, [index]: {} })); setSelectedProductNames({});
}; };
return ( return (
@@ -315,59 +355,45 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
justifyContent={'space-between'} justifyContent={'space-between'}
alignItems={'center'} alignItems={'center'}
> >
<OutlinedInput onChange={e => setKeyword(e.target.value)} /> <OutlinedInput onChange={e => setKeyword(e.target.value)} autoFocus={false} />
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel id={`packet-select-label-${index}`}>{t('packet')}</InputLabel> <InputLabel id={`packet-select-label-${index}`}>{t('packet')}</InputLabel>
<Controller <Controller
name={`packetItemDtos.${index}.packetId`} name={`packetItemDtos.${index}.packetId`}
control={control} control={control}
rules={{ required: requiredText }} render={({ field: selectField }) => (
render={({ field }) => ( <Select
<> {...selectField}
<Select autoFocus={false}
{...field} labelId={`packet-select-label-${index}`}
labelId={`packet-select-label-${index}`} id={`packet-select-${index}`}
label={t('packet')} label={t('packet')}
value={field.value || ''} MenuProps={selectMenuProps}
disabled={isLoadingPackets} onChange={e => {
renderValue={selected => e.stopPropagation();
paketName || packetsList.find(p => p.id === selected)?.name || t('select') selectField.onChange(e);
} }}
MenuProps={{ value={selectField.value || ''}
PaperProps: { renderValue={selected => {
style: { maxHeight: 280 }, const selectedPacket = packetsList.find(p => p.id === selected);
ref: packetScrollRef, return selectedPacket ? selectedPacket.name : t('loading');
onScroll: handlePacketScroll, }}
}, >
disableAutoFocus: true, {isLoadingPackets ? (
autoFocus: false, <MenuItem disabled>
}} <CircularProgress size={24} />
onClick={e => e.stopPropagation()} </MenuItem>
onChange={e => { ) : packetsList.length === 0 ? (
field.onChange(e); <MenuItem disabled>{t('not_found') || 'Paketlar topilmadi'}</MenuItem>
const selected = packetsList.find(p => p.id === e.target.value); ) : (
setPaketName(selected?.name || ''); packetsList.map(packet => (
}} <MenuItem key={packet.id} value={packet.id}>
> {packet.name}
{isLoadingPackets && packetsList.length === 0 ? (
<MenuItem disabled>
<CircularProgress size={24} />
</MenuItem> </MenuItem>
) : ( ))
packetsList.map(packet => ( )}
<MenuItem key={packet.id} value={packet.id} onClick={() => setPaketName(packet.name)}> </Select>
{packet.name}
</MenuItem>
))
)}
{isLoadingPackets && packetsList.length > 0 && (
<MenuItem disabled>
<CircularProgress size={20} />
</MenuItem>
)}
</Select>
</>
)} )}
/> />
{!!get(errors, `packetItemDtos.${index}.packetId`) && ( {!!get(errors, `packetItemDtos.${index}.packetId`) && (
@@ -388,7 +414,6 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
)} )}
</div> </div>
</Box> </Box>
{packetId && ( {packetId && (
<Box mt={2} sx={{ width: '100%' }}> <Box mt={2} sx={{ width: '100%' }}>
<Box display='flex' justifyContent='space-between' alignItems='center' mb={2}> <Box display='flex' justifyContent='space-between' alignItems='center' mb={2}>
@@ -404,7 +429,7 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
</Button> </Button>
</Box> </Box>
</Box> </Box>
{isLoadingProducts && itemsList.length === 0 ? ( {loadingItems ? (
<CircularProgress /> <CircularProgress />
) : ( ) : (
<FormControl fullWidth> <FormControl fullWidth>
@@ -415,35 +440,25 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
<Select <Select
{...field} {...field}
multiple multiple
MenuProps={{ MenuProps={selectMenuProps}
PaperProps: {
style: { maxHeight: 280 },
ref: itemScrollRef,
onScroll: handleItemScroll,
},
disableAutoFocus: true,
autoFocus: false,
}}
renderValue={selected => ( renderValue={selected => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(selected as any[]).map(id => { {(selected as any[]).map((id: number) => (
return ( <Chip
<Chip key={id}
key={id} label={
label={ selectedProductNames[id] ||
selectedProductNames[index]?.[id] || itemsList.find(p => p.id === id)?.name ||
itemsList.find(p => p.id === id)?.name || itemsList.find(p => p.id === id)?.nameRu ||
itemsList.find(p => p.id === id)?.nameRu || id
id }
} onDelete={e => {
onDelete={e => { e.stopPropagation();
e.stopPropagation(); field.onChange(field.value.filter((x: any) => x !== id));
field.onChange(field.value.filter((x: any) => x !== id)); handleProductChange({ id }, false);
handleProductChange(index, { id }, false); }}
}} />
/> ))}
);
})}
</Box> </Box>
)} )}
> >
@@ -461,7 +476,7 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
newValue = field.value.filter((x: any) => x !== product.id); newValue = field.value.filter((x: any) => x !== product.id);
} }
field.onChange(newValue); field.onChange(newValue);
handleProductChange(index, product, checked); handleProductChange(product, checked);
}} }}
> >
<Checkbox checked={field.value.includes(product.id)} /> <Checkbox checked={field.value.includes(product.id)} />
@@ -489,11 +504,10 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
backgroundColor: '#fff', backgroundColor: '#fff',
}} }}
> >
<Box component='form' onSubmit={onSubmit}> <Box component='form' onSubmit={onSubmit} sx={{ overflowAnchor: 'none' }}>
<Typography variant='h5' mb={3.5}> <Typography variant='h5' mb={3.5}>
{editMode ? t('update_box') : t('create_box')} {editMode ? t('update_box') : t('create_box')}
</Typography> </Typography>
<Grid container columnSpacing={2.5} rowSpacing={3} mb={3.5}> <Grid container columnSpacing={2.5} rowSpacing={3} mb={3.5}>
<Grid item xs={12}> <Grid item xs={12}>
<Typography fontSize='18px' fontWeight={500} color='#5D5850' mb={2}> <Typography fontSize='18px' fontWeight={500} color='#5D5850' mb={2}>
@@ -510,6 +524,7 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
{...field} {...field}
labelId='party-select-label' labelId='party-select-label'
label={t('party_name')} label={t('party_name')}
MenuProps={selectMenuProps}
onChange={e => { onChange={e => {
field.onChange(e); field.onChange(e);
handlePartyChange(e); handlePartyChange(e);
@@ -534,7 +549,6 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
{!!errors.partyId && <FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>} {!!errors.partyId && <FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>}
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Stack <Stack
sx={{ sx={{
@@ -552,13 +566,12 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
<PacketRow key={field.key} index={index} field={field} /> <PacketRow key={field.key} index={index} field={field} />
))} ))}
<BaseButton variant='outlined' onClick={appendPacket} startIcon={<AddCircleRounded />} sx={{ mt: 2 }}> <BaseButton variant='outlined' onClick={appendPacket} startIcon={<AddCircleRounded />} sx={{ mt: 2 }}>
{t('add_packet')} {t('add_more')}
</BaseButton> </BaseButton>
</div> </div>
</Stack> </Stack>
</Grid> </Grid>
</Grid> </Grid>
<BaseButton variant='contained' type='submit' disabled={loading} fullWidth sx={{ py: 1.5 }}> <BaseButton variant='contained' type='submit' disabled={loading} fullWidth sx={{ py: 1.5 }}>
{loading ? <CircularProgress size={24} color='inherit' /> : editMode ? t('update_box') : t('create_box')} {loading ? <CircularProgress size={24} color='inherit' /> : editMode ? t('update_box') : t('create_box')}
</BaseButton> </BaseButton>