init
This commit is contained in:
752
src/routes/private/boxes-create/DashboardCreateBox.tsx
Normal file
752
src/routes/private/boxes-create/DashboardCreateBox.tsx
Normal file
@@ -0,0 +1,752 @@
|
||||
'use client';
|
||||
|
||||
import BaseButton from '@/components/ui-kit/BaseButton';
|
||||
import BaseInput from '@/components/ui-kit/BaseInput';
|
||||
import { party_requests } from '@/data/party/party.requests';
|
||||
import { pageLinks } from '@/helpers/constants';
|
||||
import { notifyError, notifyUnknownError } from '@/services/notification';
|
||||
import { Box, Divider, FormHelperText, Grid, Stack, Typography, styled } from '@mui/material';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocale } from 'use-intl';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import BaseIconButton from '@/components/ui-kit/BaseIconButton';
|
||||
import { AddCircleRounded, Close } from '@mui/icons-material';
|
||||
import { box_requests } from '@/data/box/box.requests';
|
||||
import useRequest from '@/hooks/useRequest';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import { BoxStatus, CreateBoxBodyType, UpdateBoxBodyType } from '@/data/box/box.model';
|
||||
import BaseReactSelect, { selectDefaultStyles } from '@/components/ui-kit/BaseReactSelect';
|
||||
import { customer_requests } from '@/data/customers/customer.requests';
|
||||
import { useAuthContext } from '@/context/auth-context';
|
||||
import { useMyNavigation } from '@/hooks/useMyNavigation';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import { Party } from '@/data/party/party.model';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import { item_requests } from '@/data/item/item.requests';
|
||||
import get from 'lodash.get';
|
||||
import Loader from '@/components/common/Loader';
|
||||
import { Customer } from '@/data/customers/customer.model';
|
||||
import { Passport } from '@/data/passport/passport.model';
|
||||
import { passport_requests } from '@/data/passport/passport.request';
|
||||
|
||||
const StyledCreateBox = styled(Box)`
|
||||
.item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.item-row-field {
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex: 1 1 1;
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
partiesData?: { value: number; label: string }[];
|
||||
initialValues?: {
|
||||
id: number;
|
||||
box_name: string;
|
||||
net_weight: number;
|
||||
box_weight: number;
|
||||
box_type: string;
|
||||
box_size: string;
|
||||
status: BoxStatus;
|
||||
packetId: string;
|
||||
|
||||
passportName: string;
|
||||
passportId: number;
|
||||
partyId: number;
|
||||
partyName: string;
|
||||
|
||||
clientId: number;
|
||||
client_id: string;
|
||||
clientName: string;
|
||||
|
||||
products_list: {
|
||||
id: number;
|
||||
price: number | string;
|
||||
|
||||
cargoId: string;
|
||||
trekId: string;
|
||||
name: string;
|
||||
nameRu: string;
|
||||
amount: number;
|
||||
weight: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
const DashboardCreateBoxPage = ({ initialValues, partiesData }: Props) => {
|
||||
const [cargoIdValue, setCargoIdValue] = useState<string>('');
|
||||
const { user, isAdmin: isAdminUser } = useAuthContext();
|
||||
const editMode = !!initialValues && !!initialValues.id;
|
||||
const isAdmin = isAdminUser && editMode;
|
||||
const t = useMyTranslation();
|
||||
const params = useSearchParams();
|
||||
const navigation = useMyNavigation();
|
||||
const helperRef = useRef<{
|
||||
finished: boolean;
|
||||
partyFinished: boolean;
|
||||
clientFinished: boolean;
|
||||
settedDefaultParty: Party | null;
|
||||
settedDefaultClient: Customer | null;
|
||||
// settedDefaultPartyValue: { value: number; label: string }[] | null;
|
||||
}>({
|
||||
settedDefaultParty: null,
|
||||
settedDefaultClient: null,
|
||||
partyFinished: false,
|
||||
clientFinished: false,
|
||||
finished: false,
|
||||
// settedDefaultPartyValue: partiesData?.[0] || null,
|
||||
});
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<any>({
|
||||
defaultValues: {
|
||||
partyId: params.get('party_id') ? +params.get('party_id')! : '',
|
||||
box_weight: 0.9,
|
||||
box_type: 'KG',
|
||||
box_size: '50x40x40',
|
||||
status: 'READY_TO_INVOICE',
|
||||
cargoId: initialValues?.client_id,
|
||||
// passportId: {value: initialValues?.passportId},
|
||||
...(editMode
|
||||
? {}
|
||||
: {
|
||||
products_list: [
|
||||
{
|
||||
id: '',
|
||||
cargoId: '',
|
||||
trekId: '',
|
||||
name: '',
|
||||
nameRu: '',
|
||||
amount: '',
|
||||
weight: '',
|
||||
price: '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...initialValues,
|
||||
},
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const products = useFieldArray({
|
||||
control,
|
||||
name: 'products_list',
|
||||
keyName: 'key',
|
||||
});
|
||||
const controlledProductFields = watch('products_list');
|
||||
const partyIdValue = watch('partyId');
|
||||
const clientIdValue = watch('client_id');
|
||||
const cargoId = watch('cargoId');
|
||||
const requiredText = t('required');
|
||||
const [selectedPassport, setSelectedPassport] = useState<Passport | null>(null); // Tanlangan passportni saqlash uchun state (tipi Passport yoki null)
|
||||
|
||||
const passportOptionsInitial = initialValues?.passportId &&
|
||||
initialValues?.passportName && [
|
||||
{
|
||||
value: initialValues?.passportId,
|
||||
label: initialValues?.passportName,
|
||||
},
|
||||
];
|
||||
const n = "123ds"
|
||||
n.toUpperCase()
|
||||
const { data: passportOptions } = useRequest(() => passport_requests.getAll({ cargoId: cargoId?.toUpperCase() }), {
|
||||
enabled: !!cargoId,
|
||||
selectData: data => {
|
||||
// Ma'lumotlarni BaseReactSelect uchun mos formatga o'tkazish
|
||||
const passportOptions = data.data.data.map((passport: Passport) => ({
|
||||
// data.data endi Passport[]
|
||||
value: passport.id, // passport id sini value sifatida
|
||||
label: passport.fullName, // fullName ni label sifatida
|
||||
}));
|
||||
|
||||
const passportId = watch('passportId');
|
||||
|
||||
if (!passportId && initialValues?.passportId && cargoId === initialValues?.client_id) {
|
||||
const currentOption = passportOptions?.find((item: { value: number }) => item.value === initialValues?.passportId);
|
||||
setValue('passportId', currentOption);
|
||||
}
|
||||
|
||||
return passportOptions;
|
||||
},
|
||||
dependencies: [cargoId],
|
||||
placeholderData: [], // Kerak emas, chunki server PageAble qaytarmayapti
|
||||
onSuccess(data) {
|
||||
// if (data?.data.data?.[0]?.id) {
|
||||
// setValue("passportId", initialValues?.passportId)
|
||||
// setValue('passport_id', data.data.data[0].id);
|
||||
// setSelectedPassport(data.data.data[0]); // Birinchi elementni tanlash
|
||||
// }
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setValue('passportId', '');
|
||||
}, [cargoId]);
|
||||
|
||||
const { data: defaultParties, status: defaultPartiesStatus } = useRequest(() => party_requests.getAll({ status: 'COLLECTING' }), {
|
||||
enabled: true,
|
||||
selectData(data) {
|
||||
return data.data.data.data.map(p => ({ value: p.id, label: p.name }));
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (!editMode && data?.data?.data?.data?.[0]) {
|
||||
helperRef.current.settedDefaultParty = data.data.data.data[0];
|
||||
setValue('partyId', data.data.data.data[0].id);
|
||||
}
|
||||
helperRef.current.partyFinished = true;
|
||||
if (helperRef.current.clientFinished) {
|
||||
helperRef.current.finished = true;
|
||||
}
|
||||
},
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const { data: defaultClients } = useRequest(
|
||||
() =>
|
||||
customer_requests.getAll({
|
||||
page: 1,
|
||||
}),
|
||||
{
|
||||
enabled: !!partyIdValue,
|
||||
selectData(data) {
|
||||
return data.data.data.data.map(p => ({ value: p.aviaCargoId, label: p.fullName }));
|
||||
// return data.data.data.map(p => ({ value: p.id, label: p.fullName }));
|
||||
},
|
||||
dependencies: [partyIdValue],
|
||||
onSuccess(data) {
|
||||
if (!editMode && !clientIdValue && data?.data?.data?.data?.[0]) {
|
||||
helperRef.current.settedDefaultClient = data.data.data?.data?.[0];
|
||||
setValue('client_id', data.data.data.data[0].aviaCargoId);
|
||||
}
|
||||
helperRef.current.clientFinished = true;
|
||||
if (helperRef.current.partyFinished) {
|
||||
helperRef.current.finished = true;
|
||||
}
|
||||
},
|
||||
placeholderData: [],
|
||||
}
|
||||
);
|
||||
|
||||
const onPassportChange = (newValue: Passport | null) => {
|
||||
// Tipi Passport | null
|
||||
setSelectedPassport(newValue);
|
||||
if (newValue) {
|
||||
setValue('passport_id', newValue.id || null);
|
||||
} else {
|
||||
setValue('passport_id', null);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async values => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (editMode) {
|
||||
const updateBody: UpdateBoxBodyType = {
|
||||
cargoId: values.client_id,
|
||||
passportId: values.passportId.value,
|
||||
status: values.status,
|
||||
packetId: initialValues?.packetId,
|
||||
|
||||
items: values.products_list.map((item: any) => {
|
||||
const _price = +item.price ? +item.price : 0;
|
||||
const _amount = +item.amount ? +item.amount : 0;
|
||||
const _total_price = _price ? _price * _amount : 0;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
// cargoId: item.cargoId,
|
||||
trekId: item.trekId,
|
||||
// name: item.name + (item.nameRu ? ` / ${item.nameRu}` : ''),
|
||||
name: item.name,
|
||||
nameRu: item?.nameRu,
|
||||
weight: +item.weight,
|
||||
amount: +item.amount,
|
||||
price: _price,
|
||||
totalPrice: _total_price,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const item_delete_promises = initialValues.products_list
|
||||
.filter(item => {
|
||||
if (!updateBody.items.find(i => String(i.id) === String(item.id))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map(item => {
|
||||
return item_requests.delete({ itemId: item.id });
|
||||
});
|
||||
|
||||
await box_requests.update(updateBody);
|
||||
await Promise.all(item_delete_promises);
|
||||
} else {
|
||||
const createBody: CreateBoxBodyType = {
|
||||
status: values.status,
|
||||
cargoId: values.cargoId,
|
||||
passportId: values.passportId.value,
|
||||
partyId: values.partyId,
|
||||
items: values.products_list.map((item: any) => {
|
||||
return {
|
||||
trekId: item.trekId,
|
||||
name: item.name,
|
||||
weight: +item.weight,
|
||||
amount: +item.amount,
|
||||
};
|
||||
}),
|
||||
};
|
||||
await box_requests.create(createBody);
|
||||
}
|
||||
|
||||
navigation.push(pageLinks.dashboard.boxes.index);
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
const partyOptions = (inputValue: string) => {
|
||||
return party_requests.getAll({ status: 'COLLECTING', partyName: inputValue }).then(res => {
|
||||
return res.data.data.data.map(p => ({ label: p.name, value: p.id }));
|
||||
});
|
||||
};
|
||||
|
||||
// const clientOptions = (inputValue: string) => {
|
||||
// return customer_requests.getAll({ clientName: inputValue, page: 1 }).then(res => {
|
||||
// return res.data.data.data.map(p => ({ label: p.fullName, value: p.id }));
|
||||
// });
|
||||
// };
|
||||
|
||||
const appendProduct = () => {
|
||||
products.append({
|
||||
id: '',
|
||||
cargoId: '',
|
||||
trekId: '',
|
||||
name: '',
|
||||
amount: '',
|
||||
weight: '',
|
||||
price: '',
|
||||
totalPrice: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeProduct = (index: number) => {
|
||||
products.remove(index);
|
||||
};
|
||||
|
||||
const translateAndUpdateRussianName = async (text: string, index: number) => {
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
// const responseText = await box_requests.translateWithGoogleApi({ text });
|
||||
const responseText = await box_requests.translateWithMemoryApi({ text });
|
||||
setValue(`products_list.${index}.nameRu`, responseText || '');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notifyError('Translation api error');
|
||||
}
|
||||
};
|
||||
|
||||
const boxTypes = [
|
||||
{
|
||||
label: 'KG',
|
||||
value: 'KG',
|
||||
},
|
||||
{
|
||||
label: 'GABARIT',
|
||||
value: 'GABARIT',
|
||||
},
|
||||
];
|
||||
|
||||
const boxStatuses = useMemo(() => {
|
||||
const p: {
|
||||
label: string;
|
||||
value: BoxStatus;
|
||||
}[] = [
|
||||
{
|
||||
label: t('READY_TO_INVOICE'),
|
||||
value: 'READY_TO_INVOICE',
|
||||
},
|
||||
];
|
||||
|
||||
if (isAdmin) {
|
||||
p.push({
|
||||
label: t('READY'),
|
||||
value: 'READY',
|
||||
});
|
||||
}
|
||||
|
||||
return p;
|
||||
}, [isAdmin]);
|
||||
|
||||
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_packet') : t('create_packet')}
|
||||
</Typography>
|
||||
|
||||
<Grid container columnSpacing={2.5} rowSpacing={3} mb={3.5}>
|
||||
<Grid item xs={5}>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('party_name')}
|
||||
</Typography>
|
||||
<Controller
|
||||
name='partyId'
|
||||
control={control}
|
||||
rules={{ required: requiredText }}
|
||||
render={({ field, fieldState, formState }) => {
|
||||
return (
|
||||
<AsyncSelect
|
||||
onChange={(newValue: any) => {
|
||||
field.onChange(newValue.value);
|
||||
}}
|
||||
defaultValue={
|
||||
editMode
|
||||
? {
|
||||
value: initialValues.partyId,
|
||||
label: initialValues.partyName,
|
||||
}
|
||||
: partiesData?.length
|
||||
? {
|
||||
value: partiesData[0].value,
|
||||
label: partiesData[0].label,
|
||||
}
|
||||
: null
|
||||
}
|
||||
styles={selectDefaultStyles}
|
||||
noOptionsMessage={() => t('not_found')}
|
||||
loadingMessage={() => t('loading')}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
defaultOptions={defaultParties!}
|
||||
loadOptions={partyOptions}
|
||||
placeholder={t('enter_party_name_to_find')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* @ts-expect-error */}
|
||||
{!!errors.partyId?.message && <FormHelperText sx={{ color: 'red' }}>{errors.partyId?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={5}>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('status')}
|
||||
</Typography>
|
||||
<Controller
|
||||
name='status'
|
||||
control={control}
|
||||
rules={{ required: requiredText }}
|
||||
render={({ field, fieldState, formState }) => {
|
||||
return (
|
||||
<BaseReactSelect
|
||||
value={boxStatuses?.find(p => p.value === field.value)}
|
||||
onChange={(newValue: any) => {
|
||||
field.onChange(newValue.value);
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
options={boxStatuses}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* @ts-expect-error */}
|
||||
{!!errors.box_type?.message && <FormHelperText sx={{ color: 'red' }}>{errors.box_type?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={5}>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('cargo_id')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
disabled={!!initialValues}
|
||||
type='text'
|
||||
fullWidth
|
||||
inputProps={{
|
||||
step: 0.1,
|
||||
}}
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('cargo_id')}
|
||||
{...register('cargoId')}
|
||||
/>
|
||||
{!!errors.net_weight?.message && (
|
||||
// @ts-expect-error
|
||||
<FormHelperText sx={{ color: 'red' }}>{errors.net_weight?.message}</FormHelperText>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('passport')}
|
||||
</Typography>
|
||||
|
||||
<Controller
|
||||
name='passportId'
|
||||
control={control}
|
||||
rules={{ required: requiredText }}
|
||||
render={({ field, fieldState, formState }) => {
|
||||
return (
|
||||
<BaseReactSelect
|
||||
// value={selectedPassport}
|
||||
// onChange={onPassportChange}
|
||||
|
||||
// value={field.value}
|
||||
{...field}
|
||||
onChange={(newValue: any) => {
|
||||
onPassportChange(newValue);
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
// options={passportOptions || passportOptionsInitial || []}
|
||||
options={passportOptions || passportOptionsInitial || []}
|
||||
// isLoading={passportLoading}
|
||||
placeholder={t('passport')}
|
||||
isDisabled={!clientIdValue || !!initialValues}
|
||||
noOptionsMessage={() => t('not_found')}
|
||||
loadingMessage={() => t('loading')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
border: '2px solid #3489E4',
|
||||
background: '#FFF',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
{controlledProductFields.map((product: any, index: number) => {
|
||||
//
|
||||
|
||||
//
|
||||
let totalPrice = 0;
|
||||
|
||||
try {
|
||||
const p = +product.price * +product.amount;
|
||||
if (!Number.isNaN(p)) {
|
||||
totalPrice = p;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return (
|
||||
<Box key={product.key} mb={1.5}>
|
||||
<Box className='item-row' mb={1.5}>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('track_id')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: '#EBEFF5',
|
||||
color: '#929191',
|
||||
alignSelf: 'stretch',
|
||||
height: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
pl: '10px',
|
||||
pr: '10px',
|
||||
borderRadius: '8px 0 0 8px',
|
||||
}}
|
||||
>
|
||||
<span>ID</span>
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
fullWidth
|
||||
placeholder={t('id')}
|
||||
sx={{
|
||||
'.MuiInputBase-root': {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
}}
|
||||
{...register(`products_list.${index}.trekId`, { required: requiredText })}
|
||||
/>
|
||||
{!!get(errors, `products_list.${index}.trekId`) && (
|
||||
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('name')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
fullWidth
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('name')}
|
||||
{...register(`products_list.${index}.name`, { required: requiredText })}
|
||||
onBlur={event => {
|
||||
translateAndUpdateRussianName(event.target.value, index);
|
||||
}}
|
||||
/>
|
||||
{!!get(errors, `products_list.${index}.name`) && (
|
||||
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{'NAME_RU'}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
// disabled
|
||||
fullWidth
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('name')}
|
||||
{...register(`products_list.${index}.nameRu`)}
|
||||
/>
|
||||
{!!get(errors, `products_list.${index}.name`) && (
|
||||
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('quantity')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
fullWidth
|
||||
type='number'
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('quantity')}
|
||||
{...register(`products_list.${index}.amount`, { required: requiredText })}
|
||||
/>
|
||||
{!!get(errors, `products_list.${index}.amount`) && (
|
||||
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('weight')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
fullWidth
|
||||
type='number'
|
||||
inputProps={{ step: 'any', min: 0, type: 'number' }}
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('weight')}
|
||||
{...register(`products_list.${index}.weight`, { required: requiredText })}
|
||||
/>
|
||||
{!!get(errors, `products_list.${index}.amount`) && (
|
||||
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
{isAdmin && (
|
||||
<React.Fragment>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('weight')}
|
||||
</Typography>
|
||||
<Stack direction={'row'} alignItems={'center'} spacing={2.5}>
|
||||
<BaseInput
|
||||
fullWidth
|
||||
type='text'
|
||||
inputProps={{
|
||||
step: 0.1,
|
||||
}}
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('weight')}
|
||||
{...register(`products_list.${index}.weight`)}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('price')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
fullWidth
|
||||
type='text'
|
||||
inputProps={{
|
||||
step: 0.1,
|
||||
}}
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('price')}
|
||||
{...register(`products_list.${index}.price`, { required: requiredText })}
|
||||
/>
|
||||
{!!get(errors, `products_list.${index}.price`) && (
|
||||
<FormHelperText sx={{ color: 'red' }}>{requiredText}</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
<Box className='item-row-field'>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850' mb={2}>
|
||||
{t('total_price')}
|
||||
</Typography>
|
||||
<BaseInput
|
||||
fullWidth
|
||||
type='number'
|
||||
inputProps={{
|
||||
step: 0.001,
|
||||
}}
|
||||
value={totalPrice}
|
||||
mainBorderColor='#D8D8D8'
|
||||
placeholder={t('total_price')}
|
||||
// {...register(`products_list.${index}.totalPrice`, { required: requiredText })}
|
||||
/>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<Box className='item-row-field'>
|
||||
<BaseIconButton
|
||||
size='small'
|
||||
colorVariant='icon-error'
|
||||
sx={{ flexShrink: 0, height: 'auto', marginTop: 4.5 }}
|
||||
onClick={() => removeProduct(index)}
|
||||
>
|
||||
<Close />
|
||||
</BaseIconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider color='#EBEFF6' />
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<Stack alignItems={'center'}>
|
||||
<BaseButton sx={{ backgroundColor: '#239D5F' }} startIcon={<AddCircleRounded />} onClick={appendProduct}>
|
||||
{t('add_more')}
|
||||
</BaseButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<BaseButton type='submit' colorVariant='blue' loading={loading}>
|
||||
{editMode ? t('update') : t('create')}
|
||||
</BaseButton>
|
||||
</Box>
|
||||
</StyledCreateBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardCreateBoxPage;
|
||||
86
src/routes/private/boxes-create/DashboardEditBox.tsx
Normal file
86
src/routes/private/boxes-create/DashboardEditBox.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import Loader from '@/components/common/Loader';
|
||||
import { box_requests } from '@/data/box/box.requests';
|
||||
import useRequest from '@/hooks/useRequest';
|
||||
import DashboardCreateBoxPage from '@/routes/private/boxes-create/DashboardCreateBox';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardEditBoxPage = (props: Props) => {
|
||||
const params = useParams();
|
||||
const box_id = params.box_id as string;
|
||||
|
||||
const getOneBox = useRequest(
|
||||
() => {
|
||||
return box_requests.find({ packetId: box_id });
|
||||
},
|
||||
{
|
||||
selectData(data) {
|
||||
const boxData = data.data.data;
|
||||
|
||||
return {
|
||||
id: +box_id,
|
||||
box_name: boxData.packet.name,
|
||||
net_weight: +boxData.packet.brutto,
|
||||
box_weight: +boxData.packet.boxWeight,
|
||||
box_type: boxData.packet.boxType,
|
||||
box_size: boxData.packet.volume,
|
||||
passportName: boxData.packet.passportName,
|
||||
status: boxData.packet.status,
|
||||
packetId: box_id,
|
||||
|
||||
partyId: +boxData.packet.partyId,
|
||||
partyName: boxData.packet.partyName,
|
||||
|
||||
// client_id: boxData.client?.passportId,
|
||||
passportId: boxData.client?.passportId,
|
||||
client_id: boxData.packet?.cargoId,
|
||||
clientId: boxData.client?.passportId,
|
||||
clientName: boxData.client?.passportName,
|
||||
|
||||
products_list: [
|
||||
...boxData.items.map(item => {
|
||||
let name = item.name;
|
||||
let nameRu = item.nameRu;
|
||||
|
||||
// try {
|
||||
// name = item.name.split(' / ')[0];
|
||||
// nameRu = item.name.split(' / ')[1];
|
||||
// } catch (error) {
|
||||
// console.error('prepare edit values error', error);
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
price: item.price,
|
||||
|
||||
cargoId: item.cargoId,
|
||||
trekId: item.trekId,
|
||||
name: name,
|
||||
nameRu: nameRu,
|
||||
amount: +item.amount,
|
||||
weight: +item.weight,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (getOneBox.loading || !getOneBox.data) {
|
||||
return <Loader p={8} size={96} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardCreateBoxPage initialValues={getOneBox.data} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardEditBoxPage;
|
||||
1
src/routes/private/boxes-create/index.ts
Normal file
1
src/routes/private/boxes-create/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardCreateBox';
|
||||
368
src/routes/private/boxes/DashboardBoxesPage.tsx
Normal file
368
src/routes/private/boxes/DashboardBoxesPage.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
'use client';
|
||||
|
||||
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 { useAuthContext } from '@/context/auth-context';
|
||||
import { BoxStatus, BoxStatusList, IBox } from '@/data/box/box.model';
|
||||
import { box_requests } from '@/data/box/box.requests';
|
||||
import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants';
|
||||
import useDebouncedInput from '@/hooks/useDebouncedInput';
|
||||
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 { getBoxStatusStyles, getStatusColor } from '@/theme/getStatusBoxStyles';
|
||||
import { Add, AddCircleOutline, Circle, Delete, Download, Edit, FilterList, FilterListOff, Search, PlusOne } from '@mui/icons-material';
|
||||
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardBoxesPage = (props: Props) => {
|
||||
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 [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 {
|
||||
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,
|
||||
},
|
||||
{
|
||||
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: <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,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction={'row'} mb={3} spacing={3}>
|
||||
<BaseButton colorVariant='blue' startIcon={<Add />} href={pageLinks.dashboard.boxes.create}>
|
||||
{t('create_packet')}
|
||||
</BaseButton>
|
||||
</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('packets')}
|
||||
</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;
|
||||
1
src/routes/private/boxes/index.ts
Normal file
1
src/routes/private/boxes/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardBoxesPage';
|
||||
80
src/routes/private/clients/ClientModal.tsx
Normal file
80
src/routes/private/clients/ClientModal.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import BaseModal from '@/components/ui-kit/BaseModal';
|
||||
import { Customer } from '@/data/customers/customer.model';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import { Grid, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
data: Customer;
|
||||
};
|
||||
|
||||
const ClientModal = ({ onClose, data }: Props) => {
|
||||
const t = useMyTranslation();
|
||||
|
||||
return (
|
||||
<BaseModal maxWidth='600px' onClose={onClose} open>
|
||||
<Typography variant='h5' mb={3}>
|
||||
{t('client')}
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('id')}
|
||||
</Typography>
|
||||
<Typography>{data.id}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('fullName')}
|
||||
</Typography>
|
||||
<Typography>{data.fullName}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('phone')}
|
||||
</Typography>
|
||||
<Typography>{data.phone}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('address')}
|
||||
</Typography>
|
||||
<Typography>{data.address}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('pinfl')}
|
||||
</Typography>
|
||||
<Typography>{data.pinfl}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('passportSeries')}
|
||||
</Typography>
|
||||
<Typography>{data.passportSeries}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('avia_cargo_id')}
|
||||
</Typography>
|
||||
<Typography>{data.aviaCargoId}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('auto_cargo_id')}
|
||||
</Typography>
|
||||
<Typography>{data.autoCargoId}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('date_of_birth')}
|
||||
</Typography>
|
||||
<Typography>{data.dateOfBirth}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientModal;
|
||||
106
src/routes/private/clients/CreateClientModal.tsx
Normal file
106
src/routes/private/clients/CreateClientModal.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import BaseButton from '@/components/ui-kit/BaseButton';
|
||||
import BaseInput from '@/components/ui-kit/BaseInput';
|
||||
import BaseModal from '@/components/ui-kit/BaseModal';
|
||||
import { Customer } from '@/data/customers/customer.model';
|
||||
import { customer_requests } from '@/data/customers/customer.requests';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import { notifyUnknownError } from '@/services/notification';
|
||||
import { Box, Grid, Stack, Typography } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
};
|
||||
|
||||
const CreateClientModal = ({ onClose, onSuccess }: Props) => {
|
||||
const t = useMyTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
fullName: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
pinfl: '',
|
||||
passport: '',
|
||||
dateOfBirth: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async values => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const body = {
|
||||
fullName: values.fullName,
|
||||
phone: values.phone,
|
||||
address: values.address,
|
||||
pinfl: values.pinfl,
|
||||
passport: values.passport,
|
||||
dateOfBirth: values.dateOfBirth,
|
||||
};
|
||||
|
||||
const response = await customer_requests.create(body);
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseModal maxWidth='600px' onClose={onClose} open>
|
||||
<Box component={'form'} onSubmit={onSubmit}>
|
||||
<Typography variant='h5' mb={3}>
|
||||
{t('create_client')}
|
||||
</Typography>
|
||||
<Grid container spacing={2} mb={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('fullName')}
|
||||
</Typography>
|
||||
<BaseInput {...register('fullName', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('phone')}
|
||||
</Typography>
|
||||
<BaseInput {...register('phone', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('pinfl')}
|
||||
</Typography>
|
||||
<BaseInput {...register('pinfl', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('passportSeries')}
|
||||
</Typography>
|
||||
<BaseInput {...register('passport', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('address')}
|
||||
</Typography>
|
||||
<BaseInput {...register('address', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('date_of_birth')}
|
||||
</Typography>
|
||||
<BaseInput {...register('dateOfBirth', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Stack direction={'row'} justifyContent={'flex-end'}>
|
||||
<BaseButton loading={loading} type='submit'>
|
||||
{t('create')}
|
||||
</BaseButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateClientModal;
|
||||
367
src/routes/private/clients/DashboardClientsPage.tsx
Normal file
367
src/routes/private/clients/DashboardClientsPage.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
'use client';
|
||||
|
||||
import ActionPopMenu from '@/components/common/ActionPopMenu';
|
||||
import { MyTable, ColumnData } from '@/components/common/MyTable';
|
||||
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 { useAuthContext } from '@/context/auth-context';
|
||||
import { Customer } from '@/data/customers/customer.model';
|
||||
import { customer_requests } from '@/data/customers/customer.requests';
|
||||
import { Party } 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 ClientModal from '@/routes/private/clients/ClientModal';
|
||||
import CreateClientModal from '@/routes/private/clients/CreateClientModal';
|
||||
import EditClientModal from '@/routes/private/clients/EditClientModal';
|
||||
import { file_service } from '@/services/file-service';
|
||||
import { notifyUnknownError } from '@/services/notification';
|
||||
import { Add, AddCircleOutline, Circle, Delete, Edit, FilterList, FilterListOff, Search } from '@mui/icons-material';
|
||||
import { Box, Stack, SvgIcon, Tooltip, Typography } from '@mui/material';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardClientsPage = (props: Props) => {
|
||||
const t = useMyTranslation();
|
||||
const { isAdmin } = useAuthContext();
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
|
||||
const { value: aviaCargoIdValue, onChange: handleAviaCargoIdValue, setValue: setAviaCargoIdValue } = useInput('');
|
||||
const { value: autoCargoIdValue, onChange: handleAutoCargoIdValue, setValue: setAutoCargoIdValue } = useInput('');
|
||||
|
||||
const [deleteIds, setDeleteIds] = useState<number[]>([]);
|
||||
const [downloadClientLoading, setDownloadClientsLoading] = useState(false);
|
||||
|
||||
const [modal, setModal] = useState<null | 'client' | 'edit_client' | 'create_client'>(null);
|
||||
const [modalData, setModalData] = useState<null | Customer>(null);
|
||||
|
||||
const getClientsQuery = useRequest(
|
||||
() =>
|
||||
customer_requests.getAll({
|
||||
page: page,
|
||||
cargoId: keyword,
|
||||
}),
|
||||
{
|
||||
selectData(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
dependencies: [page],
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: list,
|
||||
totalElements,
|
||||
totalPages,
|
||||
} = useMemo(() => {
|
||||
if (getClientsQuery.data?.data) {
|
||||
return {
|
||||
data: getClientsQuery.data.data,
|
||||
totalElements: getClientsQuery.data.totalElements,
|
||||
totalPages: getClientsQuery.data.totalPages,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
data: [],
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
}
|
||||
}, [getClientsQuery]);
|
||||
const loading = getClientsQuery.loading;
|
||||
|
||||
const handleChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const onDownloadClientsExcel = async () => {
|
||||
if (downloadClientLoading) return;
|
||||
|
||||
try {
|
||||
setDownloadClientsLoading(true);
|
||||
const response = await customer_requests.downloadClientsExcel();
|
||||
const file = new File([response.data], 'Clients.xlsx', { type: response.data.type });
|
||||
file_service.download(file);
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setDownloadClientsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (id: number) => {
|
||||
if (deleteIds.includes(id)) return;
|
||||
|
||||
if (window.confirm(t('are_you_sure_delete_client_id', { id: id }))) {
|
||||
try {
|
||||
setDeleteIds(p => [...p, id]);
|
||||
await customer_requests.delete({ clientId: id });
|
||||
getClientsQuery.refetch();
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setDeleteIds(prev => prev.filter(i => i !== id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetFilter = () => {
|
||||
setPage(1);
|
||||
setKeyword('');
|
||||
setAviaCargoIdValue('');
|
||||
setAutoCargoIdValue('');
|
||||
};
|
||||
|
||||
const openClientModal = useCallback((data: Customer) => {
|
||||
setModal('client');
|
||||
setModalData(data);
|
||||
}, []);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setModal(null);
|
||||
setModalData(null);
|
||||
}, []);
|
||||
|
||||
const onSuccessEdit = useCallback(() => {
|
||||
closeModal();
|
||||
getClientsQuery.refetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setPage(1);
|
||||
getClientsQuery.refetch();
|
||||
}, 350);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [keyword, aviaCargoIdValue, autoCargoIdValue]);
|
||||
|
||||
const columns: ColumnData<Customer>[] = [
|
||||
{
|
||||
label: t('No'),
|
||||
width: 100,
|
||||
renderCell(data, rowIndex) {
|
||||
return (page - 1) * pageSize + rowIndex + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataKey: 'id',
|
||||
label: t('id'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataKey: 'fullName',
|
||||
label: t('fullName'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'phone',
|
||||
label: t('phone'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'address',
|
||||
label: t('address'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'dateOfBirth',
|
||||
label: t('date_of_birth'),
|
||||
width: 300,
|
||||
},
|
||||
...(isAdmin
|
||||
? [
|
||||
{
|
||||
label: '',
|
||||
width: 100,
|
||||
numeric: true,
|
||||
renderCell(data: Customer, rowIndex: number) {
|
||||
return (
|
||||
<ActionPopMenu
|
||||
buttons={[
|
||||
{
|
||||
icon: <Edit sx={{ path: { color: '#3489E4' } }} />,
|
||||
label: t('edit'),
|
||||
onClick: () => {
|
||||
setModal('edit_client');
|
||||
setModalData(data);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <Delete sx={{ path: { color: '#3489E4' } }} />,
|
||||
label: t('delete'),
|
||||
onClick: () => {
|
||||
onDelete(data.id);
|
||||
},
|
||||
dontCloseOnClick: true,
|
||||
loading: deleteIds.includes(data.id),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// ...(isAdmin
|
||||
// ? [
|
||||
// {
|
||||
// label: '',
|
||||
// width: 100,
|
||||
// numeric: true,
|
||||
// renderCell(data: Customer, rowIndex: number) {
|
||||
// return (
|
||||
// <BaseButton
|
||||
// startIcon={<Edit color='success' />}
|
||||
// onClick={(event: any) => {
|
||||
// event.stopPropagation();
|
||||
// setModal('edit_client');
|
||||
// setModalData(data);
|
||||
// }}
|
||||
// sx={{
|
||||
// color: '#222',
|
||||
// fontWeight: 400,
|
||||
// fontSize: '14px',
|
||||
// whiteSpace: 'nowrap',
|
||||
// '&:hover': {
|
||||
// backgroundColor: '#fff',
|
||||
// },
|
||||
// }}
|
||||
// variant='text'
|
||||
// >
|
||||
// {t('edit')}
|
||||
// </BaseButton>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// label: '',
|
||||
// width: 100,
|
||||
// numeric: true,
|
||||
// renderCell(data: Customer, rowIndex: number) {
|
||||
// return (
|
||||
// <BaseButton
|
||||
// loading={deleteIds.includes(data.id)}
|
||||
// startIcon={<Delete color='error' />}
|
||||
// onClick={(event: any) => {
|
||||
// event.stopPropagation();
|
||||
// onDelete(data.id);
|
||||
// }}
|
||||
// sx={{
|
||||
// color: '#222',
|
||||
// fontWeight: 400,
|
||||
// fontSize: '14px',
|
||||
// whiteSpace: 'nowrap',
|
||||
// '&:hover': {
|
||||
// backgroundColor: '#fff',
|
||||
// },
|
||||
// }}
|
||||
// variant='text'
|
||||
// >
|
||||
// {t('delete')}
|
||||
// </BaseButton>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{isAdmin && (
|
||||
<Stack direction={'row'} mb={3} spacing={3}>
|
||||
<BaseButton
|
||||
colorVariant='blue'
|
||||
startIcon={<Add />}
|
||||
onClick={() => {
|
||||
setModal('create_client');
|
||||
}}
|
||||
>
|
||||
{t('create_client')}
|
||||
</BaseButton>
|
||||
{isAdmin && (
|
||||
<BaseButton
|
||||
colorVariant='blue'
|
||||
startIcon={<Add />}
|
||||
loading={downloadClientLoading}
|
||||
onClick={onDownloadClientsExcel}
|
||||
>
|
||||
{t('download_all_clients')}
|
||||
</BaseButton>
|
||||
)}
|
||||
</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('clients')}
|
||||
</Typography>
|
||||
<Stack direction={'row'} alignItems={'center'} spacing={2}>
|
||||
{/*<BaseInput*/}
|
||||
{/* InputProps={{*/}
|
||||
{/* startAdornment: <Search color='primary' />,*/}
|
||||
{/* }}*/}
|
||||
{/* value={aviaCargoIdValue}*/}
|
||||
{/* onChange={handleAviaCargoIdValue}*/}
|
||||
{/* placeholder={t('avia_cargo_id')}*/}
|
||||
{/*/>*/}
|
||||
{/*<BaseInput*/}
|
||||
{/* InputProps={{*/}
|
||||
{/* startAdornment: <Search color='primary' />,*/}
|
||||
{/* }}*/}
|
||||
{/* value={autoCargoIdValue}*/}
|
||||
{/* onChange={handleAutoCargoIdValue}*/}
|
||||
{/* placeholder={t('auto_cargo_id')}*/}
|
||||
{/*/>*/}
|
||||
<BaseInput
|
||||
InputProps={{
|
||||
startAdornment: <Search color='primary' />,
|
||||
}}
|
||||
value={keyword}
|
||||
onChange={handleKeyword}
|
||||
placeholder={"Kargo 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} onClickRow={openClientModal} />
|
||||
</Box>
|
||||
<Stack direction={'row'} justifyContent={'center'}>
|
||||
<BasePagination page={page} pageSize={pageSize} totalCount={totalElements} onChange={handleChange} />
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{modal === 'client' && modalData && <ClientModal data={modalData} onClose={closeModal} />}
|
||||
{modal === 'edit_client' && modalData && <EditClientModal data={modalData} onClose={closeModal} onSuccess={onSuccessEdit} />}
|
||||
{modal === 'create_client' && <CreateClientModal onClose={closeModal} onSuccess={onSuccessEdit} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardClientsPage;
|
||||
106
src/routes/private/clients/EditClientModal.tsx
Normal file
106
src/routes/private/clients/EditClientModal.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import BaseButton from '@/components/ui-kit/BaseButton';
|
||||
import BaseInput from '@/components/ui-kit/BaseInput';
|
||||
import BaseModal from '@/components/ui-kit/BaseModal';
|
||||
import { Customer } from '@/data/customers/customer.model';
|
||||
import { customer_requests } from '@/data/customers/customer.requests';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import { notifyUnknownError } from '@/services/notification';
|
||||
import { Box, Grid, Stack, Typography } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
data: Customer;
|
||||
};
|
||||
|
||||
const EditClientModal = ({ onClose, data, onSuccess }: Props) => {
|
||||
const t = useMyTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
fullName: data.fullName,
|
||||
phone: data.phone,
|
||||
address: data.address,
|
||||
pinfl: data.pinfl,
|
||||
passportSeries: data.passportSeries,
|
||||
dateOfBirth: data.dateOfBirth,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async values => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await customer_requests.edit({
|
||||
clientId: data.id,
|
||||
fullName: values.fullName,
|
||||
phone: values.phone,
|
||||
address: values.address,
|
||||
pinfl: values.pinfl,
|
||||
passportSeries: values.passportSeries,
|
||||
dateOfBirth: values.dateOfBirth,
|
||||
});
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseModal maxWidth='600px' onClose={onClose} open>
|
||||
<Box component={'form'} onSubmit={onSubmit}>
|
||||
<Typography variant='h5' mb={3}>
|
||||
{t('update_client')}
|
||||
</Typography>
|
||||
<Grid container spacing={2} mb={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('fullName')}
|
||||
</Typography>
|
||||
<BaseInput {...register('fullName', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('phone')}
|
||||
</Typography>
|
||||
<BaseInput {...register('phone', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('pinfl')}
|
||||
</Typography>
|
||||
<BaseInput {...register('pinfl', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('passportSeries')}
|
||||
</Typography>
|
||||
<BaseInput {...register('passportSeries', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('address')}
|
||||
</Typography>
|
||||
<BaseInput {...register('address', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography fontWeight={700} mb={1}>
|
||||
{t('date_of_birth')}
|
||||
</Typography>
|
||||
<BaseInput {...register('dateOfBirth', { required: true })} fullWidth />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Stack direction={'row'} justifyContent={'flex-end'}>
|
||||
<BaseButton loading={loading} type='submit'>
|
||||
{t('update')}
|
||||
</BaseButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditClientModal;
|
||||
1
src/routes/private/clients/index.ts
Normal file
1
src/routes/private/clients/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardClientsPage';
|
||||
27
src/routes/private/dashboard-home/DashboardHome.tsx
Normal file
27
src/routes/private/dashboard-home/DashboardHome.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import CheckOrderDashboard from '@/routes/private/dashboard-home/components/CheckOrderDashboard';
|
||||
import LastPartiesStats from '@/routes/private/dashboard-home/components/LastPartiesStats';
|
||||
import Statistics from '@/routes/private/dashboard-home/components/Statistics';
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardHome = (props: Props) => {
|
||||
return (
|
||||
<Box>
|
||||
<Box mb={3}>
|
||||
<CheckOrderDashboard />
|
||||
</Box>
|
||||
<Box mb={3}>
|
||||
<Statistics />
|
||||
</Box>
|
||||
<Box>
|
||||
<LastPartiesStats />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardHome;
|
||||
@@ -0,0 +1,168 @@
|
||||
import { CheckOrderResultModal } from '@/components/common/CheckOrderResultModal';
|
||||
import Loader from '@/components/common/Loader';
|
||||
import { useAuthContext } from '@/context/auth-context';
|
||||
import { Order } from '@/data/order/order.model';
|
||||
import { order_requests } from '@/data/order/order.requests';
|
||||
import useInput from '@/hooks/useInput';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import { notifyUnknownError } from '@/services/notification';
|
||||
import { Box, CircularProgress, Grid, IconButton, Paper, Stack, SvgIcon, Typography, styled } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
|
||||
.title {
|
||||
color: #000;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0.96px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.right-block {
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.right-label {
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.72px;
|
||||
}
|
||||
|
||||
.right-description {
|
||||
color: #5d5c5c;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.72px;
|
||||
}
|
||||
|
||||
.check-form {
|
||||
border-radius: 12px;
|
||||
border: 2px solid #edf1f7;
|
||||
background: #fff;
|
||||
max-width: 600px;
|
||||
height: 56px;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
border-radius: 12px;
|
||||
border: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
&:focus {
|
||||
outline-offset: 1px;
|
||||
outline: 2px dashed #7fa6e7;
|
||||
}
|
||||
}
|
||||
button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
|
||||
border-radius: 12px;
|
||||
background: #7fa6e7;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
|
||||
&:focus {
|
||||
outline-offset: 1px;
|
||||
outline: 2px dashed #7fa6e7;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CheckOrderDashboard = (props: Props) => {
|
||||
const { user } = useAuthContext();
|
||||
const t = useMyTranslation();
|
||||
const orderNumInput = useInput('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [order, setOrder] = useState<Order | null>(null);
|
||||
|
||||
const closeModal = () => setOpen(false);
|
||||
|
||||
const onCheckSubmit = async (event: any) => {
|
||||
event?.preventDefault?.();
|
||||
|
||||
if (loading || !orderNumInput.value.trim()) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await order_requests.check({ trekId: orderNumInput.value });
|
||||
|
||||
if (response.data.data[0]) {
|
||||
setOpen(true);
|
||||
setOrder(response.data.data[0]);
|
||||
orderNumInput.setValue('');
|
||||
} else {
|
||||
notifyUnknownError(t('search_available_in_trekid'));
|
||||
}
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
<Grid container spacing={'55px'}>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='title' mb={1}>
|
||||
{t('check_order')}
|
||||
</Typography>
|
||||
<Box component={'form'} className='check-form' onSubmit={onCheckSubmit}>
|
||||
<input type='text' value={orderNumInput.value} onChange={orderNumInput.onChange} />
|
||||
<IconButton disabled={loading} onClick={onCheckSubmit}>
|
||||
<SvgIcon>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'>
|
||||
<path
|
||||
d='M24.0413 22.1557L29.7516 27.866L27.866 29.7516L22.1557 24.0413C20.1025 25.684 17.4987 26.6666 14.6667 26.6666C8.04266 26.6666 2.66666 21.2906 2.66666 14.6666C2.66666 8.04263 8.04266 2.66663 14.6667 2.66663C21.2907 2.66663 26.6667 8.04263 26.6667 14.6666C26.6667 17.4986 25.684 20.1025 24.0413 22.1557ZM21.3663 21.1664C22.9967 19.4861 24 17.1941 24 14.6666C24 9.50996 19.8233 5.33329 14.6667 5.33329C9.50999 5.33329 5.33332 9.50996 5.33332 14.6666C5.33332 19.8233 9.50999 24 14.6667 24C17.1941 24 19.4861 22.9966 21.1664 21.3662L21.3663 21.1664Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Stack spacing={1} className='right-block'>
|
||||
<Stack direction={'row'} spacing={1}>
|
||||
<Typography className='right-label'>Manzil:</Typography>
|
||||
<Typography className='right-description'>{user?.address}</Typography>
|
||||
</Stack>
|
||||
<Stack direction={'row'} spacing={1}>
|
||||
<Typography className='right-label'>Shaxs:</Typography>
|
||||
<Typography className='right-description'>{user?.fullName}</Typography>
|
||||
</Stack>
|
||||
<Stack direction={'row'} spacing={1}>
|
||||
<Typography className='right-label'>Telefon raqam:</Typography>
|
||||
<Typography className='right-description'>{user?.phone}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{open && order && <CheckOrderResultModal onClose={closeModal} open result={order} />}
|
||||
</StyledBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckOrderDashboard;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const LastPartiesStats = (props: Props) => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant='h5'>Oxirgi reyslar</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LastPartiesStats;
|
||||
74
src/routes/private/dashboard-home/components/Statistics.tsx
Normal file
74
src/routes/private/dashboard-home/components/Statistics.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Scrollbar } from '@/components/common/Scrollbar';
|
||||
import { Box, Stack, Typography, styled } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
.stats-box {
|
||||
padding: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
color: #5d5850;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.8px;
|
||||
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 48px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 56px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {};
|
||||
|
||||
const Statistics = (props: Props) => {
|
||||
const [stats] = useState([
|
||||
{
|
||||
title: 'Xitoy omboridagi tovarlar',
|
||||
value: '53 542',
|
||||
color: '#3489E4',
|
||||
},
|
||||
{
|
||||
title: 'Toshkent omboridagi tovarlar',
|
||||
value: '47 865',
|
||||
color: '#FD9C2B',
|
||||
},
|
||||
{
|
||||
title: 'Mijozlar qabul qilgan tovarlar',
|
||||
value: '35 643',
|
||||
color: '#17D792',
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
<Scrollbar>
|
||||
<Stack direction={'row'} spacing={3}>
|
||||
{stats.map((stat, index) => {
|
||||
return (
|
||||
<Box className='stats-box' key={index}>
|
||||
<Typography className='title'>{stat.title}</Typography>
|
||||
<Typography className='value' sx={{ color: stat.color }}>
|
||||
{stat.value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Scrollbar>
|
||||
</StyledBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default Statistics;
|
||||
1
src/routes/private/dashboard-home/index.ts
Normal file
1
src/routes/private/dashboard-home/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardHome';
|
||||
418
src/routes/private/items/DashboardItemsPage.tsx
Normal file
418
src/routes/private/items/DashboardItemsPage.tsx
Normal 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;
|
||||
174
src/routes/private/items/components/AddPhotosModal.tsx
Normal file
174
src/routes/private/items/components/AddPhotosModal.tsx
Normal 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;
|
||||
142
src/routes/private/items/components/EditItemModal.tsx
Normal file
142
src/routes/private/items/components/EditItemModal.tsx
Normal 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;
|
||||
1
src/routes/private/items/index.ts
Normal file
1
src/routes/private/items/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardItemsPage';
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import BaseButton from '@/components/ui-kit/BaseButton';
|
||||
import BaseInput from '@/components/ui-kit/BaseInput';
|
||||
import { party_requests } from '@/data/party/party.requests';
|
||||
import { pageLinks } from '@/helpers/constants';
|
||||
import useInput from '@/hooks/useInput';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import { notifyUnknownError } from '@/services/notification';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import { useLocale } from 'use-intl';
|
||||
|
||||
type Props = {
|
||||
initialValues?: {
|
||||
id: number;
|
||||
party_name: string;
|
||||
};
|
||||
};
|
||||
|
||||
const DashboardCreatePartyPage = ({ initialValues }: Props) => {
|
||||
const editMode = !!initialValues && !!initialValues.id;
|
||||
const t = useMyTranslation();
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const partyNameInput = useInput(editMode ? initialValues.party_name : '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit: React.FormEventHandler<HTMLFormElement> = async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
setLoading(true);
|
||||
if (editMode) {
|
||||
await party_requests.update({ name: partyNameInput.value, id: initialValues.id });
|
||||
} else {
|
||||
await party_requests.create({ name: partyNameInput.value });
|
||||
}
|
||||
router.push(`/${locale}` + pageLinks.dashboard.parties.index);
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
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_party') : t('create_party')}
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2} maxWidth={340} mb={3}>
|
||||
<Typography fontSize={'18px'} fontWeight={500} color='#5D5850'>
|
||||
{t('party_name')}
|
||||
</Typography>
|
||||
<BaseInput placeholder={t('party_name')} value={partyNameInput.value} onChange={partyNameInput.onChange} />
|
||||
</Stack>
|
||||
|
||||
<BaseButton type='submit' colorVariant='blue' loading={loading}>
|
||||
{editMode ? t('update') : t('create')}
|
||||
</BaseButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardCreatePartyPage;
|
||||
41
src/routes/private/parties-create/DashboardEditPartyPage.tsx
Normal file
41
src/routes/private/parties-create/DashboardEditPartyPage.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import Loader from '@/components/common/Loader';
|
||||
import { party_requests } from '@/data/party/party.requests';
|
||||
import useRequest from '@/hooks/useRequest';
|
||||
import DashboardCreatePartyPage from '@/routes/private/parties-create/DashboardCreatePartyPage';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardEditPartyPage = (props: Props) => {
|
||||
const params = useParams();
|
||||
const party_id = params.party_id as string;
|
||||
|
||||
const getOneParty = useRequest(
|
||||
() => {
|
||||
return party_requests.find({ partyId: party_id });
|
||||
},
|
||||
{
|
||||
selectData(data) {
|
||||
return {
|
||||
id: +party_id,
|
||||
party_name: data.data.data.name,
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (getOneParty.loading) {
|
||||
return <Loader p={8} size={96} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardCreatePartyPage initialValues={getOneParty.data!} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardEditPartyPage;
|
||||
1
src/routes/private/parties-create/index.ts
Normal file
1
src/routes/private/parties-create/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardCreatePartyPage';
|
||||
378
src/routes/private/parties/DashboardPartiesPage.tsx
Normal file
378
src/routes/private/parties/DashboardPartiesPage.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
'use client';
|
||||
|
||||
import ActionPopMenu from '@/components/common/ActionPopMenu';
|
||||
import Loader from '@/components/common/Loader';
|
||||
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 BaseReactSelect from '@/components/ui-kit/BaseReactSelect';
|
||||
import { useAuthContext } from '@/context/auth-context';
|
||||
import { Party, PartyStatus, PartyStatusList, PartyStatusOptions } 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 { 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, AddCircleOutline, Circle, Delete, Download, Edit, FilterList, FilterListOff, Search } from '@mui/icons-material';
|
||||
import { Box, CircularProgress, Stack, SvgIcon, Tooltip, Typography } from '@mui/material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { getBoxStatusStyles } from '@/theme/getStatusBoxStyles';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardPartiesPage = (props: Props) => {
|
||||
const t = useMyTranslation();
|
||||
const navigation = useMyNavigation();
|
||||
const { isAdmin, isAdminOrUzbek } = useAuthContext();
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const { value: keyword, onChange: handleKeyword, setValue: setKeyword } = useInput('');
|
||||
const [partyStatusFilter, setPartyStatusFilter] = useState<PartyStatus | undefined>(undefined);
|
||||
|
||||
const [deleteIds, setDeleteIds] = useState<number[]>([]);
|
||||
const [downloadIds, setDownloadIds] = useState<number[]>([]);
|
||||
const [downloadItemIds, setDownloadItemIds] = useState<number[]>([]);
|
||||
const [changeStatusIds, setChangeStatusIds] = useState<number[]>([]);
|
||||
|
||||
const partyStatusOptions = useMemo(() => {
|
||||
const p = ['COLLECTING', 'ON_THE_WAY', 'IN_CUSTOMS', 'IN_WAREHOUSE'] as PartyStatus[];
|
||||
if (isAdmin) {
|
||||
// p.push('ARRIVED');
|
||||
p.push('DELIVERED');
|
||||
}
|
||||
return p;
|
||||
}, [isAdmin]);
|
||||
|
||||
const getPartiesQuery = useRequest(
|
||||
() =>
|
||||
party_requests.getAll({
|
||||
page: page,
|
||||
partyName: keyword,
|
||||
status: partyStatusFilter,
|
||||
direction: 'desc',
|
||||
sort: 'id',
|
||||
}),
|
||||
{
|
||||
dependencies: [page, partyStatusFilter],
|
||||
selectData(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: list,
|
||||
totalElements,
|
||||
totalPages,
|
||||
} = useMemo(() => {
|
||||
if (getPartiesQuery.data?.data) {
|
||||
return {
|
||||
data: getPartiesQuery.data.data,
|
||||
totalElements: getPartiesQuery.data.totalElements,
|
||||
totalPages: getPartiesQuery.data.totalPages,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
data: [],
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
}
|
||||
}, [getPartiesQuery]);
|
||||
const loading = getPartiesQuery.loading;
|
||||
|
||||
const handleChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const resetFilter = () => {
|
||||
setPage(1);
|
||||
setKeyword('');
|
||||
setPartyStatusFilter(undefined);
|
||||
};
|
||||
|
||||
const onDelete = async (id: number, name: string) => {
|
||||
if (!window.confirm(t('are_you_sure_delete_party', { name }))) return;
|
||||
|
||||
if (deleteIds.includes(id)) return;
|
||||
|
||||
try {
|
||||
setDeleteIds(p => [...p, id]);
|
||||
await party_requests.delete({ partyId: id });
|
||||
getPartiesQuery.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 party_requests.downloadExcel({ partyId: id });
|
||||
const file = new File([response.data], 'Party-excel.xlsx', { type: response.data.type });
|
||||
file_service.download(file);
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setDownloadIds(prev => prev.filter(i => i !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadPartyItems = async (id: number) => {
|
||||
if (downloadItemIds.includes(id)) return;
|
||||
|
||||
try {
|
||||
setDownloadItemIds(p => [...p, id]);
|
||||
const response = await party_requests.downloadPartyItemsExcel({ partyId: id });
|
||||
const file = new File([response.data], 'Party-items-excel.xlsx', { type: response.data.type });
|
||||
file_service.download(file);
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setDownloadItemIds(prev => prev.filter(i => i !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeStatus = async (id: number, newStatus: PartyStatus) => {
|
||||
if (changeStatusIds.includes(id)) return;
|
||||
|
||||
try {
|
||||
setChangeStatusIds(p => [...p, id]);
|
||||
await party_requests.changeStatus({ partyId: id, status: newStatus });
|
||||
getPartiesQuery.refetch();
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setChangeStatusIds(prev => prev.filter(i => i !== id));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setPage(1);
|
||||
getPartiesQuery.refetch();
|
||||
}, 350);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [keyword]);
|
||||
|
||||
const columns: ColumnData<Party>[] = [
|
||||
{
|
||||
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('name'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'totalBoxes',
|
||||
label: t('count_of_boxes'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'partyStatus',
|
||||
label: t('party_status'),
|
||||
width: 300,
|
||||
renderHeaderCell(rowIndex) {
|
||||
return (
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<span>{t('party_status')}</span>
|
||||
<ActionPopMenu
|
||||
buttons={PartyStatusList.map(stat => {
|
||||
return {
|
||||
icon: <Circle sx={{ path: { color: getStatusColor(stat) } }} />,
|
||||
label: t(stat),
|
||||
onClick() {
|
||||
setPartyStatusFilter(stat);
|
||||
setPage(1);
|
||||
},
|
||||
};
|
||||
})}
|
||||
mainIcon={<FilterList />}
|
||||
placement={{
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
},
|
||||
transformOrigin: {
|
||||
horizontal: 'center',
|
||||
vertical: 'top',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
renderCell(data) {
|
||||
return isAdminOrUzbek ? (
|
||||
<StatusChangePopup
|
||||
anchor={{
|
||||
status: data.partyStatus,
|
||||
text: t(data.partyStatus),
|
||||
}}
|
||||
loading={changeStatusIds.includes(data.id)}
|
||||
buttons={partyStatusOptions.map(stat => {
|
||||
return {
|
||||
icon: (
|
||||
<Circle
|
||||
sx={{
|
||||
path: {
|
||||
color: getStatusColor(stat),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
label: t(stat),
|
||||
onClick: () => {
|
||||
onChangeStatus(data.id, stat);
|
||||
},
|
||||
};
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<BaseButton sx={{ ...getBoxStatusStyles(data.partyStatus), alignItems: 'center', whiteSpace: 'nowrap' }} fullWidth>
|
||||
{t(data.partyStatus)}
|
||||
</BaseButton>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
width: 100,
|
||||
numeric: true,
|
||||
renderCell(data, rowIndex) {
|
||||
return (
|
||||
<ActionPopMenu
|
||||
buttons={[
|
||||
// {
|
||||
// icon: <AddCircleOutline />,
|
||||
// label: t('add_box'),
|
||||
// onClick: () => {
|
||||
// navigation.push(pageLinks.dashboard.boxes.create + `?party_id=${data.id}`);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
icon: <Edit sx={{ path: { color: '#3489E4' } }} />,
|
||||
label: t('edit'),
|
||||
onClick: () => {
|
||||
navigation.push(pageLinks.dashboard.parties.edit(data.id));
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <Delete sx={{ path: { color: '#3489E4' } }} />,
|
||||
label: t('delete'),
|
||||
onClick: () => {
|
||||
onDelete(data.id, data.name);
|
||||
},
|
||||
dontCloseOnClick: true,
|
||||
loading: deleteIds.includes(data.id),
|
||||
},
|
||||
...(isAdmin
|
||||
? [
|
||||
{
|
||||
icon: <Download sx={{ path: { color: '#3489E4' } }} />,
|
||||
label: t('download_all_items'),
|
||||
onClick: () => {
|
||||
onDownloadPartyItems(data.id);
|
||||
},
|
||||
dontCloseOnClick: true,
|
||||
loading: downloadItemIds.includes(data.id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Boolean(data.partyStatus === 'ON_THE_WAY' || data.partyStatus === 'ARRIVED')
|
||||
? [
|
||||
{
|
||||
icon: <Download sx={{ path: { color: '#3489E4' } }} />,
|
||||
label: t('download_excel'),
|
||||
onClick: () => {
|
||||
onDownloadExcel(data.id);
|
||||
},
|
||||
loading: downloadIds.includes(data.id),
|
||||
dontCloseOnClick: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction={'row'} mb={3}>
|
||||
<BaseButton colorVariant='blue' startIcon={<Add />} href={pageLinks.dashboard.parties.create}>
|
||||
{t('create_party')}
|
||||
</BaseButton>
|
||||
</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('parties')}
|
||||
</Typography>
|
||||
<Stack direction={'row'} alignItems={'center'} spacing={2}>
|
||||
<BaseInput
|
||||
InputProps={{
|
||||
startAdornment: <Search color='primary' />,
|
||||
}}
|
||||
value={keyword}
|
||||
onChange={handleKeyword}
|
||||
placeholder={t('filter_party_name')}
|
||||
/>
|
||||
|
||||
<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 DashboardPartiesPage;
|
||||
1
src/routes/private/parties/index.ts
Normal file
1
src/routes/private/parties/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardPartiesPage';
|
||||
199
src/routes/private/staffs/DashboardStaffsPage.tsx
Normal file
199
src/routes/private/staffs/DashboardStaffsPage.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import { MyTable, ColumnData } from '@/components/common/MyTable';
|
||||
import BaseButton from '@/components/ui-kit/BaseButton';
|
||||
import BasePagination from '@/components/ui-kit/BasePagination';
|
||||
import { staff_requests } from '@/data/staff/staff.requests';
|
||||
import { User } from '@/data/user/user.model';
|
||||
import { DEFAULT_PAGE_SIZE, pageLinks } from '@/helpers/constants';
|
||||
import { useMyTranslation } from '@/hooks/useMyTranslation';
|
||||
import useRequest from '@/hooks/useRequest';
|
||||
import CreateStaffModal from '@/routes/private/staffs/components/CreateStaffModal';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const DashboardStaffsPage = (props: Props) => {
|
||||
const t = useMyTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const [modal, setModal] = useState<null | 'create_staff'>(null);
|
||||
|
||||
const getStaffsQuery = useRequest(
|
||||
() =>
|
||||
staff_requests.getAll({
|
||||
page: page,
|
||||
}),
|
||||
{
|
||||
selectData(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
dependencies: [page],
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: list,
|
||||
totalElements,
|
||||
totalPages,
|
||||
} = useMemo(() => {
|
||||
if (getStaffsQuery.data?.data) {
|
||||
return {
|
||||
data: getStaffsQuery.data.data,
|
||||
totalElements: getStaffsQuery.data.totalElements,
|
||||
totalPages: getStaffsQuery.data.totalPages,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
data: [],
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
}
|
||||
}, [getStaffsQuery]);
|
||||
const loading = getStaffsQuery.loading;
|
||||
|
||||
const handleChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
setModal('create_staff');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
const onSuccessCreate = useCallback(() => {
|
||||
getStaffsQuery.refetch();
|
||||
closeModal();
|
||||
}, []);
|
||||
|
||||
const columns: ColumnData<User>[] = [
|
||||
{
|
||||
label: t('No'),
|
||||
width: 100,
|
||||
renderCell(data, rowIndex) {
|
||||
return (page - 1) * pageSize + rowIndex + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
dataKey: 'id',
|
||||
label: t('id'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataKey: 'username',
|
||||
label: t('username'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'fullName',
|
||||
label: t('fullName'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'phone',
|
||||
label: t('phone'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'role',
|
||||
label: t('role'),
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
dataKey: 'active',
|
||||
label: t('status'),
|
||||
width: 300,
|
||||
renderCell(data, rowIndex) {
|
||||
return data.active ? t('active') : t('not_active');
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: '', //t('actions'),
|
||||
// width: 100,
|
||||
// numeric: true,
|
||||
// renderCell(data, rowIndex) {
|
||||
// return (
|
||||
// <ActionPopMenu
|
||||
// buttons={[
|
||||
// {
|
||||
// icon: <AddCircleOutline />,
|
||||
// label: t('add_box'),
|
||||
// onClick: () => {},
|
||||
// },
|
||||
// {
|
||||
// icon: <Edit />,
|
||||
// label: t('edit'),
|
||||
// onClick: () => {},
|
||||
// },
|
||||
// {
|
||||
// icon: <Delete />,
|
||||
// label: t('delete'),
|
||||
// onClick: () => {},
|
||||
// },
|
||||
// ]}
|
||||
// />
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction={'row'} mb={3}>
|
||||
<BaseButton colorVariant='blue' startIcon={<Add />} onClick={openCreateModal}>
|
||||
{t('create_staff')}
|
||||
</BaseButton>
|
||||
</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('staffs')}
|
||||
</Typography>
|
||||
{/* <Stack direction={'row'} alignItems={'center'} spacing={2}>
|
||||
<BaseInput
|
||||
InputProps={{
|
||||
startAdornment: <Search color='primary' />,
|
||||
}}
|
||||
placeholder={t('search')}
|
||||
/>
|
||||
|
||||
<BaseButton colorVariant='gray' startIcon={<FilterListOff />} size='small'>
|
||||
{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 === 'create_staff' && <CreateStaffModal open onClose={closeModal} onSuccess={onSuccessCreate} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardStaffsPage;
|
||||
174
src/routes/private/staffs/components/CreateStaffModal.tsx
Normal file
174
src/routes/private/staffs/components/CreateStaffModal.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
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 { 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;
|
||||
};
|
||||
|
||||
const CreateStaffModal = ({ onClose, open, onSuccess }: Props) => {
|
||||
const t = useMyTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<{
|
||||
username: string;
|
||||
password: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
role: 'CHINA_WORKER',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async values => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await staff_requests.create({ ...values });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
notifyUnknownError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
const ROLES = [
|
||||
{
|
||||
label: 'CHINA_WORKER',
|
||||
value: 'CHINA_WORKER',
|
||||
},
|
||||
{
|
||||
label: 'UZB_WORKER',
|
||||
value: 'UZB_WORKER',
|
||||
},
|
||||
// {
|
||||
// label: 'ADMIN',
|
||||
// value: 'ADMIN',
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseModal maxWidth='600px' onClose={onClose} open={open}>
|
||||
<StyledBox component={'form'} onSubmit={onSubmit}>
|
||||
<Typography className='title'>{t('create_staff')}</Typography>
|
||||
|
||||
<Grid container spacing={3} mb={3}>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='label'>{t('fullName')}</Typography>
|
||||
<BaseInput
|
||||
error={!!errors.fullName}
|
||||
mainBorderColor='#D8D8D8'
|
||||
{...register('fullName', { required: true })}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='label'>{t('role')}</Typography>
|
||||
<Controller
|
||||
name='role'
|
||||
control={control}
|
||||
render={({ field, fieldState, formState }) => {
|
||||
return (
|
||||
<BaseReactSelect
|
||||
value={ROLES.find(p => p.value === field.value)}
|
||||
onChange={(newValue: any) => {
|
||||
field.onChange(newValue.value);
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
options={ROLES}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='label'>{t('username')}</Typography>
|
||||
<BaseInput
|
||||
error={!!errors.username}
|
||||
mainBorderColor='#D8D8D8'
|
||||
{...register('username', { required: true })}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='label'>{t('password')}</Typography>
|
||||
<BaseInput
|
||||
error={!!errors.password}
|
||||
mainBorderColor='#D8D8D8'
|
||||
{...register('password', { required: true })}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='label'>{t('address')}</Typography>
|
||||
<BaseInput
|
||||
error={!!errors.address}
|
||||
mainBorderColor='#D8D8D8'
|
||||
{...register('address', { required: true })}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography className='label'>{t('phone')}</Typography>
|
||||
<BaseInput
|
||||
error={!!errors.phone}
|
||||
type='number'
|
||||
mainBorderColor='#D8D8D8'
|
||||
{...register('phone', { required: true })}
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Stack direction={'row'} justifyContent={'flex-start'} alignItems={'center'} spacing={3}>
|
||||
<BaseButton colorVariant='blue' type='submit' loading={loading}>
|
||||
{t('create')}
|
||||
</BaseButton>
|
||||
<BaseButton variant='outlined' type='button' colorVariant='blue-outlined' disabled={loading} onClick={onClose}>
|
||||
{t('cancel')}
|
||||
</BaseButton>
|
||||
</Stack>
|
||||
</StyledBox>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStaffModal;
|
||||
1
src/routes/private/staffs/index.ts
Normal file
1
src/routes/private/staffs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './DashboardStaffsPage';
|
||||
Reference in New Issue
Block a user