Merge branch 'dev' into 'main'

Dev

See merge request azizziy/cpost!29
This commit is contained in:
Azizbek Usmonov
2025-06-20 11:01:27 +05:00
3 changed files with 122 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
import { IBox, CreateBoxBodyType, UpdateBoxBodyType, IBoxDetail, BoxStatus } from '@/data/box/box.model'; import { BoxStatus, CreateBoxBodyType, IBox, IBoxDetail, UpdateBoxBodyType } from '@/data/box/box.model';
import { CommonResponseType, PageAble } from '@/helpers/types'; import { CommonResponseType, PageAble } from '@/helpers/types';
import { request } from '@/services/request'; import { request } from '@/services/request';
import axios from 'axios'; import axios from 'axios';
@@ -7,6 +7,7 @@ export const box_requests = {
async getAll(params?: { async getAll(params?: {
page?: number; page?: number;
sort?: string; sort?: string;
pageable?: string | number;
direction?: string; direction?: string;
cargoId?: string; cargoId?: string;
partyId?: string | number; partyId?: string | number;

View File

@@ -29,7 +29,7 @@ export const item_requests = {
async find(params: { itemId?: number | string }) { async find(params: { itemId?: number | string }) {
return request.get<CommonResponseType<Product[]>>('/items/find', { params }); return request.get<CommonResponseType<Product[]>>('/items/find', { params });
}, },
async list(params: { packetId?: number | string }) { async list(params: { packetId?: number | string; page?: number | string }) {
return request.get<CommonResponseType<{ data: Product[] }>>('/items/list', { params }); return request.get<CommonResponseType<{ data: Product[] }>>('/items/list', { params });
}, },
async delete(params: { itemId: number | string }) { async delete(params: { itemId: number | string }) {

View File

@@ -32,7 +32,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, useState } from 'react'; import { useEffect, useRef, 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)`
@@ -68,9 +68,35 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
const { push } = useMyNavigation(); const { push } = useMyNavigation();
const [partyId, setPartyId] = useState<number | string>(''); const [partyId, setPartyId] = useState<number | string>('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
console.log(initialValues);
// Packetlar uchun scroll state
const [packetsPage, setPacketsPage] = useState(1);
const [packetsList, setPacketsList] = useState<any[]>([]);
const [packetsHasMore, setPacketsHasMore] = useState(true);
const packetScrollRef = useRef<HTMLDivElement>(null);
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 { const {
register,
control, control,
handleSubmit, handleSubmit,
setValue, setValue,
@@ -122,7 +148,6 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
const requiredText = t('required'); const requiredText = t('required');
// Fetch parties data
const { data: parties = [], isLoading: isLoadingParties } = useQuery({ const { data: parties = [], isLoading: isLoadingParties } = useQuery({
queryKey: ['parties-list', 'COLLECTING'], queryKey: ['parties-list', 'COLLECTING'],
queryFn: () => party_requests.getAll({ status: 'COLLECTING' }), queryFn: () => party_requests.getAll({ status: 'COLLECTING' }),
@@ -133,18 +158,6 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
})), })),
}); });
// Fetch packets data
const { data: packets = [], isLoading: isLoadingPackets } = useQuery({
queryKey: ['packets-list', partyId, keyword],
queryFn: () =>
box_requests.getAll({
partyId: partyId,
cargoId: keyword,
}),
select: data => data?.data?.data?.data.filter((box: any) => box.status === 'READY_TO_INVOICE') ?? [],
enabled: !!partyId,
});
const onSubmit = handleSubmit(async values => { const onSubmit = handleSubmit(async values => {
try { try {
setLoading(true); setLoading(true);
@@ -184,6 +197,9 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
setValue('partyName', selectedParty.name); setValue('partyName', selectedParty.name);
setPartyId(selectedParty.id); setPartyId(selectedParty.id);
setKeyword(''); setKeyword('');
setPacketsPage(1);
setPacketsList([]);
setPacketsHasMore(true);
setValue('packetItemDtos', [{ packetId: 0, itemDtos: [] }]); setValue('packetItemDtos', [{ packetId: 0, itemDtos: [] }]);
} }
}; };
@@ -201,18 +217,55 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
remove(index); remove(index);
}; };
// Component for each packet row to isolate product selection
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`);
// Fetch products specific to this packet // Itemlar uchun scroll state
const { data: products, isLoading: isLoadingProducts } = useQuery({ const [itemsPage, setItemsPage] = useState(1);
queryKey: ['product-list', packetId], const [itemsList, setItemsList] = useState<any[]>([]);
queryFn: () => item_requests.list({ packetId }), const [itemsHasMore, setItemsHasMore] = useState(true);
select: data => data?.data?.data.data ?? [], const itemScrollRef = useRef<HTMLDivElement>(null);
const { isLoading: isLoadingProducts } = useQuery({
queryKey: ['product-list', packetId, itemsPage],
queryFn: () => item_requests.list({ packetId, page: itemsPage }),
enabled: !!packetId, enabled: !!packetId,
onSuccess: data => {
const newItems = data?.data?.data?.data || [];
setItemsList(prev => (itemsPage === 1 ? newItems : [...prev, ...newItems]));
// const totalPages = data?.data?.data || 0;
// setItemsHasMore(itemsPage < totalPages);
},
}); });
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);
}
};
useEffect(() => {
// Reset items list when packet changes
setItemsPage(1);
setItemsList([]);
setItemsHasMore(true);
}, [packetId]);
const selectedProducts = watch(`packetItemDtos.${index}.itemDtos`) || [];
const handleSelectAll = (checked: boolean) => {
if (checked) {
setValue(
`packetItemDtos.${index}.itemDtos`,
itemsList.map((p: any) => p.id)
);
} else {
setValue(`packetItemDtos.${index}.itemDtos`, []);
}
};
return ( return (
<Box className='item-row' mb={2} display={'flex'} flexDirection={'column'}> <Box className='item-row' mb={2} display={'flex'} flexDirection={'column'}>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
@@ -247,18 +300,30 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
}} }}
value={field.value || ''} value={field.value || ''}
disabled={isLoadingPackets} disabled={isLoadingPackets}
MenuProps={{
PaperProps: {
style: { maxHeight: 280 },
ref: packetScrollRef,
onScroll: handlePacketScroll,
},
}}
> >
{isLoadingPackets ? ( {isLoadingPackets && packetsList.length === 0 ? (
<MenuItem disabled> <MenuItem disabled>
<CircularProgress size={24} /> <CircularProgress size={24} />
</MenuItem> </MenuItem>
) : ( ) : (
packets.map(packet => ( packetsList.map(packet => (
<MenuItem key={packet.id} value={packet.id}> <MenuItem key={packet.id} value={packet.id}>
{packet.name || packet.name || `Box ${packet.id}`} {packet.name || `Box ${packet.id}`}
</MenuItem> </MenuItem>
)) ))
)} )}
{isLoadingPackets && packetsList.length > 0 && (
<MenuItem disabled>
<CircularProgress size={20} />
</MenuItem>
)}
</Select> </Select>
)} )}
/> />
@@ -286,30 +351,16 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
<Typography fontSize='18px' sx={{ textAlign: 'start' }} fontWeight={500} color='#5D5850' mb={2}> <Typography fontSize='18px' sx={{ textAlign: 'start' }} fontWeight={500} color='#5D5850' mb={2}>
{t('products')} {t('products')}
</Typography> </Typography>
{isLoadingProducts ? ( {isLoadingProducts && itemsList.length === 0 ? (
<CircularProgress /> <CircularProgress />
) : ( ) : (
<FormControl fullWidth> <FormControl fullWidth>
<Box display='flex' alignItems='center' mb={1}> <Box display='flex' alignItems='center' mb={1}>
<Controller
name={`packetItemDtos.${index}.itemDtos`}
control={control}
render={({ field: { onChange } }) => (
<Checkbox <Checkbox
checked={watch(`packetItemDtos.${index}.itemDtos`)?.length === products?.length} indeterminate={selectedProducts.length > 0 && selectedProducts.length < itemsList.length}
onChange={e => { checked={itemsList.length > 0 && selectedProducts.length === itemsList.length}
if (e.target.checked) { onChange={e => handleSelectAll(e.target.checked)}
setValue(`packetItemDtos.${index}.itemDtos`, products?.map(p => p.id) || []); disabled={itemsList.length === 0}
} else {
setValue(`packetItemDtos.${index}.itemDtos`, []);
}
}}
indeterminate={
watch(`packetItemDtos.${index}.itemDtos`)?.length > 0 &&
watch(`packetItemDtos.${index}.itemDtos`)?.length < products?.length!
}
/>
)}
/> />
<Typography variant='body2'>{t('select_all')}</Typography> <Typography variant='body2'>{t('select_all')}</Typography>
</Box> </Box>
@@ -319,31 +370,44 @@ const DashboardCreateRealBoxPage = ({ initialValues, partiesData }: Props) => {
render={({ field }) => ( render={({ field }) => (
<Select <Select
{...field} {...field}
labelId={`product-select-label-${index}`}
multiple multiple
value={field.value || []} MenuProps={{
onChange={e => field.onChange(e.target.value)} 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(value => ( {(selected as any[]).map(value => (
<Chip <Chip
key={value} key={value}
label={products?.find(p => p.id === value)?.name} label={itemsList.find(p => p.id === value)?.name}
onDelete={() => { onDelete={e => {
const newValue = (field.value || []).filter(id => id !== value); e.stopPropagation();
field.onChange(newValue); field.onChange(field.value.filter((id: any) => id !== value));
}} }}
/> />
))} ))}
</Box> </Box>
)} )}
> >
{products?.map(product => ( {itemsList.map(product => (
<MenuItem key={product.id} value={product.id}> <MenuItem key={product.id} value={product.id} onClick={e => e.stopPropagation()}>
<Checkbox checked={(field.value || []).includes(product.id)} /> <Checkbox checked={field.value.includes(product.id)} />
<ListItemText primary={product.name} /> <ListItemText primary={product.name} />
</MenuItem> </MenuItem>
))} ))}
{isLoadingProducts && itemsHasMore && (
<MenuItem disabled>
<Box display='flex' justifyContent='center' width='100%'>
<CircularProgress size={20} />
</Box>
</MenuItem>
)}
</Select> </Select>
)} )}
/> />