585 lines
22 KiB
TypeScript
585 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import ActionPopMenu from '@/components/common/ActionPopMenu';
|
|
import { ColumnData, MyTable } from '@/components/common/MyTable';
|
|
import StatusChangePopup from '@/components/common/StatusChangePopup';
|
|
import BaseButton from '@/components/ui-kit/BaseButton';
|
|
import BaseInput from '@/components/ui-kit/BaseInput';
|
|
import BasePagination from '@/components/ui-kit/BasePagination';
|
|
import { useAuthContext } from '@/context/auth-context';
|
|
import { BoxStatus, BoxStatusList, IBox } 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 { 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 { file_service } from '@/services/file-service';
|
|
import { notifyUnknownError } from '@/services/notification';
|
|
import { getStatusColor } from '@/theme/getStatusBoxStyles';
|
|
import { Add, Circle, Delete, Download, Edit, FilterList, FilterListOff, RemoveRedEye, Search } from '@mui/icons-material';
|
|
import { Box, Button, Card, CardContent, Modal, Stack, TextField, Typography } from '@mui/material';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
|
|
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 [open, setOpen] = useState(false);
|
|
const handleOpen = () => setOpen(true);
|
|
const handleClose = () => setOpen(false);
|
|
const t = useMyTranslation();
|
|
const navigation = useMyNavigation();
|
|
const { isAdmin } = useAuthContext();
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
|
|
const [boxStatusFilter, setBoxStatusFilter] = useState<BoxStatus | undefined>(undefined);
|
|
const [trackId, setTrackId] = useState<string>();
|
|
|
|
const [deleteIds, setDeleteIds] = useState<number[]>([]);
|
|
const [downloadIds, setDownloadIds] = useState<number[]>([]);
|
|
const [changeStatusIds, setChangeStatusIds] = useState<number[]>([]);
|
|
|
|
const boxStatusOptions = useMemo(() => {
|
|
const p = ['READY_TO_INVOICE'] as BoxStatus[];
|
|
if (isAdmin) {
|
|
p.push('READY');
|
|
}
|
|
return p;
|
|
}, [isAdmin]);
|
|
|
|
const getBoxesQuery = useRequest(
|
|
() =>
|
|
box_requests.getAll({
|
|
page: page,
|
|
cargoId: keyword,
|
|
status: boxStatusFilter,
|
|
direction: 'desc',
|
|
sort: 'id',
|
|
}),
|
|
{
|
|
dependencies: [page, boxStatusFilter],
|
|
selectData(data) {
|
|
return data.data.data;
|
|
},
|
|
}
|
|
);
|
|
|
|
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 {
|
|
data: list,
|
|
totalElements,
|
|
totalPages,
|
|
} = useMemo(() => {
|
|
if (getBoxesQuery.data?.data) {
|
|
return {
|
|
data: getBoxesQuery.data.data,
|
|
totalElements: getBoxesQuery.data.totalElements,
|
|
totalPages: getBoxesQuery.data.totalPages,
|
|
};
|
|
} else {
|
|
return {
|
|
data: [],
|
|
totalElements: 0,
|
|
totalPages: 0,
|
|
};
|
|
}
|
|
}, [getBoxesQuery]);
|
|
const loading = getBoxesQuery.loading;
|
|
|
|
const handleChange = (newPage: number) => {
|
|
setTimeout(() => {
|
|
setPage(newPage);
|
|
}, 100);
|
|
};
|
|
|
|
const resetFilter = () => {
|
|
setPage(1);
|
|
setKeyword('');
|
|
setBoxStatusFilter(undefined);
|
|
};
|
|
|
|
const onDelete = async (id: number) => {
|
|
if (deleteIds.includes(id)) return;
|
|
|
|
try {
|
|
setDeleteIds(p => [...p, id]);
|
|
await box_requests.delete({ packetId: id });
|
|
getBoxesQuery.refetch();
|
|
} catch (error) {
|
|
notifyUnknownError(error);
|
|
} finally {
|
|
setDeleteIds(prev => prev.filter(i => i !== id));
|
|
}
|
|
};
|
|
|
|
const onDownloadExcel = async (id: number) => {
|
|
if (downloadIds.includes(id)) return;
|
|
|
|
try {
|
|
setDownloadIds(p => [...p, id]);
|
|
const response = await box_requests.downloadExcel({ packetId: id });
|
|
const file = new File([response.data], 'Box-excel.xlsx', { type: response.data.type });
|
|
file_service.download(file);
|
|
} catch (error) {
|
|
notifyUnknownError(error);
|
|
} finally {
|
|
setDownloadIds(prev => prev.filter(i => i !== id));
|
|
}
|
|
};
|
|
|
|
const onChangeStatus = async (id: number, newStatus: BoxStatus) => {
|
|
if (changeStatusIds.includes(id)) return;
|
|
|
|
try {
|
|
setChangeStatusIds(p => [...p, id]);
|
|
await box_requests.changeStatus({ packetId: id, status: newStatus });
|
|
getBoxesQuery.refetch();
|
|
} catch (error) {
|
|
notifyUnknownError(error);
|
|
} finally {
|
|
setChangeStatusIds(prev => prev.filter(i => i !== id));
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const timeoutId = setTimeout(() => {
|
|
setPage(1);
|
|
getBoxesQuery.refetch();
|
|
}, 350);
|
|
return () => clearTimeout(timeoutId);
|
|
}, [keyword]);
|
|
|
|
// No, PartyName, PacketName, PartyTozaOg'irlik, CountOfItems, WeightOfItems, CargoID, PassportNameFamily - PacketStatusForInvoice
|
|
const columns: ColumnData<IBox>[] = [
|
|
{
|
|
label: t('No'),
|
|
width: 100,
|
|
renderCell(data, rowIndex) {
|
|
return (page - 1) * pageSize + rowIndex + 1;
|
|
},
|
|
},
|
|
{
|
|
dataKey: 'id',
|
|
label: "Qo'shish",
|
|
width: 120,
|
|
renderCell: data => {
|
|
return (
|
|
<Button onClick={() => navigation.push(pageLinks.dashboard.boxes.edit(data.id))}>
|
|
<Add />
|
|
</Button>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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 getOneBox = useRequest(
|
|
() => {
|
|
return box_requests.find({ packetId: data.id });
|
|
},
|
|
{
|
|
selectData(data) {
|
|
const boxData = data.data.data;
|
|
|
|
return {
|
|
products_list: [
|
|
...boxData.items.map(item => {
|
|
let name = item.name;
|
|
let nameRu = item.nameRu;
|
|
|
|
return {
|
|
id: item.id,
|
|
price: item.price,
|
|
|
|
cargoId: item.cargoId,
|
|
trekId: item.trekId,
|
|
name: name,
|
|
acceptedNumber: item.acceptedNumber,
|
|
nameRu: nameRu,
|
|
amount: +item.amount,
|
|
weight: +item.weight,
|
|
};
|
|
}),
|
|
],
|
|
};
|
|
},
|
|
}
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
{(() => {
|
|
const total = getOneBox.data?.products_list.reduce(
|
|
(acc, product) => {
|
|
console.log(product, 'totalAccepted');
|
|
acc.totalAmount += +product.amount || 0;
|
|
acc.totalAccepted += +product.acceptedNumber || 0;
|
|
return acc;
|
|
},
|
|
{ totalAmount: 0, totalAccepted: 0 }
|
|
);
|
|
|
|
return (
|
|
<Typography>
|
|
{total?.totalAmount} | {total?.totalAccepted}
|
|
</Typography>
|
|
);
|
|
})()}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
dataKey: 'totalNetWeight',
|
|
label: t('party_weight'),
|
|
width: 120,
|
|
},
|
|
{
|
|
dataKey: 'cargoId',
|
|
label: t('cargo_id'),
|
|
width: 120,
|
|
},
|
|
{
|
|
dataKey: 'passportName',
|
|
label: t('client'),
|
|
width: 120,
|
|
},
|
|
{
|
|
dataKey: 'status',
|
|
label: t('status'),
|
|
width: 240,
|
|
renderHeaderCell(rowIndex) {
|
|
return (
|
|
<Stack direction={'row'} alignItems={'center'}>
|
|
<span>{t('box_status')}</span>
|
|
<ActionPopMenu
|
|
buttons={BoxStatusList.map(stat => {
|
|
return {
|
|
icon: <Circle sx={{ path: { color: getStatusColor(stat) } }} />,
|
|
label: t(stat),
|
|
onClick() {
|
|
setBoxStatusFilter(stat);
|
|
setPage(1);
|
|
},
|
|
};
|
|
})}
|
|
mainIcon={<FilterList />}
|
|
placement={{
|
|
anchorOrigin: {
|
|
vertical: 'bottom',
|
|
horizontal: 'center',
|
|
},
|
|
transformOrigin: {
|
|
horizontal: 'center',
|
|
vertical: 'top',
|
|
},
|
|
}}
|
|
/>
|
|
</Stack>
|
|
);
|
|
},
|
|
renderCell(data) {
|
|
return (
|
|
<StatusChangePopup
|
|
anchor={{
|
|
status: data.status,
|
|
text: t(data.status),
|
|
}}
|
|
loading={changeStatusIds.includes(data.id)}
|
|
buttons={boxStatusOptions.map(stat => {
|
|
return {
|
|
icon: (
|
|
<Circle
|
|
sx={{
|
|
path: {
|
|
color: getStatusColor(stat),
|
|
},
|
|
}}
|
|
/>
|
|
),
|
|
label: t(stat),
|
|
onClick: () => {
|
|
onChangeStatus(data.id, stat);
|
|
},
|
|
};
|
|
})}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: '',
|
|
width: 100,
|
|
numeric: true,
|
|
renderCell(data, rowIndex) {
|
|
return (
|
|
<ActionPopMenu
|
|
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' } }} />,
|
|
label: t('edit'),
|
|
onClick: () => {
|
|
navigation.push(pageLinks.dashboard.boxes.edit(data.id));
|
|
},
|
|
},
|
|
{
|
|
icon: <Delete sx={{ path: { color: '#3489E4' } }} />,
|
|
label: t('delete'),
|
|
onClick: () => {
|
|
onDelete(data.id);
|
|
},
|
|
dontCloseOnClick: true,
|
|
loading: deleteIds.includes(data.id),
|
|
},
|
|
...(data.status === 'READY'
|
|
? [
|
|
{
|
|
icon: <Download sx={{ path: { color: '#3489E4' } }} />,
|
|
label: t('download_excel'),
|
|
onClick: () => {
|
|
onDownloadExcel(data.id);
|
|
},
|
|
loading: downloadIds.includes(data.id),
|
|
dontCloseOnClick: true,
|
|
},
|
|
]
|
|
: []),
|
|
]}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
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 (
|
|
<Box>
|
|
<Stack direction={'row'} mb={3} spacing={3}>
|
|
<BaseButton colorVariant='blue' startIcon={<Add />} href={pageLinks.dashboard.boxes.create}>
|
|
{t('create_packet')}
|
|
</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>
|
|
<Box
|
|
width={1}
|
|
mb={3}
|
|
sx={{
|
|
padding: '28px',
|
|
borderRadius: '16px',
|
|
backgroundColor: '#fff',
|
|
}}
|
|
>
|
|
<Stack mb={3.5} direction={'row'} justifyContent={'space-between'} alignItems={'center'}>
|
|
<Typography
|
|
sx={{
|
|
fontSize: '20px',
|
|
lineHeight: '24px',
|
|
fontWeight: 600,
|
|
textTransform: 'capitalize',
|
|
color: '#000',
|
|
}}
|
|
>
|
|
{t('packet')}
|
|
</Typography>
|
|
<Stack direction={'row'} alignItems={'center'} spacing={2}>
|
|
<BaseInput
|
|
InputProps={{
|
|
startAdornment: <Search color='primary' />,
|
|
}}
|
|
placeholder={t('filter_packet_name')}
|
|
value={keyword}
|
|
onChange={e => {
|
|
setKeyword(e.target.value);
|
|
}}
|
|
/>
|
|
|
|
<BaseButton colorVariant='gray' startIcon={<FilterListOff />} size='small' onClick={resetFilter}>
|
|
{t('reset_filter')}
|
|
</BaseButton>
|
|
</Stack>
|
|
</Stack>
|
|
<Box mb={6}>
|
|
<MyTable columns={columns} data={list} loading={loading} />
|
|
</Box>
|
|
<Stack direction={'row'} justifyContent={'center'}>
|
|
<BasePagination page={page} pageSize={pageSize} totalCount={totalElements} onChange={handleChange} />
|
|
</Stack>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default DashboardBoxesPage;
|