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

View File

@@ -0,0 +1,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;

View 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;

View File

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

View 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;

View File

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

View 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;

View 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;

View 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;

View 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;

View File

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

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View 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;

View File

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

View 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;

View File

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

View 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;

View 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;

View File

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