Compare commits

..

No commits in common. "main" and "feature/graphic-car-body-select" have entirely different histories.

42 changed files with 1429 additions and 981 deletions

View File

@ -39,10 +39,9 @@
"dry-wash.order-create.car-body-select.options.sports-car" : "Sports-car", "dry-wash.order-create.car-body-select.options.sports-car" : "Sports-car",
"dry-wash.order-create.car-body-select.options.other": "Other", "dry-wash.order-create.car-body-select.options.other": "Other",
"dry-wash.order-create.form.submit-button.label": "Submit", "dry-wash.order-create.form.submit-button.label": "Submit",
"dry-wash.order-create.create-order-query.success.title": "The order is successfully created",
"dry-wash.order-create.create-order-query.error.title": "Failed to create an order",
"dry-wash.order-view.title": "Your order", "dry-wash.order-view.title": "Your order",
"dry-wash.order-view.get-order-query.error.title": "Failed to fetch the details of order #{{number}}", "dry-wash.order-view.error.title": "Error",
"dry-wash.order-view.fetch.error": "Failed to fetch the details of order #{{number}}",
"dry-wash.order-view.details.title": "Order #{{number}}", "dry-wash.order-view.details.title": "Order #{{number}}",
"dry-wash.order-view.details.owner": "Owner", "dry-wash.order-view.details.owner": "Owner",
"dry-wash.order-view.details.car": "Car", "dry-wash.order-view.details.car": "Car",
@ -63,7 +62,6 @@
"dry-wash.arm.order.table.header.washingTime": "Washing Time", "dry-wash.arm.order.table.header.washingTime": "Washing Time",
"dry-wash.arm.order.table.header.orderDate": "Order Date", "dry-wash.arm.order.table.header.orderDate": "Order Date",
"dry-wash.arm.order.table.header.status": "Status", "dry-wash.arm.order.table.header.status": "Status",
"dry-wash.arm.order.table.header.masters": "Master",
"dry-wash.arm.order.table.header.telephone": "Telephone", "dry-wash.arm.order.table.header.telephone": "Telephone",
"dry-wash.arm.order.table.header.location": "Location", "dry-wash.arm.order.table.header.location": "Location",
"dry-wash.arm.master.title": "Masters", "dry-wash.arm.master.title": "Masters",
@ -74,10 +72,6 @@
"dry-wash.arm.master.table.header.phone": "Phone", "dry-wash.arm.master.table.header.phone": "Phone",
"dry-wash.arm.master.table.header.actions": "Actions", "dry-wash.arm.master.table.header.actions": "Actions",
"dry-wash.arm.master.table.actionsMenu.delete": "Delete Master", "dry-wash.arm.master.table.actionsMenu.delete": "Delete Master",
"dry-wash.arm.master.schedule.empty": "free",
"dry-wash.arm.master.editable.aria.cancel": "Undo changes",
"dry-wash.arm.master.editable.aria.save": "Save changes ",
"dry-wash.arm.master.editable.aria.edit": "Edit",
"dry-wash.arm.master.drawer.title": "Add New Master", "dry-wash.arm.master.drawer.title": "Add New Master",
"dry-wash.arm.master.drawer.inputName.label": "Full Name", "dry-wash.arm.master.drawer.inputName.label": "Full Name",
"dry-wash.arm.master.drawer.inputName.placeholder": "Enter Full Name", "dry-wash.arm.master.drawer.inputName.placeholder": "Enter Full Name",

View File

@ -11,7 +11,6 @@
"dry-wash.arm.order.table.header.washingTime": "Время мойки", "dry-wash.arm.order.table.header.washingTime": "Время мойки",
"dry-wash.arm.order.table.header.orderDate": "Дата заказа", "dry-wash.arm.order.table.header.orderDate": "Дата заказа",
"dry-wash.arm.order.table.header.status": "Статус", "dry-wash.arm.order.table.header.status": "Статус",
"dry-wash.arm.order.table.header.masters": "Мастер",
"dry-wash.arm.order.table.header.telephone": "Телефон", "dry-wash.arm.order.table.header.telephone": "Телефон",
"dry-wash.arm.order.table.header.location": "Расположение", "dry-wash.arm.order.table.header.location": "Расположение",
"dry-wash.arm.order.table.empty": "Список пуст", "dry-wash.arm.order.table.empty": "Список пуст",
@ -24,10 +23,6 @@
"dry-wash.arm.master.table.header.phone": "Телефон", "dry-wash.arm.master.table.header.phone": "Телефон",
"dry-wash.arm.master.table.header.actions": "Действия", "dry-wash.arm.master.table.header.actions": "Действия",
"dry-wash.arm.master.table.actionsMenu.delete": "Удалить мастера", "dry-wash.arm.master.table.actionsMenu.delete": "Удалить мастера",
"dry-wash.arm.master.schedule.empty": "Свободен",
"dry-wash.arm.master.editable.aria.cancel": "Отменить изменения",
"dry-wash.arm.master.editable.aria.save": "Сохранить изменения",
"dry-wash.arm.master.editable.aria.edit": "Редактировать",
"dry-wash.arm.master.drawer.title": "Добавить нового мастера", "dry-wash.arm.master.drawer.title": "Добавить нового мастера",
"dry-wash.arm.master.drawer.inputName.label": "ФИО", "dry-wash.arm.master.drawer.inputName.label": "ФИО",
"dry-wash.arm.master.drawer.inputName.placeholder": "Введите ФИО", "dry-wash.arm.master.drawer.inputName.placeholder": "Введите ФИО",
@ -78,10 +73,9 @@
"dry-wash.order-create.car-body-select.options.sports-car": "Спорткар", "dry-wash.order-create.car-body-select.options.sports-car": "Спорткар",
"dry-wash.order-create.car-body-select.options.other": "Другой", "dry-wash.order-create.car-body-select.options.other": "Другой",
"dry-wash.order-create.form.submit-button.label": "Отправить", "dry-wash.order-create.form.submit-button.label": "Отправить",
"dry-wash.order-create.create-order-query.success.title": "Заказ успешно создан",
"dry-wash.order-create.create-order-query.error.title": "Не удалось создать заказ",
"dry-wash.order-view.title": "Ваш заказ", "dry-wash.order-view.title": "Ваш заказ",
"dry-wash.order-view.get-order-query.error.title": "Не удалось загрузить детали заказа №{{number}}", "dry-wash.order-view.error.title": "Ошибка",
"dry-wash.order-view.fetch.error": "Не удалось загрузить детали заказа №{{number}}",
"dry-wash.order-view.details.title": "Заказ №{{number}}", "dry-wash.order-view.details.title": "Заказ №{{number}}",
"dry-wash.order-view.details.owner": "Владелец", "dry-wash.order-view.details.owner": "Владелец",
"dry-wash.order-view.details.car": "Автомобиль", "dry-wash.order-view.details.car": "Автомобиль",
@ -95,4 +89,4 @@
"dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы", "dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы",
"dry-wash.errorBoundary.button.reload": "Перезагрузить страницу", "dry-wash.errorBoundary.button.reload": "Перезагрузить страницу",
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}" "dry-wash.washTime.timeSlot": "{{start}} - {{end}}"
} }

1330
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "dry-wash", "name": "dry-wash",
"version": "0.5.0", "version": "0.3.0",
"description": "<a id=\"readme-top\"></a>", "description": "<a id=\"readme-top\"></a>",
"main": "./src/index.tsx", "main": "./src/index.tsx",
"scripts": { "scripts": {
@ -19,7 +19,7 @@
"dependencies": { "dependencies": {
"@brojs/cli": "^1.6.3", "@brojs/cli": "^1.6.3",
"@chakra-ui/icons": "^2.2.4", "@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.10.5", "@chakra-ui/react": "^2.4.2",
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@fontsource/open-sans": "^5.1.0", "@fontsource/open-sans": "^5.1.0",

View File

@ -1,5 +1,4 @@
import { getConfigValue } from '@brojs/cli'; import { getConfigValue } from '@brojs/cli';
import dayjs from 'dayjs';
enum ArmEndpoints { enum ArmEndpoints {
ORDERS = '/arm/orders', ORDERS = '/arm/orders',
@ -10,15 +9,12 @@ const armService = () => {
const endpoint = getConfigValue('dry-wash.api'); const endpoint = getConfigValue('dry-wash.api');
const fetchOrders = async ({ date }: { date: Date }) => { const fetchOrders = async ({ date }: { date: Date }) => {
const startDate = dayjs(date).startOf('day').toISOString();
const endDate = dayjs(date).endOf('day').toISOString();
const response = await fetch(`${endpoint}${ArmEndpoints.ORDERS}`, { const response = await fetch(`${endpoint}${ArmEndpoints.ORDERS}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ startDate, endDate }), body: JSON.stringify({ date }),
}); });
if (!response.ok) { if (!response.ok) {
@ -38,67 +34,7 @@ const armService = () => {
return await response.json(); return await response.json();
}; };
const addMaster = async ({ return { fetchOrders, fetchMasters };
name,
phone,
}: {
name: string;
phone: string;
}) => {
const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, phone }),
});
if (!response.ok) {
throw new Error(`Failed to fetch masters: ${response.status}`);
}
return await response.json();
};
const deleteMaster = async ({ id }: { id: string }) => {
const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`Failed to fetch masters: ${response.status}`);
}
return await response.json();
};
const updateMaster = async ({
id,
name,
phone,
}: {
id: string;
name?: string;
phone?: string;
}) => {
const body = JSON.stringify({ name, phone });
const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body,
});
if (!response.ok) {
throw new Error(`Failed to fetch update masters: ${response.status}`);
}
return await response.json();
};
return { fetchOrders, fetchMasters, addMaster, deleteMaster, updateMaster };
}; };
export { armService, ArmEndpoints }; export { armService, ArmEndpoints };

View File

@ -1 +0,0 @@
export * from './landing';

25
src/api/landing.ts Normal file
View File

@ -0,0 +1,25 @@
import { getConfigValue } from '@brojs/cli';
import { Order } from '../models/landing';
enum LandingEndpoints {
ORDER_VIEW = '/order'
}
const LandingService = () => {
const endpoint = getConfigValue('dry-wash.api');
const fetchOrder = async (orderId: Order.Id) => {
const response = await fetch(`${endpoint}${LandingEndpoints.ORDER_VIEW}/${orderId}`);
if (!response.ok) {
throw new Error(`Failed to fetch order: ${response.status}`);
}
return await response.json();
};
return { fetchOrder };
};
export { LandingService, LandingEndpoints };

View File

@ -1,107 +0,0 @@
import { getConfigValue } from '@brojs/cli';
import { useEffect, useState } from 'react';
import { CreateOrder, GetOrder } from '../models/api';
import { QueryState, Trigger } from './types';
enum LandingEndpoints {
ORDER = '/order',
ORDER_CREATE = '/order/create',
}
const endpoint = getConfigValue('dry-wash.api');
const useCreateOrderMutation = <D extends CreateOrder.Response>(): [
Trigger<CreateOrder.Params, QueryState<D>['data']>,
QueryState<D>,
] => {
const [isLoading, setIsLoading] = useState<QueryState<D>['isLoading']>(false);
const [isSuccess, setIsSuccess] = useState<QueryState<D>['isSuccess']>();
const [data, setData] = useState<QueryState<D>['data']>();
const [isError, setIsError] = useState<QueryState<D>['isError']>();
const [error, setError] = useState<QueryState<D>['error']>();
const createOrder = async ({ body }: CreateOrder.Params) => {
setIsLoading(true);
try {
const response = await fetch(
`${endpoint}${LandingEndpoints.ORDER_CREATE}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);
if (!response.ok) {
const errorResponseObject =
(await response.json()) as QueryState<D>['error'];
setIsError(true);
setError(errorResponseObject);
throw errorResponseObject;
}
const dataResponseObject =
(await response.json()) as QueryState<D>['data'];
setIsSuccess(true);
setData(dataResponseObject);
return dataResponseObject;
} catch (error) {
setIsError(true);
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return [createOrder, { isLoading, isSuccess, data, isError, error }];
};
const useGetOrderQuery = <D extends GetOrder.Response>({
orderId,
}: GetOrder.Params): QueryState<D> => {
const [isLoading, setIsLoading] = useState<QueryState<D>['isLoading']>(true);
const [isSuccess, setIsSuccess] = useState<QueryState<D>['isSuccess']>();
const [data, setData] = useState<QueryState<D>['data']>();
const [isError, setIsError] = useState<QueryState<D>['isError']>();
const [error, setError] = useState<QueryState<D>['error']>();
useEffect(() => {
(async () => {
try {
const response = await fetch(
`${endpoint}${LandingEndpoints.ORDER}/${orderId}`,
);
if (!response.ok) {
const errorResponseObject =
(await response.json()) as QueryState<D>['error'];
setIsError(true);
setError(errorResponseObject);
throw errorResponseObject;
}
const dataResponseObject =
(await response.json()) as QueryState<D>['data'];
setIsSuccess(true);
setData(dataResponseObject);
} catch (error) {
setIsError(true);
setError(error);
throw error;
} finally {
setIsLoading(false);
}
})();
}, []);
return { isLoading, isSuccess, data, isError, error };
};
export { useCreateOrderMutation, useGetOrderQuery };

View File

@ -1,22 +0,0 @@
export type QueryData<D> = {
success: true;
body: D;
};
export type QueryErrorData = {
success: false;
error: string;
};
export type QueryState<D> = {
isLoading: boolean;
isSuccess: boolean;
data: QueryData<D>;
isError: boolean;
error: {
status: number;
data: QueryErrorData;
};
};
export type Trigger<P, D> = (params: P) => Promise<D>;

View File

@ -1,119 +0,0 @@
import React, { useState } from 'react';
import {
Editable,
EditableInput,
EditablePreview,
Flex,
IconButton,
Input,
useEditableControls,
ButtonGroup,
Stack,
useToast,
} from '@chakra-ui/react';
import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
interface EditableWrapperProps {
value: string;
onSubmit: ({
id,
name,
phone,
}: {
id: string;
name?: string;
phone?: string;
}) => Promise<unknown>;
as: 'phone' | 'name';
id: string;
}
const EditableWrapper = ({ value, onSubmit, as, id }: EditableWrapperProps) => {
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.editable',
});
const toast = useToast();
const [currentValue, setCurrentValue] = useState<string>(value);
const handleSubmit = async (newValue: string) => {
if (currentValue === newValue) return;
try {
await onSubmit({ id, [as]: newValue });
setCurrentValue(newValue);
toast({
title: 'Успешно!',
description: 'Данные обновлены.',
status: 'success',
duration: 2000,
isClosable: true,
position: 'top-right',
});
} catch (error) {
toast({
title: 'Ошибка!',
description: 'Не удалось обновить данные.',
status: 'error',
duration: 2000,
isClosable: true,
position: 'top-right',
});
console.error('Ошибка при обновлении данных:', error);
}
};
function EditableControls() {
const {
isEditing,
getSubmitButtonProps,
getCancelButtonProps,
getEditButtonProps,
} = useEditableControls();
return isEditing ? (
<ButtonGroup justifyContent='center' size='sm'>
<IconButton
aria-label={t('aria.save')}
icon={<CheckIcon />}
{...getSubmitButtonProps()}
/>
<IconButton
aria-label={t('aria.cancel')}
icon={<CloseIcon />}
{...getCancelButtonProps()}
/>
</ButtonGroup>
) : (
<Flex justifyContent='center'>
<IconButton
aria-label={t('aria.edit')}
size='sm'
icon={<EditIcon />}
{...getEditButtonProps()}
/>
</Flex>
);
}
return (
<Editable
textAlign='center'
defaultValue={currentValue}
fontSize='2xl'
isPreviewFocusable={false}
onSubmit={handleSubmit}
>
<Stack direction={['column', 'row']} spacing='15px'>
<EditablePreview />
<Input as={EditableInput} />
<EditableControls />
</Stack>
</Editable>
);
};
export default EditableWrapper;

View File

@ -5,54 +5,20 @@ import {
MenuList, MenuList,
MenuItem, MenuItem,
IconButton, IconButton,
useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { EditIcon } from '@chakra-ui/icons'; import { EditIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { armService } from '../../api/arm'; const MasterActionsMenu = () => {
interface MasterActionsMenu {
id: string;
}
const MasterActionsMenu = ({ id }: MasterActionsMenu) => {
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.table.actionsMenu', keyPrefix: 'dry-wash.arm.master.table.actionsMenu',
}); });
const { deleteMaster } = armService();
const toast = useToast();
const handleClickDelete = async () => {
try {
await deleteMaster({ id });
toast({
title: 'Мастер удалён.',
description: `Мастер с ID "${id}" успешно удалён.`,
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
} catch (error) {
toast({
title: 'Ошибка удаления мастера.',
description: 'Не удалось удалить мастера. Попробуйте ещё раз.',
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
console.error(error);
}
};
return ( return (
<Menu> <Menu>
<MenuButton icon={<EditIcon />} as={IconButton} variant='outline' /> <MenuButton icon={<EditIcon />} as={IconButton} variant='outline' />
<MenuList> <MenuList>
<MenuItem onClick={handleClickDelete}>{t('delete')}</MenuItem> <MenuItem>{t('delete')}</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
); );

View File

@ -11,48 +11,15 @@ import {
DrawerFooter, DrawerFooter,
DrawerHeader, DrawerHeader,
DrawerOverlay, DrawerOverlay,
useToast,
InputGroup,
InputLeftElement,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PhoneIcon } from '@chakra-ui/icons';
import { armService } from '../../api/arm';
const MasterDrawer = ({ isOpen, onClose }) => { const MasterDrawer = ({ isOpen, onClose }) => {
const { addMaster } = armService();
const toast = useToast();
const [newMaster, setNewMaster] = useState({ name: '', phone: '' }); const [newMaster, setNewMaster] = useState({ name: '', phone: '' });
const handleSave = async () => { const handleSave = () => {
if (newMaster.name.trim() === '' || newMaster.phone.trim() === '') { console.log(`Сохранение мастера: ${newMaster}`);
return; onClose();
}
try {
await addMaster(newMaster);
toast({
title: 'Мастер создан.',
description: `Мастер "${newMaster.name}" успешно добавлен.`,
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
} catch (error) {
toast({
title: 'Ошибка при создании мастера.',
description: 'Не удалось добавить мастера. Попробуйте еще раз.',
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
console.error(error);
}
}; };
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
@ -69,7 +36,6 @@ const MasterDrawer = ({ isOpen, onClose }) => {
<FormControl mb='4'> <FormControl mb='4'>
<FormLabel>{t('inputName.label')}</FormLabel> <FormLabel>{t('inputName.label')}</FormLabel>
<Input <Input
// isInvalid
value={newMaster.name} value={newMaster.name}
onChange={(e) => onChange={(e) =>
setNewMaster({ ...newMaster, name: e.target.value }) setNewMaster({ ...newMaster, name: e.target.value })
@ -78,21 +44,14 @@ const MasterDrawer = ({ isOpen, onClose }) => {
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel>{t('inputPhone.label')}</FormLabel> <FormLabel> {t('inputPhone.label')}</FormLabel>
<Input
<InputGroup> value={newMaster.phone}
<InputLeftElement pointerEvents='none'> onChange={(e) =>
<PhoneIcon color='gray.300' /> setNewMaster({ ...newMaster, phone: e.target.value })
</InputLeftElement> }
<Input placeholder={t('inputPhone.placeholder')}
// isInvalid />
value={newMaster.phone}
onChange={(e) =>
setNewMaster({ ...newMaster, phone: e.target.value })
}
placeholder={t('inputPhone.placeholder')}
/>
</InputGroup>
</FormControl> </FormControl>
</DrawerBody> </DrawerBody>
<DrawerFooter> <DrawerFooter>

View File

@ -1,11 +1,8 @@
import React from 'react'; import React from 'react';
import { Badge, Stack, Td, Tr, Text } from '@chakra-ui/react'; import { Badge, Link, Stack, Td, Tr } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import MasterActionsMenu from '../MasterActionsMenu'; import MasterActionsMenu from '../MasterActionsMenu';
import { getTimeSlot } from '../../lib'; import { getTimeSlot } from '../../lib/date-helpers';
import EditableWrapper from '../Editable/Editable';
import { armService } from '../../api/arm';
export interface Schedule { export interface Schedule {
id: string; id: string;
@ -16,49 +13,28 @@ export interface Schedule {
export type MasterProps = { export type MasterProps = {
id: string; id: string;
name: string; name: string;
phone: string;
schedule: Schedule[]; schedule: Schedule[];
phone: string;
}; };
const MasterItem = ({ name, phone, id, schedule }) => { const MasterItem = ({ name, schedule, phone }) => {
const { updateMaster } = armService();
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master',
});
return ( return (
<Tr> <Tr>
<Td>{name}</Td>
<Td> <Td>
<EditableWrapper <Stack direction='row'>
id={id} {schedule.map(({ startWashTime, endWashTime }, index) => (
as={'name'} <Badge colorScheme={'green'} key={index}>
value={name} {getTimeSlot(startWashTime, endWashTime)}
onSubmit={updateMaster} </Badge>
/> ))}
</Stack>
</Td> </Td>
<Td> <Td>
{schedule?.length > 0 ? ( <Link href='tel:'>{phone}</Link>
<Stack direction='row'>
{schedule?.map(({ startWashTime, endWashTime }, index: number) => (
<Badge colorScheme={'green'} key={index}>
{getTimeSlot(startWashTime, endWashTime)}
</Badge>
))}
</Stack>
) : (
<Text color='gray.500'>{t('schedule.empty')}</Text>
)}
</Td> </Td>
<Td> <Td>
<EditableWrapper <MasterActionsMenu />
id={id}
as={'phone'}
value={phone}
onSubmit={updateMaster}
/>
</Td>
<Td>
<MasterActionsMenu id={id} />
</Td> </Td>
</Tr> </Tr>
); );

View File

@ -3,8 +3,7 @@ import { Td, Tr, Link, Select } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MasterProps } from '../MasterItem/MasterItem'; import { getTimeSlot } from '../../lib/date-helpers';
import { getTimeSlot } from '../../lib';
const statuses = [ const statuses = [
'pending' as const, 'pending' as const,
@ -25,19 +24,6 @@ export type OrderProps = {
status?: GetArrItemType<typeof statuses>; status?: GetArrItemType<typeof statuses>;
phone?: string; phone?: string;
location?: string; location?: string;
master: MasterProps;
notes: '';
allMasters: MasterProps[];
};
type Status = (typeof statuses)[number];
const statusColors: Record<Status, string> = {
pending: 'yellow.100',
progress: 'blue.100',
working: 'orange.100',
canceled: 'red.100',
complete: 'green.100',
}; };
const OrderItem = ({ const OrderItem = ({
@ -48,16 +34,12 @@ const OrderItem = ({
status, status,
phone, phone,
location, location,
master,
allMasters,
}: OrderProps) => { }: OrderProps) => {
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.order', keyPrefix: 'dry-wash.arm.order',
}); });
const [statusSelect, setStatus] = useState(status); const [statusSelect, setStatus] = useState(status);
const bgColor = statusColors[statusSelect];
const [masterSelect, setMaster] = useState(master?.name);
return ( return (
<Tr> <Tr>
@ -69,7 +51,6 @@ const OrderItem = ({
value={statusSelect} value={statusSelect}
onChange={(e) => setStatus(e.target.value as OrderProps['status'])} onChange={(e) => setStatus(e.target.value as OrderProps['status'])}
placeholder={t(`status.placeholder`)} placeholder={t(`status.placeholder`)}
bg={bgColor}
> >
{statuses.map((status) => ( {statuses.map((status) => (
<option key={status} value={status}> <option key={status} value={status}>
@ -78,19 +59,6 @@ const OrderItem = ({
))} ))}
</Select> </Select>
</Td> </Td>
<Td>
<Select
value={masterSelect}
onChange={(e) => setMaster(e.target.value as OrderProps['status'])}
placeholder={t(`status.placeholder`)}
>
{allMasters.map((item) => (
<option key={item.id} value={item.name}>
{item.name}
</option>
))}
</Select>
</Td>
<Td> <Td>
<Link href='tel:'>{phone}</Link> <Link href='tel:'>{phone}</Link>
</Td> </Td>

View File

@ -19,14 +19,12 @@ import OrderItem from '../OrderItem';
import { OrderProps } from '../OrderItem/OrderItem'; import { OrderProps } from '../OrderItem/OrderItem';
import { armService } from '../../api/arm'; import { armService } from '../../api/arm';
import DateNavigator from '../DateNavigator'; import DateNavigator from '../DateNavigator';
import { MasterProps } from '../MasterItem/MasterItem';
const TABLE_HEADERS = [ const TABLE_HEADERS = [
'carNumber' as const, 'carNumber' as const,
'washingTime' as const, 'washingTime' as const,
'orderDate' as const, 'orderDate' as const,
'status' as const, 'status' as const,
'masters' as const,
'telephone' as const, 'telephone' as const,
'location' as const, 'location' as const,
]; ];
@ -37,29 +35,21 @@ const Orders = () => {
}); });
const { fetchOrders } = armService(); const { fetchOrders } = armService();
const { fetchMasters } = armService();
const toast = useToast(); const toast = useToast();
const [orders, setOrders] = useState<OrderProps[]>([]); const [orders, setOrders] = useState<OrderProps[]>([]);
const [allMasters, setAllMasters] = useState<MasterProps[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [currentDate, setCurrentDate] = useState(new Date()); const [currentDate, setCurrentDate] = useState(new Date());
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadOrders = async () => {
setLoading(true); setLoading(true);
setError(null);
try { try {
const [ordersData, mastersData] = await Promise.all([ const data = await fetchOrders({ date: currentDate });
fetchOrders({ date: currentDate }), setOrders(data.body);
fetchMasters(),
]);
setOrders(ordersData.body);
setAllMasters(mastersData.body);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
toast({ toast({
@ -74,8 +64,8 @@ const Orders = () => {
} }
}; };
loadData(); loadOrders();
}, [currentDate, toast, t]); }, [toast, t, currentDate]);
return ( return (
<Box p='8'> <Box p='8'>
@ -122,7 +112,6 @@ const Orders = () => {
!error && !error &&
orders.map((order, index) => ( orders.map((order, index) => (
<OrderItem <OrderItem
allMasters={allMasters}
key={index} key={index}
{...order} {...order}
status={order.status as OrderProps['status']} status={order.status as OrderProps['status']}

View File

@ -18,9 +18,7 @@ import { CarBodySelectOption } from './types';
export const CarBodySelect = forwardRef<HTMLInputElement, InputProps>( export const CarBodySelect = forwardRef<HTMLInputElement, InputProps>(
function CarBodySelect(props, ref) { function CarBodySelect(props, ref) {
const initialOption = carBodySelectOptions.find(({ value }) => value === Number(props.value)); const [selected, setSelected] = useState<Partial<CarBodySelectOption>>({});
const [selected, setSelected] = useState<Partial<CarBodySelectOption>>(initialOption);
const handleOptionClick = (option: CarBodySelectOption) => { const handleOptionClick = (option: CarBodySelectOption) => {
setSelected(option); setSelected(option);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@ -15,7 +15,7 @@ export const CarNumberInput = forwardRef<HTMLInputElement, InputProps>(
// @ts-ignore // @ts-ignore
onChange?.(formattedValue); onChange?.(formattedValue);
}} }}
maxLength={12} maxLength={8}
/> />
); );
}, },

View File

@ -3,11 +3,11 @@ const VALID_LETTER = 'а|в|е|к|м|н|о|р|с|т|у|х';
const invalidCharsRe = new RegExp(`[^(${VALID_LETTER})0-9]`, 'gi'); const invalidCharsRe = new RegExp(`[^(${VALID_LETTER})0-9]`, 'gi');
const cleanValue = (value: string) => value.replace(invalidCharsRe, ''); const cleanValue = (value: string) => value.replace(invalidCharsRe, '');
const validCarNumberInputRe = new RegExp(`^([${VALID_LETTER}]{1}|$)((?:[0-9]|$)(?:[0-9]|$)(?:[0-9]|$))([${VALID_LETTER}]{1,2}|$)((?:[0-9]|$)(?:[0-9]|$)(?:[0-9]|$))$`, 'gi'); const validCarNumberInputRe = new RegExp(`^([${VALID_LETTER}]{1}|$)((?:[0-9]|$)(?:[0-9]|$)(?:[0-9]|$))([${VALID_LETTER}]{1,2}|$)$`, 'gi');
const isValidInput = (cleanedValue: string) => validCarNumberInputRe.test(cleanedValue); const isValidInput = (cleanedValue: string) => validCarNumberInputRe.test(cleanedValue);
const formatAsCarNumber = (cleanedValue: string) => { const formatAsCarNumber = (cleanedValue: string) => {
return cleanedValue.replace(validCarNumberInputRe, (_, p1, p2, p3, p4) => [p1, p2, p3, p4].join(' ')).toUpperCase(); return cleanedValue.replace(validCarNumberInputRe, (_, p1, p2, p3) => [p1, p2, p3].join(' ')).toUpperCase();
}; };
const getWithoutLastChar = (value: string) => value.substring(0, value.length - 1); const getWithoutLastChar = (value: string) => value.substring(0, value.length - 1);
@ -25,7 +25,7 @@ export const handleInputChange = (value: string | undefined | null) => {
return getWithoutLastChar(value).trim(); return getWithoutLastChar(value).trim();
}; };
const validCarNumberRe = new RegExp(`^[${VALID_LETTER}][0-9]{3}[${VALID_LETTER}]{2}[0-9]{2,3}$`, 'i'); const validCarNumberRe = new RegExp(`^[${VALID_LETTER}][0-9]{3}[${VALID_LETTER}]{2}$`, 'i');
export const isValidCarNumber = (value: string) => { export const isValidCarNumber = (value: string) => {
const cleanedValue = cleanValue(value); const cleanedValue = cleanValue(value);

View File

@ -1,4 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { InputProps, SelectProps } from "@chakra-ui/react";
import { Order } from "../../../models/landing";
import { FormFieldProps } from "./field"; import { FormFieldProps } from "./field";
import { OrderFormValues } from "./types"; import { OrderFormValues } from "./types";
@ -26,4 +29,39 @@ export const useGetValidationRules = () => {
validate: (value: string) => isValidCarNumber(value) || t('car-number-field.invalid') validate: (value: string) => isValidCarNumber(value) || t('car-number-field.invalid')
}, },
} satisfies Record<string, FormFieldProps['rules']>; } satisfies Record<string, FormFieldProps['rules']>;
};
const removeAllSpaces = (str: string) => str.replace(/\s+/g, '');
const getValidCarBodyStyle = (fieldValue: string) => {
const carBodyAsNumber = Number(fieldValue);
return Number.isNaN(carBodyAsNumber) ? undefined : carBodyAsNumber;
};
export const formatFormValues = ({ phone, carNumber, carBody, carColor, carLocation, availableDatetimeBegin, availableDatetimeEnd }: OrderFormValues): Order.Create => {
return {
customer: {
phone
},
car: {
number: removeAllSpaces(carNumber),
body: getValidCarBodyStyle(carBody),
color: carColor
},
washing: {
location: carLocation,
begin: availableDatetimeBegin,
end: availableDatetimeEnd,
}
};
};
export const onSubmit = (values: OrderFormValues) => {
return new Promise((resolve) => {
console.log(formatFormValues(values));
resolve(formatFormValues(values));
});
};
export const inputCommonStyles: Partial<InputProps & SelectProps> = {
}; };

View File

@ -1,2 +1 @@
export type { OrderFormValues, OrderFormProps } from './types';
export { OrderForm } from './order-form'; export { OrderForm } from './order-form';

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { FC } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Box, Flex, FormControl, FormLabel, VStack } from '@chakra-ui/react'; import { Box, Flex, FormControl, FormLabel, VStack } from '@chakra-ui/react';
@ -7,19 +7,14 @@ import { CarBodySelect } from './car-body';
import { CarColorInput } from './car-color'; import { CarColorInput } from './car-color';
import { CarNumberInput } from './car-number'; import { CarNumberInput } from './car-number';
import { FormInputField, FormControllerField } from './field'; import { FormInputField, FormControllerField } from './field';
import { OrderFormProps, OrderFormValues } from './types'; import { OrderFormValues } from './types';
import { PhoneInput } from './phone'; import { PhoneInput } from './phone';
import { SubmitButton } from './submit'; import { SubmitButton } from './submit';
import { defaultValues, useGetValidationRules } from './helper'; import { defaultValues, onSubmit, useGetValidationRules } from './helper';
import { DateTimeInput } from './date-time'; import { DateTimeInput } from './date-time';
import { import { LocationInput, MapComponent, StringLocation, YMapsProvider } from './location';
LocationInput,
MapComponent,
StringLocation,
YMapsProvider,
} from './location';
export const OrderForm = ({ onSubmit, loading }: OrderFormProps) => { export const OrderForm: FC = () => {
const { const {
handleSubmit, handleSubmit,
control, control,
@ -128,7 +123,7 @@ export const OrderForm = ({ onSubmit, loading }: OrderFormProps) => {
}} }}
/> />
</YMapsProvider> </YMapsProvider>
<SubmitButton isLoading={isSubmitting || loading} mt={4} /> <SubmitButton isLoading={isSubmitting} mt={4} />
</VStack> </VStack>
</Box> </Box>
); );

View File

@ -1,5 +1,3 @@
import { SubmitHandler } from "react-hook-form";
export type OrderFormValues = { export type OrderFormValues = {
phone: string; phone: string;
carNumber: string; carNumber: string;
@ -8,9 +6,4 @@ export type OrderFormValues = {
carLocation: string; carLocation: string;
availableDatetimeBegin: string; availableDatetimeBegin: string;
availableDatetimeEnd: string; availableDatetimeEnd: string;
};
export type OrderFormProps = {
onSubmit: SubmitHandler<OrderFormValues>;
loading: boolean;
}; };

View File

@ -17,19 +17,7 @@ import { carBodySelectOptions } from '../../order-form/form/car-body/helper';
import { OrderStatus } from './status'; import { OrderStatus } from './status';
type OrderDetailsProps = Pick< type OrderDetailsProps = Order.View;
Order.View,
| 'id'
| 'status'
| 'phone'
| 'carNumber'
| 'carBody'
| 'carColor'
| 'location'
| 'startWashTime'
| 'endWashTime'
| 'created'
>;
export const OrderDetails: FC<OrderDetailsProps> = ({ export const OrderDetails: FC<OrderDetailsProps> = ({
id, id,
@ -39,8 +27,8 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
carBody, carBody,
carColor, carColor,
location, location,
startWashTime, datetimeBegin,
endWashTime, datetimeEnd,
}) => { }) => {
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-view.details', keyPrefix: 'dry-wash.order-view.details',
@ -87,8 +75,8 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
{ {
label: t('datetime-range'), label: t('datetime-range'),
value: [ value: [
formatDatetime(startWashTime), formatDatetime(datetimeBegin),
formatDatetime(endWashTime), formatDatetime(datetimeEnd),
].join(' - '), ].join(' - '),
}, },
].map(({ label, value }, i) => ( ].map(({ label, value }, i) => (

View File

@ -1,31 +1,14 @@
import React, { ComponentType, FC, PropsWithChildren } from 'react'; import React, { FC, PropsWithChildren } from 'react';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { default as landingTheme } from './theme-config'; import { default as landingTheme } from './theme-config';
import Fonts from './Fonts'; import Fonts from './Fonts';
import { toastOptions } from './toast-options';
export const LandingThemeProvider: FC<PropsWithChildren> = ({ children }) => { export const LandingThemeProvider: FC<PropsWithChildren> = ({ children }) => {
return ( return (
<ChakraProvider theme={landingTheme} toastOptions={toastOptions}> <ChakraProvider theme={landingTheme}>
<Fonts /> <Fonts />
{children} {children}
</ChakraProvider> </ChakraProvider>
); );
}; };
export function withLandingThemeProvider<T extends JSX.IntrinsicAttributes>(WrappedComponent: ComponentType<T>) {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const ComponentWithLandingThemeProvider = (props: T) => {
return (
<LandingThemeProvider>
<WrappedComponent {...props} />
</LandingThemeProvider>
);
};
ComponentWithLandingThemeProvider.displayName = `withLandingThemeProvider(${displayName})`;
return ComponentWithLandingThemeProvider;
}

View File

@ -1 +1 @@
export { LandingThemeProvider, withLandingThemeProvider } from './LandingThemeProvider'; export { LandingThemeProvider } from './LandingThemeProvider';

View File

@ -1,8 +0,0 @@
import { ToastProviderProps } from "@chakra-ui/react";
export const toastOptions: ToastProviderProps = {
defaultOptions: {
position: 'top-right',
isClosable: true,
}
};

View File

@ -1 +1 @@
export * from './order'; export * from './order-view';

View File

@ -0,0 +1,14 @@
import { Order } from "../landing";
export type FetchOrderQueryResponse = {
id: string;
orderDate: string;
carNumber: string;
carBody: number;
carColor?: string;
startWashTime: string;
endWashTime: string;
status: Order.Status;
phone: string;
location: string;
};

View File

@ -1,18 +0,0 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { Order } from "../landing";
export namespace CreateOrder {
export type Response = {
id: Order.Id
};
export type Params = {
body: Order.Create
};
};
export namespace GetOrder {
export type Response = Order.View;
export type Params = {
orderId: Order.Id
};
};

View File

@ -1 +0,0 @@
export type IsoDate = string; // YYYY-MM-DDThh:mm:ss.mmmZ

View File

@ -1,4 +1,4 @@
export type RegistrationNumber = string; // А012ВЕ16 export type RegistrationNumber = string; // А012ВЕ
export type Color = string; // #000000 export type Color = string; // #000000

View File

@ -1,5 +1,3 @@
import { IsoDate } from "../common";
import { Car, Customer, Washing } from "."; import { Car, Customer, Washing } from ".";
export type Id = string; export type Id = string;
@ -27,16 +25,14 @@ export type Create = {
}; };
export type View = { export type View = {
id: Id;
orderDate: string,
status: Status,
phone: Customer.PhoneNumber; phone: Customer.PhoneNumber;
carNumber: Car.RegistrationNumber; carNumber: Car.RegistrationNumber;
carBody: Car.BodyStyle; carBody: Car.BodyStyle;
carColor?: Car.Color; carColor?: Car.Color;
location: Washing.Location; location: Washing.Location;
startWashTime: Washing.AvailableBeginDateTime; datetimeBegin: Washing.AvailableBeginDateTime;
endWashTime: Washing.AvailableEndDateTime; datetimeEnd: Washing.AvailableEndDateTime;
status: Status,
notes: string;
created: IsoDate;
updated: IsoDate;
id: Id;
}; };

View File

@ -1,7 +1,5 @@
import { IsoDate } from "../common"; export type Location = string; // ?
export type Location = string; // 55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500 export type AvailableBeginDateTime = string; // YYYY-MM-DDThh:mm
export type AvailableBeginDateTime = IsoDate; export type AvailableEndDateTime = string; // YYYY-MM-DDThh:mm
export type AvailableEndDateTime = IsoDate;

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AbsoluteCenter, Box, Spinner } from '@chakra-ui/react'; import { AbsoluteCenter, Spinner } from '@chakra-ui/react';
import LayoutArm from '../../components/LayoutArm'; import LayoutArm from '../../components/LayoutArm';
import authLogin from '../../keycloak'; import authLogin from '../../keycloak';
import { URLs } from '../../__data__/urls'; import { URLs } from '../../__data__/urls';
const Page = () => { const Page = () => {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
@ -19,11 +20,9 @@ const Page = () => {
if (!user) if (!user)
return ( return (
<Box position='relative' height='100vh'> <AbsoluteCenter>
<AbsoluteCenter> <Spinner />
<Spinner /> </AbsoluteCenter>
</AbsoluteCenter>
</Box>
); );
return <LayoutArm />; return <LayoutArm />;

View File

@ -1,29 +0,0 @@
import dayjs from "dayjs";
import { Order } from "../../models/landing";
import { OrderFormValues } from "../../components/order-form";
const removeAllSpaces = (str: string) => str.replace(/\s+/g, '');
const getValidCarBodyStyle = (fieldValue: string) => {
const carBodyAsNumber = Number(fieldValue);
return Number.isNaN(carBodyAsNumber) ? undefined : carBodyAsNumber;
};
export const formatFormValues = ({ phone, carNumber, carBody, carColor, carLocation, availableDatetimeBegin, availableDatetimeEnd }: OrderFormValues): Order.Create => {
return {
customer: {
phone
},
car: {
number: removeAllSpaces(carNumber),
body: getValidCarBodyStyle(carBody),
color: carColor
},
washing: {
location: carLocation,
begin: dayjs(availableDatetimeBegin).toISOString(),
end: dayjs(availableDatetimeEnd).toISOString(),
}
};
};

View File

@ -1,63 +1,32 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Container, Heading, useToast, VStack } from '@chakra-ui/react'; import { Container, Heading, VStack } from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import { withLandingThemeProvider } from '../../containers'; import { LandingThemeProvider } from '../../containers';
import { OrderForm, OrderFormProps } from '../../components/order-form'; import { OrderForm } from '../../components/order-form';
import { useCreateOrderMutation } from '../../api';
import { URLs } from '../../__data__/urls';
import { formatFormValues } from './helper';
const Page: FC = () => { const Page: FC = () => {
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-create', keyPrefix: 'dry-wash.order-create',
}); });
const [createOrder, createOrderMutation] = useCreateOrderMutation();
const toast = useToast();
const navigate = useNavigate();
const onOrderFormSubmit: OrderFormProps['onSubmit'] = (values) => {
createOrder({ body: formatFormValues(values) })
.then(({ body: { id: orderId } }) => {
navigate({ pathname: URLs.orderView.getUrl(orderId) });
toast({
status: 'success',
title: t('create-order-query.success.title'),
});
})
.catch(({ error: errorMessage }) => {
toast({
status: 'error',
title: t('create-order-query.error.title'),
description: errorMessage,
});
});
};
return ( return (
<Container <LandingThemeProvider>
w='full' <Container
maxWidth='container.xl' w='full'
minH='100vh' maxWidth='container.xl'
padding={0} minH='100vh'
bg='white' padding={0}
centerContent bg='white'
> centerContent
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}> >
<Heading textAlign='center' mt={4}> <VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
{t('title')} <Heading textAlign='center' mt={4}>{t('title')}</Heading>
</Heading> <OrderForm />
<OrderForm </VStack>
onSubmit={onOrderFormSubmit} </Container>
loading={createOrderMutation.isLoading} </LandingThemeProvider>
/>
</VStack>
</Container>
); );
}; };
export default withLandingThemeProvider(Page); export default Page;

View File

@ -0,0 +1,40 @@
import { useEffect, useState } from 'react';
import { LandingService } from '../../api/landing';
import { Order } from '../../models/landing';
import { FetchOrderQueryResponse } from '../../models/api';
export const useFetchOrderDetails = ({
orderId,
}: {
orderId: Order.View['id'];
}) => {
const { fetchOrder } = LandingService();
const [data, setData] = useState<FetchOrderQueryResponse>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const data = await fetchOrder(orderId);
setData(data.body);
} catch (error) {
setError(error.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
return {
isLoading,
data,
error,
};
};

View File

@ -1,32 +1,36 @@
import React, { FC } from 'react'; import React, { FC, useEffect } from 'react';
import { Alert, AlertDescription, AlertIcon, AlertTitle, HStack, Spinner } from '@chakra-ui/react'; import { HStack, Spinner, useToast } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Container, Heading, VStack } from '@chakra-ui/react'; import { Container, Heading, VStack } from '@chakra-ui/react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import { LandingThemeProvider } from '../../containers';
LandingThemeProvider,
withLandingThemeProvider,
} from '../../containers';
import { OrderDetails } from '../../components/order-view'; import { OrderDetails } from '../../components/order-view';
import { Order } from '../../models/landing';
import { useGetOrderQuery } from '../../api'; import { useFetchOrderDetails } from './helper';
const Page: FC = () => { const Page: FC = () => {
const { t } = useTranslation('~', { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.order-view', keyPrefix: 'dry-wash.order-view',
}); });
const { orderId } = useParams<Order.Id>(); const { orderId } = useParams();
const {
isLoading, const { isLoading, data, error } = useFetchOrderDetails({ orderId });
isSuccess,
data: { body: order } = {}, const toast = useToast();
isError, useEffect(() => {
error, if (error) {
} = useGetOrderQuery({ toast({
orderId, title: t('error.title'),
}); description: t('fetch.error'),
status: 'error',
duration: 5000,
isClosable: true,
position: 'bottom-right',
});
}
}, [error]);
return ( return (
<LandingThemeProvider> <LandingThemeProvider>
@ -47,37 +51,20 @@ const Page: FC = () => {
<Spinner size='lg' /> <Spinner size='lg' />
</HStack> </HStack>
) : ( ) : (
<> data && (
<> <OrderDetails
{isSuccess && ( id={data.id}
<OrderDetails orderDate={data.orderDate}
id={order.id} status={data.status}
status={order.status} phone={data.phone}
phone={order.phone} carNumber={data.carNumber}
carNumber={order.carNumber} carBody={data.carBody}
carBody={order.carBody} carColor={data.carColor}
carColor={order.carColor} location={data.location}
location={order.location} datetimeBegin={data.startWashTime}
startWashTime={order.startWashTime} datetimeEnd={data.endWashTime}
endWashTime={order.endWashTime} />
created={order.created} )
/>
)}
</>
<>
{isError && (
<Alert status='error'>
<AlertIcon />
<AlertTitle>
{t('get-order-query.error.title', {
number: orderId,
})}
</AlertTitle>
<AlertDescription>{error.data?.error}</AlertDescription>
</Alert>
)}
</>
</>
)} )}
</VStack> </VStack>
</Container> </Container>
@ -85,4 +72,4 @@ const Page: FC = () => {
); );
}; };
export default withLandingThemeProvider(Page); export default Page;

View File

@ -23,36 +23,6 @@ router.get('/arm/masters', (req, res) => {
); );
}); });
router.post('/arm/masters', (req, res) => {
res
.status(/error/.test(STUBS.masters) ? 500 : 200)
.send(
/^error$/.test(STUBS.masters)
? commonError
: require(`../json/arm-masters/${STUBS.masters}.json`),
);
});
router.patch('/arm/masters/:id', (req, res) => {
res
.status(/error/.test(STUBS.masters) ? 500 : 200)
.send(
/^error$/.test(STUBS.masters)
? commonError
: require(`../json/arm-masters/${STUBS.masters}.json`),
);
});
router.delete('/arm/masters/:id', (req, res) => {
res
.status(/error/.test(STUBS.masters) ? 500 : 200)
.send(
/^error$/.test(STUBS.masters)
? commonError
: require(`../json/arm-masters/${STUBS.masters}.json`),
);
});
router.post('/arm/orders', (req, res) => { router.post('/arm/orders', (req, res) => {
res res
.status(/error/.test(STUBS.orders) ? 500 : 200) .status(/error/.test(STUBS.orders) ? 500 : 200)
@ -76,12 +46,6 @@ router.get('/order/:orderId', ({ params }, res) => {
); );
}); });
router.post('/order/create', (req, res) => {
res
.status(200)
.send({ success: true, body: { ok: true } });
});
router.use('/admin', require('./admin')); router.use('/admin', require('./admin'));
module.exports = router; module.exports = router;

View File

@ -2,12 +2,22 @@
"success": true, "success": true,
"body": [ "body": [
{ {
"id": "4545423234", "id": "masters1",
"name": "Иван Иванов", "name": "Иван Иванов",
"schedule": [ {
"id": "order1",
"startWashTime": "2024-11-24T10:30:00.000Z",
"endWashTime": "2024-11-24T16:30:00.000Z"
},
{
"id": "order2",
"startWashTime": "2024-11-24T11:30:00.000Z",
"endWashTime": "2024-11-24T17:30:00.000Z"
}],
"phone": "+7 900 123 45 67" "phone": "+7 900 123 45 67"
}, },
{ {
"id": "345354234", "id": "masters12",
"name": "Иван Иванов", "name": "Иван Иванов",
"schedule": [ { "schedule": [ {
"id": "order1", "id": "order1",

View File

@ -9,9 +9,7 @@
"orderDate": "2024-11-24T08:41:46.366Z", "orderDate": "2024-11-24T08:41:46.366Z",
"status": "progress", "status": "progress",
"phone": "79001234563", "phone": "79001234563",
"location": "Казань, ул. Баумана, 1", "location": "Казань, ул. Баумана, 1"
"master": [],
"notes": ""
}, },
{ {
"id": "order2", "id": "order2",
@ -21,9 +19,7 @@
"orderDate": "2024-11-24T07:40:46.366Z", "orderDate": "2024-11-24T07:40:46.366Z",
"status": "progress", "status": "progress",
"phone": "79001234567", "phone": "79001234567",
"location": "Казань, ул. Баумана, 43", "location": "Казань, ул. Баумана, 43"
"master": [],
"notes": ""
} }
] ]
} }

View File

@ -1,17 +1,14 @@
{ {
"success": true, "success": true,
"body": { "body": {
"phone": "+79876543210", "id": "order1",
"carNumber": "А123АА16", "orderDate": "2024-11-24T08:41:46.366Z",
"carBody": 2, "status": "progress",
"carColor": "#ffffff", "carNumber": "A123BC",
"startWashTime": "2025-01-19T14:03:00.000Z", "carBody": 1,
"endWashTime": "2025-01-19T14:03:00.000Z", "startWashTime": "2024-11-24T10:30:00.000Z",
"location": "55.793833888711006,49.19037910644527 Республика Татарстан (Татарстан), Казань, жилой район Седьмое Небо", "endWashTime": "2024-11-24T16:30:00.000Z",
"status": "progress", "phone": "79001234563",
"notes": "", "location": "55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500"
"created": "2025-01-19T14:04:02.985Z",
"updated": "2025-01-19T14:04:02.987Z",
"id": "678d06527d78ec30be2679d8"
} }
} }