571 lines
26 KiB
TypeScript
571 lines
26 KiB
TypeScript
'use client';
|
|
|
|
import BaseButton from '@/components/ui-kit/BaseButton';
|
|
import BaseIconButton from '@/components/ui-kit/BaseIconButton';
|
|
import { useAuthContext } from '@/context/auth-context';
|
|
import { box_requests } from '@/data/box/box.requests';
|
|
import { item_requests } from '@/data/item/item.requests';
|
|
import { party_requests } from '@/data/party/party.requests';
|
|
import { FormValues, RealCreateBoxBodyType, UpdateRealBoxBodyType } from '@/data/real-box/real-box.model';
|
|
import { real_box_requests } from '@/data/real-box/real-box.requests';
|
|
import { pageLinks } from '@/helpers/constants';
|
|
import { useMyNavigation } from '@/hooks/useMyNavigation';
|
|
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
|
import { notifyUnknownError } from '@/services/notification';
|
|
import { AddCircleRounded, Close } from '@mui/icons-material';
|
|
import {
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
Chip,
|
|
CircularProgress,
|
|
FormControl,
|
|
FormHelperText,
|
|
Grid,
|
|
InputLabel,
|
|
ListItemText,
|
|
MenuItem,
|
|
OutlinedInput,
|
|
Select,
|
|
Stack,
|
|
styled,
|
|
Typography,
|
|
} from '@mui/material';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import get from 'lodash.get';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
|
|
|
const StyledCreateBox = styled(Box)`
|
|
.item-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
|
|
& > * {
|
|
flex: 1 1 1;
|
|
}
|
|
`;
|
|
|
|
interface Props {
|
|
partiesData?: { value: number; label: string }[];
|
|
initialValues?: {
|
|
id?: number;
|
|
boxId?: string;
|
|
partyId?: number;
|
|
partyName?: string;
|
|
paketIds?: Array<any>;
|
|
};
|
|
}
|
|
|
|
const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
|
|
const { user } = useAuthContext();
|
|
const editMode = !!initialValues?.id;
|
|
const t = useMyTranslation();
|
|
const params = useSearchParams();
|
|
const { push } = useMyNavigation();
|
|
const [partyId, setPartyId] = useState<number | string>('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
setValue,
|
|
reset,
|
|
watch,
|
|
formState: { errors },
|
|
} = useForm<FormValues>({
|
|
defaultValues: {
|
|
partyName: initialValues?.partyName || '',
|
|
packetItemDtos: initialValues?.paketIds
|
|
? initialValues.paketIds.map(paket => ({
|
|
packetId: paket.id,
|
|
itemDtos: paket.items ? paket.items.map((item: any) => item.id) : [],
|
|
}))
|
|
: params.get('party_id')
|
|
? [{ packetId: +params.get('party_id')!, itemDtos: [] }]
|
|
: [{ packetId: 0, itemDtos: [] }],
|
|
id: initialValues?.id,
|
|
boxId: initialValues?.boxId,
|
|
partyId: initialValues?.partyId,
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (initialValues) {
|
|
reset({
|
|
partyName: initialValues.partyName || '',
|
|
packetItemDtos: initialValues.paketIds
|
|
? initialValues.paketIds.map(paket => ({
|
|
packetId: paket.id,
|
|
itemDtos: paket.items ? paket.items.map((item: any) => item.id) : [],
|
|
}))
|
|
: [{ packetId: 0, itemDtos: [] }],
|
|
id: initialValues.id,
|
|
boxId: initialValues.boxId,
|
|
partyId: initialValues.partyId,
|
|
});
|
|
if (initialValues.partyId) {
|
|
setPartyId(initialValues.partyId);
|
|
}
|
|
}
|
|
}, [initialValues, reset]);
|
|
|
|
const { fields, append, remove } = useFieldArray({
|
|
control,
|
|
name: 'packetItemDtos',
|
|
keyName: 'key',
|
|
});
|
|
|
|
const requiredText = t('required');
|
|
|
|
const { data: parties = [], isLoading: isLoadingParties } = useQuery({
|
|
queryKey: ['parties-list', 'COLLECTING'],
|
|
queryFn: () => party_requests.getAll({ status: 'COLLECTING' }),
|
|
select: data =>
|
|
data.data.data.data.map((p: any) => ({
|
|
id: p.id,
|
|
name: p.name,
|
|
})),
|
|
});
|
|
|
|
const onSubmit = handleSubmit(async values => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
if (editMode) {
|
|
const updateBody: UpdateRealBoxBodyType = {
|
|
boxId: initialValues!.boxId!,
|
|
partyName: values.partyName,
|
|
packetItemDtos: values.packetItemDtos.map(packet => ({
|
|
packetId: packet.packetId,
|
|
itemDtos: packet.itemDtos,
|
|
})),
|
|
};
|
|
await real_box_requests.update(updateBody);
|
|
} else {
|
|
const createBody: RealCreateBoxBodyType = {
|
|
partyName: values.partyName,
|
|
packetItemDtos: values.packetItemDtos.map(packet => ({
|
|
packetId: packet.packetId,
|
|
itemDtos: packet.itemDtos,
|
|
})),
|
|
};
|
|
await real_box_requests.create(createBody);
|
|
}
|
|
|
|
push(pageLinks.dashboard.real_boxes.index);
|
|
} catch (error) {
|
|
notifyUnknownError(error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
const handlePacketChange = (index: number, value: number) => {
|
|
setValue(`packetItemDtos.${index}.packetId`, value);
|
|
setValue(`packetItemDtos.${index}.itemDtos`, []);
|
|
};
|
|
|
|
const appendPacket = () => {
|
|
append({ packetId: 0, itemDtos: [] });
|
|
};
|
|
|
|
const removePacket = (index: number) => {
|
|
remove(index);
|
|
};
|
|
const [packetSearchTerm, setPacketSearchTerm] = useState('');
|
|
|
|
const packetSearchInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handlePartyChange = (event: any) => {
|
|
const selectedParty = parties.find(p => p.id === event.target.value);
|
|
if (selectedParty) {
|
|
setValue('partyName', selectedParty.name);
|
|
setPartyId(selectedParty.id);
|
|
setValue('packetItemDtos', [{ packetId: 0, itemDtos: [] }]);
|
|
}
|
|
};
|
|
|
|
const [paketName, setPaketName] = useState<string>('');
|
|
const PacketRow = ({ index, field }: { index: number; field: any }) => {
|
|
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 { isFetching: isLoadingPackets } = useQuery({
|
|
queryKey: ['packets-list', partyId, keyword, packetsPage],
|
|
queryFn: () => box_requests.getAll({ partyId, cargoId: keyword, page: packetsPage }),
|
|
enabled: !!partyId,
|
|
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>) => {
|
|
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
|
|
if (isNearBottom && !isLoadingPackets && packetsHasMore) {
|
|
setPacketsPage(prev => prev + 1);
|
|
}
|
|
};
|
|
|
|
const handleItemScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
|
|
|
if (isNearBottom && !isLoadingProducts && itemsHasMore) {
|
|
setItemsPage(prev => prev + 1);
|
|
}
|
|
};
|
|
|
|
const handleProductChange = (packetIndex: number, product: any, checked: boolean) => {
|
|
setSelectedProductNames(prev => {
|
|
const prevNames = prev[packetIndex] || {};
|
|
if (checked) {
|
|
return { ...prev, [packetIndex]: { ...prevNames, [product.id]: product.name || product.nameRu || String(product.id) } };
|
|
} else {
|
|
const newNames = { ...prevNames };
|
|
delete newNames[product.id];
|
|
return { ...prev, [packetIndex]: newNames };
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleSelectAllProducts = async () => {
|
|
if (!packetId) return;
|
|
let allProducts: any[] = [];
|
|
let page = 1;
|
|
let totalPages = 1;
|
|
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) {
|
|
setValue(
|
|
`packetItemDtos.${index}.itemDtos`,
|
|
allProducts.map((p: any) => p.id)
|
|
);
|
|
setSelectedProductNames((prev: any) => ({
|
|
...prev,
|
|
[index]: allProducts.reduce(
|
|
(acc, p) => ({
|
|
...acc,
|
|
[p.id]: p.name || p.nameRu || String(p.id),
|
|
}),
|
|
{}
|
|
),
|
|
}));
|
|
}
|
|
};
|
|
|
|
const handleClearAll = () => {
|
|
setValue(`packetItemDtos.${index}.itemDtos`, []);
|
|
setSelectedProductNames((prev: any) => ({ ...prev, [index]: {} }));
|
|
};
|
|
|
|
return (
|
|
<Box className='item-row' mb={2} display={'flex'} flexDirection={'column'}>
|
|
<div style={{ width: '100%' }}>
|
|
<Typography fontSize='18px' sx={{ textAlign: 'start' }} fontWeight={500} color='#5D5850'>
|
|
{t('packet')}
|
|
</Typography>
|
|
</div>
|
|
<Box
|
|
className='item-row-field'
|
|
sx={{ width: '100%' }}
|
|
flex={1}
|
|
display={'flex'}
|
|
gap={2}
|
|
justifyContent={'space-between'}
|
|
alignItems={'center'}
|
|
>
|
|
<OutlinedInput onChange={e => setKeyword(e.target.value)} />
|
|
<div style={{ width: '100%' }}>
|
|
<FormControl fullWidth>
|
|
<InputLabel id={`packet-select-label-${index}`}>{t('packet')}</InputLabel>
|
|
<Controller
|
|
name={`packetItemDtos.${index}.packetId`}
|
|
control={control}
|
|
rules={{ required: requiredText }}
|
|
render={({ field }) => (
|
|
<>
|
|
<Select
|
|
{...field}
|
|
labelId={`packet-select-label-${index}`}
|
|
label={t('packet')}
|
|
value={field.value || ''}
|
|
disabled={isLoadingPackets}
|
|
renderValue={selected =>
|
|
paketName || packetsList.find(p => p.id === selected)?.name || t('select')
|
|
}
|
|
MenuProps={{
|
|
PaperProps: {
|
|
style: { maxHeight: 280 },
|
|
ref: packetScrollRef,
|
|
onScroll: handlePacketScroll,
|
|
},
|
|
disableAutoFocus: true,
|
|
autoFocus: false,
|
|
}}
|
|
onClick={e => e.stopPropagation()}
|
|
onChange={e => {
|
|
field.onChange(e);
|
|
const selected = packetsList.find(p => p.id === e.target.value);
|
|
setPaketName(selected?.name || '');
|
|
}}
|
|
>
|
|
{isLoadingPackets && packetsList.length === 0 ? (
|
|
<MenuItem disabled>
|
|
<CircularProgress size={24} />
|
|
</MenuItem>
|
|
) : (
|
|
packetsList.map(packet => (
|
|
<MenuItem key={packet.id} value={packet.id} onClick={() => setPaketName(packet.name)}>
|
|
{packet.name}
|
|
</MenuItem>
|
|
))
|
|
)}
|
|
{isLoadingPackets && packetsList.length > 0 && (
|
|
<MenuItem disabled>
|
|
<CircularProgress size={20} />
|
|
</MenuItem>
|
|
)}
|
|
</Select>
|
|
</>
|
|
)}
|
|
/>
|
|
{!!get(errors, `packetItemDtos.${index}.packetId`) && (
|
|
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
|
)}
|
|
</FormControl>
|
|
</div>
|
|
<div>
|
|
{fields.length > 1 && (
|
|
<BaseIconButton
|
|
size='small'
|
|
colorVariant='icon-error'
|
|
sx={{ flexShrink: 0, height: 'auto', marginTop: 1 }}
|
|
onClick={() => removePacket(index)}
|
|
>
|
|
<Close />
|
|
</BaseIconButton>
|
|
)}
|
|
</div>
|
|
</Box>
|
|
|
|
{packetId && (
|
|
<Box mt={2} sx={{ width: '100%' }}>
|
|
<Box display='flex' justifyContent='space-between' alignItems='center' mb={2}>
|
|
<Typography fontSize='18px' sx={{ textAlign: 'start' }} fontWeight={500} color='#5D5850'>
|
|
{t('products')}
|
|
</Typography>
|
|
<Box>
|
|
<Button size='small' color='primary' variant='outlined' sx={{ mr: 1 }} onClick={handleSelectAllProducts}>
|
|
{t('select_all') || 'Barcha mahsulotni tanlash'}
|
|
</Button>
|
|
<Button size='small' color='error' variant='outlined' onClick={handleClearAll}>
|
|
{t('cancel') || 'Hammasini bekor qilish'}
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
{isLoadingProducts && itemsList.length === 0 ? (
|
|
<CircularProgress />
|
|
) : (
|
|
<FormControl fullWidth>
|
|
<Controller
|
|
name={`packetItemDtos.${index}.itemDtos`}
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Select
|
|
{...field}
|
|
multiple
|
|
MenuProps={{
|
|
PaperProps: {
|
|
style: { maxHeight: 280 },
|
|
ref: itemScrollRef,
|
|
onScroll: handleItemScroll,
|
|
},
|
|
disableAutoFocus: true,
|
|
autoFocus: false,
|
|
}}
|
|
renderValue={selected => (
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
{(selected as any[]).map(id => {
|
|
return (
|
|
<Chip
|
|
key={id}
|
|
label={
|
|
selectedProductNames[index]?.[id] ||
|
|
itemsList.find(p => p.id === id)?.name ||
|
|
itemsList.find(p => p.id === id)?.nameRu ||
|
|
id
|
|
}
|
|
onDelete={e => {
|
|
e.stopPropagation();
|
|
field.onChange(field.value.filter((x: any) => x !== id));
|
|
handleProductChange(index, { id }, false);
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</Box>
|
|
)}
|
|
>
|
|
{itemsList.map(product => (
|
|
<MenuItem
|
|
key={product.id}
|
|
value={product.id}
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
const checked = !field.value.includes(product.id);
|
|
let newValue;
|
|
if (checked) {
|
|
newValue = [...field.value, product.id];
|
|
} else {
|
|
newValue = field.value.filter((x: any) => x !== product.id);
|
|
}
|
|
field.onChange(newValue);
|
|
handleProductChange(index, product, checked);
|
|
}}
|
|
>
|
|
<Checkbox checked={field.value.includes(product.id)} />
|
|
<ListItemText primary={product.name || product.nameRu || product.id} />
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
)}
|
|
/>
|
|
</FormControl>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<StyledCreateBox
|
|
width={1}
|
|
mb={3}
|
|
sx={{
|
|
padding: '28px',
|
|
borderRadius: '16px',
|
|
backgroundColor: '#fff',
|
|
}}
|
|
>
|
|
<Box component='form' onSubmit={onSubmit}>
|
|
<Typography variant='h5' mb={3.5}>
|
|
{editMode ? t('update_box') : t('create_box')}
|
|
</Typography>
|
|
|
|
<Grid container columnSpacing={2.5} rowSpacing={3} mb={3.5}>
|
|
<Grid item xs={12}>
|
|
<Typography fontSize='18px' fontWeight={500} color='#5D5850' mb={2}>
|
|
{t('party_name')}
|
|
</Typography>
|
|
<FormControl fullWidth>
|
|
<InputLabel id='party-select-label'>{t('party_name')}</InputLabel>
|
|
<Controller
|
|
name='partyId'
|
|
control={control}
|
|
rules={{ required: requiredText }}
|
|
render={({ field }) => (
|
|
<Select
|
|
{...field}
|
|
labelId='party-select-label'
|
|
label={t('party_name')}
|
|
onChange={e => {
|
|
field.onChange(e);
|
|
handlePartyChange(e);
|
|
}}
|
|
value={field.value || ''}
|
|
disabled={isLoadingParties}
|
|
>
|
|
{isLoadingParties ? (
|
|
<MenuItem disabled>
|
|
<CircularProgress size={24} />
|
|
</MenuItem>
|
|
) : (
|
|
parties.map(party => (
|
|
<MenuItem key={party.id} value={party.id}>
|
|
{party.name}
|
|
</MenuItem>
|
|
))
|
|
)}
|
|
</Select>
|
|
)}
|
|
/>
|
|
{!!errors.partyId && <FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={12}>
|
|
<Stack
|
|
sx={{
|
|
borderRadius: '8px',
|
|
border: '2px solid #3489E4',
|
|
background: '#FFF',
|
|
padding: '24px',
|
|
gap: '12px',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<div>
|
|
{fields.map((field, index) => (
|
|
<PacketRow key={field.key} index={index} field={field} />
|
|
))}
|
|
<BaseButton variant='outlined' onClick={appendPacket} startIcon={<AddCircleRounded />} sx={{ mt: 2 }}>
|
|
{t('add_packet')}
|
|
</BaseButton>
|
|
</div>
|
|
</Stack>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<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')}
|
|
</BaseButton>
|
|
</Box>
|
|
</StyledCreateBox>
|
|
);
|
|
};
|
|
|
|
export default DashboardCreateRealBoxPage;
|