Compare commits
74 Commits
feature/pl
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
| fea4a54c83 | |||
| 9349a41f84 | |||
| 8103fdcb56 | |||
| a59864a3e2 | |||
| a252301ea0 | |||
| 48cdfb92bd | |||
|
|
279da05b32 | ||
|
|
c6d906ad62 | ||
|
|
418a121418 | ||
|
|
7647958e52 | ||
|
|
9e024af568 | ||
|
|
7fe62e7d6e | ||
|
|
f2db3774c9 | ||
|
|
02e3fdffa0 | ||
|
|
965d2d0be1 | ||
|
|
9121fcaa43 | ||
|
|
290bb3f0a3 | ||
|
|
e6e73e5289 | ||
|
|
b959fcf8db | ||
|
|
5e25848e22 | ||
|
|
ecf6c60cdc | ||
|
|
91fc91230b | ||
| d312445bf2 | |||
|
|
351420bc62 | ||
|
|
61b042eee6 | ||
|
|
ac006267a2 | ||
|
|
63d9d069c0 | ||
| c83ebf02bc | |||
|
|
1968df7bb3 | ||
|
|
811e0e3f24 | ||
| a2ffd6f38f | |||
|
|
20017cad3c | ||
|
|
de54ac6669 | ||
|
|
eda869622e | ||
|
|
7b3889aa02 | ||
|
|
cee124fca5 | ||
|
|
b77eccc8e8 | ||
|
|
ebfaa7ea8f | ||
|
|
0027cc09b1 | ||
|
|
dd612d662c | ||
|
|
69251745fa | ||
|
|
253e3b3856 | ||
|
|
c9c17340c6 | ||
|
|
7fc5455c37 | ||
|
|
24779e2592 | ||
|
|
9111724519 | ||
|
|
c2511e0917 | ||
| b7d935f557 | |||
| fdc20e7464 | |||
| a616d3815b | |||
| 56f65fbd3a | |||
|
|
73d649f519 | ||
|
|
cf6b8ebd51 | ||
|
|
251ea5184e | ||
|
|
f07f7aeba5 | ||
|
|
bbc96a2f27 | ||
|
|
88242c5681 | ||
|
|
b2a067a644 | ||
|
|
52fec13377 | ||
|
|
1d4b796a39 | ||
|
|
142067e9ce | ||
| 3c291baa0e | |||
|
|
3cdc47b91b | ||
|
|
901d8d78a1 | ||
|
|
7736592830 | ||
|
|
8d447c9461 | ||
| 6506b89dc7 | |||
| 89d432b360 | |||
| f6cc2efb86 | |||
|
|
0ec9e146b9 | ||
|
|
1669f01879 | ||
| 658e23d4e3 | |||
| ed8ae95436 | |||
| a00aaff29d |
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
]
|
||||
}
|
||||
28
Jenkinsfile
vendored
28
Jenkinsfile
vendored
@@ -1,7 +1,7 @@
|
||||
pipeline {
|
||||
agent {
|
||||
docker {
|
||||
image 'node:20'
|
||||
image 'node:22'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,19 +30,21 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
stage('checks') {
|
||||
parallel {
|
||||
stage('eslint') {
|
||||
steps {
|
||||
sh 'npm run eslint'
|
||||
}
|
||||
}
|
||||
stage('eslint') {
|
||||
steps {
|
||||
sh 'npm run eslint'
|
||||
}
|
||||
}
|
||||
|
||||
stage('build') {
|
||||
steps {
|
||||
sh 'npm run build'
|
||||
}
|
||||
}
|
||||
stage('test') {
|
||||
steps {
|
||||
sh 'npm run test'
|
||||
}
|
||||
}
|
||||
|
||||
stage('build') {
|
||||
steps {
|
||||
sh 'npm run build'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
__mocks__/app-context-mock.tsx
Normal file
18
__mocks__/app-context-mock.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { jest } from '@jest/globals';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import ErrorBoundary from '../src/components/ErrorBoundary';
|
||||
import { store } from '../src/__data__/store';
|
||||
|
||||
export const AppContext = jest.fn(({ children }: PropsWithChildren) => (
|
||||
<Provider store={store}>
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
</Provider>
|
||||
));
|
||||
18
__mocks__/brojs-cli-mock.ts
Normal file
18
__mocks__/brojs-cli-mock.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
jest.mock('@brojs/cli', () => ({
|
||||
getConfigValue: jest.fn(() => '/api'),
|
||||
getFeatures: jest.fn(() => ({
|
||||
['order-view-status-polling']: { value: '3000' }
|
||||
})),
|
||||
getNavigationValue: jest.fn((navKey: string) => {
|
||||
switch (navKey) {
|
||||
case 'dry-wash.main':
|
||||
return '/dry-wash';
|
||||
case 'dry-wash.order.create':
|
||||
return '/order';
|
||||
case 'dry-wash.order.view':
|
||||
return '/order/:orderId';
|
||||
}
|
||||
}),
|
||||
}));
|
||||
6
__mocks__/lottiefiles-mock.tsx
Normal file
6
__mocks__/lottiefiles-mock.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import React from 'react';
|
||||
|
||||
jest.mock('@lottiefiles/react-lottie-player', () => ({
|
||||
Player: jest.fn(() => <></>),
|
||||
}));
|
||||
13
__mocks__/react-i18next.ts
Normal file
13
__mocks__/react-i18next.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import localeRu from '../locales/ru.json';
|
||||
|
||||
module.exports = {
|
||||
useTranslation: (_, options) => {
|
||||
const { keyPrefix } = options ?? {};
|
||||
return {
|
||||
t: keyPrefix ? (key: string) => localeRu[`${keyPrefix}.${key}`] : undefined,
|
||||
i18n: {
|
||||
language: 'ru'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
11
__mocks__/react-yandex-maps-mock.tsx
Normal file
11
__mocks__/react-yandex-maps-mock.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import React from 'react';
|
||||
|
||||
jest.mock('@pbe/react-yandex-maps', () => ({
|
||||
YMaps: jest.fn(() => <></>),
|
||||
Map: jest.fn(() => <></>),
|
||||
Placemark: jest.fn(() => <></>),
|
||||
GeolocationControl: jest.fn(() => <></>),
|
||||
ZoomControl: jest.fn(() => <></>),
|
||||
withYMaps: jest.fn(() => <></>),
|
||||
}));
|
||||
20
__mocks__/server/handlers.ts
Normal file
20
__mocks__/server/handlers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { http, delay, HttpResponse } from 'msw';
|
||||
|
||||
import OrderPendingMock from '../../stubs/json/landing-order-view/id1-success-pending.json';
|
||||
import OrderErrorMock from '../../stubs/json/landing-order-view/id1-error.json';
|
||||
|
||||
export const handlers = [
|
||||
http.get('/api/order/:id', async ({ params }) => {
|
||||
await delay();
|
||||
|
||||
const { id } = params;
|
||||
if (id === 'id1') {
|
||||
return HttpResponse.json(OrderPendingMock);
|
||||
}
|
||||
|
||||
return new HttpResponse(null, {
|
||||
status: 500,
|
||||
statusText: OrderErrorMock.message
|
||||
});
|
||||
})
|
||||
];
|
||||
5
__mocks__/server/server.ts
Normal file
5
__mocks__/server/server.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
import { handlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
1
__mocks__/style-mock.ts
Normal file
1
__mocks__/style-mock.ts
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@@ -16,12 +16,27 @@ module.exports = {
|
||||
'dry-wash.order.view': '/order/:orderId',
|
||||
'dry-wash.arm.master': 'master',
|
||||
'dry-wash.arm.order': 'order',
|
||||
'dry-wash.arm.map': 'map',
|
||||
'dry-wash.arm': '/arm/*',
|
||||
},
|
||||
features: {
|
||||
'dry-wash-pl': {
|
||||
'dry-wash': {
|
||||
// add your features here in the format [featureName]: { value: string }
|
||||
'order-view-status-polling': { value: '3000' }
|
||||
'order-view-status-polling': {
|
||||
on: true,
|
||||
value: '3000',
|
||||
key: 'order-view-status-polling',
|
||||
},
|
||||
'car-img-upload': {
|
||||
on: true,
|
||||
value: 'true',
|
||||
key: 'car-img-upload',
|
||||
},
|
||||
'order-cost': {
|
||||
on: true,
|
||||
value: '1000',
|
||||
key: 'order-cost',
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach('check server is up', async ({ page }) => {
|
||||
const makeOrderText = page.getByText('Сделать заказ', { exact: true });
|
||||
await expect(makeOrderText).toBeVisible();
|
||||
} catch (error) {
|
||||
console.error('server not up');
|
||||
console.error('server not up', error);
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'babel-jest',
|
||||
},
|
||||
@@ -8,10 +9,14 @@ module.exports = {
|
||||
collectCoverage: true,
|
||||
clearMocks: true,
|
||||
moduleNameMapper: {
|
||||
'\\.(svg|webp)$': '<rootDir>/__mocks__/file',
|
||||
'\\.(svg|webp)$': '<rootDir>/__mocks__/file-mock',
|
||||
'\\.(css|scss)$': '<rootDir>/__mocks__/style-mock',
|
||||
'react-i18next': '<rootDir>/__mocks__/react-i18next',
|
||||
},
|
||||
testEnvironmentOptions: {
|
||||
customExportConditions: [''],
|
||||
},
|
||||
testEnvironment: 'jest-fixed-jsdom',
|
||||
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/e2e'],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest-preset-it/jest.setup.js', '<rootDir>/__mocks__/brojs-cli-mock.ts', '<rootDir>/__mocks__/lottiefiles-mock.tsx'],
|
||||
};
|
||||
|
||||
5
jest-preset-it/jest.setup.js
Normal file
5
jest-preset-it/jest.setup.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef
|
||||
require('@testing-library/jest-dom');
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
global.__webpack_public_path__ = '';
|
||||
@@ -39,6 +39,7 @@
|
||||
"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.form.submit-button.label": "Submit",
|
||||
"dry-wash.order-create.order-creation-title": "Creating order ...",
|
||||
"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",
|
||||
@@ -49,6 +50,12 @@
|
||||
"dry-wash.order-view.details.location": "Where",
|
||||
"dry-wash.order-view.details.datetime-range": "When",
|
||||
"dry-wash.order-view.details.alert": "The operator will contact you about the payment at the specified phone number",
|
||||
"dry-wash.order-view.upload-car-image.field.label": "Upload a photo of your car, and our service will quickly calculate the pre-order price!",
|
||||
"dry-wash.order-view.upload-car-image.field.help": "Allowed formats: .jpg, .png. Maximum size: 5MB",
|
||||
"dry-wash.order-view.upload-car-image.file-input.placeholder": "Upload a file",
|
||||
"dry-wash.order-view.upload-car-image.file-input.button": "Upload",
|
||||
"dry-wash.order-view.upload-car-image-query.success.title": "The car image is successfully uploaded",
|
||||
"dry-wash.order-view.upload-car-image-query.error.title": "Failed to upload the car image",
|
||||
"dry-wash.arm.master.add": "Add",
|
||||
"dry-wash.arm.order.title": "Orders",
|
||||
"dry-wash.arm.order.table.empty": "Table empty",
|
||||
@@ -57,7 +64,7 @@
|
||||
"dry-wash.arm.order.status.complete": "Completed",
|
||||
"dry-wash.arm.order.status.pending": "Pending",
|
||||
"dry-wash.arm.order.status.working": "Working",
|
||||
"dry-wash.arm.order.status.canceled": "Canceled",
|
||||
"dry-wash.arm.order.status.cancelled": "Canceled",
|
||||
"dry-wash.arm.order.status.placeholder": "Select status",
|
||||
"dry-wash.arm.order.master.placeholder": "Select master",
|
||||
"dry-wash.arm.order.table.header.carNumber": "Car Number",
|
||||
@@ -104,5 +111,8 @@
|
||||
"dry-wash.errorBoundary.title": "Something went wrong",
|
||||
"dry-wash.errorBoundary.description": "We are already working on fixing the issue",
|
||||
"dry-wash.errorBoundary.button.reload": "Reload Page",
|
||||
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}"
|
||||
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}",
|
||||
"dry-wash.arm.map.title": "Map of orders",
|
||||
"dry-wash.arm.map.carNumber": "Car Number",
|
||||
"dry-wash.arm.map.status": "Status"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"dry-wash.arm.order.status.complete": "Завершено",
|
||||
"dry-wash.arm.order.status.pending": "В ожидании",
|
||||
"dry-wash.arm.order.status.working": "В работе",
|
||||
"dry-wash.arm.order.status.canceled": "Отменено",
|
||||
"dry-wash.arm.order.status.cancelled": "Отменено",
|
||||
"dry-wash.arm.order.status.placeholder": "Выберите статус",
|
||||
"dry-wash.arm.order.master.placeholder": "Выберите мастера",
|
||||
"dry-wash.arm.order.table.header.carNumber": "Номер машины",
|
||||
@@ -25,10 +25,16 @@
|
||||
"dry-wash.arm.master.table.header.phone": "Телефон",
|
||||
"dry-wash.arm.master.table.header.actions": "Действия",
|
||||
"dry-wash.arm.master.table.actionsMenu.delete": "Удалить мастера",
|
||||
"dry-wash.arm.master.table.actionsMenu.toast.success": "Мастер удалён",
|
||||
"dry-wash.arm.master.table.actionsMenu.toast.error.title": "Ошибка!",
|
||||
"dry-wash.arm.master.table.actionsMenu.toast.error.description": "Не удалось удалить мастера. Попробуйте ещё раз.",
|
||||
"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.editable.toast.success": "Успешно!",
|
||||
"dry-wash.arm.master.editable.toast.error.description": "Не удалось обновить данные",
|
||||
"dry-wash.arm.master.editable.toast.error.title": "Ошибка!",
|
||||
"dry-wash.arm.master.drawer.title": "Добавить нового мастера",
|
||||
"dry-wash.arm.master.drawer.inputName.label": "ФИО",
|
||||
"dry-wash.arm.master.drawer.inputName.placeholder": "Введите ФИО",
|
||||
@@ -88,6 +94,7 @@
|
||||
"dry-wash.order-create.car-body-select.options.sports-car": "Спорткар",
|
||||
"dry-wash.order-create.car-body-select.options.other": "Другой",
|
||||
"dry-wash.order-create.form.submit-button.label": "Отправить",
|
||||
"dry-wash.order-create.order-creation-title": "Создаем заказ ...",
|
||||
"dry-wash.order-create.create-order-query.success.title": "Заказ успешно создан",
|
||||
"dry-wash.order-create.create-order-query.error.title": "Не удалось создать заказ",
|
||||
"dry-wash.order-view.title": "Ваш заказ",
|
||||
@@ -98,11 +105,20 @@
|
||||
"dry-wash.order-view.details.location": "Где",
|
||||
"dry-wash.order-view.details.datetime-range": "Когда",
|
||||
"dry-wash.order-view.details.alert": "С вами свяжется оператор насчет оплаты по указанному номеру телефона",
|
||||
"dry-wash.order-view.upload-car-image.field.label": "Загрузите фото вашего автомобиля, и наш сервис быстро рассчитает предварительную стоимость заказа!",
|
||||
"dry-wash.order-view.upload-car-image.field.help": "Допустимые форматы: .jpg, .png. Максимальный размер: 5МБ",
|
||||
"dry-wash.order-view.upload-car-image.file-input.placeholder": "Загрузите файл",
|
||||
"dry-wash.order-view.upload-car-image.file-input.button": "Загрузить",
|
||||
"dry-wash.order-view.upload-car-image-query.success.title": "Изображение автомобиля успешно загружено",
|
||||
"dry-wash.order-view.upload-car-image-query.error.title": "Не удалось загрузить изображение автомобиля",
|
||||
"dry-wash.notFound.title": "Страница не найдена",
|
||||
"dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.",
|
||||
"dry-wash.notFound.button.back": "Вернуться на главную",
|
||||
"dry-wash.errorBoundary.title": "Что-то пошло не так",
|
||||
"dry-wash.errorBoundary.description": "Мы уже работаем над исправлением проблемы",
|
||||
"dry-wash.errorBoundary.button.reload": "Перезагрузить страницу",
|
||||
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}"
|
||||
"dry-wash.washTime.timeSlot": "{{start}} - {{end}}",
|
||||
"dry-wash.arm.map.title": " Карта заказов",
|
||||
"dry-wash.arm.map.carNumber": " Номер автомобиля",
|
||||
"dry-wash.arm.map.status": "Статус"
|
||||
}
|
||||
|
||||
30705
package-lock.json
generated
30705
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dry-wash",
|
||||
"version": "0.5.0",
|
||||
"version": "0.9.2",
|
||||
"description": "<a id=\"readme-top\"></a>",
|
||||
"main": "./src/index.tsx",
|
||||
"scripts": {
|
||||
@@ -9,8 +9,8 @@
|
||||
"build": "npm run clean && brojs build --dev",
|
||||
"build:prod": "npm run clean && brojs build",
|
||||
"clean": "rimraf dist",
|
||||
"eslint": "npx eslint .",
|
||||
"eslint:fix": "npx eslint . --fix",
|
||||
"eslint": "npx eslint src",
|
||||
"eslint:fix": "npx eslint src --fix",
|
||||
"preversion": "npm run eslint"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -58,8 +58,11 @@
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@stylistic/eslint-plugin": "^2.10.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/testing-library__jest-dom": "^5.14.9",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { getFeatures } from "@brojs/cli";
|
||||
|
||||
const features = getFeatures('dry-wash-pl');
|
||||
const features = getFeatures('dry-wash');
|
||||
|
||||
export const FEATURE = {
|
||||
orderViewStatusPolling: {
|
||||
isOn: Boolean(features['order-view-status-polling']),
|
||||
isOn: Boolean(features?.['order-view-status-polling']),
|
||||
getValue: () => {
|
||||
const interval = parseInt(features['order-view-status-polling'].value);
|
||||
const interval = parseInt(features?.['order-view-status-polling']?.value);
|
||||
if (!Number.isNaN(interval)) {
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
},
|
||||
carImageUpload: {
|
||||
isOn: Boolean(features?.['car-img-upload'])
|
||||
}
|
||||
};
|
||||
@@ -1,20 +1,53 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { getConfigValue } from '@brojs/cli';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { Master } from '../../models/api/master';
|
||||
import { Master, OrderArm } from '../../models/api';
|
||||
|
||||
import { extractBodyFromResponse } from './utils';
|
||||
|
||||
export type UpdateMasterPayload = Required<Pick<Master, 'id'>> &
|
||||
Partial<Omit<Master, 'id'>>;
|
||||
|
||||
type UpdateOrderProps = Required<Pick<OrderArm, 'id'>> &
|
||||
Partial<Pick<OrderArm, 'status' | 'notes'>> & {
|
||||
master?: string;
|
||||
};
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'api',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: getConfigValue('dry-wash.api') }),
|
||||
tagTypes: ['Masters'],
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: new URL(getConfigValue('dry-wash.api'), location.origin).href,
|
||||
}),
|
||||
tagTypes: ['Masters', 'Orders'],
|
||||
endpoints: (builder) => ({
|
||||
getMasters: builder.query<Master[], void>({
|
||||
query: () => ({ url: '/arm/masters' }),
|
||||
transformResponse: extractBodyFromResponse<Master[]>,
|
||||
providesTags: ['Masters'],
|
||||
}),
|
||||
updateOrders: builder.mutation<void, UpdateOrderProps>({
|
||||
query: ({ id, status, notes, master }) => ({
|
||||
url: `/order/${id}`,
|
||||
method: 'PATCH',
|
||||
body: { status, notes, master },
|
||||
}),
|
||||
invalidatesTags: ['Orders'],
|
||||
}),
|
||||
getOrders: builder.query<OrderArm[], { date: Date }>({
|
||||
query: ({ date }) => {
|
||||
const startDate = dayjs(date).startOf('day').toISOString();
|
||||
const endDate = dayjs(date).endOf('day').toISOString();
|
||||
return {
|
||||
url: '/arm/orders',
|
||||
method: 'POST',
|
||||
body: { startDate, endDate },
|
||||
};
|
||||
},
|
||||
transformResponse: extractBodyFromResponse<OrderArm[]>,
|
||||
providesTags: ['Orders'],
|
||||
}),
|
||||
|
||||
addMaster: builder.mutation<void, Pick<Master, 'name' | 'phone'>>({
|
||||
query: (master) => ({
|
||||
url: '/arm/masters',
|
||||
@@ -23,5 +56,29 @@ export const api = createApi({
|
||||
}),
|
||||
invalidatesTags: ['Masters'],
|
||||
}),
|
||||
deleteMaster: builder.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({
|
||||
url: `/arm/masters/${id}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: ['Masters'],
|
||||
}),
|
||||
updateMaster: builder.mutation<void, UpdateMasterPayload>({
|
||||
query: ({ id, name, phone }) => ({
|
||||
url: `/arm/masters/${id}`,
|
||||
method: 'PATCH',
|
||||
body: { name, phone },
|
||||
}),
|
||||
invalidatesTags: ['Masters'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetMastersQuery,
|
||||
useAddMasterMutation,
|
||||
useDeleteMasterMutation,
|
||||
useUpdateMasterMutation,
|
||||
useGetOrdersQuery,
|
||||
useUpdateOrdersMutation,
|
||||
} = api;
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import { GetOrder } from "../../models/api";
|
||||
import { GetOrder, CreateOrder, UploadCarImage } from "../../models/api";
|
||||
|
||||
import { api } from "./api";
|
||||
import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils";
|
||||
|
||||
export const landingApi = api.injectEndpoints({
|
||||
endpoints: ({ query }) => ({
|
||||
endpoints: ({ mutation, query }) => ({
|
||||
getOrder: query<GetOrder.Response, GetOrder.Params>({
|
||||
query: ({ orderId }) => `/order/${orderId}`,
|
||||
transformResponse: extractBodyFromResponse<GetOrder.Response>,
|
||||
transformErrorResponse: extractErrorMessageFromResponse,
|
||||
})
|
||||
}),
|
||||
createOrder: mutation<CreateOrder.Response, CreateOrder.Params>({
|
||||
query: ({ body }) => ({
|
||||
url: `/order/create`,
|
||||
body,
|
||||
method: 'POST'
|
||||
}),
|
||||
transformResponse: extractBodyFromResponse<CreateOrder.Response>,
|
||||
transformErrorResponse: extractErrorMessageFromResponse,
|
||||
}),
|
||||
uploadCarImage: mutation<UploadCarImage.Response, UploadCarImage.Params>({
|
||||
query: ({ orderId, body }) => ({
|
||||
url: `/order/${orderId}/upload-car-img`,
|
||||
body,
|
||||
method: 'POST'
|
||||
}),
|
||||
transformErrorResponse: extractErrorMessageFromResponse,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||
|
||||
import { BaseResponse } from "../../models/api";
|
||||
import { BaseResponse } from '../../models/api';
|
||||
|
||||
export const extractBodyFromResponse = <Body>(response: BaseResponse<Body>) => {
|
||||
console.log('response', response);
|
||||
if (response.success) {
|
||||
return response.body;
|
||||
}
|
||||
};
|
||||
|
||||
export const extractErrorMessageFromResponse = ({ data }: FetchBaseQueryError) => {
|
||||
if (typeof data === 'object' && 'message' in data && typeof data.message === 'string') {
|
||||
return data.message;
|
||||
export const extractErrorMessageFromResponse = ({
|
||||
data,
|
||||
}: FetchBaseQueryError) => {
|
||||
if (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'error' in data &&
|
||||
typeof data.error === 'string'
|
||||
) {
|
||||
return data.error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,7 +7,9 @@ export const store = configureStore({
|
||||
[api.reducerPath]: api.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(api.middleware),
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: false
|
||||
}).concat(api.middleware),
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -33,6 +33,18 @@ export const URLs = {
|
||||
url: getNavigationValue('dry-wash.arm.order'),
|
||||
isOn: Boolean(getNavigationValue('dry-wash.arm.order')),
|
||||
},
|
||||
armMap: {
|
||||
url: getNavigationValue('dry-wash.arm.map'),
|
||||
isOn: Boolean(getNavigationValue('dry-wash.arm.map')),
|
||||
getUrl({ lat, lon, currentDate }) {
|
||||
return (
|
||||
getFullUrls('/arm') +
|
||||
'/' +
|
||||
getNavigationValue('dry-wash.arm.map') +
|
||||
`?lat=${lat}&lon=${lon}¤tDate=${currentDate}`
|
||||
);
|
||||
},
|
||||
},
|
||||
armBase: {
|
||||
url: getFullUrls(getNavigationValue('dry-wash.arm')),
|
||||
isOn: Boolean(getNavigationValue('dry-wash.arm')),
|
||||
|
||||
139
src/api/arm.ts
139
src/api/arm.ts
@@ -1,139 +0,0 @@
|
||||
import { getConfigValue } from '@brojs/cli';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
enum ArmEndpoints {
|
||||
ORDERS = '/arm/orders',
|
||||
MASTERS = '/arm/masters',
|
||||
}
|
||||
|
||||
const armService = () => {
|
||||
const endpoint = getConfigValue('dry-wash.api');
|
||||
|
||||
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}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ startDate, endDate }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch orders: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const fetchMasters = async () => {
|
||||
const response = await fetch(`${endpoint}${ArmEndpoints.MASTERS}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch masters: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const addMaster = async ({
|
||||
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 updateOrders = async ({
|
||||
id,
|
||||
status,
|
||||
notes,
|
||||
masterId,
|
||||
}: {
|
||||
id: string;
|
||||
status?: string;
|
||||
notes?: string;
|
||||
masterId?: string;
|
||||
}) => {
|
||||
const body = JSON.stringify({ status, notes, masterId });
|
||||
|
||||
const response = await fetch(`${endpoint}/order/${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();
|
||||
};
|
||||
|
||||
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,
|
||||
updateOrders,
|
||||
};
|
||||
};
|
||||
|
||||
export { armService, ArmEndpoints };
|
||||
@@ -1 +0,0 @@
|
||||
export * from './landing';
|
||||
@@ -1,65 +0,0 @@
|
||||
import { getConfigValue } from '@brojs/cli';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CreateOrder } from '../models/api';
|
||||
|
||||
import { QueryState, Trigger } from './types';
|
||||
|
||||
enum LandingEndpoints {
|
||||
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 }];
|
||||
};
|
||||
|
||||
export { useCreateOrderMutation };
|
||||
@@ -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>;
|
||||
1
src/assets/animation/index.ts
Normal file
1
src/assets/animation/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as OrderCreationAnimation } from './order-creation.json';
|
||||
1
src/assets/animation/order-creation.json
Normal file
1
src/assets/animation/order-creation.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Text } from '@chakra-ui/react';
|
||||
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface DateNavigatorProps {
|
||||
currentDate: Date;
|
||||
@@ -19,7 +20,7 @@ const DateNavigator = ({
|
||||
<ArrowBackIcon />
|
||||
</Button>
|
||||
<Text mx='4' fontSize='lg' fontWeight='bold'>
|
||||
{currentDate.toLocaleDateString()}
|
||||
{dayjs(currentDate).format('DD.MM.YYYY')}
|
||||
</Text>
|
||||
<Button onClick={onNextDate}>
|
||||
<ArrowForwardIcon />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
@@ -9,63 +9,51 @@ import {
|
||||
useEditableControls,
|
||||
ButtonGroup,
|
||||
Stack,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUpdateMasterMutation } from '../../__data__/service/api';
|
||||
import useShowToast from '../../hooks/useShowToast';
|
||||
|
||||
interface EditableWrapperProps {
|
||||
value: string;
|
||||
onSubmit: ({
|
||||
id,
|
||||
name,
|
||||
phone,
|
||||
}: {
|
||||
id: string;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
}) => Promise<unknown>;
|
||||
as: 'phone' | 'name';
|
||||
fieldName: 'phone' | 'name';
|
||||
id: string;
|
||||
}
|
||||
|
||||
const EditableWrapper = ({ value, onSubmit, as, id }: EditableWrapperProps) => {
|
||||
const EditableWrapper = ({ value, fieldName, id }: EditableWrapperProps) => {
|
||||
const [updateMaster, { isError, isSuccess, error }] =
|
||||
useUpdateMasterMutation();
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master.editable',
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const showToast = useShowToast();
|
||||
const [currentValue, setCurrentValue] = useState<string>(value);
|
||||
|
||||
const handleSubmit = async (newValue: string) => {
|
||||
if (currentValue === newValue) return;
|
||||
|
||||
try {
|
||||
await onSubmit({ id, [as]: newValue });
|
||||
await updateMaster({ id, [fieldName]: 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);
|
||||
}
|
||||
setCurrentValue(newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
showToast(t('toast.success'), 'success');
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
showToast(t('toast.error.title'), 'error', t('toast.error.description'));
|
||||
console.error(t('toast.error.description'), error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
function EditableControls() {
|
||||
const {
|
||||
isEditing,
|
||||
|
||||
@@ -33,22 +33,22 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
|
||||
render() {
|
||||
const { hasError } = this.state;
|
||||
//TODO: добавить анимацию после залива 404 страницы
|
||||
//TODO: может сделать обертку для хука, чтоб язык менялся без перезагрузки
|
||||
|
||||
if (hasError) {
|
||||
return (
|
||||
<Center minH='100vh'>
|
||||
<Center minH='100vh' data-testid='error-boundary'>
|
||||
<VStack spacing={4} textAlign='center'>
|
||||
<Heading as='h1' size='2xl'>
|
||||
<Heading as='h1' size='2xl' data-testid='error-title'>
|
||||
{i18next.t('~:dry-wash.errorBoundary.title')}
|
||||
</Heading>
|
||||
<Text fontSize='lg'>
|
||||
<Text fontSize='lg' data-testid='error-description'>
|
||||
{i18next.t('~:dry-wash.errorBoundary.description')}
|
||||
</Text>
|
||||
<Button
|
||||
colorScheme='teal'
|
||||
size='lg'
|
||||
variant='outline'
|
||||
data-testid='error-reload-button'
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
{i18next.t('~:dry-wash.errorBoundary.button.reload')}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { describe, expect, it, jest } from '@jest/globals';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import ErrorBoundary from '../ErrorBoundary';
|
||||
import { store } from '../../../__data__/store';
|
||||
|
||||
const ProblematicComponent = () => {
|
||||
useEffect(() => {
|
||||
throw new Error('Test Error');
|
||||
}, []);
|
||||
|
||||
return <div>Этот текст не должен появиться</div>;
|
||||
};
|
||||
jest.mock('@brojs/cli', () => {
|
||||
return {
|
||||
getNavigationValue: () => '/auth/login',
|
||||
getConfigValue: () => '/api',
|
||||
};
|
||||
});
|
||||
|
||||
describe('ErrorBoundary', () => {
|
||||
it('должен отобразить запасной UI при ошибке', async () => {
|
||||
// Подавляем вывод ошибки в консоль во время теста
|
||||
const consoleSpy = jest.spyOn(console, 'error');
|
||||
consoleSpy.mockImplementation(() => {});
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<ProblematicComponent />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const button = await waitFor(() =>
|
||||
screen.getByTestId('error-reload-button'),
|
||||
);
|
||||
|
||||
expect(button).not.toBeNull();
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
// Восстанавливаем console.error после теста
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ErrorBoundary должен отобразить запасной UI при ошибке 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1o0ed15"
|
||||
data-testid="error-boundary"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-zefqyp"
|
||||
>
|
||||
<h1
|
||||
class="chakra-heading css-0"
|
||||
data-testid="error-title"
|
||||
/>
|
||||
<p
|
||||
class="chakra-text css-1ezsviu"
|
||||
data-testid="error-description"
|
||||
/>
|
||||
<button
|
||||
class="chakra-button css-4xx2wk"
|
||||
data-testid="error-reload-button"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
63
src/components/Header/Header.tsx
Normal file
63
src/components/Header/Header.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Box, Button, Heading, HStack, Divider, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { URLs } from '../../__data__/urls';
|
||||
|
||||
const Header = () => {
|
||||
const location = useLocation();
|
||||
const isActive = (keyword: string) => location.pathname.includes(keyword);
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master.sideBar',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box as='header' bg='gray.50' boxShadow='md' px={6} py={4} w='100%'>
|
||||
<Flex gap={50} align='center'>
|
||||
<Heading color='green' size='lg'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
<HStack spacing={4}>
|
||||
{URLs.armOrder.isOn && (
|
||||
<Button
|
||||
as={Link}
|
||||
to={URLs.armOrder.url}
|
||||
colorScheme={isActive(URLs.armOrder.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armOrder.url) ? 'outline' : 'ghost'}
|
||||
>
|
||||
{t('orders')}
|
||||
</Button>
|
||||
)}
|
||||
<Divider orientation='vertical' height='30px' />
|
||||
{URLs.armMaster.isOn && (
|
||||
<Button
|
||||
as={Link}
|
||||
to={URLs.armMaster.url}
|
||||
colorScheme={isActive(URLs.armMaster.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armMaster.url) ? 'outline' : 'ghost'}
|
||||
data-testid='master-button'
|
||||
>
|
||||
{t('master')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{URLs.armMap.isOn && (
|
||||
<Button
|
||||
as={Link}
|
||||
to={URLs.armMap.url}
|
||||
colorScheme={isActive(URLs.armMap.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armMap.url) ? 'outline' : 'ghost'}
|
||||
data-testid='master-button'
|
||||
>
|
||||
Карта заказов
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
1
src/components/Header/index.ts
Normal file
1
src/components/Header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Header';
|
||||
@@ -2,10 +2,11 @@ import { Box, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import Sidebar from '../Sidebar';
|
||||
import Orders from '../Orders';
|
||||
import Masters from '../Masters';
|
||||
import { URLs } from '../../__data__/urls';
|
||||
import Header from '../Header';
|
||||
import OrdersMap from '../Map';
|
||||
|
||||
const LayoutArm = () => {
|
||||
let defaultRedirect = null;
|
||||
@@ -17,8 +18,8 @@ const LayoutArm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex h='100vh'>
|
||||
<Sidebar />
|
||||
<Flex flexDirection='column' h='100vh'>
|
||||
<Header />
|
||||
<Box flex='1' bg='gray.50'>
|
||||
<Routes>
|
||||
<Route index element={<Navigate to={defaultRedirect} replace />} />
|
||||
@@ -28,6 +29,9 @@ const LayoutArm = () => {
|
||||
{URLs.armMaster.isOn && (
|
||||
<Route path={URLs.armMaster.url} element={<Masters />} />
|
||||
)}
|
||||
{URLs.armMap.isOn && (
|
||||
<Route path={URLs.armMap.url} element={<OrdersMap />} />
|
||||
)}
|
||||
</Routes>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
82
src/components/Map/Map.tsx
Normal file
82
src/components/Map/Map.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex, Heading, Spinner } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { YMaps, Map, Placemark } from '@pbe/react-yandex-maps';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useGetOrdersQuery } from '../../__data__/service/api';
|
||||
import getCoordinates from '../../utils/getCoordinates';
|
||||
|
||||
const OrdersMap = () => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.map',
|
||||
});
|
||||
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const latFromUrl = parseFloat(params.get('lat') || 55.78);
|
||||
const lonFromUrl = parseFloat(params.get('lon') || 49.12);
|
||||
const currentDate = useMemo(
|
||||
() =>
|
||||
params.get('currentDate')
|
||||
? new Date(params.get('currentDate'))
|
||||
: new Date(),
|
||||
[],
|
||||
);
|
||||
|
||||
const {
|
||||
data: ordersData,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = useGetOrdersQuery({ date: currentDate });
|
||||
|
||||
// Получаем координаты из location
|
||||
const orders = ordersData
|
||||
?.map((order) => {
|
||||
const coords = getCoordinates(order.location);
|
||||
return coords ? { ...order, ...coords } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<Box p='8'>
|
||||
<Heading size='lg' mb='5'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
|
||||
{isLoading && (
|
||||
<Flex justifyContent='center' alignItems='center'>
|
||||
<Spinner size='lg' />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{ center: [latFromUrl, lonFromUrl], zoom: 12 }}
|
||||
width='100%'
|
||||
height='70vh'
|
||||
modules={['geoObject.addon.balloon']}
|
||||
>
|
||||
{orders.map(({ id, lat, lon, carNumber, status }) => (
|
||||
<Placemark
|
||||
key={id}
|
||||
geometry={[lat, lon]}
|
||||
options={{
|
||||
preset: 'islands#blueAutoIcon',
|
||||
iconColor: 'blue',
|
||||
balloonPanelMaxMapArea: 0,
|
||||
}}
|
||||
properties={{
|
||||
balloonContent: `<strong>${t('carNumber')}</strong> ${carNumber}<br/><strong>${t('status')}</strong> ${status}`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Map>
|
||||
</YMaps>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersMap;
|
||||
1
src/components/Map/index.ts
Normal file
1
src/components/Map/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Map';
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { EditIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { armService } from '../../api/arm';
|
||||
import { useDeleteMasterMutation } from '../../__data__/service/api';
|
||||
import useShowToast from '../../hooks/useShowToast';
|
||||
|
||||
interface MasterActionsMenu {
|
||||
id: string;
|
||||
@@ -21,38 +21,35 @@ const MasterActionsMenu = ({ id }: MasterActionsMenu) => {
|
||||
keyPrefix: 'dry-wash.arm.master.table.actionsMenu',
|
||||
});
|
||||
|
||||
const { deleteMaster } = armService();
|
||||
const toast = useToast();
|
||||
const [deleteMaster, { isSuccess, isError, error, isLoading }] =
|
||||
useDeleteMasterMutation();
|
||||
|
||||
const showToast = useShowToast();
|
||||
|
||||
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',
|
||||
});
|
||||
await deleteMaster({ id });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
showToast(t('toast.success'), 'success');
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
showToast(t('toast.error.title'), 'error', t('toast.error.description'));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}, [isError]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton icon={<EditIcon />} as={IconButton} variant='outline' />
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleClickDelete}>{t('delete')}</MenuItem>
|
||||
<MenuItem onClick={handleClickDelete} isDisabled={isLoading}>
|
||||
{t('delete')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
useToast,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
FormErrorMessage,
|
||||
@@ -20,9 +19,9 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PhoneIcon } from '@chakra-ui/icons';
|
||||
|
||||
import { api } from '../../__data__/service/api';
|
||||
import showToast from '../../helpers/showToast';
|
||||
import { useAddMasterMutation } from '../../__data__/service/api';
|
||||
import { DrawerInputs } from '../../models/arm/form';
|
||||
import useShowToast from '../../hooks/useShowToast';
|
||||
|
||||
interface MasterDrawerProps {
|
||||
isOpen: boolean;
|
||||
@@ -50,28 +49,19 @@ const MasterDrawer = ({ isOpen, onClose }: MasterDrawerProps) => {
|
||||
const isEmptyFields = trimMaster.name === '' || trimMaster.phone === '';
|
||||
|
||||
if (isEmptyFields) {
|
||||
showToast({
|
||||
toast,
|
||||
title: t('toast.error.base'),
|
||||
description: t('toast.error.empty-fields'),
|
||||
status: 'error',
|
||||
});
|
||||
showToast(t('toast.error.base'), 'error', t('toast.error.empty-fields'));
|
||||
return;
|
||||
}
|
||||
|
||||
await addMaster(trimMaster);
|
||||
};
|
||||
|
||||
const [addMaster, { error, isSuccess }] = api.useAddMasterMutation();
|
||||
const toast = useToast();
|
||||
const [addMaster, { error, isSuccess }] = useAddMasterMutation();
|
||||
const showToast = useShowToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
showToast({
|
||||
toast,
|
||||
title: t('toast.create-master'),
|
||||
status: 'success',
|
||||
});
|
||||
showToast(t('toast.create-master'), 'success');
|
||||
reset();
|
||||
onClose();
|
||||
}
|
||||
@@ -79,12 +69,11 @@ const MasterDrawer = ({ isOpen, onClose }: MasterDrawerProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
showToast({
|
||||
toast,
|
||||
title: t('toast.error.create-master'),
|
||||
description: t('toast.error.create-master-details'),
|
||||
status: 'error',
|
||||
});
|
||||
showToast(
|
||||
t('toast.error.create-master'),
|
||||
'error',
|
||||
t('toast.error.create-master-details'),
|
||||
);
|
||||
console.error(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
@@ -5,10 +5,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import MasterActionsMenu from '../MasterActionsMenu';
|
||||
import { getTimeSlot } from '../../lib';
|
||||
import EditableWrapper from '../Editable/Editable';
|
||||
import { armService } from '../../api/arm';
|
||||
|
||||
const MasterItem = ({ name, phone, id, schedule }) => {
|
||||
const { updateMaster } = armService();
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master',
|
||||
});
|
||||
@@ -16,12 +14,7 @@ const MasterItem = ({ name, phone, id, schedule }) => {
|
||||
return (
|
||||
<Tr>
|
||||
<Td>
|
||||
<EditableWrapper
|
||||
id={id}
|
||||
as={'name'}
|
||||
value={name}
|
||||
onSubmit={updateMaster}
|
||||
/>
|
||||
<EditableWrapper id={id} fieldName={'name'} value={name} />
|
||||
</Td>
|
||||
<Td>
|
||||
{schedule?.length > 0 ? (
|
||||
@@ -37,12 +30,7 @@ const MasterItem = ({ name, phone, id, schedule }) => {
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<EditableWrapper
|
||||
id={id}
|
||||
as={'phone'}
|
||||
value={phone}
|
||||
onSubmit={updateMaster}
|
||||
/>
|
||||
<EditableWrapper id={id} fieldName={'phone'} value={phone} />
|
||||
</Td>
|
||||
<Td>
|
||||
<MasterActionsMenu id={id} />
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Button,
|
||||
useDisclosure,
|
||||
Flex,
|
||||
useToast,
|
||||
Td,
|
||||
Text,
|
||||
Spinner,
|
||||
@@ -19,7 +18,8 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MasterItem from '../MasterItem';
|
||||
import MasterDrawer from '../MasterDrawer';
|
||||
import { api } from '../../__data__/service/api';
|
||||
import { useGetMastersQuery } from '../../__data__/service/api';
|
||||
import useShowToast from '../../hooks/useShowToast';
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
'name' as const,
|
||||
@@ -30,26 +30,17 @@ const TABLE_HEADERS = [
|
||||
|
||||
const Masters = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const toast = useToast();
|
||||
const showToast = useShowToast();
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master',
|
||||
});
|
||||
|
||||
const {
|
||||
data: masters,
|
||||
error,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = api.useGetMastersQuery();
|
||||
const { data: masters, error, isLoading, isSuccess } = useGetMastersQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast({
|
||||
title: t('error.title'),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
showToast(t('error.title'), 'error');
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
|
||||
@@ -1,44 +1,20 @@
|
||||
import React, { ChangeEvent, useState } from 'react';
|
||||
import { Td, Tr, Link, Select } from '@chakra-ui/react';
|
||||
import { Td, Tr, Link, Select, Button } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import { ViewIcon } from '@chakra-ui/icons';
|
||||
import { Link as LinkRouter } from 'react-router-dom';
|
||||
|
||||
import { getTimeSlot } from '../../lib';
|
||||
import { Master } from '../../models/api/master';
|
||||
import { armService } from '../../api/arm';
|
||||
|
||||
const statuses = [
|
||||
'pending' as const,
|
||||
'progress' as const,
|
||||
'working' as const,
|
||||
'canceled' as const,
|
||||
'complete' as const,
|
||||
];
|
||||
|
||||
type GetArrItemType<ArrType> =
|
||||
ArrType extends Array<infer ItemType> ? ItemType : never;
|
||||
|
||||
export type OrderProps = {
|
||||
carNumber?: string;
|
||||
startWashTime?: string;
|
||||
endWashTime?: string;
|
||||
orderDate?: string;
|
||||
status?: GetArrItemType<typeof statuses>;
|
||||
phone?: string;
|
||||
location?: string;
|
||||
master: Master;
|
||||
notes: '';
|
||||
allMasters: Master[];
|
||||
id: string;
|
||||
};
|
||||
|
||||
type Status = (typeof statuses)[number];
|
||||
import { useUpdateOrdersMutation } from '../../__data__/service/api';
|
||||
import { OrderArm, Status, statuses } from '../../models/api';
|
||||
import getCoordinates from '../../utils/getCoordinates';
|
||||
import { URLs } from '../../__data__/urls';
|
||||
|
||||
const statusColors: Record<Status, string> = {
|
||||
pending: 'yellow.100',
|
||||
progress: 'blue.100',
|
||||
working: 'orange.100',
|
||||
canceled: 'red.100',
|
||||
cancelled: 'red.100',
|
||||
complete: 'green.100',
|
||||
};
|
||||
|
||||
@@ -53,16 +29,16 @@ const OrderItem = ({
|
||||
master,
|
||||
allMasters,
|
||||
id,
|
||||
}: OrderProps) => {
|
||||
const { updateOrders } = armService();
|
||||
|
||||
currentDate,
|
||||
}: OrderArm) => {
|
||||
const [updateOrders] = useUpdateOrdersMutation();
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.order',
|
||||
});
|
||||
|
||||
const [statusSelect, setStatus] = useState(status);
|
||||
const bgColor = statusColors[statusSelect];
|
||||
const [masterSelect, setMaster] = useState(master?.name);
|
||||
const [masterSelectId, setMasterSelectId] = useState(master);
|
||||
|
||||
const handelChangeMasters = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const masterName = e.target.value;
|
||||
@@ -71,19 +47,25 @@ const OrderItem = ({
|
||||
);
|
||||
|
||||
if (selectedMaster) {
|
||||
setMaster(masterName);
|
||||
updateOrders({ id, masterId: selectedMaster.id });
|
||||
setMasterSelectId(selectedMaster.id);
|
||||
updateOrders({ id, master: selectedMaster.id });
|
||||
} else {
|
||||
console.error('Master not found');
|
||||
}
|
||||
};
|
||||
|
||||
const handeChangeStatus = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
const status = e.target.value;
|
||||
const status = e.target.value as OrderArm['status'];
|
||||
updateOrders({ id, status });
|
||||
setStatus(e.target.value as OrderProps['status']);
|
||||
setStatus(status);
|
||||
};
|
||||
|
||||
const masterSelectChange = allMasters.find(
|
||||
(master) => master.id === masterSelectId,
|
||||
);
|
||||
|
||||
const { lat = 55.78, lon = 49.12 } = getCoordinates(location);
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>{carNumber}</Td>
|
||||
@@ -107,7 +89,7 @@ const OrderItem = ({
|
||||
</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={masterSelect}
|
||||
value={masterSelectChange?.name}
|
||||
onChange={handelChangeMasters}
|
||||
placeholder={t(`master.placeholder`)}
|
||||
>
|
||||
@@ -121,7 +103,14 @@ const OrderItem = ({
|
||||
<Td>
|
||||
<Link href='tel:'>{phone}</Link>
|
||||
</Td>
|
||||
<Td>{location}</Td>
|
||||
<Td>
|
||||
<Button
|
||||
as={LinkRouter}
|
||||
to={URLs.armMap.getUrl({ lat, lon, currentDate })}
|
||||
>
|
||||
<ViewIcon />
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,16 +10,18 @@ import {
|
||||
Spinner,
|
||||
Text,
|
||||
Td,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import OrderItem from '../OrderItem';
|
||||
import { OrderProps } from '../OrderItem/OrderItem';
|
||||
import { armService } from '../../api/arm';
|
||||
import DateNavigator from '../DateNavigator';
|
||||
import { Master } from '../../models/api/master';
|
||||
import {
|
||||
useGetMastersQuery,
|
||||
useGetOrdersQuery,
|
||||
} from '../../__data__/service/api';
|
||||
import useShowToast from '../../hooks/useShowToast';
|
||||
import { OrderArm } from '../../models/api';
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
'carNumber' as const,
|
||||
@@ -34,54 +36,40 @@ const Orders = () => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.order',
|
||||
});
|
||||
const showToast = useShowToast();
|
||||
|
||||
const { fetchOrders } = armService();
|
||||
const { fetchMasters } = armService();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const [orders, setOrders] = useState<OrderProps[]>([]);
|
||||
const [allMasters, setAllMasters] = useState<Master[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const {
|
||||
data: orders,
|
||||
isLoading: isOrdersLoading,
|
||||
isSuccess: isOrdersSuccess,
|
||||
isError: isOrdersError,
|
||||
error: ordersError,
|
||||
} = useGetOrdersQuery({ date: currentDate });
|
||||
|
||||
const {
|
||||
data: masters,
|
||||
isLoading: isMastersLoading,
|
||||
isSuccess: isMastersSuccess,
|
||||
isError: isMastersError,
|
||||
error: mastersError,
|
||||
} = useGetMastersQuery();
|
||||
|
||||
const isLoading = isOrdersLoading || isMastersLoading;
|
||||
const isSuccess = isOrdersSuccess && isMastersSuccess;
|
||||
const isError = isOrdersError || isMastersError;
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [ordersData, mastersData] = await Promise.all([
|
||||
fetchOrders({ date: currentDate }),
|
||||
fetchMasters(),
|
||||
]);
|
||||
|
||||
setOrders(ordersData.body);
|
||||
setAllMasters(mastersData.body);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
toast({
|
||||
title: t('error.title'),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [currentDate, toast, t]);
|
||||
if (isError) {
|
||||
showToast(t('error.title'), 'error');
|
||||
}
|
||||
}, [isError, ordersError, mastersError, t]);
|
||||
|
||||
return (
|
||||
<Box p='8'>
|
||||
<Heading size='lg' mb='5'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
|
||||
<DateNavigator
|
||||
currentDate={currentDate}
|
||||
onPreviousDate={() =>
|
||||
@@ -103,28 +91,28 @@ const Orders = () => {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{loading && (
|
||||
{isLoading && (
|
||||
<Tr>
|
||||
<Td colSpan={TABLE_HEADERS.length} textAlign='center' py='8'>
|
||||
<Spinner size='lg' />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!loading && orders.length === 0 && !error && (
|
||||
{isSuccess && orders.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={TABLE_HEADERS.length}>
|
||||
<Text>{t('table.empty')}</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{!loading &&
|
||||
!error &&
|
||||
{isSuccess &&
|
||||
orders.map((order, index) => (
|
||||
<OrderItem
|
||||
allMasters={allMasters}
|
||||
allMasters={masters}
|
||||
key={index}
|
||||
{...order}
|
||||
status={order.status as OrderProps['status']}
|
||||
status={order.status as OrderArm['status']}
|
||||
currentDate={currentDate}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
|
||||
@@ -5,6 +5,7 @@ export const PageSpinner: FC = () => {
|
||||
return (
|
||||
<Flex w='full' h='100vh' justifyContent='center' alignItems='center'>
|
||||
<Spinner
|
||||
data-testid='spinner'
|
||||
thickness='5px'
|
||||
speed='0.65s'
|
||||
emptyColor='gray.200'
|
||||
|
||||
35
src/components/PopoverTemplate/PopoverTemplate.tsx
Normal file
35
src/components/PopoverTemplate/PopoverTemplate.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description: string;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
const PopoverTemplate = ({ title, description, trigger }: Props) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button>{trigger}</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
{title && <PopoverHeader>{title}!</PopoverHeader>}
|
||||
<PopoverBody mr={5}>{description}</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopoverTemplate;
|
||||
1
src/components/PopoverTemplate/index.ts
Normal file
1
src/components/PopoverTemplate/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './PopoverTemplate';
|
||||
48
src/components/PriceCar/PriceCar.tsx
Normal file
48
src/components/PriceCar/PriceCar.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Box, Image, Progress, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { getFeatures } from '@brojs/cli';
|
||||
|
||||
const PRICE_INCREASE_PERCENT_PER_RATING = 10; // 10% за каждый балл
|
||||
|
||||
export const PriceCar = ({ image, rating }) => {
|
||||
const BASE_WASH_PRICE: number = Number(
|
||||
getFeatures('dry-wash')['order-cost']?.value || 1000,
|
||||
);
|
||||
|
||||
const calculateWashPrice = (rating: number) => {
|
||||
const priceIncrease =
|
||||
(BASE_WASH_PRICE * PRICE_INCREASE_PERCENT_PER_RATING * rating) / 100;
|
||||
return BASE_WASH_PRICE + priceIncrease;
|
||||
};
|
||||
|
||||
const washPrice = calculateWashPrice(rating);
|
||||
const progressValue = (rating / 10) * 100;
|
||||
return (
|
||||
<Box
|
||||
alignItems='center'
|
||||
gap={5}
|
||||
width='100%'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
>
|
||||
<Image
|
||||
maxWidth='600px'
|
||||
objectFit='contain'
|
||||
borderRadius='md'
|
||||
src={image}
|
||||
alt='Car Image'
|
||||
/>
|
||||
{rating ? (
|
||||
<Box width='100%' maxW='600px'>
|
||||
<Text>Рейтинг загрязнения машины:</Text>
|
||||
<Progress value={progressValue} size='sm' colorScheme='red' mt={2} />
|
||||
<Text mt={2}>Стоимость мойки: {washPrice.toFixed(2)} руб.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Text>Не удалость определить уровень загрязнения машины</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceCar;
|
||||
1
src/components/PriceCar/index.ts
Normal file
1
src/components/PriceCar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './PriceCar';
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Box, Button, Heading, VStack, Divider } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { URLs } from '../../__data__/urls';
|
||||
|
||||
const Sidebar = () => {
|
||||
const location = useLocation();
|
||||
const isActive = (keyword: string) => location.pathname.includes(keyword);
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master.sideBar',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderRight='1px solid black'
|
||||
bg='gray.50'
|
||||
color='white'
|
||||
w='250px'
|
||||
p='5'
|
||||
pt='8'
|
||||
>
|
||||
<Heading color='green' size='lg' mb='5'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
|
||||
<VStack align='start' spacing='4'>
|
||||
<Divider />
|
||||
{URLs.armOrder.isOn && (
|
||||
<Button
|
||||
as={Link}
|
||||
to={URLs.armOrder.url}
|
||||
w='100%'
|
||||
colorScheme={isActive(URLs.armOrder.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armOrder.url) ? 'solid' : 'ghost'}
|
||||
>
|
||||
{t('orders')}
|
||||
</Button>
|
||||
)}
|
||||
<Divider />
|
||||
{URLs.armMaster.isOn && (
|
||||
<Button
|
||||
as={Link}
|
||||
to={URLs.armMaster.url}
|
||||
w='100%'
|
||||
colorScheme={isActive(URLs.armMaster.url) ? 'green' : 'blue'}
|
||||
variant={isActive(URLs.armMaster.url) ? 'solid' : 'ghost'}
|
||||
>
|
||||
{t('master')}
|
||||
</Button>
|
||||
)}
|
||||
<Divider />
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Sidebar';
|
||||
@@ -9,12 +9,12 @@ import { BenefitsSectionProps } from './types';
|
||||
import { iconsMap } from './helper';
|
||||
|
||||
export const BenefitsSection: FC<BenefitsSectionProps> = ({
|
||||
data: { heading, description, list } = {},
|
||||
data: { heading, description, list } = {}, ...props
|
||||
}) => {
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<PageSection {...props}>
|
||||
<VStack w='full' spacing={2}>
|
||||
<Heading as='h2'>{t(heading)}</Heading>
|
||||
<Text>{t(description)}</Text>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { Text } from '@chakra-ui/react';
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export const Copyright: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.landing.footer'
|
||||
});
|
||||
|
||||
return <Text color='whiteAlpha.500'>{t('dry-wash.landing.footer.copyright', { currentYear })}</Text>;
|
||||
return <Text color='whiteAlpha.500'>{t('copyright', { currentYear })}</Text>;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SiteLogo, PageSection } from '../';
|
||||
|
||||
import { Copyright } from './Copyright';
|
||||
|
||||
export const Footer: FC = () => {
|
||||
export const Footer: FC = (props) => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.landing.footer.links',
|
||||
});
|
||||
@@ -15,11 +15,17 @@ export const Footer: FC = () => {
|
||||
const listData = [
|
||||
{ to: '#', label: t('privacy-policy') },
|
||||
{ to: '#', label: t('service-terms') },
|
||||
{ to: '#', label: t('faq') },
|
||||
{ to: '#', label: t('faq') },
|
||||
];
|
||||
|
||||
return (
|
||||
<PageSection as='footer' py={5} bg='gray.700' color='white'>
|
||||
<PageSection
|
||||
as='footer'
|
||||
py={5}
|
||||
bg='gray.700'
|
||||
color='white'
|
||||
{...props}
|
||||
>
|
||||
<SiteLogo />
|
||||
<Copyright />
|
||||
<List spacing={2}>
|
||||
|
||||
@@ -10,11 +10,18 @@ import { HeroSectionProps } from './types';
|
||||
export const HeroSection: FC<HeroSectionProps> = ({
|
||||
data: { headline, description, video } = {},
|
||||
flexShrink,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return (
|
||||
<Box flexShrink={flexShrink} as='header' pos='relative' zIndex={0}>
|
||||
<Box
|
||||
flexShrink={flexShrink}
|
||||
as='header'
|
||||
pos='relative'
|
||||
zIndex={0}
|
||||
{...props}
|
||||
>
|
||||
<Box
|
||||
as='video'
|
||||
src={`${__webpack_public_path__}/remote-assets/${video}`}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Image } from '@chakra-ui/react';
|
||||
import { LogoSvg } from '../../../assets/icons';
|
||||
|
||||
export const SiteLogo: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return <Image src={LogoSvg} alt={t('~:dry-wash.landing.site-logo')} w={40} />;
|
||||
return <Image src={LogoSvg} alt={t('site-logo')} w={40} />;
|
||||
};
|
||||
|
||||
// todo: replace Image by SVG React component
|
||||
|
||||
@@ -9,11 +9,12 @@ import { SocialProofSectionProps } from './types';
|
||||
|
||||
export const SocialProofSection: FC<SocialProofSectionProps> = ({
|
||||
data: { heading } = {},
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation('~', { keyPrefix: 'dry-wash.landing' });
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<PageSection {...props}>
|
||||
<Heading as='h2'>{t(heading)}</Heading>
|
||||
<ReviewsSlider />
|
||||
<HStack w='full' justify='flex-end'>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
YMapsProvider,
|
||||
} from './location';
|
||||
|
||||
export const OrderForm = ({ onSubmit, loading }: OrderFormProps) => {
|
||||
export const OrderForm = ({ onSubmit, loading, ...props }: OrderFormProps) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
@@ -41,7 +41,7 @@ export const OrderForm = ({ onSubmit, loading }: OrderFormProps) => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box p={4} marginInline='auto'>
|
||||
<Box p={4} marginInline='auto' {...props}>
|
||||
<VStack
|
||||
as='form'
|
||||
noValidate
|
||||
|
||||
102
src/components/order-view/car-img/car-img-form.tsx
Normal file
102
src/components/order-view/car-img/car-img-form.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { FC, memo, useRef } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { landingApi } from '../../../__data__/service/landing.api';
|
||||
import { UploadCarImage } from '../../../models/api';
|
||||
|
||||
import { useHandleUploadCarImageResponse } from './helper';
|
||||
|
||||
type FormValues = {
|
||||
carImg: File & {
|
||||
fileName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CarImageFormProps = {
|
||||
orderId: UploadCarImage.Params['orderId'];
|
||||
};
|
||||
|
||||
export const CarImageForm: FC<CarImageFormProps> = memo(function CarImageForm({
|
||||
orderId,
|
||||
}) {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormValues>({ shouldFocusError: true });
|
||||
|
||||
const [uploadCarImage, uploadCarImageMutation] =
|
||||
landingApi.useUploadCarImageMutation();
|
||||
useHandleUploadCarImageResponse(uploadCarImageMutation);
|
||||
|
||||
const onSubmit = (formData: FormValues) => {
|
||||
const body = new FormData();
|
||||
body.append('file', formData.carImg);
|
||||
uploadCarImage({ orderId, body });
|
||||
};
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.order-view.upload-car-image',
|
||||
});
|
||||
|
||||
return (
|
||||
<form>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor='carImg'>{t('field.label')}</FormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='carImg'
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<HStack gap={0}>
|
||||
<Input
|
||||
{...field}
|
||||
ref={fileInputRef}
|
||||
accept='image/png,image/jpeg'
|
||||
value={value?.fileName}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.files[0]);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
type='file'
|
||||
hidden
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('file-input.placeholder')}
|
||||
value={value?.name || ''}
|
||||
readOnly
|
||||
borderRightRadius={0}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
fileInputRef.current.click();
|
||||
}}
|
||||
isLoading={isSubmitting || uploadCarImageMutation.isLoading}
|
||||
colorScheme='primary'
|
||||
paddingInline={8}
|
||||
borderLeftRadius={0}
|
||||
>
|
||||
{t('file-input.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormErrorMessage>{errors.carImg?.message}</FormErrorMessage>
|
||||
<FormHelperText>{t('field.help')}</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
35
src/components/order-view/car-img/helper.ts
Normal file
35
src/components/order-view/car-img/helper.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect } from "react";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { isErrorMessage } from "../../../models/api";
|
||||
|
||||
export const useHandleUploadCarImageResponse = (query: {
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
error?: unknown;
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.order-view.upload-car-image-query',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isError) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('error.title'),
|
||||
description: isErrorMessage(query.error) ? query.error : undefined,
|
||||
});
|
||||
}
|
||||
}, [query.isError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isSuccess) {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('success.title'),
|
||||
});
|
||||
}
|
||||
}, [query.isSuccess]);
|
||||
};
|
||||
1
src/components/order-view/car-img/index.ts
Normal file
1
src/components/order-view/car-img/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CarImageForm } from './car-img-form';
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
Heading,
|
||||
HStack,
|
||||
UnorderedList,
|
||||
VStack,
|
||||
ListItem,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { Order } from '../../../models/landing';
|
||||
import { formatDatetime } from '../../../lib';
|
||||
@@ -41,6 +43,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
|
||||
location,
|
||||
startWashTime,
|
||||
endWashTime,
|
||||
created
|
||||
}) => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.order-view.details',
|
||||
@@ -50,7 +53,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<VStack p={4} alignItems='flex-start' gap={4}>
|
||||
<>
|
||||
<HStack
|
||||
width='full'
|
||||
flexWrap='wrap'
|
||||
@@ -58,7 +61,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
|
||||
gap={2}
|
||||
>
|
||||
<Heading as='h2' size='lg'>
|
||||
{t('title', { number: orderNumber })}
|
||||
{t('title', { number: orderNumber })} ({dayjs(created).format('LLLL')})
|
||||
</Heading>
|
||||
<OrderStatus value={status} />
|
||||
</HStack>
|
||||
@@ -104,7 +107,7 @@ export const OrderDetails: FC<OrderDetailsProps> = ({
|
||||
<AlertIcon />
|
||||
{t('alert')}
|
||||
</Alert>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { UseToastOptions } from '@chakra-ui/react';
|
||||
|
||||
interface ShowToast {
|
||||
toast: (options: UseToastOptions) => void;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: 'info' | 'warning' | 'success' | 'error';
|
||||
}
|
||||
|
||||
const showToast = ({ toast, title, description, status }: ShowToast) => {
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
};
|
||||
|
||||
export default showToast;
|
||||
28
src/hooks/useShowToast.ts
Normal file
28
src/hooks/useShowToast.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useShowToast = () => {
|
||||
const toast = useToast();
|
||||
|
||||
const showToast = useCallback(
|
||||
(
|
||||
title: string,
|
||||
status: 'info' | 'warning' | 'success' | 'error',
|
||||
description?: string,
|
||||
) => {
|
||||
toast({
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
[toast],
|
||||
);
|
||||
|
||||
return showToast;
|
||||
};
|
||||
|
||||
export default useShowToast;
|
||||
@@ -5,9 +5,11 @@ type SuccessResponse<Body> = {
|
||||
|
||||
export type ErrorMessage = string;
|
||||
|
||||
export const isErrorMessage = (error: unknown): error is ErrorMessage => typeof error === 'string';
|
||||
|
||||
type ErrorResponse = {
|
||||
success: false;
|
||||
message: ErrorMessage;
|
||||
error: ErrorMessage;
|
||||
};
|
||||
|
||||
export type BaseResponse<Body> = SuccessResponse<Body> | ErrorResponse;
|
||||
export type BaseResponse<Body> = SuccessResponse<Body> | ErrorResponse;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './order';
|
||||
export * from './order';
|
||||
export * from './master';
|
||||
|
||||
@@ -1,21 +1,60 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
import { Order } from "../landing";
|
||||
import { Order } from '../landing';
|
||||
|
||||
import { ErrorMessage } from "./common";
|
||||
import { ErrorMessage } from './common';
|
||||
import { Master } from './master';
|
||||
|
||||
export namespace GetOrder {
|
||||
export type Response = Order.View;
|
||||
export type Params = {
|
||||
orderId: Order.Id
|
||||
orderId: Order.Id;
|
||||
};
|
||||
export type Error = ErrorMessage;
|
||||
}
|
||||
|
||||
export namespace CreateOrder {
|
||||
export type Response = {
|
||||
id: Order.Id
|
||||
id: Order.Id;
|
||||
};
|
||||
export type Params = {
|
||||
body: Order.Create
|
||||
body: Order.Create;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export namespace UploadCarImage {
|
||||
export type Response = void;
|
||||
export type Params = {
|
||||
orderId: Order.Id;
|
||||
/**
|
||||
* @example { file: File }
|
||||
*/
|
||||
body: FormData;
|
||||
};
|
||||
}
|
||||
|
||||
type GetArrItemType<ArrType> =
|
||||
ArrType extends Array<infer ItemType> ? ItemType : never;
|
||||
|
||||
export const statuses = [
|
||||
'pending' as const,
|
||||
'progress' as const,
|
||||
'cancelled' as const,
|
||||
'complete' as const,
|
||||
];
|
||||
|
||||
export type Status = (typeof statuses)[number];
|
||||
|
||||
export type OrderArm = {
|
||||
carNumber?: string;
|
||||
startWashTime?: string;
|
||||
endWashTime?: string;
|
||||
orderDate?: string;
|
||||
status?: GetArrItemType<typeof statuses>;
|
||||
phone?: string;
|
||||
location?: string;
|
||||
master: string | [];
|
||||
notes: '';
|
||||
allMasters: Master[];
|
||||
id: string;
|
||||
currentDate: Date;
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './i18n';
|
||||
export * from './i18n';
|
||||
export * from './landing';
|
||||
@@ -1,29 +1,30 @@
|
||||
import { IsoDate } from "../common";
|
||||
import { IsoDate } from '../common';
|
||||
|
||||
import { Car, Customer, Washing } from ".";
|
||||
import { Car, Customer, Washing } from '.';
|
||||
|
||||
export type Id = string;
|
||||
|
||||
export type Status = 'pending' |
|
||||
'progress' |
|
||||
'working' |
|
||||
'canceled' |
|
||||
'complete';
|
||||
export type Status =
|
||||
| 'pending'
|
||||
| 'progress'
|
||||
| 'working'
|
||||
| 'canceled'
|
||||
| 'complete';
|
||||
|
||||
export type Create = {
|
||||
customer: {
|
||||
phone: Customer.PhoneNumber,
|
||||
phone: Customer.PhoneNumber;
|
||||
};
|
||||
car: {
|
||||
number: Car.RegistrationNumber,
|
||||
body: Car.BodyStyle,
|
||||
color: Car.Color,
|
||||
},
|
||||
number: Car.RegistrationNumber;
|
||||
body: Car.BodyStyle;
|
||||
color: Car.Color;
|
||||
};
|
||||
washing: {
|
||||
location: Washing.Location
|
||||
begin: Washing.AvailableBeginDateTime,
|
||||
end: Washing.AvailableEndDateTime,
|
||||
}
|
||||
location: Washing.Location;
|
||||
begin: Washing.AvailableBeginDateTime;
|
||||
end: Washing.AvailableEndDateTime;
|
||||
};
|
||||
};
|
||||
|
||||
export type Number = string;
|
||||
@@ -36,10 +37,12 @@ export type View = {
|
||||
location: Washing.Location;
|
||||
startWashTime: Washing.AvailableBeginDateTime;
|
||||
endWashTime: Washing.AvailableEndDateTime;
|
||||
orderNumber: Number,
|
||||
status: Status,
|
||||
orderNumber: Number;
|
||||
status: Status;
|
||||
notes: string;
|
||||
created: IsoDate;
|
||||
updated: IsoDate;
|
||||
id: Id;
|
||||
};
|
||||
image?: string;
|
||||
imageRating?: string;
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as LandingSuccessStub } from './success';
|
||||
export type { default as LandingSuccessStub } from './success';
|
||||
@@ -1,325 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Arm Page render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1yeiifd"
|
||||
>
|
||||
<div
|
||||
class="css-1fp6kaj"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-9q1d0h"
|
||||
>
|
||||
title
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1oen434"
|
||||
>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-1upb9tn"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-uxt1e8"
|
||||
href="/auth/login"
|
||||
>
|
||||
orders
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-1upb9tn"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-uxt1e8"
|
||||
href="/auth/login"
|
||||
>
|
||||
master
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-1upb9tn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-sy55x5"
|
||||
>
|
||||
<div
|
||||
class="css-hpgf8j"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-r7q7qr"
|
||||
>
|
||||
title
|
||||
</h2>
|
||||
<div
|
||||
class="css-1me9tx"
|
||||
>
|
||||
<button
|
||||
class="chakra-button css-4xx2wk"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<p
|
||||
class="chakra-text css-1bntq7d"
|
||||
>
|
||||
2/2/2025
|
||||
</p>
|
||||
<button
|
||||
class="chakra-button css-4xx2wk"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<table
|
||||
class="chakra-table css-0"
|
||||
>
|
||||
<thead
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.carNumber
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.orderDate
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.status
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.masters
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.telephone
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.location
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-12rlgei"
|
||||
colspan="6"
|
||||
>
|
||||
<div
|
||||
class="chakra-spinner css-1y7joxr"
|
||||
>
|
||||
<span
|
||||
class="css-8b45rq"
|
||||
>
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Arm Page render 2`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1yeiifd"
|
||||
>
|
||||
<div
|
||||
class="css-1fp6kaj"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-9q1d0h"
|
||||
>
|
||||
title
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1oen434"
|
||||
>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-1upb9tn"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-uxt1e8"
|
||||
href="/auth/login"
|
||||
>
|
||||
orders
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-1upb9tn"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-uxt1e8"
|
||||
href="/auth/login"
|
||||
>
|
||||
master
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-1upb9tn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-sy55x5"
|
||||
>
|
||||
<div
|
||||
class="css-hpgf8j"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-r7q7qr"
|
||||
>
|
||||
title
|
||||
</h2>
|
||||
<div
|
||||
class="css-1me9tx"
|
||||
>
|
||||
<button
|
||||
class="chakra-button css-4xx2wk"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<p
|
||||
class="chakra-text css-1bntq7d"
|
||||
>
|
||||
2/2/2025
|
||||
</p>
|
||||
<button
|
||||
class="chakra-button css-4xx2wk"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<table
|
||||
class="chakra-table css-0"
|
||||
>
|
||||
<thead
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.carNumber
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.orderDate
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.status
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.masters
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.telephone
|
||||
</th>
|
||||
<th
|
||||
class="css-0"
|
||||
>
|
||||
table.header.location
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-12rlgei"
|
||||
colspan="6"
|
||||
>
|
||||
<div
|
||||
class="chakra-spinner css-1y7joxr"
|
||||
>
|
||||
<span
|
||||
class="css-8b45rq"
|
||||
>
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
472
src/pages/__tests__/__snapshots__/landing.test.tsx.snap
Normal file
472
src/pages/__tests__/__snapshots__/landing.test.tsx.snap
Normal file
@@ -0,0 +1,472 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Landing page renders page structure 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="chakra-container css-3n6qh3"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-oo194l"
|
||||
>
|
||||
<header
|
||||
class="css-18v4itn"
|
||||
data-testid="hero-section"
|
||||
>
|
||||
<video
|
||||
autoplay=""
|
||||
class="css-ekis1p"
|
||||
loop=""
|
||||
poster="file"
|
||||
src="/remote-assets/demo.mp4"
|
||||
/>
|
||||
<section
|
||||
class="chakra-stack css-1thnzu5"
|
||||
>
|
||||
<div
|
||||
class="css-gmuwbf"
|
||||
>
|
||||
<img
|
||||
alt="Логотип компании «Dry Master»"
|
||||
class="chakra-image css-1pkgess"
|
||||
src="file"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-stack css-o51fhk"
|
||||
>
|
||||
<h1
|
||||
class="chakra-heading css-stv3c1"
|
||||
>
|
||||
Оживите свою поездку с помощью экологически чистого ухода!
|
||||
</h1>
|
||||
<p
|
||||
class="chakra-text css-1wocpjt"
|
||||
>
|
||||
Ощутите максимальное удобство сухой мойки автомобилей, созданной для того, чтобы планета стала чище
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
class="chakra-button css-y8aoxt"
|
||||
href="/dry-wash/order"
|
||||
>
|
||||
Сделать заказ
|
||||
</a>
|
||||
</section>
|
||||
</header>
|
||||
<main
|
||||
class="chakra-stack css-t04jrd"
|
||||
>
|
||||
<section
|
||||
class="chakra-stack css-w9e24l"
|
||||
data-testid="benefits-section"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-10k9nsp"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1dklj6k"
|
||||
>
|
||||
Преимущества экологичной автомойки
|
||||
</h2>
|
||||
<p
|
||||
class="chakra-text css-0"
|
||||
>
|
||||
Откройте для себя преимущества наших услуг по химчистке автомобилей
|
||||
</p>
|
||||
</div>
|
||||
<ul
|
||||
class="css-1okvvnm"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
class="css-vxcmzt"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-cobjw9"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M6.05 8.05a7.001 7.001 0 0 0-.02 9.88c1.47-3.4 4.09-6.24 7.36-7.93A15.952 15.952 0 0 0 8 19.32c2.6 1.23 5.8.78 7.95-1.37C19.43 14.47 20 4 20 4S9.53 4.57 6.05 8.05z"
|
||||
/>
|
||||
</svg>
|
||||
Экологически безопасные продукты
|
||||
</li>
|
||||
<li
|
||||
class="css-vxcmzt"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-cobjw9"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="m14.17 13.71 1.4-2.42c.09-.15.05-.34-.08-.45l-1.48-1.16c.03-.22.05-.45.05-.68s-.02-.46-.05-.69l1.48-1.16c.13-.11.17-.3.08-.45l-1.4-2.42c-.09-.15-.27-.21-.43-.15l-1.74.7c-.36-.28-.75-.51-1.18-.69l-.26-1.85a.364.364 0 0 0-.35-.29h-2.8c-.17 0-.32.13-.35.3L6.8 4.15c-.42.18-.82.41-1.18.69l-1.74-.7c-.16-.06-.34 0-.43.15l-1.4 2.42c-.09.15-.05.34.08.45l1.48 1.16c-.03.22-.05.45-.05.68s.02.46.05.69l-1.48 1.16c-.13.11-.17.3-.08.45l1.4 2.42c.09.15.27.21.43.15l1.74-.7c.36.28.75.51 1.18.69l.26 1.85c.03.16.18.29.35.29h2.8c.17 0 .32-.13.35-.3l.26-1.85c.42-.18.82-.41 1.18-.69l1.74.7c.16.06.34 0 .43-.15zM8.81 11c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM21.92 18.67l-.96-.74c.02-.14.04-.29.04-.44 0-.15-.01-.3-.04-.44l.95-.74c.08-.07.11-.19.05-.29l-.9-1.55c-.05-.1-.17-.13-.28-.1l-1.11.45c-.23-.18-.48-.33-.76-.44l-.17-1.18a.216.216 0 0 0-.21-.2h-1.79c-.11 0-.21.08-.22.19l-.17 1.18c-.27.12-.53.26-.76.44l-1.11-.45a.23.23 0 0 0-.28.1l-.9 1.55c-.05.1-.04.22.05.29l.95.74a3.145 3.145 0 0 0 0 .88l-.95.74c-.08.07-.11.19-.05.29l.9 1.55c.05.1.17.13.28.1l1.11-.45c.23.18.48.33.76.44l.17 1.18c.02.11.11.19.22.19h1.79c.11 0 .21-.08.22-.19l.17-1.18c.27-.12.53-.26.75-.44l1.12.45c.1.04.22 0 .28-.1l.9-1.55c.06-.09.03-.21-.05-.28zm-4.29.16a1.35 1.35 0 1 1 .001-2.701 1.35 1.35 0 0 1-.001 2.701z"
|
||||
/>
|
||||
</svg>
|
||||
Быстрое и эффективное обслуживание
|
||||
</li>
|
||||
<li
|
||||
class="css-vxcmzt"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-cobjw9"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 0 1 0-5 2.5 2.5 0 0 1 0 5z"
|
||||
/>
|
||||
</svg>
|
||||
Удобный мобильный доступ
|
||||
</li>
|
||||
<li
|
||||
class="css-vxcmzt"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-cobjw9"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M16.48 10.41c-.39.39-1.04.39-1.43 0l-4.47-4.46-7.05 7.04-.66-.63a3 3 0 0 1 0-4.24l4.24-4.24a3 3 0 0 1 4.24 0L16.48 9c.39.39.39 1.02 0 1.41zm.7-2.12c.78.78.78 2.05 0 2.83-1.27 1.27-2.61.22-2.83 0l-3.76-3.76-5.57 5.57a.996.996 0 0 0 0 1.41c.39.39 1.02.39 1.42 0l4.62-4.62.71.71-4.62 4.62a.996.996 0 0 0 0 1.41c.39.39 1.02.39 1.42 0l4.62-4.62.71.71-4.62 4.62a.996.996 0 1 0 1.41 1.41l4.62-4.62.71.71-4.62 4.62a.996.996 0 1 0 1.41 1.41l8.32-8.34a3 3 0 0 0 0-4.24l-4.24-4.24a3.001 3.001 0 0 0-4.18-.06l4.47 4.47z"
|
||||
/>
|
||||
</svg>
|
||||
Надежный и заслуживающий доверия
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="chakra-stack css-1ybfgv"
|
||||
>
|
||||
<a
|
||||
class="chakra-button css-vda7qx"
|
||||
href="/dry-wash/order"
|
||||
>
|
||||
Сделать заказ
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="chakra-stack css-w9e24l"
|
||||
data-testid="social-proof-section"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1dklj6k"
|
||||
>
|
||||
Нас выбирают
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-tabs css-wk0uhf"
|
||||
>
|
||||
<div
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-tabs__tablist css-b6qpy9"
|
||||
role="tablist"
|
||||
>
|
||||
<button
|
||||
aria-controls="tabs-:r0:--tabpanel-0"
|
||||
aria-disabled="false"
|
||||
aria-selected="true"
|
||||
class="chakra-tabs__tab css-57d70n"
|
||||
data-index="0"
|
||||
id="tabs-:r0:--tab-0"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<p
|
||||
class="chakra-text css-gyp8mm"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabs-:r0:--tabpanel-1"
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
class="chakra-tabs__tab css-57d70n"
|
||||
data-index="1"
|
||||
id="tabs-:r0:--tab-1"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<p
|
||||
class="chakra-text css-gyp8mm"
|
||||
>
|
||||
1
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabs-:r0:--tabpanel-2"
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
class="chakra-tabs__tab css-57d70n"
|
||||
data-index="2"
|
||||
id="tabs-:r0:--tab-2"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<p
|
||||
class="chakra-text css-gyp8mm"
|
||||
>
|
||||
2
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="tabs-:r0:--tabpanel-3"
|
||||
aria-disabled="false"
|
||||
aria-selected="false"
|
||||
class="chakra-tabs__tab css-57d70n"
|
||||
data-index="3"
|
||||
id="tabs-:r0:--tab-3"
|
||||
role="tab"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<p
|
||||
class="chakra-text css-gyp8mm"
|
||||
>
|
||||
3
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-tabs__tab-panels css-8atqhb"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="tabs-:r0:--tab-0"
|
||||
class="chakra-tabs__tab-panel css-a5mhaz"
|
||||
id="tabs-:r0:--tabpanel-0"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="chakra-card css-wxdwo7"
|
||||
>
|
||||
<span
|
||||
class="chakra-avatar css-3i6ll4"
|
||||
>
|
||||
<div
|
||||
aria-label="Анна Смирнова"
|
||||
class="chakra-avatar__initials css-1ebyn6"
|
||||
role="img"
|
||||
>
|
||||
АС
|
||||
</div>
|
||||
</span>
|
||||
<q
|
||||
class="chakra-text css-w3tajv"
|
||||
>
|
||||
Недавно воспользовалась услугами сухой мойки автомобилей и осталась крайне удовлетворена. Процесс был проведён профессионально: сотрудники использовали качественные средства, которые не повредили лакокрасочное покрытие. Особенно впечатлила возможность мыть машину без воды, что не только экономит ресурсы, но и бережет окружающую среду. Рекомендую всем, кто заботится о своём автомобиле и экологии!
|
||||
</q>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="tabs-:r0:--tab-1"
|
||||
class="chakra-tabs__tab-panel css-a5mhaz"
|
||||
hidden=""
|
||||
id="tabs-:r0:--tabpanel-1"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="chakra-card css-wxdwo7"
|
||||
>
|
||||
<span
|
||||
class="chakra-avatar css-qvlgfo"
|
||||
>
|
||||
<div
|
||||
aria-label="Дмитрий Петров"
|
||||
class="chakra-avatar__initials css-1ebyn6"
|
||||
role="img"
|
||||
>
|
||||
ДП
|
||||
</div>
|
||||
</span>
|
||||
<q
|
||||
class="chakra-text css-w3tajv"
|
||||
>
|
||||
Как же я рад, что нашел эту сухую мойку! Моя машина сияет, как новенькая! 🌟 Сначала был скептически настроен, думал, как же без воды можно отмыть всё это? Но результат превзошёл все ожидания! Ветеринар мойки профессионально подошёл к делу, и она теперь выглядит потрясающе. Если вы хотите, чтобы ваш автомобиль всегда выглядел на 100%, обязательно попробуйте!
|
||||
</q>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="tabs-:r0:--tab-2"
|
||||
class="chakra-tabs__tab-panel css-a5mhaz"
|
||||
hidden=""
|
||||
id="tabs-:r0:--tabpanel-2"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="chakra-card css-wxdwo7"
|
||||
>
|
||||
<span
|
||||
class="chakra-avatar css-1d83cyw"
|
||||
>
|
||||
<div
|
||||
aria-label="Алексей Сидоров"
|
||||
class="chakra-avatar__initials css-1ebyn6"
|
||||
role="img"
|
||||
>
|
||||
АС
|
||||
</div>
|
||||
</span>
|
||||
<q
|
||||
class="chakra-text css-w3tajv"
|
||||
>
|
||||
Сухая мойка автомобилей - интересное решение, которое я опробовал недавно. В целом остался доволен качеством работы. Однако, не все загрязнения удалось удалить с первого раза, но сотрудник предложил дополнительные услуги, что меня устроило. Плюс, большое внимание уделили защите поверхности, что тоже немаловажно. Думаю, в следующий раз снова воспользуюсь этой услугой.
|
||||
</q>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-labelledby="tabs-:r0:--tab-3"
|
||||
class="chakra-tabs__tab-panel css-a5mhaz"
|
||||
hidden=""
|
||||
id="tabs-:r0:--tabpanel-3"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="chakra-card css-wxdwo7"
|
||||
>
|
||||
<span
|
||||
class="chakra-avatar css-ct2cfw"
|
||||
>
|
||||
<div
|
||||
aria-label="Екатерина Иванова"
|
||||
class="chakra-avatar__initials css-1ebyn6"
|
||||
role="img"
|
||||
>
|
||||
ЕИ
|
||||
</div>
|
||||
</span>
|
||||
<q
|
||||
class="chakra-text css-w3tajv"
|
||||
>
|
||||
К сожалению, мой опыт с сухой мойкой автомобилей оказался неудачным. Ожидала увидеть чистую машину после процедуры, но многие участки остались незаделанными. Кроме того, процедура заняла больше времени, чем мне обещали. Возможно, в этом конкретном центре что-то пошло не так, но я бы не стала повторно обращаться за этой услугой.
|
||||
</q>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-stack css-1ybfgv"
|
||||
>
|
||||
<a
|
||||
class="chakra-button css-vda7qx"
|
||||
href="/dry-wash/order"
|
||||
>
|
||||
Сделать заказ
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer
|
||||
class="chakra-stack css-1fatox6"
|
||||
data-testid="footer"
|
||||
>
|
||||
<img
|
||||
alt="Логотип компании «Dry Master»"
|
||||
class="chakra-image css-1pkgess"
|
||||
src="file"
|
||||
/>
|
||||
<p
|
||||
class="chakra-text css-znk605"
|
||||
>
|
||||
© {{currentYear}} DryMaster. Все права защищены
|
||||
</p>
|
||||
<ul
|
||||
class="css-bhb3xr"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
<a
|
||||
class="chakra-link css-spn4bz"
|
||||
href="/"
|
||||
>
|
||||
Политика конфиденциальности
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
<a
|
||||
class="chakra-link css-spn4bz"
|
||||
href="/"
|
||||
>
|
||||
Условия обслуживания
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
<a
|
||||
class="chakra-link css-spn4bz"
|
||||
href="/"
|
||||
>
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
643
src/pages/__tests__/__snapshots__/masters.test.tsx.snap
Normal file
643
src/pages/__tests__/__snapshots__/masters.test.tsx.snap
Normal file
@@ -0,0 +1,643 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Master Page should display master list and show details when master button is clicked 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1yeiifd"
|
||||
>
|
||||
<div
|
||||
class="css-13owfwq"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-173d1bl"
|
||||
>
|
||||
Сухой мастер
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1cggwyz"
|
||||
>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-svjswr"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-18yoix2"
|
||||
href="/order"
|
||||
>
|
||||
Заказы
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-svjswr"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-1kg18wp"
|
||||
data-testid="master-button"
|
||||
href="/master"
|
||||
>
|
||||
Мастера
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-svjswr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-jiwy8d"
|
||||
>
|
||||
<div
|
||||
class="css-1glkkdp"
|
||||
>
|
||||
<div
|
||||
class="css-sd3fvu"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1jb3vzl"
|
||||
>
|
||||
|
||||
Мастера
|
||||
</h2>
|
||||
<button
|
||||
class="chakra-button css-h211ee"
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
<table
|
||||
class="chakra-table css-5605sr"
|
||||
>
|
||||
<thead
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Имя
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Актуальная занятость
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Телефон
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Действия
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-editable css-vtl58r"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-c2wmld"
|
||||
>
|
||||
<span
|
||||
class="chakra-editable__preview css-1gasyng"
|
||||
>
|
||||
Иван Иванов
|
||||
</span>
|
||||
<input
|
||||
class="chakra-editable__input chakra-input css-2lpiar"
|
||||
hidden=""
|
||||
value="Иван Иванов"
|
||||
/>
|
||||
<div
|
||||
class="css-1l4w6pd"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="chakra-button css-1pqvhxt"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<p
|
||||
class="chakra-text css-q9k0mw"
|
||||
>
|
||||
Свободен
|
||||
</p>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-editable css-vtl58r"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-c2wmld"
|
||||
>
|
||||
<span
|
||||
class="chakra-editable__preview css-1gasyng"
|
||||
>
|
||||
+7 900 123 45 67
|
||||
</span>
|
||||
<input
|
||||
class="chakra-editable__input chakra-input css-2lpiar"
|
||||
hidden=""
|
||||
value="+7 900 123 45 67"
|
||||
/>
|
||||
<div
|
||||
class="css-1l4w6pd"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="chakra-button css-1pqvhxt"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<button
|
||||
aria-controls="menu-list-:r2:"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
class="chakra-button chakra-menu__menu-button css-13sr8jm"
|
||||
id="menu-button-:r2:"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="css-ktd6ms"
|
||||
style="visibility: hidden; position: absolute; min-width: max-content; inset: 0 auto auto 0;"
|
||||
>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
class="chakra-menu__menu-list css-s5t7bz"
|
||||
id="menu-list-:r2:"
|
||||
role="menu"
|
||||
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.8) translateZ(0);"
|
||||
tabindex="-1"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="chakra-menu__menuitem css-y7jzs3"
|
||||
data-index="0"
|
||||
id="menu-list-:r2:-menuitem-:r3:"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
Удалить мастера
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-editable css-vtl58r"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-c2wmld"
|
||||
>
|
||||
<span
|
||||
class="chakra-editable__preview css-1gasyng"
|
||||
>
|
||||
Олег Макаров
|
||||
</span>
|
||||
<input
|
||||
class="chakra-editable__input chakra-input css-2lpiar"
|
||||
hidden=""
|
||||
value="Олег Макаров"
|
||||
/>
|
||||
<div
|
||||
class="css-1l4w6pd"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="chakra-button css-1pqvhxt"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<p
|
||||
class="chakra-text css-q9k0mw"
|
||||
>
|
||||
Свободен
|
||||
</p>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-editable css-vtl58r"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-c2wmld"
|
||||
>
|
||||
<span
|
||||
class="chakra-editable__preview css-1gasyng"
|
||||
>
|
||||
79001234567
|
||||
</span>
|
||||
<input
|
||||
class="chakra-editable__input chakra-input css-2lpiar"
|
||||
hidden=""
|
||||
value="79001234567"
|
||||
/>
|
||||
<div
|
||||
class="css-1l4w6pd"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="chakra-button css-1pqvhxt"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<button
|
||||
aria-controls="menu-list-:r5:"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
class="chakra-button chakra-menu__menu-button css-13sr8jm"
|
||||
id="menu-button-:r5:"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="css-ktd6ms"
|
||||
style="visibility: hidden; position: absolute; min-width: max-content; inset: 0 auto auto 0;"
|
||||
>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
class="chakra-menu__menu-list css-s5t7bz"
|
||||
id="menu-list-:r5:"
|
||||
role="menu"
|
||||
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.8) translateZ(0);"
|
||||
tabindex="-1"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="chakra-menu__menuitem css-y7jzs3"
|
||||
data-index="0"
|
||||
id="menu-list-:r5:-menuitem-:r6:"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
Удалить мастера
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-editable css-vtl58r"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-c2wmld"
|
||||
>
|
||||
<span
|
||||
class="chakra-editable__preview css-1gasyng"
|
||||
>
|
||||
Иван Галкин
|
||||
</span>
|
||||
<input
|
||||
class="chakra-editable__input chakra-input css-2lpiar"
|
||||
hidden=""
|
||||
value="Иван Галкин"
|
||||
/>
|
||||
<div
|
||||
class="css-1l4w6pd"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="chakra-button css-1pqvhxt"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-1f0wxn3"
|
||||
>
|
||||
<span
|
||||
class="chakra-badge css-1g1qw76"
|
||||
/>
|
||||
<span
|
||||
class="chakra-badge css-1g1qw76"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-editable css-vtl58r"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-c2wmld"
|
||||
>
|
||||
<span
|
||||
class="chakra-editable__preview css-1gasyng"
|
||||
>
|
||||
+7 900 123 45 67
|
||||
</span>
|
||||
<input
|
||||
class="chakra-editable__input chakra-input css-2lpiar"
|
||||
hidden=""
|
||||
value="+7 900 123 45 67"
|
||||
/>
|
||||
<div
|
||||
class="css-1l4w6pd"
|
||||
>
|
||||
<button
|
||||
aria-label="Edit"
|
||||
class="chakra-button css-1pqvhxt"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<button
|
||||
aria-controls="menu-list-:r8:"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
class="chakra-button chakra-menu__menu-button css-13sr8jm"
|
||||
id="menu-button-:r8:"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
/>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="css-ktd6ms"
|
||||
style="visibility: hidden; position: absolute; min-width: max-content; inset: 0 auto auto 0;"
|
||||
>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
class="chakra-menu__menu-list css-s5t7bz"
|
||||
id="menu-list-:r8:"
|
||||
role="menu"
|
||||
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.8) translateZ(0);"
|
||||
tabindex="-1"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="chakra-menu__menuitem css-y7jzs3"
|
||||
data-index="0"
|
||||
id="menu-list-:r8:-menuitem-:r9:"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
Удалить мастера
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
666
src/pages/__tests__/__snapshots__/order-create.test.tsx.snap
Normal file
666
src/pages/__tests__/__snapshots__/order-create.test.tsx.snap
Normal file
@@ -0,0 +1,666 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Create Order page renders page structure 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="chakra-container css-3n6qh3"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-oo194l"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1c6lvb5"
|
||||
data-testid="heading"
|
||||
>
|
||||
Заказать мойку
|
||||
</h2>
|
||||
<div
|
||||
class="css-1cq1v9f"
|
||||
data-testid="order-form"
|
||||
>
|
||||
<form
|
||||
class="chakra-stack css-1dhetpf"
|
||||
novalidate=""
|
||||
>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="phone"
|
||||
id="field-:r0:-label"
|
||||
>
|
||||
Номер телефона
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="chakra-form__required-indicator css-1tfjd1n"
|
||||
role="presentation"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
aria-required="true"
|
||||
autocomplete="tel"
|
||||
class="chakra-input css-moii5c"
|
||||
id="field-:r0:"
|
||||
name="phone"
|
||||
required=""
|
||||
type="tel"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="carNumber"
|
||||
id="field-:r1:-label"
|
||||
>
|
||||
Номер автомобиля
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="chakra-form__required-indicator css-1tfjd1n"
|
||||
role="presentation"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
aria-required="true"
|
||||
class="chakra-input css-moii5c"
|
||||
id="field-:r1:"
|
||||
maxlength="12"
|
||||
name="carNumber"
|
||||
required=""
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="carColor"
|
||||
id="field-:r2:-label"
|
||||
>
|
||||
Цвет автомобиля
|
||||
</label>
|
||||
<input
|
||||
class="chakra-input css-moii5c"
|
||||
id="field-:r2:"
|
||||
list=":r3:"
|
||||
name="carColor"
|
||||
value=""
|
||||
/>
|
||||
<datalist
|
||||
id=":r3:"
|
||||
>
|
||||
<option
|
||||
label="white"
|
||||
value="#ffffff"
|
||||
>
|
||||
white
|
||||
</option>
|
||||
<option
|
||||
label="black"
|
||||
value="#000000"
|
||||
>
|
||||
black
|
||||
</option>
|
||||
<option
|
||||
label="silver"
|
||||
value="#c0c0c0"
|
||||
>
|
||||
silver
|
||||
</option>
|
||||
<option
|
||||
label="gray"
|
||||
value="#808080"
|
||||
>
|
||||
gray
|
||||
</option>
|
||||
<option
|
||||
label="beige-brown"
|
||||
value="#796745"
|
||||
>
|
||||
beige-brown
|
||||
</option>
|
||||
<option
|
||||
label="red"
|
||||
value="#b90000"
|
||||
>
|
||||
red
|
||||
</option>
|
||||
<option
|
||||
label="blue"
|
||||
value="#003B62"
|
||||
>
|
||||
blue
|
||||
</option>
|
||||
<option
|
||||
label="green"
|
||||
value="#078d51"
|
||||
>
|
||||
green
|
||||
</option>
|
||||
</datalist>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="carBody"
|
||||
id="field-:r4:-label"
|
||||
>
|
||||
Тип кузова автомобиля
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="chakra-form__required-indicator css-1tfjd1n"
|
||||
role="presentation"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="css-8atqhb"
|
||||
>
|
||||
<input
|
||||
aria-readonly="true"
|
||||
aria-required="true"
|
||||
class="chakra-input css-moii5c"
|
||||
id="field-:r4:"
|
||||
name="carBody"
|
||||
placeholder="Не указан"
|
||||
readonly=""
|
||||
required=""
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="chakra-popover__popper css-iy22zq"
|
||||
style="visibility: hidden; position: absolute; inset: 0 auto auto 0;"
|
||||
>
|
||||
<section
|
||||
aria-describedby="popover-body-:r8:"
|
||||
class="chakra-popover__content css-1mvj5hv"
|
||||
id="popover-content-:r8:"
|
||||
role="dialog"
|
||||
style="transform-origin: var(--popper-transform-origin); opacity: 0; visibility: hidden; transform: scale(0.95) translateZ(0);"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="chakra-popover__body css-1uqsyei"
|
||||
id="popover-body-:r8:"
|
||||
>
|
||||
<div
|
||||
class="css-124gwxm"
|
||||
role="radiogroup"
|
||||
>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:r9:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="1"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Седан
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:ra:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="2"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Хэтчбек
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rb:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="3"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Кроссовер
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rc:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="4"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Внедорожник
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rd:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="5"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Универсал
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:re:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="6"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Купе
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rf:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="7"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Минивэн
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rg:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="8"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Пикап
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rh:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="9"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Лифтбек
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:ri:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="10"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Спорткар
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:rj:"
|
||||
name="radio-:r5:"
|
||||
required=""
|
||||
style="border: 0px; clip: rect(0px, 0px, 0px, 0px); height: 1px; width: 1px; margin: -1px; padding: 0px; overflow: hidden; white-space: nowrap; position: absolute;"
|
||||
type="radio"
|
||||
value="99"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="css-daiawq"
|
||||
>
|
||||
<div
|
||||
class="css-dvxtzn"
|
||||
>
|
||||
<img
|
||||
class="chakra-image css-wtpnzt"
|
||||
src="file"
|
||||
/>
|
||||
Другой
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="field-:rk:"
|
||||
id="field-:rk:-label"
|
||||
>
|
||||
В какое время автомобиль доступен?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="chakra-form__required-indicator css-1tfjd1n"
|
||||
role="presentation"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="css-17bf4de"
|
||||
>
|
||||
<div
|
||||
class="css-brwuli"
|
||||
>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<input
|
||||
class="chakra-input css-moii5c"
|
||||
id="field-:rl:"
|
||||
max=""
|
||||
name="availableDatetimeBegin"
|
||||
type="datetime-local"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-brwuli"
|
||||
>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<input
|
||||
class="chakra-input css-moii5c"
|
||||
id="field-:rm:"
|
||||
min=""
|
||||
name="availableDatetimeEnd"
|
||||
type="datetime-local"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="carLocation"
|
||||
id="field-:rn:-label"
|
||||
>
|
||||
Где находится автомобиль?
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="chakra-form__required-indicator css-1tfjd1n"
|
||||
role="presentation"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
<div />
|
||||
<div
|
||||
class="chakra-form__helper-text css-186pyma"
|
||||
id="field-:rn:-helptext"
|
||||
>
|
||||
Например, 55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
<button
|
||||
class="chakra-button css-13n15nk"
|
||||
type="submit"
|
||||
>
|
||||
Отправить
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
237
src/pages/__tests__/__snapshots__/order-view.test.tsx.snap
Normal file
237
src/pages/__tests__/__snapshots__/order-view.test.tsx.snap
Normal file
@@ -0,0 +1,237 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Страница просмотра заказа отображает детали заказа после успешной загрузки 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="chakra-container css-3n6qh3"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-oo194l"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1c6lvb5"
|
||||
data-testid="heading"
|
||||
>
|
||||
Ваш заказ
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1n38vgh"
|
||||
data-testid="order-details"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-1ofajmr"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1jb3vzl"
|
||||
>
|
||||
Заказ №{{number}}
|
||||
(
|
||||
Sunday, January 19, 2025 5:04 PM
|
||||
)
|
||||
</h2>
|
||||
<span
|
||||
class="css-6jfsiv"
|
||||
>
|
||||
В ожидании
|
||||
</span>
|
||||
</div>
|
||||
<ul
|
||||
class="css-1laduhn"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
Владелец
|
||||
:
|
||||
|
||||
<span
|
||||
class="chakra-text css-1qxum2s"
|
||||
>
|
||||
+79876543210
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
Автомобиль
|
||||
:
|
||||
|
||||
<span
|
||||
class="chakra-text css-1qxum2s"
|
||||
>
|
||||
А123АА16, Хэтчбек, #ffffff
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
Где
|
||||
:
|
||||
|
||||
<span
|
||||
class="chakra-text css-1qxum2s"
|
||||
>
|
||||
55.793833888711006,49.19037910644527 Республика Татарстан (Татарстан), Казань, жилой район Седьмое Небо
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="css-0"
|
||||
>
|
||||
Когда
|
||||
:
|
||||
|
||||
<span
|
||||
class="chakra-text css-1qxum2s"
|
||||
>
|
||||
17:03 19.01.2025 - 17:03 19.01.2025
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="chakra-alert css-d7mf12"
|
||||
data-status="info"
|
||||
role="alert"
|
||||
>
|
||||
<span
|
||||
class="chakra-alert__icon css-14ogjxt"
|
||||
data-status="info"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-qk6lof"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12,0A12,12,0,1,0,24,12,12.013,12.013,0,0,0,12,0Zm.25,5a1.5,1.5,0,1,1-1.5,1.5A1.5,1.5,0,0,1,12.25,5ZM14.5,18.5h-4a1,1,0,0,1,0-2h.75a.25.25,0,0,0,.25-.25v-4.5a.25.25,0,0,0-.25-.25H10.5a1,1,0,0,1,0-2h1a2,2,0,0,1,2,2v4.75a.25.25,0,0,0,.25.25h.75a1,1,0,1,1,0,2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
С вами свяжется оператор насчет оплаты по указанному номеру телефона
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Страница просмотра заказа отображает индикатор загрузки деталей заказа 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="chakra-container css-3n6qh3"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-oo194l"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1c6lvb5"
|
||||
data-testid="heading"
|
||||
>
|
||||
Ваш заказ
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-egjlgc"
|
||||
data-testid="loader"
|
||||
>
|
||||
<div
|
||||
class="chakra-spinner css-1j92705"
|
||||
>
|
||||
<span
|
||||
class="css-8b45rq"
|
||||
>
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Страница просмотра заказа отображает ошибку при некорректном ID заказа 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="chakra-container css-3n6qh3"
|
||||
>
|
||||
<div
|
||||
class="chakra-stack css-oo194l"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1c6lvb5"
|
||||
data-testid="heading"
|
||||
>
|
||||
Ваш заказ
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-alert css-g4o1ra"
|
||||
data-status="error"
|
||||
data-testid="error"
|
||||
role="alert"
|
||||
>
|
||||
<span
|
||||
class="chakra-alert__icon css-14ogjxt"
|
||||
data-status="error"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-qk6lof"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M11.983,0a12.206,12.206,0,0,0-8.51,3.653A11.8,11.8,0,0,0,0,12.207,11.779,11.779,0,0,0,11.8,24h.214A12.111,12.111,0,0,0,24,11.791h0A11.766,11.766,0,0,0,11.983,0ZM10.5,16.542a1.476,1.476,0,0,1,1.449-1.53h.027a1.527,1.527,0,0,1,1.523,1.47,1.475,1.475,0,0,1-1.449,1.53h-.027A1.529,1.529,0,0,1,10.5,16.542ZM11,12.5v-6a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<div
|
||||
class="chakra-alert__title css-tidvy5"
|
||||
data-status="error"
|
||||
>
|
||||
Не удалось загрузить детали заказа
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
428
src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap
Normal file
428
src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap
Normal file
@@ -0,0 +1,428 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Страница заказов должна корректно отображать список заказов после загрузки данных 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1yeiifd"
|
||||
>
|
||||
<div
|
||||
class="css-13owfwq"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-173d1bl"
|
||||
>
|
||||
Сухой мастер
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1cggwyz"
|
||||
>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-svjswr"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-1kg18wp"
|
||||
href="/auth/login"
|
||||
>
|
||||
Заказы
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-svjswr"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-1kg18wp"
|
||||
data-testid="master-button"
|
||||
href="/auth/login"
|
||||
>
|
||||
Мастера
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="horizontal"
|
||||
class="chakra-divider css-svjswr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-jiwy8d"
|
||||
>
|
||||
<div
|
||||
class="css-1glkkdp"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-1xer3cv"
|
||||
>
|
||||
Заказы
|
||||
</h2>
|
||||
<div
|
||||
class="css-1u3smh"
|
||||
>
|
||||
<button
|
||||
class="chakra-button css-ez23ye"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<p
|
||||
class="chakra-text css-52ukzg"
|
||||
>
|
||||
23.02.2025
|
||||
</p>
|
||||
<button
|
||||
class="chakra-button css-ez23ye"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<table
|
||||
class="chakra-table css-5605sr"
|
||||
>
|
||||
<thead
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Номер машины
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Дата заказа
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Статус
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Мастер
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Телефон
|
||||
</th>
|
||||
<th
|
||||
class="css-1szkfps"
|
||||
>
|
||||
Расположение
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="css-0"
|
||||
>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
A123BC
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
24.11.2024
|
||||
|
||||
<br />
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-select__wrapper css-42b2qy"
|
||||
>
|
||||
<select
|
||||
class="chakra-select css-11j19cx"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
Выберите статус
|
||||
</option>
|
||||
<option
|
||||
value="pending"
|
||||
>
|
||||
В ожидании
|
||||
</option>
|
||||
<option
|
||||
value="progress"
|
||||
>
|
||||
Выполняется
|
||||
</option>
|
||||
<option
|
||||
value="working"
|
||||
>
|
||||
В работе
|
||||
</option>
|
||||
<option
|
||||
value="canceled"
|
||||
>
|
||||
Отменено
|
||||
</option>
|
||||
<option
|
||||
value="complete"
|
||||
>
|
||||
Завершено
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="chakra-select__icon-wrapper css-iohxn1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-select__icon"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
style="width: 1em; height: 1em; color: currentColor;"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-select__wrapper css-42b2qy"
|
||||
>
|
||||
<select
|
||||
class="chakra-select css-161pkch"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
Выберите мастера
|
||||
</option>
|
||||
<option
|
||||
value="Иван Иванов"
|
||||
>
|
||||
Иван Иванов
|
||||
</option>
|
||||
<option
|
||||
value="Олег Макаров"
|
||||
>
|
||||
Олег Макаров
|
||||
</option>
|
||||
<option
|
||||
value="Иван Галкин"
|
||||
>
|
||||
Иван Галкин
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="chakra-select__icon-wrapper css-iohxn1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-select__icon"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
style="width: 1em; height: 1em; color: currentColor;"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<a
|
||||
class="chakra-link css-spn4bz"
|
||||
href="tel:"
|
||||
>
|
||||
79001234563
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
Казань, ул. Баумана, 1
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="css-0"
|
||||
>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
A245BC
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
24.11.2024
|
||||
|
||||
<br />
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-select__wrapper css-42b2qy"
|
||||
>
|
||||
<select
|
||||
class="chakra-select css-lvra4l"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
Выберите статус
|
||||
</option>
|
||||
<option
|
||||
value="pending"
|
||||
>
|
||||
В ожидании
|
||||
</option>
|
||||
<option
|
||||
value="progress"
|
||||
>
|
||||
Выполняется
|
||||
</option>
|
||||
<option
|
||||
value="working"
|
||||
>
|
||||
В работе
|
||||
</option>
|
||||
<option
|
||||
value="canceled"
|
||||
>
|
||||
Отменено
|
||||
</option>
|
||||
<option
|
||||
value="complete"
|
||||
>
|
||||
Завершено
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="chakra-select__icon-wrapper css-iohxn1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-select__icon"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
style="width: 1em; height: 1em; color: currentColor;"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<div
|
||||
class="chakra-select__wrapper css-42b2qy"
|
||||
>
|
||||
<select
|
||||
class="chakra-select css-161pkch"
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
Выберите мастера
|
||||
</option>
|
||||
<option
|
||||
value="Иван Иванов"
|
||||
>
|
||||
Иван Иванов
|
||||
</option>
|
||||
<option
|
||||
value="Олег Макаров"
|
||||
>
|
||||
Олег Макаров
|
||||
</option>
|
||||
<option
|
||||
value="Иван Галкин"
|
||||
>
|
||||
Иван Галкин
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
class="chakra-select__icon-wrapper css-iohxn1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="chakra-select__icon"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
style="width: 1em; height: 1em; color: currentColor;"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
<a
|
||||
class="chakra-link css-spn4bz"
|
||||
href="tel:"
|
||||
>
|
||||
79001234567
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
class="css-zgoslk"
|
||||
>
|
||||
Казань, ул. Баумана, 43
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
hidden=""
|
||||
id="__chakra_env"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
22
src/pages/__tests__/landing.test.tsx
Normal file
22
src/pages/__tests__/landing.test.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppContext } from '../../../__mocks__/app-context-mock';
|
||||
import Page from '../landing';
|
||||
|
||||
describe('Landing page', () => {
|
||||
test('renders page structure', () => {
|
||||
const { container } = render(
|
||||
<AppContext>
|
||||
<Page />
|
||||
</AppContext>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
expect(screen.getByTestId('hero-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('benefits-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('social-proof-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
133
src/pages/__tests__/masters.test.tsx
Normal file
133
src/pages/__tests__/masters.test.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
jest,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
afterAll,
|
||||
} from '@jest/globals';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import { store } from '../../__data__/store';
|
||||
import Page from '../arm';
|
||||
|
||||
const server = setupServer(
|
||||
http.get('/api/arm/masters', () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
body: [
|
||||
{
|
||||
id: '4545423234',
|
||||
name: 'Иван Иванов',
|
||||
phone: '+7 900 123 45 67',
|
||||
},
|
||||
{
|
||||
name: 'Олег Макаров',
|
||||
phone: '79001234567',
|
||||
id: '23423442',
|
||||
},
|
||||
{
|
||||
id: '345354234',
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
http.post('/api/arm/orders', () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
body: [
|
||||
{
|
||||
id: 'order1',
|
||||
carNumber: 'A123BC',
|
||||
startWashTime: '2024-11-24T10:30:00.000Z',
|
||||
endWashTime: '2024-11-24T16:30:00.000Z',
|
||||
orderDate: '2024-11-24T08:41:46.366Z',
|
||||
status: 'pending',
|
||||
phone: '79001234563',
|
||||
location: 'Казань, ул. Баумана, 1',
|
||||
master: {
|
||||
name: 'Олег Макаров',
|
||||
phone: '79001234567',
|
||||
id: '23423442',
|
||||
},
|
||||
notes: '',
|
||||
},
|
||||
{
|
||||
id: 'order2',
|
||||
carNumber: 'A245BC',
|
||||
startWashTime: '2024-11-24T11:30:00.000Z',
|
||||
endWashTime: '2024-11-24T17:30:00.000Z',
|
||||
orderDate: '2024-11-24T07:40:46.366Z',
|
||||
status: 'progress',
|
||||
phone: '79001234567',
|
||||
location: 'Казань, ул. Баумана, 43',
|
||||
master: [],
|
||||
notes: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@brojs/cli', () => {
|
||||
return {
|
||||
getNavigationValue: (key: string) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require('../../../bro.config').navigations[key],
|
||||
getConfigValue: () => '/api',
|
||||
};
|
||||
});
|
||||
|
||||
describe('Master Page', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
it('should display master list and show details when master button is clicked', async () => {
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Page mockUser={{ name: 'ilnaz' }} />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const button = await waitFor(() => screen.getByTestId('master-button'));
|
||||
fireEvent.click(button);
|
||||
|
||||
// Проверяем отображение всех мастеров
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Иван Иванов')).toBeInTheDocument();
|
||||
expect(screen.getByText('Олег Макаров')).toBeInTheDocument();
|
||||
expect(screen.getByText('Иван Галкин')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
20
src/pages/__tests__/order-create.test.tsx
Normal file
20
src/pages/__tests__/order-create.test.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppContext } from '../../../__mocks__/app-context-mock';
|
||||
import Page from '../order-create';
|
||||
|
||||
describe('Create Order page', () => {
|
||||
test('renders page structure', () => {
|
||||
const { container } = render(
|
||||
<AppContext>
|
||||
<Page />
|
||||
</AppContext>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
expect(screen.getByTestId('heading')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('order-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
77
src/pages/__tests__/order-view.test.tsx
Normal file
77
src/pages/__tests__/order-view.test.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { AppContext } from '../../../__mocks__/app-context-mock';
|
||||
import Page from '../order-view';
|
||||
import { server } from '../../../__mocks__/server/server';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Страница просмотра заказа', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
test('отображает индикатор загрузки деталей заказа', () => {
|
||||
(useParams as jest.Mock).mockReturnValue({ orderId: 'id1' });
|
||||
|
||||
const { container } = render(
|
||||
<AppContext>
|
||||
<Page />
|
||||
</AppContext>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('heading')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('loader')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('order-details')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('error')).not.toBeInTheDocument();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('отображает детали заказа после успешной загрузки', async () => {
|
||||
(useParams as jest.Mock).mockReturnValue({ orderId: 'id1' });
|
||||
|
||||
const { container } = render(
|
||||
<AppContext>
|
||||
<Page />
|
||||
</AppContext>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('heading')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('loader')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('order-details')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('error')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('отображает ошибку при некорректном ID заказа', async () => {
|
||||
(useParams as jest.Mock).mockReturnValue({ orderId: null });
|
||||
|
||||
const { container } = render(
|
||||
<AppContext>
|
||||
<Page />
|
||||
</AppContext>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('heading')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('loader')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('order-details')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
63
src/pages/__tests__/ordersEmpty.test.tsx
Normal file
63
src/pages/__tests__/ordersEmpty.test.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
jest,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
afterAll,
|
||||
} from '@jest/globals';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import { store } from '../../__data__/store';
|
||||
import Page from '../arm';
|
||||
|
||||
const server = setupServer(
|
||||
http.post('/api/arm/orders', () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
body: [],
|
||||
});
|
||||
}),
|
||||
http.get('/api/arm/masters', () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
body: [],
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@brojs/cli', () => {
|
||||
return {
|
||||
getNavigationValue: () => '/auth/login',
|
||||
getConfigValue: () => '/api',
|
||||
};
|
||||
});
|
||||
|
||||
describe('order page', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
it('получение пустого списка', async () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Page mockUser={{ name: 'ilnaz' }} />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => screen.getByText('Список пуст'));
|
||||
});
|
||||
});
|
||||
98
src/pages/__tests__/ordersError.test.tsx
Normal file
98
src/pages/__tests__/ordersError.test.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
jest,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
afterAll,
|
||||
expect,
|
||||
} from '@jest/globals';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import { store } from '../../__data__/store';
|
||||
import Page from '../arm';
|
||||
import { PageSpinner } from '../../components';
|
||||
|
||||
const server = setupServer(
|
||||
http.post('/api/arm/orders', () => {
|
||||
return HttpResponse.json({}, { status: 500 });
|
||||
}),
|
||||
http.get('/api/arm/masters', () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
body: [
|
||||
{
|
||||
id: '4545423234',
|
||||
name: 'Иван Иванов',
|
||||
phone: '+7 900 123 45 67',
|
||||
},
|
||||
{
|
||||
name: 'Олег Макаров',
|
||||
phone: '79001234567',
|
||||
id: '23423442',
|
||||
},
|
||||
{
|
||||
id: '345354234',
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@brojs/cli', () => {
|
||||
return {
|
||||
getNavigationValue: () => '/auth/login',
|
||||
getConfigValue: () => '/api',
|
||||
};
|
||||
});
|
||||
|
||||
describe('order page', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
it('обработка ошибки при загрузке данных', async () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Page mockUser={{ name: 'ilnaz' }} />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => screen.getByText('Ошибка при загрузке данных'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Routers', () => {
|
||||
it('отображает PageSpinner ', async () => {
|
||||
render(<PageSpinner />);
|
||||
|
||||
expect(await screen.findByTestId('spinner')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
@@ -12,7 +12,11 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import { store } from '../../__data__/store';
|
||||
import Page from '../arm';
|
||||
|
||||
const server = setupServer(
|
||||
@@ -87,17 +91,6 @@ const server = setupServer(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('react-i18next', () => {
|
||||
return {
|
||||
useTranslation: () => {
|
||||
return {
|
||||
t: (key: never) => `${key}`,
|
||||
i18n: {},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@brojs/cli', () => {
|
||||
return {
|
||||
getNavigationValue: () => '/auth/login',
|
||||
@@ -105,24 +98,24 @@ jest.mock('@brojs/cli', () => {
|
||||
};
|
||||
});
|
||||
|
||||
describe('Arm Page', () => {
|
||||
describe('Страница заказов', () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
it('render ', async () => {
|
||||
server.events.on('request:start', ({ request }) => {
|
||||
console.log('Outgoing:', request.method, request.url);
|
||||
});
|
||||
|
||||
it('должна корректно отображать список заказов после загрузки данных', async () => {
|
||||
const { container } = render(
|
||||
<BrowserRouter>
|
||||
<Page mockUser={{ name: 'ilnaz' }} />
|
||||
</BrowserRouter>,
|
||||
<Provider store={store}>
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Page mockUser={{ name: 'ilnaz' }} />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</ChakraProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
await waitFor(() => screen.getByText('A123BC'));
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
@@ -30,18 +30,19 @@ const Page: FC = () => {
|
||||
<HeroSection
|
||||
data={landingSuccessStub['body']['hero-section']}
|
||||
flexShrink={0}
|
||||
data-testid='hero-section'
|
||||
/>
|
||||
<VStack as='main' flexGrow={1}>
|
||||
{landingSuccessStub.body.sections.map(({ type, ...data }, i) => {
|
||||
if (isBenefitsSectionData(type, data)) {
|
||||
return <BenefitsSection key={i} data={data} />;
|
||||
return <BenefitsSection key={i} data={data} data-testid='benefits-section' />;
|
||||
}
|
||||
if (isSocialProofSectionData(type, data)) {
|
||||
return <SocialProofSection key={i} data={data} />;
|
||||
return <SocialProofSection key={i} data={data} data-testid='social-proof-section' />;
|
||||
}
|
||||
})}
|
||||
</VStack>
|
||||
<Footer />
|
||||
<Footer data-testid='footer' />
|
||||
</VStack>
|
||||
</Container>
|
||||
</LandingThemeProvider>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useToast } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Order } from "../../models/landing";
|
||||
import { OrderFormValues } from "../../components/order-form";
|
||||
import { isErrorMessage } from "../../models/api";
|
||||
import { URLs } from '../../__data__/urls';
|
||||
|
||||
const removeAllSpaces = (str: string) => str.replace(/\s+/g, '');
|
||||
|
||||
@@ -26,4 +32,40 @@ export const formatFormValues = ({ phone, carNumber, carBody, carColor, carLocat
|
||||
end: dayjs(availableDatetimeEnd).toISOString(),
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const useHandleCreateOrderMutationResponse = (query: {
|
||||
isSuccess: boolean;
|
||||
data?: {
|
||||
id: Parameters<typeof URLs.orderView.getUrl>[0];
|
||||
};
|
||||
isError: boolean;
|
||||
error?: unknown;
|
||||
}) => {
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.order-create.create-order-query',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isError) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('error.title'),
|
||||
description: isErrorMessage(query.error) ? query.error : undefined,
|
||||
});
|
||||
}
|
||||
}, [query.isError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isSuccess) {
|
||||
const orderId = query.data.id;
|
||||
navigate({ pathname: URLs.orderView.getUrl(orderId) });
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('success.title'),
|
||||
});
|
||||
}
|
||||
}, [query.isSuccess]);
|
||||
};
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Container, Heading, useToast, VStack } from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Container, Heading, VStack } from '@chakra-ui/react';
|
||||
import { Player as LottiePlayer } from '@lottiefiles/react-lottie-player';
|
||||
|
||||
import { withLandingThemeProvider } from '../../containers';
|
||||
import { OrderForm, OrderFormProps } from '../../components/order-form';
|
||||
import { useCreateOrderMutation } from '../../api';
|
||||
import { URLs } from '../../__data__/urls';
|
||||
import { landingApi } from '../../__data__/service/landing.api';
|
||||
import { OrderCreationAnimation } from '../../assets/animation';
|
||||
|
||||
import { formatFormValues } from './helper';
|
||||
import {
|
||||
formatFormValues,
|
||||
useHandleCreateOrderMutationResponse,
|
||||
} from './helper';
|
||||
|
||||
const Page: FC = () => {
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.order-create',
|
||||
});
|
||||
|
||||
const [createOrder, createOrderMutation] = useCreateOrderMutation();
|
||||
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [createOrder, createOrderMutation] =
|
||||
landingApi.useCreateOrderMutation();
|
||||
useHandleCreateOrderMutationResponse(createOrderMutation);
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
createOrder({ body: formatFormValues(values) });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -48,13 +36,25 @@ const Page: FC = () => {
|
||||
centerContent
|
||||
>
|
||||
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
|
||||
<Heading textAlign='center' mt={4}>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
<OrderForm
|
||||
onSubmit={onOrderFormSubmit}
|
||||
loading={createOrderMutation.isLoading}
|
||||
/>
|
||||
{createOrderMutation.isUninitialized ? (
|
||||
<>
|
||||
<Heading textAlign='center' mt={4} data-testid='heading'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
<OrderForm
|
||||
onSubmit={onOrderFormSubmit}
|
||||
loading={createOrderMutation.isLoading}
|
||||
data-testid='order-form'
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LottiePlayer autoplay loop src={OrderCreationAnimation} />
|
||||
<Heading textAlign='center' mt={4}>
|
||||
{t('order-creation-title')}
|
||||
</Heading>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,10 @@ import {
|
||||
import { OrderDetails } from '../../components/order-view';
|
||||
import { Order } from '../../models/landing';
|
||||
import { landingApi } from '../../__data__/service/landing.api';
|
||||
import { ErrorMessage } from '../../models/api';
|
||||
import { isErrorMessage } from '../../models/api';
|
||||
import { FEATURE } from '../../__data__/features';
|
||||
import { CarImageForm } from '../../components/order-view/car-img';
|
||||
import PriceCar from '../../components/PriceCar';
|
||||
|
||||
const Page: FC = () => {
|
||||
const { t } = useTranslation('~', {
|
||||
@@ -45,7 +47,7 @@ const Page: FC = () => {
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
const errorMessage = error as ErrorMessage;
|
||||
const errorMessage = isErrorMessage(error) ? error : undefined;
|
||||
|
||||
return (
|
||||
<LandingThemeProvider>
|
||||
@@ -58,40 +60,62 @@ const Page: FC = () => {
|
||||
centerContent
|
||||
>
|
||||
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
|
||||
<Heading textAlign='center' mt={4}>
|
||||
<Heading textAlign='center' mt={4} data-testid='heading'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
{isLoading ? (
|
||||
<HStack width='full' justifyContent='center'>
|
||||
<HStack width='full' justifyContent='center' data-testid='loader'>
|
||||
<Spinner size='lg' />
|
||||
</HStack>
|
||||
) : (
|
||||
<>
|
||||
<>
|
||||
{isSuccess && (
|
||||
<OrderDetails
|
||||
orderNumber={order.orderNumber}
|
||||
status={order.status}
|
||||
phone={order.phone}
|
||||
carNumber={order.carNumber}
|
||||
carBody={order.carBody}
|
||||
carColor={order.carColor}
|
||||
location={order.location}
|
||||
startWashTime={order.startWashTime}
|
||||
endWashTime={order.endWashTime}
|
||||
created={order.created}
|
||||
/>
|
||||
<VStack
|
||||
p={4}
|
||||
alignItems='flex-start'
|
||||
gap={4}
|
||||
data-testid='order-details'
|
||||
>
|
||||
<OrderDetails
|
||||
orderNumber={order.orderNumber}
|
||||
status={order.status}
|
||||
phone={order.phone}
|
||||
carNumber={order.carNumber}
|
||||
carBody={order.carBody}
|
||||
carColor={order.carColor}
|
||||
location={order.location}
|
||||
startWashTime={order.startWashTime}
|
||||
endWashTime={order.endWashTime}
|
||||
created={order.created}
|
||||
/>
|
||||
{FEATURE.carImageUpload.isOn && (
|
||||
<CarImageForm orderId={orderId} />
|
||||
)}
|
||||
{FEATURE.carImageUpload.isOn && order.image && (
|
||||
<PriceCar
|
||||
image={order?.image}
|
||||
rating={order?.imageRating}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
{isError && (
|
||||
<Alert status='error' alignItems='flex-start'>
|
||||
<Alert
|
||||
status='error'
|
||||
alignItems='flex-start'
|
||||
data-testid='error'
|
||||
>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>
|
||||
{t('get-order-query.error.title')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
{errorMessage && (
|
||||
<AlertDescription>{errorMessage}</AlertDescription>
|
||||
)}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
13
src/utils/getCoordinates.ts
Normal file
13
src/utils/getCoordinates.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const getCoordinates = (location: string) => {
|
||||
if (!location) return null;
|
||||
|
||||
const [lat, lon] = location
|
||||
.split(',')
|
||||
.map((coord) => parseFloat(coord.trim()));
|
||||
|
||||
if (isNaN(lat) || isNaN(lon)) return null;
|
||||
|
||||
return { lat, lon };
|
||||
};
|
||||
|
||||
export default getCoordinates;
|
||||
@@ -2,7 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const router = require('express').Router();
|
||||
|
||||
const STUBS = { masters: 'success', orders: 'success', orderView: 'success-pending' };
|
||||
const STUBS = { masters: 'success', orders: 'success', orderCreate: 'success', orderView: 'success-pending', orderCarImg: 'success' };
|
||||
|
||||
router.get('/set/:name/:value', (req, res) => {
|
||||
const { name, value } = req.params;
|
||||
@@ -14,24 +14,35 @@ router.get('/set/:name/:value', (req, res) => {
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.send(`<div>
|
||||
<fieldset>
|
||||
<legend>Мастера</legend>
|
||||
<fieldset>
|
||||
<legend>Мастера</legend>
|
||||
${generateRadioInput('masters', 'success')}
|
||||
${generateRadioInput('masters', 'error')}
|
||||
${generateRadioInput('masters', 'empty')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Заказы</legend>
|
||||
<fieldset>
|
||||
<legend>Заказы</legend>
|
||||
${generateRadioInput('orders', 'success')}
|
||||
${generateRadioInput('orders', 'error')}
|
||||
${generateRadioInput('orders', 'empty')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Лендинг - Детали заказа</legend>
|
||||
<fieldset>
|
||||
<legend>Лендинг - Сделать заказ</legend>
|
||||
${generateRadioInput('orderCreate', 'success')}
|
||||
${generateRadioInput('orderCreate', 'error')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Лендинг - Детали заказа</legend>
|
||||
${generateRadioInput('orderView', 'success-pending')}
|
||||
${generateRadioInput('orderView', 'success-working')}
|
||||
${generateRadioInput('orderView', 'error')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Лендинг - Детали заказа, фото машины</legend>
|
||||
${generateRadioInput('orderCarImg', 'success')}
|
||||
${generateRadioInput('orderCarImg', 'error-file-type')}
|
||||
${generateRadioInput('orderCarImg', 'error-file-size')}
|
||||
</fieldset>
|
||||
</div>`);
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ router.delete('/arm/masters/:id', (req, res) => {
|
||||
);
|
||||
});
|
||||
|
||||
router.patch('/orders/:id', (req, res) => {
|
||||
router.patch('/order/:id', (req, res) => {
|
||||
res
|
||||
.status(/error/.test(STUBS.orders) ? 500 : 200)
|
||||
.send(
|
||||
@@ -87,7 +87,31 @@ router.get('/order/:orderId', ({ params }, res) => {
|
||||
});
|
||||
|
||||
router.post('/order/create', (req, res) => {
|
||||
res.status(200).send({ success: true, body: { ok: true } });
|
||||
const stubName = STUBS.orderCreate;
|
||||
|
||||
res
|
||||
.status(/error/.test(stubName) ? 500 : 200)
|
||||
.send(
|
||||
/^error$/.test(stubName)
|
||||
? commonError
|
||||
: require(`../json/landing-order-create/${stubName}.json`),
|
||||
);
|
||||
});
|
||||
|
||||
router.post('/order/:orderId/upload-car-img', (req, res) => {
|
||||
const { orderId } = req.params;
|
||||
const stubName = `${orderId}-${STUBS.orderCarImg}`;
|
||||
|
||||
try {
|
||||
res
|
||||
.status(/error/.test(stubName) ? 500 : 200)
|
||||
.send(require(`../json/landing-order-car-image-upload/${stubName}.json`));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res
|
||||
.status(500)
|
||||
.send(commonError);
|
||||
}
|
||||
});
|
||||
|
||||
router.use('/admin', require('./admin'));
|
||||
|
||||
@@ -9,12 +9,8 @@
|
||||
"orderDate": "2024-11-24T08:41:46.366Z",
|
||||
"status": "pending",
|
||||
"phone": "79001234563",
|
||||
"location": "Казань, ул. Баумана, 1",
|
||||
"master": {
|
||||
"name": "Олег Макаров",
|
||||
"phone": "79001234567",
|
||||
"id": "23423442"
|
||||
},
|
||||
"location": "55.779905316526424,49.12446113769528 Республика Татарстан (Татарстан),н Казань, ул. Баумана, 1",
|
||||
"master": "4545423234",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
@@ -25,7 +21,7 @@
|
||||
"orderDate": "2024-11-24T07:40:46.366Z",
|
||||
"status": "progress",
|
||||
"phone": "79001234567",
|
||||
"location": "Казань, ул. Баумана, 43",
|
||||
"location": "55.75060416346278,48.746329576898944 Республика Татарстан (Татарстан), Верхнеуслонский район, Иннополис",
|
||||
"master": [],
|
||||
"notes": ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid car image file size. Limit is 5MB"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid car image file type. Allowed types: jpg, png"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
4
stubs/json/landing-order-create/error.json
Normal file
4
stubs/json/landing-order-create/error.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"error": "Не удалось создать заказ"
|
||||
}
|
||||
6
stubs/json/landing-order-create/success.json
Normal file
6
stubs/json/landing-order-create/success.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"success": true,
|
||||
"body": {
|
||||
"id": "id1"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"message": "Не удалось загрузить детали заказа"
|
||||
"error": "Не удалось загрузить детали заказа"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user