Files
cpost-front/src/routes/private/real-boxes-create/DashboardCreateRealBox.tsx
Samandar Turg'unboev 6b48f507a4 added page acceptance
2025-07-12 18:11:24 +05:00

593 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, useMemo, 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>(initialValues?.partyId || '');
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 {
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,
},
});
// 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(() => {
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);
}
if (initialValues.paketIds) {
initialValues.paketIds.forEach((paket: any) => {
fetchAllItemsForPacket(paket.id);
});
}
}
}, [initialValues, reset]);
// useFieldArray keyName="key" orqali unique key
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 appendPacket = () => {
append({ packetId: 0, itemDtos: [] });
setTimeout(() => {
document.activeElement instanceof HTMLElement && document.activeElement.blur();
}, 0);
};
const removePacket = (index: number) => {
remove(index);
};
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 PacketRow = ({ index, field }: { index: number; field: any }) => {
const packetId = watch(`packetItemDtos.${index}.packetId`);
const [keyword, setKeyword] = useState<string>('');
const [selectedProductNames, setSelectedProductNames] = useState<{ [productId: number]: string }>({});
const packetsList = keyword ? allPackets.filter(p => (p.name || '').toLowerCase().includes(keyword.toLowerCase())) : allPackets;
const itemsList = allItemsMap[packetId] || [];
const loadingItems = !allItemsMap[packetId] && !!packetId;
useEffect(() => {
if (packetId && !allItemsMap[packetId]) {
fetchAllItemsForPacket(packetId);
}
}, [packetId]);
useEffect(() => {
if (packetId && field?.itemDtos?.length && itemsList.length) {
const names: { [productId: number]: string } = {};
field.itemDtos.forEach((id: number) => {
const prod = itemsList.find(p => p.id === id);
names[id] = prod?.name || prod?.nameRu || String(id);
});
setSelectedProductNames(names);
}
}, [field?.itemDtos, itemsList, packetId]);
const handleProductChange = (product: any, checked: boolean) => {
setSelectedProductNames(prev => {
if (checked) {
return { ...prev, [product.id]: product.name || product.nameRu || String(product.id) };
} else {
const newNames = { ...prev };
delete newNames[product.id];
return newNames;
}
});
};
useEffect(() => {
const element = document.getElementById(`packet-select-${index}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [fields.length]);
const handleSelectAllProducts = async () => {
if (!packetId) return;
let allProducts = allItemsMap[packetId] || [];
if (!allProducts.length) {
allProducts = await fetchAllItemsForPacket(packetId);
}
if (allProducts.length > 0) {
setValue(
`packetItemDtos.${index}.itemDtos`,
allProducts.map((p: any) => p.id)
);
setSelectedProductNames(
allProducts.reduce(
(acc, p) => ({
...acc,
[p.id]: p.name || p.nameRu || String(p.id),
}),
{}
)
);
}
};
const handleClearAll = () => {
setValue(`packetItemDtos.${index}.itemDtos`, []);
setSelectedProductNames({});
};
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)} autoFocus={false} />
<div style={{ width: '100%' }}>
<FormControl fullWidth>
<InputLabel id={`packet-select-label-${index}`}>{t('packet')}</InputLabel>
<Controller
name={`packetItemDtos.${index}.packetId`}
control={control}
render={({ field: selectField }) => (
<Select
{...selectField}
autoFocus={false}
labelId={`packet-select-label-${index}`}
id={`packet-select-${index}`}
label={t('packet')}
MenuProps={selectMenuProps}
onChange={e => {
e.stopPropagation();
selectField.onChange(e);
}}
value={selectField.value || ''}
renderValue={selected => {
const selectedPacket = packetsList.find(p => p.id === selected);
return selectedPacket ? selectedPacket.name : t('loading');
}}
>
{isLoadingPackets ? (
<MenuItem disabled>
<CircularProgress size={24} />
</MenuItem>
) : packetsList.length === 0 ? (
<MenuItem disabled>{t('not_found') || 'Paketlar topilmadi'}</MenuItem>
) : (
packetsList.map(packet => (
<MenuItem key={packet.id} value={packet.id}>
{packet.name}
</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>
{loadingItems ? (
<CircularProgress />
) : (
<FormControl fullWidth>
<Controller
name={`packetItemDtos.${index}.itemDtos`}
control={control}
render={({ field }) => (
<Select
{...field}
multiple
MenuProps={selectMenuProps}
renderValue={selected => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(selected as any[]).map((id: number) => (
<Chip
key={id}
label={
selectedProductNames[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({ 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(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} sx={{ overflowAnchor: 'none' }}>
<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')}
MenuProps={selectMenuProps}
onChange={e => {
field.onChange(e);
handlePartyChange(e);
}}
value={field.value || initialValues?.partyId || ''} // Bu yerda partyId ishlatish kerak
disabled={isLoadingParties}
renderValue={selected => {
// Agar edit mode bo'lsa va initialValues mavjud bo'lsa
if (editMode && initialValues?.partyName && selected === initialValues?.partyId) {
return initialValues.partyName;
}
// Aks holda parties ro'yxatidan topish
const selectedParty = parties.find(p => p.id === selected);
return selectedParty ? selectedParty.name : '';
}}
>
{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_more')}
</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;