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

View File

@@ -0,0 +1,40 @@
'use client';
import AboutSection from '@/routes/public/homepage/sections/AboutSection';
import ContactsSection from '@/routes/public/homepage/sections/ContactsSection';
import FaqSection from '@/routes/public/homepage/sections/FaqSection';
import HeroSection from '@/routes/public/homepage/sections/HeroSection';
import InfoSection from '@/routes/public/homepage/sections/InfoSection';
import NewsSection from '@/routes/public/homepage/sections/NewsSection';
import OurServices from '@/routes/public/homepage/sections/OurServices';
import PartnersSection from '@/routes/public/homepage/sections/PartnersSection';
import PricingSection from '@/routes/public/homepage/sections/PricingSection';
import React, { useEffect } from 'react';
import AOS from 'aos';
type Props = {};
const Homepage = (props: Props) => {
useEffect(() => {
AOS.init({
once: true,
duration: 650,
});
}, []);
return (
<>
<HeroSection />
<AboutSection />
<OurServices />
<InfoSection />
<PricingSection />
<PartnersSection />
<NewsSection />
<ContactsSection />
<FaqSection />
</>
);
};
export default Homepage;

View File

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

View File

@@ -0,0 +1,128 @@
'use client';
import Container from '@/components/common/Container';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { Box, Grid, Typography, css, styled } from '@mui/material';
import React, { useState } from 'react';
const StyledBox = styled(Box)`
padding: 90px 0;
.about-title {
color: #0076be;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px;
margin-bottom: 100px;
}
.about-big-img {
width: 100%;
max-width: 440px;
aspect-ratio: 1/1;
}
.about-card-title {
color: #000;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 125% */
margin-bottom: 14px;
}
.about-card-text {
color: #000;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
.about-card-img {
width: 60px;
height: 60px;
display: block;
margin-bottom: 16px;
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.about-big-img {
width: 100%;
max-width: 100%;
aspect-ratio: 1/1;
}
.about-title {
font-size: 24px;
line-height: normal;
margin-bottom: 30px;
}
}
`}
`;
type Props = {};
const AboutSection = (props: Props) => {
const t = useMyTranslation();
const [cards] = useState([
{
title: 'good_quality',
text: `Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took.`,
imgSrc: '/static/hero.png',
},
{
title: 'fast',
text: `Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took.`,
imgSrc: '/static/hero.png',
},
{
title: 'online_watching',
text: `Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took.`,
imgSrc: '/static/hero.png',
},
{
title: 'safe_warehouse',
text: `Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took.`,
imgSrc: '/static/hero.png',
},
]);
return (
<StyledBox id='about'>
<Container>
<Typography className='about-title' data-aos='fade-up'>
{t('about_us')}
</Typography>
<Grid
container
spacing={{
xs: 4,
md: 8,
}}
>
<Grid item xs={12} md={6} data-aos='fade-right'>
<img className='about-big-img' src='/static/images/airplane.png' alt='' />
</Grid>
<Grid item xs={12} md={6} data-aos='fade-left'>
<Grid container columnSpacing={8} rowGap={4}>
{cards.map((card, index) => {
return (
<Grid item xs={12} md={6} key={index}>
<img src='/static/images/airplane.png' className='about-card-img' alt='' />
<Typography className='about-card-title'>{t(card.title)}</Typography>
<Typography className='about-card-text'>{t(card.text)}</Typography>
</Grid>
);
})}
</Grid>
</Grid>
</Grid>
</Container>
</StyledBox>
);
};
export default AboutSection;

View File

@@ -0,0 +1,203 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import Container from '@/components/common/Container';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { Box, Grid, Typography, css, styled } from '@mui/material';
import React, { useState } from 'react';
const StyledBox = styled(Box)`
padding: 90px 0;
.contacts-container {
}
.contacts-title {
color: #0076be;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px;
margin-bottom: 16px;
}
.contacts-subtitle {
color: #07275c;
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
max-width: 515px;
margin: 0 auto;
margin-bottom: 50px;
}
.contacts-card-inner {
}
.contacts-cards-wrapper {
padding: 24px 32px;
border-radius: 8px;
background: #eaf2fb;
margin-bottom: 56px;
}
.contacts-card-title {
color: #07275c;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px; /* 125% */
margin-bottom: 8px;
}
.contacts-card-text {
color: #0076be;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
svg {
width: 32px;
height: 32px;
}
.contacts-map {
border-radius: 8px;
box-shadow: 8px 8px 16px 0px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.contacts-title {
font-size: 24px;
line-height: normal;
margin-bottom: 10px;
}
.contacts-map {
iframe {
height: 200px;
}
}
}
`}
`;
type Props = {};
const ContactsSection = (props: Props) => {
const t = useMyTranslation();
const [cards] = useState([
{
title: t('address'),
text: [t(`real_address`)],
svg: (
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'>
<path
d='M24.4853 23.1521L16 31.6373L7.51472 23.1521C2.82843 18.4657 2.82843 10.8678 7.51472 6.18147C12.201 1.49517 19.7989 1.49517 24.4853 6.18147C29.1716 10.8678 29.1716 18.4657 24.4853 23.1521ZM16 20.0001C18.9455 20.0001 21.3333 17.6122 21.3333 14.6667C21.3333 11.7212 18.9455 9.33342 16 9.33342C13.0545 9.33342 10.6667 11.7212 10.6667 14.6667C10.6667 17.6122 13.0545 20.0001 16 20.0001ZM16 17.3334C14.5272 17.3334 13.3333 16.1395 13.3333 14.6667C13.3333 13.194 14.5272 12.0001 16 12.0001C17.4728 12.0001 18.6667 13.194 18.6667 14.6667C18.6667 16.1395 17.4728 17.3334 16 17.3334Z'
fill='#0076BE'
/>
</svg>
),
},
{
title: 'email',
text: [`info@cpost.com`, 'cpostuz@gmail.com'],
svg: (
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'>
<path
d='M2.99028 9.13915L15.3192 1.74482C15.7414 1.49155 16.269 1.49164 16.6912 1.74506L29.0096 9.13912C29.2104 9.25962 29.3332 9.47659 29.3332 9.71072V26.6667C29.3332 27.4031 28.7362 28 27.9998 28H3.99984C3.26346 28 2.6665 27.4031 2.6665 26.6667V9.71088C2.6665 9.47666 2.78941 9.25963 2.99028 9.13915ZM24.4606 10.9918L16.0806 18.2439L7.52946 10.9836L5.80354 13.0164L16.0973 21.7561L26.2057 13.0082L24.4606 10.9918Z'
fill='#0076BE'
/>
</svg>
),
},
{
title: 'phone',
text: [`+998 99 434 45 55`, `+998 99 456 76 33`],
svg: (
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'>
<path
d='M30 22.8756V28.3762C30 29.1928 29.3686 29.8704 28.5541 29.928C27.8737 29.976 27.3187 30 26.8889 30C13.1431 30 2 18.8569 2 5.11111C2 4.68133 2.02403 4.12624 2.0721 3.44589C2.12969 2.63137 2.80724 2 3.62378 2H9.1246C9.52388 2 9.85829 2.30243 9.89838 2.69969C9.93438 3.05633 9.96784 3.34266 9.99878 3.5587C10.3134 5.75623 10.9562 7.84789 11.8758 9.78249C12.0234 10.093 11.9271 10.4647 11.6473 10.6645L8.28997 13.0628C10.3339 17.8373 14.1627 21.6661 18.9372 23.7101L21.331 20.3585C21.5334 20.0754 21.9096 19.978 22.2238 20.1272C24.1583 21.0461 26.2497 21.688 28.4469 22.002C28.6616 22.0328 28.9461 22.0659 29.3003 22.1017C29.6976 22.1418 30 22.4762 30 22.8756Z'
fill='#0076BE'
/>
</svg>
),
},
{
title: 'telegram',
text: [`@cpost_uz`, `@cpostuz_bot`],
svg: (
<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'>
<path
d='M2.42918 9.84011C7.66585 7.53636 14.4425 4.72761 15.3779 4.33886C17.8317 3.32136 18.5846 3.51636 18.2092 5.76969C17.9396 7.38928 17.1617 12.7514 16.5417 16.0889C16.1738 18.068 15.3483 18.3026 14.0504 17.4464C13.4263 17.0343 10.2758 14.9509 9.5921 14.4618C8.96793 14.0159 8.1071 13.4797 9.18668 12.4234C9.57085 12.0472 12.0892 9.64261 14.0513 7.77094C14.3083 7.52511 13.9854 7.12136 13.6888 7.31844C11.0442 9.07219 7.37751 11.5064 6.91085 11.8234C6.20585 12.3022 5.52876 12.5218 4.31335 12.1726C3.39501 11.9089 2.49793 11.5943 2.14876 11.4743C0.804182 11.0126 1.12335 10.4147 2.42918 9.84011Z'
fill='#0076BE'
/>
</svg>
),
},
{
title: 'instagram',
text: [`@cpost_uz`],
svg: (
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' fill='none'>
<path
d='M16.0013 12.0001C13.7915 12.0001 12.0013 13.7909 12.0013 16.0001C12.0013 18.2098 13.7921 20.0001 16.0013 20.0001C18.2111 20.0001 20.0013 18.2093 20.0013 16.0001C20.0013 13.7903 18.2105 12.0001 16.0013 12.0001ZM16.0013 9.33341C19.6819 9.33341 22.668 12.3162 22.668 16.0001C22.668 19.6807 19.6852 22.6667 16.0013 22.6667C12.3207 22.6667 9.33464 19.6839 9.33464 16.0001C9.33464 12.3195 12.3174 9.33341 16.0013 9.33341ZM24.668 8.99895C24.668 9.91909 23.9203 10.6656 23.0013 10.6656C22.0812 10.6656 21.3347 9.91795 21.3347 8.99895C21.3347 8.07995 22.0823 7.33341 23.0013 7.33341C23.9191 7.33225 24.668 8.07995 24.668 8.99895ZM16.0013 5.33341C12.702 5.33341 12.1642 5.34215 10.6297 5.41048C9.5842 5.45956 8.88337 5.60017 8.23221 5.85297C7.65354 6.0774 7.23612 6.34539 6.79136 6.79015C6.34489 7.23661 6.07738 7.65292 5.85386 8.23185C5.60048 8.88451 5.4599 9.58423 5.4117 10.6283C5.3427 12.1003 5.33464 12.6148 5.33464 16.0001C5.33464 19.2994 5.34337 19.8371 5.41169 21.3715C5.4608 22.4166 5.6016 23.1185 5.85378 23.7681C6.0788 24.3475 6.34733 24.7659 6.78994 25.2086C7.23817 25.6562 7.65572 25.9246 8.22916 26.1459C8.88825 26.4007 9.58864 26.5415 10.6295 26.5897C12.1016 26.6586 12.616 26.6667 16.0013 26.6667C19.3007 26.6667 19.8384 26.6579 21.3728 26.5897C22.4156 26.5407 23.118 26.3995 23.7693 26.1475C24.3472 25.9231 24.7669 25.6537 25.2099 25.2114C25.6581 24.7625 25.926 24.3459 26.1476 23.7711C26.4017 23.1145 26.5428 22.4131 26.5909 21.3719C26.6599 19.8998 26.668 19.3853 26.668 16.0001C26.668 12.7008 26.6592 12.163 26.5909 10.6286C26.5419 9.58549 26.4007 8.88207 26.1484 8.23099C25.9245 7.65392 25.6556 7.23521 25.2112 6.79015C24.764 6.34295 24.3488 6.07599 23.7695 5.85264C23.1173 5.59947 22.4165 5.45869 21.3731 5.41049C19.9011 5.34148 19.3865 5.33341 16.0013 5.33341ZM16.0013 2.66675C19.6235 2.66675 20.0757 2.68008 21.498 2.74675C22.9168 2.81231 23.8847 3.03675 24.7347 3.36675C25.6135 3.70564 26.3557 4.16341 27.0968 4.90452C27.8368 5.64564 28.2947 6.39008 28.6347 7.26675C28.9635 8.11564 29.188 9.08452 29.2547 10.5034C29.318 11.9256 29.3347 12.3779 29.3347 16.0001C29.3347 19.6223 29.3213 20.0745 29.2547 21.4967C29.1891 22.9157 28.9635 23.8834 28.6347 24.7334C28.2957 25.6123 27.8368 26.3545 27.0968 27.0957C26.3557 27.8357 25.6101 28.2934 24.7347 28.6334C23.8847 28.9623 22.9168 29.1867 21.498 29.2534C20.0757 29.3167 19.6235 29.3334 16.0013 29.3334C12.3791 29.3334 11.9268 29.3201 10.5046 29.2534C9.08574 29.1878 8.11908 28.9623 7.26797 28.6334C6.39018 28.2945 5.64685 27.8357 4.90574 27.0957C4.16464 26.3545 3.70797 25.609 3.36797 24.7334C3.03797 23.8834 2.81464 22.9157 2.74797 21.4967C2.68464 20.0745 2.66797 19.6223 2.66797 16.0001C2.66797 12.3779 2.6813 11.9256 2.74797 10.5034C2.81352 9.08341 3.03797 8.11675 3.36797 7.26675C3.70685 6.38897 4.16464 5.64564 4.90574 4.90452C5.64685 4.16341 6.3913 3.70675 7.26797 3.36675C8.11797 3.03675 9.08464 2.81341 10.5046 2.74675C11.9268 2.68341 12.3791 2.66675 16.0013 2.66675Z'
fill='#0076BE'
/>
</svg>
),
},
]);
return (
<StyledBox id='contacts'>
<Container className='contacts-container'>
<Typography className='contacts-title' data-aos='fade-up'>
{t('contacts')}
</Typography>
<Typography className='contacts-subtitle' data-aos='fade-up'>
{t('contacts_subtitle')}
</Typography>
<Box className='contacts-cards-wrapper'>
<Grid container spacing={'24px'} justifyContent={'space-between'}>
{cards.map((card, index) => {
return (
<Grid item xs={6} sm={4} md={2} key={index} className='card' data-aos='fade-up'>
<div className='contacts-card-inner'>
{card.svg}
<Typography className='contacts-card-title' data-aos='fade-up'>
{card.title}
</Typography>
{card.text.map((txt, index) => (
<Typography key={index} className='contacts-card-text' data-aos='fade-up'>
{txt}
</Typography>
))}
</div>
</Grid>
);
})}
</Grid>
</Box>
<Box className='contacts-map'>
<iframe
src='https://yandex.com/map-widget/v1/?ll=69.259404%2C41.320359&z=15&l=map'
width='100%'
height='400'
frameBorder='0'
/>
</Box>
</Container>
</StyledBox>
);
};
export default ContactsSection;

View File

@@ -0,0 +1,234 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import Container from '@/components/common/Container';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { Box, Collapse, Grid, Stack, Typography, css, styled } from '@mui/material';
import React, { useEffect, useState } from 'react';
const StyledBox = styled(Box)`
padding: 90px 0;
background-image: url('/static/images/faq-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
position: relative;
&:after {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #07275ccc;
content: '';
}
.faq-container {
position: relative;
z-index: 3;
}
.faq-title {
color: #fff;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px; /* 111.111% */
margin-bottom: 16px;
}
.faq-subtitle {
color: #fff;
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
max-width: 484px;
margin: 0 auto 46px;
}
.faq-card-inner {
border-radius: 16px;
background: #fff;
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.06);
padding: 37px 24px 42px;
}
.faq-card-title {
color: #1f6bbd;
text-align: center;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 18px;
margin-bottom: 12px;
}
.faq-card-text {
color: #33517d;
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px;
max-width: 276px;
margin: 0 auto;
}
.faq-card-img {
width: 116px;
height: 116px;
display: block;
margin: 0 auto;
margin-bottom: 25px;
}
.questions-wrapper {
}
.question-item {
padding: 20px;
border-radius: 16px;
background: #fff;
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.06);
}
.question-title {
color: #1f6bbd;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 18px;
}
.question-text {
color: #33517d;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px;
}
.question-btn {
background-color: transparent;
border: 0;
padding: 0;
transition: transform 0.15s ease;
svg {
width: 20px;
height: 20px;
}
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.faq-title {
font-size: 24px;
line-height: normal;
margin-bottom: 10px;
}
}
`}
`;
type Props = {};
const FaqSection = (props: Props) => {
const t = useMyTranslation();
const [questions] = useState([
{
question: 'faq.faq1.question',
answer: 'faq.faq1.answer',
},
{
question: 'faq.faq1.question',
answer: 'faq.faq1.answer',
},
{
question: 'faq.faq1.question',
answer: 'faq.faq1.answer',
},
{
question: 'faq.faq1.question',
answer: 'faq.faq1.answer',
},
{
question: 'faq.faq1.question',
answer: 'faq.faq1.answer',
},
]);
return (
<StyledBox>
<Container className='faq-container'>
<Typography className='faq-title' data-aos='fade-up'>
{t('faq.title')}
</Typography>
<Typography className='faq-subtitle' data-aos='fade-up'>
{t('faq.subtitle')}
</Typography>
<Grid container spacing={'28px'}>
<Grid item xs={12} md={6}>
<Stack spacing={'28px'} className='questions-wrapper'>
{questions.map((question, index) => {
return <QuestionItem item={question} key={index} />;
})}
</Stack>
</Grid>
<Grid item xs={12} md={6}>
<Stack spacing={'28px'} className='questions-wrapper'>
{questions.map((question, index) => {
return <QuestionItem item={question} key={index} />;
})}
</Stack>
</Grid>
</Grid>
</Container>
</StyledBox>
);
};
const QuestionItem = ({ item }: { item: { question: string; answer: string } }) => {
const t = useMyTranslation();
const [expanded, setExpanded] = React.useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
return (
<Box className='question-item' data-aos='fade-up'>
<Stack direction={'row'} onClick={handleExpandClick} justifyContent={'space-between'} sx={{ cursor: 'pointer' }}>
<Typography className='question-title'>{t(item.question)}</Typography>
<button
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label='show more'
className='question-btn'
style={{
transform: expanded ? 'rotate(180deg)' : '',
}}
>
<svg xmlns='http://www.w3.org/2000/svg' width='20' height='18' viewBox='0 0 20 18' fill='none'>
<path
d='M9.31212 10.4545L13.6432 6L14.8806 7.27272L9.31212 13L3.74365 7.27272L4.9811 6L9.31212 10.4545Z'
fill='#1F6BBD'
/>
</svg>
</button>
</Stack>
<Collapse in={expanded} timeout='auto' unmountOnExit>
<Typography className='question-text'>{t(item.answer)}</Typography>
</Collapse>
</Box>
);
};
export default FaqSection;

View File

@@ -0,0 +1,114 @@
'use client';
import Container from '@/components/common/Container';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { Box, Button, Typography, css, styled } from '@mui/material';
import React from 'react';
const StyledBox = styled(Box)`
padding: 154px 0 180px;
background-image: url('/static/images/hero.png');
background-position: center;
background-size: cover;
position: relative;
&:after {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
content: '';
}
.contentZIndex {
position: relative;
z-index: 3;
}
.hero-title {
color: #fff;
font-size: 64px;
font-style: normal;
font-weight: 600;
line-height: 64px;
max-width: 550px;
margin-bottom: 16px;
}
.hero-subtitle {
color: #fff;
font-size: 24px;
font-style: normal;
font-weight: 400;
line-height: 28px;
margin-bottom: 48px;
}
.hero-btn {
padding: 10px 16px;
border-radius: 8px;
background: #0076be;
color: #fff;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.72px;
text-transform: capitalize;
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 60px 0 60px;
.hero-title {
font-size: 32px;
line-height: normal;
}
.hero-subtitle {
font-size: 18px;
line-height: normal;
}
}
`}
`;
type Props = {};
const HeroSection = (props: Props) => {
const t = useMyTranslation();
return (
<StyledBox>
<Container>
<Typography className='hero-title contentZIndex' data-aos='fade-up'>
{t('hero_title')}
</Typography>
<Typography className='hero-subtitle contentZIndex' data-aos='fade-up'>
{t('hero_subtitle')}
</Typography>
<Button
data-aos='fade-up'
disableElevation
className='hero-btn contentZIndex'
startIcon={
<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'>
<path
d='M21 8C22.1046 8 23 8.89543 23 10V14C23 15.1046 22.1046 16 21 16H19.9381C19.446 19.9463 16.0796 23 12 23V21C15.3137 21 18 18.3137 18 15V9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9V16H3C1.89543 16 1 15.1046 1 14V10C1 8.89543 1.89543 8 3 8H4.06189C4.55399 4.05369 7.92038 1 12 1C16.0796 1 19.446 4.05369 19.9381 8H21ZM7.75944 15.7849L8.81958 14.0887C9.74161 14.6662 10.8318 15 12 15C13.1682 15 14.2584 14.6662 15.1804 14.0887L16.2406 15.7849C15.0112 16.5549 13.5576 17 12 17C10.4424 17 8.98882 16.5549 7.75944 15.7849Z'
fill='white'
/>
</svg>
}
>
{t('contacts')}
</Button>
</Container>
</StyledBox>
);
};
export default HeroSection;

View File

@@ -0,0 +1,127 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import Container from '@/components/common/Container';
import { Box, Grid, Typography, css, styled } from '@mui/material';
import React, { useState } from 'react';
const StyledBox = styled(Box)`
padding: 90px 0;
.info-container {
}
.info-title {
color: #0076be;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px;
margin-bottom: 8px;
}
.info-subtitle {
color: #1f1f1f;
text-align: center;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px;
margin-bottom: 72px;
}
.info-card-inner {
}
.info-card-title {
color: #1f6bbd;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 20px;
margin-bottom: 8px;
}
.info-card-text {
color: #1f1f1f;
font-size: 10px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
.info-card-img {
width: auto;
height: 80px;
display: block;
margin-bottom: 20px;
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.info-title {
font-size: 24px;
line-height: normal;
}
.info-subtitle {
font-size: 16px;
}
}
`}
`;
type Props = {};
const InfoSection = (props: Props) => {
const [cards] = useState([
{
title: 'Havo Transporti',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.`,
imgSrc: '/static/images/icon1.png',
},
{
title: 'Avtomobil Transporti',
text: `We dont store your banking credentials. Your data is only accessed by you and if you have given consent, by your finansial advisor.`,
imgSrc: '/static/images/icon2.png',
},
{
title: 'Dengiz Transporti',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.`,
imgSrc: '/static/images/icon3.png',
},
{
title: 'Dengiz Transporti',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.`,
imgSrc: '/static/images/icon3.png',
},
]);
return (
<StyledBox>
<Container className='info-container'>
<Typography className='info-title' data-aos='fade-up'>
Lorem ipsum dolor sit amet, consectetur adipiscing
</Typography>
<Typography className='info-subtitle' data-aos='fade-up'>
Lorem ipsum dolor sit amet, consectetur adipiscing
</Typography>
<Grid container spacing={'28px'}>
{cards.map((card, index) => {
return (
<Grid item xs={12} sm={6} md={3} key={index} className='card' data-aos='fade-up'>
<div className='info-card-inner'>
<img src={card.imgSrc} className='info-card-img' alt='' />
<Typography className='info-card-title'>{card.title}</Typography>
<Typography className='info-card-text'>{card.text}</Typography>
</div>
</Grid>
);
})}
</Grid>
</Container>
</StyledBox>
);
};
export default InfoSection;

View File

@@ -0,0 +1,278 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import Container from '@/components/common/Container';
import { Box, Grid, Stack, Typography, css, styled } from '@mui/material';
import React, { useRef, useState } from 'react';
import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react';
import { Autoplay } from 'swiper/modules';
import { useMyTranslation } from '@/hooks/useMyTranslation';
const StyledBox = styled(Box)`
padding: 90px 0;
background-image: url('/static/images/news-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
position: relative;
&:after {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #07275ccc;
content: '';
}
.news-container {
position: relative;
z-index: 3;
}
.news-header {
margin-bottom: 48px;
}
.news-title {
color: #fff;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px;
margin-bottom: 16px;
}
.news-subtitle {
color: #fff;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
max-width: 484px;
}
.card {
height: auto;
}
.news-card-inner {
border-radius: 16px;
background: #fff;
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.06);
overflow: hidden;
height: 100%;
}
.news-card-title {
color: #1f6bbd;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 18px;
margin-bottom: 8px;
}
.news-card-text {
color: #33517d;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 15px;
}
.news-card-img {
width: 100%;
height: 175px;
display: block;
margin: 0 auto;
object-fit: cover;
}
.news-card-text-wrapper {
padding: 12px 20px 15px;
}
.my-slide-btn {
border-radius: 50%;
background-color: #fff;
border: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
transition: opacity 0.3s ease;
cursor: pointer;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.news-title {
font-size: 24px;
line-height: normal;
margin-bottom: 10px;
}
}
`}
`;
type Props = {};
const NewsSection = (props: Props) => {
const t = useMyTranslation();
const swiperRef = useRef<SwiperRef>(null);
const [cards] = useState([
{
title: '15 Latest Development in Webflow.',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news1.png',
id: 1,
},
{
title: 'Create your wedding website',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news2.png',
id: 2,
},
{
title: 'Schedule ceremony site tours',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news3.png',
id: 3,
},
{
title: 'Write a sweet note to your partner',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news4.png',
id: 4,
},
{
title: '15 Latest Development in Webflow.',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news1.png',
id: 5,
},
{
title: 'Create your wedding website',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news2.png',
id: 6,
},
{
title: 'Schedule ceremony site tours',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news3.png',
id: 7,
},
{
title: 'Write a sweet note to your partner',
text: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt consectetur adipiscing elit...`,
imgSrc: '/static/images/news4.png',
id: 8,
},
]);
const slidePrev = () => {
swiperRef.current?.swiper.slidePrev();
};
const slideNext = () => {
swiperRef.current?.swiper.slideNext();
};
return (
<StyledBox>
<Container className='news-container'>
<Stack
direction={{ xs: 'column', md: 'row' }}
alignItems={'flex-end'}
justifyContent={'space-between'}
className='news-header'
spacing={3}
>
<Box>
<Typography className='news-title' data-aos='fade-up'>
{t('news')}
</Typography>
<Typography className='news-subtitle' data-aos='fade-up'>
{t('news_subtitle')}
</Typography>
</Box>
<Stack direction={'row'} alignItems={'center'} spacing={2} data-aos='fade-up'>
<button className='my-slide-btn' onClick={slidePrev}>
<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'>
<path
d='M7.8284 13.0001L13.1924 18.3641L11.7782 19.7783L4 12.0001L11.7782 4.222L13.1924 5.6362L7.8284 11.0001L20 11.0001L20 13.0001L7.8284 13.0001Z'
fill='#0076BE'
/>
</svg>
</button>
<button className='my-slide-btn' onClick={slideNext}>
<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'>
<path
d='M16.1716 10.9999L10.8076 5.63589L12.2218 4.22168L20 11.9999L12.2218 19.778L10.8076 18.3638L16.1716 12.9999H4V10.9999H16.1716Z'
fill='#0076BE'
/>
</svg>
</button>
</Stack>
</Stack>
<Box data-aos='fade-up'>
<Swiper
modules={[Autoplay]}
autoplay={{
delay: 4500,
disableOnInteraction: false,
}}
loop={true}
breakpoints={{
100: {
slidesPerView: 1,
spaceBetween: 20,
},
600: {
slidesPerView: 2,
spaceBetween: 20,
},
900: {
slidesPerView: 3,
spaceBetween: 30,
},
1100: {
slidesPerView: 4,
spaceBetween: 35,
},
}}
ref={swiperRef}
>
{cards.map((card, index) => {
return (
<SwiperSlide className='card' key={card.id}>
<div className='news-card-inner'>
<img src={card.imgSrc} className='news-card-img' alt='' />
<div className='news-card-text-wrapper'>
<Typography className='news-card-title'>{card.title}</Typography>
<Typography className='news-card-text'>{card.text}</Typography>
</div>
</div>
</SwiperSlide>
);
})}
</Swiper>
</Box>
</Container>
</StyledBox>
);
};
export default NewsSection;

View File

@@ -0,0 +1,153 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import Container from '@/components/common/Container';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { Box, Grid, Typography, css, styled } from '@mui/material';
import React, { useState } from 'react';
const StyledBox = styled(Box)`
padding: 90px 0;
background-image: url('/static/images/services-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
position: relative;
&:after {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #07275ccc;
content: '';
}
.services-container {
position: relative;
z-index: 3;
}
.services-title {
color: #fff;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px; /* 111.111% */
margin-bottom: 60px;
}
.services-card-inner {
border-radius: 16px;
background: #fff;
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.06);
padding: 37px 24px 42px;
height: 100%;
}
.services-card-title {
color: #1f6bbd;
text-align: center;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 18px;
margin-bottom: 12px;
}
.services-card-text {
color: #33517d;
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px;
max-width: 276px;
margin: 0 auto;
}
.services-card-img {
width: 116px;
height: 116px;
display: block;
margin: 0 auto;
margin-bottom: 25px;
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.services-title {
font-size: 24px;
line-height: normal;
margin-bottom: 30px;
}
}
`}
`;
type Props = {};
const OurServices = (props: Props) => {
const t = useMyTranslation();
const [cards] = useState([
{
title: 'air_transport',
text: 'air_transport__subtitle',
imgSrc: '/static/images/service1.png',
},
{
title: 'car_transport',
text: 'car_transport__subtitle',
imgSrc: '/static/images/service2.png',
},
{
title: 'water_transport',
text: 'water_transport__subtitle',
imgSrc: '/static/images/service3.png',
},
{
title: 'railway_transport',
text: 'railway_transport__subtitle',
imgSrc: '/static/images/service1.png',
},
{
title: 'warehouse_yivu_and_guanchjou',
text: 'warehouse_yivu_and_guanchjou__subtitle',
imgSrc: '/static/images/service2.png',
},
{
title: 'customs_office_service',
text: 'customs_office_service__subtitle',
imgSrc: '/static/images/service3.png',
},
]);
return (
<StyledBox id='services'>
<Container className='services-container'>
<Typography className='services-title' data-aos='fade-up'>
{t('our_services')}
</Typography>
<Grid container spacing={'28px'}>
{cards.map((card, index) => {
return (
<Grid item xs={12} sm={6} md={4} key={index} className='card' data-aos='fade-up'>
<div className='services-card-inner'>
<img src={card.imgSrc} className='services-card-img' alt='' />
<Typography className='services-card-title'>{t(card.title)}</Typography>
<Typography className='services-card-text'>{t(card.text)}</Typography>
</div>
</Grid>
);
})}
</Grid>
</Container>
</StyledBox>
);
};
export default OurServices;

View File

@@ -0,0 +1,105 @@
'use client';
/* eslint-disable @next/next/no-img-element */
import React from 'react';
import { Box, Typography, css, styled } from '@mui/material';
import { useMyTranslation } from '@/hooks/useMyTranslation';
const StyledBox = styled(Box)`
padding: 70px 0 85px;
.partners-title {
color: #0076be;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 600;
line-height: 40px;
margin-bottom: 45px;
}
.partners-list-wrapper {
display: flex;
overflow-x: hidden;
user-select: none;
mask-image: linear-gradient(to right, hsl(0 0% 0% / 0), hsl(0 0% 0% / 1) 10%, hsl(0 0% 0% / 1) 90%, hsl(0 0% 0% / 0));
}
.partners-list {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-around;
gap: 1rem;
min-width: 100%;
animation: loop 15s linear infinite;
}
.partners-item {
display: block;
margin-inline: 2rem;
width: auto;
}
@keyframes loop {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.partners-title {
font-size: 24px;
line-height: normal;
margin-bottom: 30px;
}
.partners-item {
display: block;
margin-inline: 1rem;
width: 100%;
max-width: 120px;
}
}
`}
`;
type Props = {};
const PartnersSection = (props: Props) => {
const t = useMyTranslation();
const items = [
{ src: '/static/images/brand1.png' },
{ src: '/static/images/brand2.png' },
{ src: '/static/images/brand3.png' },
{ src: '/static/images/brand4.png' },
];
return (
<StyledBox>
<Typography className='partners-title' data-aos='fade-up'>
{t('our_partners')}
</Typography>
<Box className='partners-list-wrapper'>
<Box className='partners-list'>
{items.map((item, index) => (
<img className='partners-item' key={index} src={item.src} alt='Diip.uz' />
))}
</Box>
<Box className='partners-list'>
{items.map((item, index) => (
<img className='partners-item' key={index} src={item.src} alt='Diip.uz' />
))}
</Box>
</Box>
</StyledBox>
);
};
export default PartnersSection;

View File

@@ -0,0 +1,200 @@
/* eslint-disable @next/next/no-img-element */
'use client';
import Container from '@/components/common/Container';
import NextLink from '@/components/common/NextLink';
import { useMyTranslation } from '@/hooks/useMyTranslation';
import { Box, Grid, Typography, css, styled } from '@mui/material';
import React, { useState } from 'react';
const StyledBox = styled(Box)`
padding: 90px 0;
background-image: url('/static/images/pricing-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
position: relative;
&:after {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #07275ccc;
content: '';
}
.pricing-container {
position: relative;
z-index: 3;
}
.pricing-title {
color: #fff;
text-align: center;
font-size: 36px;
font-style: normal;
font-weight: 700;
line-height: 40px;
margin-bottom: 78px;
}
.pricing-card-inner {
border-radius: 16px;
background: #fff;
box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.06);
padding: 25px 32px 20px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.pricing-card-title {
color: #363636;
text-align: center;
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 24px;
margin-bottom: 12px;
}
.pricing-card-price {
color: #0076be;
text-align: center;
font-size: 32px;
font-style: normal;
font-weight: 700;
line-height: 40px;
margin-bottom: 12px;
}
.pricing-card-limit,
.pricing-card-weight {
color: #363636;
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 16px;
margin-bottom: 8px;
span {
color: #363636;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 16px;
}
}
.pricing-card-more {
color: #0076be;
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 700;
line-height: 16px;
display: block;
margin: 24px auto 0;
}
.pricing-card-text {
color: #33517d;
text-align: center;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 18px;
max-width: 276px;
margin: 0 auto;
}
.pricing-card-img {
width: 50px;
height: 50px;
display: block;
margin: 0 auto;
margin-bottom: 20px;
}
${({ theme }) => css`
${theme.breakpoints.down('md')} {
padding: 40px 0;
.pricing-title {
font-size: 24px;
line-height: normal;
margin-bottom: 30px;
}
}
`}
`;
type Props = {};
const Pricing = (props: Props) => {
const t = useMyTranslation();
const [cards] = useState([
{
title: 'pricing.pricing1.title',
priceText: 'pricing.pricing1.price',
limitDay: 'pricing.pricing1.limit',
minWeight: 'pricing.pricing1.min_weight',
imgSrc: '/static/images/airplane.svg',
},
{
title: 'pricing.pricing2.title',
priceText: 'pricing.pricing2.price',
limitDay: 'pricing.pricing2.limit',
minWeight: 'pricing.pricing2.min_weight',
imgSrc: '/static/images/truck.svg',
},
{
title: 'pricing.pricing3.title',
priceText: 'pricing.pricing3.price',
limitDay: 'pricing.pricing3.limit',
minWeight: 'pricing.pricing3.min_weight',
imgSrc: '/static/images/long-truck.svg',
},
{
title: 'pricing.pricing4.title',
priceText: 'pricing.pricing4.price',
limitDay: 'pricing.pricing4.limit',
minWeight: 'pricing.pricing4.min_weight',
imgSrc: '/static/images/metro.svg',
},
]);
return (
<StyledBox id='tariffs'>
<Container className='pricing-container'>
<Typography className='pricing-title' data-aos='fade-up'>
{t('pricing.title')}
</Typography>
<Grid container spacing={'28px'}>
{cards.map((card, index) => {
return (
<Grid item xs={12} sm={6} md={3} key={index} className='card' data-aos='fade-up'>
<div className='pricing-card-inner'>
<img src={card.imgSrc} className='pricing-card-img' alt='' />
<Typography className='pricing-card-title'>{t(card.title)}</Typography>
<Typography className='pricing-card-price'>{t(card.priceText)}</Typography>
<Typography className='pricing-card-limit'>
{t('pricing.limit')} <span>{t(card.limitDay)}</span>
</Typography>
<Typography className='pricing-card-weight'>
{t('pricing.min_weight')} <span>{t(card.minWeight)}</span>
</Typography>
<NextLink href='#' className='pricing-card-more'>
{t('pricing.more')}
</NextLink>
</div>
</Grid>
);
})}
</Grid>
</Container>
</StyledBox>
);
};
export default Pricing;

View File

@@ -0,0 +1,14 @@
import HeroSection from '@/routes/public/news-page/sections/HeroSection';
import React from 'react';
type Props = {};
const NewsPage = (props: Props) => {
return (
<>
<HeroSection />
</>
);
};
export default NewsPage;

View File

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

View File

@@ -0,0 +1,9 @@
import React from 'react';
type Props = {};
const HeroSection = (props: Props) => {
return <div>HeroSection</div>;
};
export default HeroSection;