This commit is contained in:
azizziy
2025-05-20 17:02:10 +05:00
commit c01e852a59
257 changed files with 27766 additions and 0 deletions

View File

@@ -0,0 +1,418 @@
'use client';
import AsyncSelect from 'react-select/async';
import ActionPopMenu from '@/components/common/ActionPopMenu';
import { MyTable, ColumnData } from '@/components/common/MyTable';
import StatusChangePopup from '@/components/common/StatusChangePopup';
import BaseButton from '@/components/ui-kit/BaseButton';
import BaseIconButton from '@/components/ui-kit/BaseIconButton';
import BaseInput from '@/components/ui-kit/BaseInput';
import BasePagination from '@/components/ui-kit/BasePagination';
import { BoxStatus, BoxStatusList } from '@/data/box/box.model';
import { Product } from '@/data/item/item.mode';
import { item_requests } from '@/data/item/item.requests';
import { Party, PartyStatus, PartyStatusList } from '@/data/party/party.model';
import { party_requests } from '@/data/party/party.requests';
import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants';
import useInput from '@/hooks/useInput';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import useRequest from '@/hooks/useRequest';
import EditItemModal from '@/routes/private/items/components/EditItemModal';
import { notifyUnknownError } from '@/services/notification';
import { getBoxStatusStyles, getStatusColor } from '@/theme/getStatusBoxStyles';
import { Add, AddCircleOutline, Check, Circle, Delete, Edit, FilterList, FilterListOff, Search } from '@mui/icons-material';
import { Box, Stack, SvgIcon, Tooltip, Typography } from '@mui/material';
import { useRouter } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
import { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect';
import { box_requests } from '@/data/box/box.requests';
import { useAuthContext } from '@/context/auth-context';
import { UserRoleEnum } from '@/data/user/user.model';
import AddPhotosModal from '@/routes/private/items/components/AddPhotosModal';
type Props = {};
const DashboardGoodsPage = (props: Props) => {
const t = useMyTranslation();
const { user } = useAuthContext();
const [page, setPage] = useState(1);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const [modal, setModal] = useState<null | 'add_photo_item' | 'edit_item'>(null);
const [selectedItem, setSelectedItem] = useState<Product | null>(null);
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
const { value: trackKeyword, onChange: handleTrackKeyword, setValue: setTrackKeyword } = useInput('');
const [partyFilter, setPartyFilter] = useState<{ label: string; value: number } | undefined>(undefined);
const [boxFilter, setBoxFilter] = useState<{ label: string; value: number } | undefined>(undefined);
const [boxStatusFilter, setBoxStatusFilter] = useState<BoxStatus | undefined>(undefined);
const [deleteIds, setDeleteIds] = useState<number[]>([]);
const getItemsQuery = useRequest(
() =>
item_requests.getAll({
name: keyword,
page: page,
status: boxStatusFilter,
packetId: boxFilter?.value,
partyId: partyFilter?.value,
trekId: trackKeyword,
direction: 'desc',
sort: 'id',
}),
{
selectData(data) {
return data.data.data;
},
dependencies: [page, boxStatusFilter, boxFilter, partyFilter],
}
);
const {
data: list,
totalElements,
totalPages,
} = useMemo(() => {
if (getItemsQuery.data?.data) {
return {
data: getItemsQuery.data.data,
totalElements: getItemsQuery.data.totalElements,
totalPages: getItemsQuery.data.totalPages,
};
} else {
return {
data: [],
totalElements: 0,
totalPages: 0,
};
}
}, [getItemsQuery]);
const loading = getItemsQuery.loading;
const handleChange = (newPage: number) => {
setPage(newPage);
};
const resetFilter = () => {
setPage(1);
setKeyword('');
setTrackKeyword('');
setBoxStatusFilter(undefined);
// @ts-expect-error
setPartyFilter(null);
// @ts-expect-error
setBoxFilter(null);
};
const closeModal = () => {
setModal(null);
setSelectedItem(null);
};
const openEditModal = (item: Product) => {
setModal('edit_item');
setSelectedItem(item);
};
const openAddPhotoModal = (item: Product) => {
setModal('add_photo_item');
setSelectedItem(item);
};
const onSuccessEdit = () => {
getItemsQuery.refetch();
closeModal();
};
const onDelete = async (id: number) => {
if (deleteIds.includes(id)) return;
try {
setDeleteIds(p => [...p, id]);
await item_requests.delete({ itemId: id });
getItemsQuery.refetch();
} catch (error) {
notifyUnknownError(error);
} finally {
setDeleteIds(prev => prev.filter(i => i !== id));
}
};
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 { data: defaultBoxOptions } = 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],
}
);
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 }));
});
};
const boxOptions = (inputValue: string) => {
return box_requests.getAll({ cargoId: inputValue }).then(res => {
return res.data.data.data.map(p => ({ label: p.name, value: p.id }));
});
};
useEffect(() => {
const timeoutId = setTimeout(() => {
setPage(1);
getItemsQuery.refetch();
}, 350);
return () => clearTimeout(timeoutId);
}, [keyword, trackKeyword]);
const columns: ColumnData<Product>[] = [
{
label: t('No'),
width: 100,
renderCell(data, rowIndex) {
return (page - 1) * pageSize + rowIndex + 1;
},
},
{
dataKey: 'id',
label: t('id'),
width: 100,
},
{
dataKey: 'name',
label: t('product_name'),
width: 300,
},
{
dataKey: 'trekId',
label: t('track_id'),
width: 300,
},
{
dataKey: 'boxName',
label: t('box_number'),
width: 300,
},
{
dataKey: 'partyName',
label: t('party'),
width: 300,
},
{
dataKey: 'weight',
label: t('weight'),
width: 300,
},
{
dataKey: 'amount',
label: t('quantity'),
width: 300,
},
{
dataKey: 'cargoId',
label: t('cargo_id'),
width: 300,
},
{
dataKey: 'price',
label: t('price'),
width: 150,
},
{
dataKey: 'totalPrice',
label: t('total_price'),
width: 150,
},
{
dataKey: 'status',
label: t('box_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 <Box sx={{ ...getBoxStatusStyles(data.status) }}>{t(data.status)}</Box>;
},
},
{
label: '',
width: 50,
numeric: true,
renderCell(data, rowIndex) {
const isUzbekUser = user?.role === UserRoleEnum.UZB_WORKER;
if (isUzbekUser && data.hasImage) {
return <Check />;
} else if (isUzbekUser && !data.hasImage) {
return (
<ActionPopMenu
buttons={[
{
icon: <Add sx={{ path: { color: '#3489E4' } }} />,
label: t('add_photo_to_item'),
onClick: () => {
openAddPhotoModal(data);
},
},
]}
/>
);
}
return (
<ActionPopMenu
buttons={[
{
icon: <Edit sx={{ path: { color: '#3489E4' } }} />,
label: t('edit'),
onClick: () => {
openEditModal(data);
},
},
{
icon: <Delete sx={{ path: { color: '#3489E4' } }} />,
label: t('delete'),
onClick: () => {
onDelete(data.id);
},
dontCloseOnClick: true,
loading: deleteIds.includes(data.id),
},
]}
/>
);
},
},
];
return (
<Box>
<Box
width={1}
mb={3}
sx={{
padding: '28px',
borderRadius: '16px',
backgroundColor: '#fff',
}}
>
<Stack mb={3.5} direction={'row'} justifyContent={'space-between'} alignItems={'flex-start'}>
<Typography
sx={{
fontSize: '20px',
lineHeight: '24px',
fontWeight: 600,
textTransform: 'capitalize',
color: '#000',
}}
>
{t('products')}
</Typography>
<Stack direction={'row'} justifyContent={'flex-end'} alignItems={'center'} gap={2} flexWrap={'wrap'}>
<BaseInput
InputProps={{
startAdornment: <Search color='primary' />,
}}
value={keyword}
onChange={handleKeyword}
placeholder={t('filter_item_name')}
/>
<AsyncSelect
isClearable
value={partyFilter}
onChange={(newValue: any) => {
setPartyFilter(newValue);
setPage(1);
}}
styles={selectDefaultStyles}
noOptionsMessage={() => t('not_found')}
loadingMessage={() => t('loading')}
defaultOptions={defaultPartyOptions!}
loadOptions={partyOptions}
placeholder={t('filter_party_name')}
/>
<AsyncSelect
isClearable
value={boxFilter}
onChange={(newValue: any) => {
setBoxFilter(newValue);
setPage(1);
}}
styles={selectDefaultStyles}
noOptionsMessage={() => t('enter_box_name_to_find')}
loadingMessage={() => t('loading')}
defaultOptions={defaultBoxOptions!}
loadOptions={boxOptions}
placeholder={t('filter_box_name')}
/>
<BaseInput value={trackKeyword} onChange={handleTrackKeyword} placeholder={t('filter_track_id')} />
<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>
{modal === 'edit_item' && !!selectedItem && (
<EditItemModal item={selectedItem} onClose={closeModal} onSuccess={onSuccessEdit} open />
)}
{modal === 'add_photo_item' && !!selectedItem && (
<AddPhotosModal item={selectedItem} onClose={closeModal} onSuccess={onSuccessEdit} open />
)}
</Box>
);
};
export default DashboardGoodsPage;

View File

@@ -0,0 +1,174 @@
/* eslint-disable @next/next/no-img-element */
import BaseButton from '@/components/ui-kit/BaseButton';
import BaseInput from '@/components/ui-kit/BaseInput';
import BaseModal from '@/components/ui-kit/BaseModal';
import BaseReactSelect from '@/components/ui-kit/BaseReactSelect';
import { Product } from '@/data/item/item.mode';
import { item_requests } from '@/data/item/item.requests';
import { staff_requests } from '@/data/staff/staff.requests';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { notifyUnknownError } from '@/services/notification';
import { UploadFile } from '@mui/icons-material';
import { Box, Grid, Stack, Typography, styled } from '@mui/material';
import React, { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
const StyledBox = styled(Box)`
.title {
color: #000;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 24px;
margin-bottom: 28px;
}
.label {
color: #5d5850;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
margin-bottom: 8px;
}
`;
type Props = {
onClose: () => void;
open: boolean;
onSuccess: () => void;
item: Product;
};
const AddPhotosModal = ({ onClose, open, onSuccess, item }: Props) => {
const t = useMyTranslation();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<{
file: any;
summa: number;
weight: number;
}>({
defaultValues: {
summa: 0,
weight: 0,
},
});
const fileValue = watch('file');
const fileUrl = fileValue ? URL.createObjectURL(fileValue) : '';
const onChangeFile = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files?.[0]) {
setValue('file', event.target.files?.[0]);
}
};
const onSubmit = handleSubmit(async values => {
try {
setLoading(true);
const formdata = new FormData();
formdata.append('file', values.file);
await item_requests.addPhotos({ itemId: item.id, weight: values.weight, summa: values.summa }, formdata);
onSuccess();
} catch (error) {
notifyUnknownError(error);
} finally {
setLoading(false);
}
});
return (
<BaseModal maxWidth='600px' onClose={onClose} open={open}>
<StyledBox component={'form'} onSubmit={onSubmit}>
<Typography className='title'>{t('add_photo_to_item')}</Typography>
<Grid container spacing={3} mb={3}>
<Grid item xs={6}>
<Typography className='label'>{t('summa')}</Typography>
<BaseInput
type='number'
inputProps={{
step: 0.01,
}}
error={!!errors.summa}
mainBorderColor='#D8D8D8'
{...register('summa', { required: true })}
fullWidth
/>
</Grid>
<Grid item xs={6}>
<Typography className='label'>{t('weight')}</Typography>
<BaseInput
type='number'
inputProps={{
step: 0.01,
}}
error={!!errors.weight}
mainBorderColor='#D8D8D8'
{...register('weight', { required: true })}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<Typography className='label'>{t('photo')}</Typography>
<Box
mb={1}
component={'label'}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid #3389E4',
borderRadius: '12px',
minHeight: '120px',
cursor: 'pointer',
}}
>
<UploadFile color='primary' />
<input
type='file'
className='visually-hidden'
onChange={onChangeFile}
accept='.jpeg, .jpg, .png, .webp, .jfif'
/>
</Box>
{fileUrl && (
<img
src={fileUrl}
style={{
margin: '0 auto',
maxWidth: '300px',
width: '100%',
height: 'auto',
display: 'block',
}}
alt=''
/>
)}
</Grid>
</Grid>
<Stack direction={'row'} justifyContent={'flex-start'} alignItems={'center'} spacing={3}>
<BaseButton colorVariant='blue' type='submit' loading={loading}>
{t('update')}
</BaseButton>
<BaseButton variant='outlined' type='button' colorVariant='blue-outlined' disabled={loading} onClick={onClose}>
{t('cancel')}
</BaseButton>
</Stack>
</StyledBox>
</BaseModal>
);
};
export default AddPhotosModal;

View File

@@ -0,0 +1,142 @@
import BaseButton from '@/components/ui-kit/BaseButton';
import BaseInput from '@/components/ui-kit/BaseInput';
import BaseModal from '@/components/ui-kit/BaseModal';
import BaseReactSelect from '@/components/ui-kit/BaseReactSelect';
import { Product } from '@/data/item/item.mode';
import { item_requests } from '@/data/item/item.requests';
import { staff_requests } from '@/data/staff/staff.requests';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { notifyUnknownError } from '@/services/notification';
import { Box, Grid, Stack, Typography, styled } from '@mui/material';
import React, { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
const StyledBox = styled(Box)`
.title {
color: #000;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 24px;
margin-bottom: 28px;
}
.label {
color: #5d5850;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
margin-bottom: 8px;
}
`;
type Props = {
onClose: () => void;
open: boolean;
onSuccess: () => void;
item: Product;
};
const EditItemModal = ({ onClose, open, onSuccess, item }: Props) => {
const t = useMyTranslation();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<{
cargoId: string;
trekId: string;
name: string;
amount: number;
weight: number;
}>({
defaultValues: {
amount: item.amount,
cargoId: item.cargoId,
name: item.name,
trekId: item.trekId,
weight: item.weight,
},
});
const onSubmit = handleSubmit(async values => {
try {
setLoading(true);
await item_requests.update({ itemId: item.id, ...values });
onSuccess();
} catch (error) {
notifyUnknownError(error);
} finally {
setLoading(false);
}
});
return (
<BaseModal maxWidth='600px' onClose={onClose} open={open}>
<StyledBox component={'form'} onSubmit={onSubmit}>
<Typography className='title'>{t('update_item')}</Typography>
<Grid container spacing={3} mb={3}>
<Grid item xs={6}>
<Typography className='label'>{t('name')}</Typography>
<BaseInput error={!!errors.name} mainBorderColor='#D8D8D8' {...register('name', { required: true })} fullWidth />
</Grid>
<Grid item xs={6}>
<Typography className='label'>{t('cargo_id')}</Typography>
<BaseInput
error={!!errors.cargoId}
mainBorderColor='#D8D8D8'
{...register('cargoId', { required: true })}
fullWidth
/>
</Grid>
<Grid item xs={6}>
<Typography className='label'>{t('track_id')}</Typography>
<BaseInput
error={!!errors.trekId}
mainBorderColor='#D8D8D8'
{...register('trekId', { required: true })}
fullWidth
/>
</Grid>
<Grid item xs={6}>
<Typography className='label'>{t('quantity')}</Typography>
<BaseInput
error={!!errors.amount}
mainBorderColor='#D8D8D8'
{...register('amount', { required: true })}
fullWidth
/>
</Grid>
<Grid item xs={6}>
<Typography className='label'>{t('weight')}</Typography>
<BaseInput
error={!!errors.weight}
inputProps={{
step: 0.1,
}}
type='number'
mainBorderColor='#D8D8D8'
{...register('weight', { required: true })}
fullWidth
/>
</Grid>
</Grid>
<Stack direction={'row'} justifyContent={'flex-start'} alignItems={'center'} spacing={3}>
<BaseButton colorVariant='blue' type='submit' loading={loading}>
{t('update')}
</BaseButton>
<BaseButton variant='outlined' type='button' colorVariant='blue-outlined' disabled={loading} onClick={onClose}>
{t('cancel')}
</BaseButton>
</Stack>
</StyledBox>
</BaseModal>
);
};
export default EditItemModal;

View File

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