Compare commits
80 Commits
f6cc2efb86
...
feat/arm-m
| Author | SHA1 | Date | |
|---|---|---|---|
| 1603d64caf | |||
| 9b84ab4c4d | |||
| 18c13628ba | |||
| 9f2d98a9ea | |||
| fea4a54c83 | |||
| f70aff175d | |||
|
|
0b9b2f4dbc | ||
|
|
87a4dcefdd | ||
|
|
0679ad92ef | ||
| 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 | |||
| 859fa4f2e1 | |||
| 658e23d4e3 | |||
| ed8ae95436 | |||
| a00aaff29d |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -130,4 +130,10 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.idea
|
||||
.idea
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
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: {
|
||||
|
||||
26
e2e/example.spec.ts
Normal file
26
e2e/example.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach('check server is up', async ({ page }) => {
|
||||
try {
|
||||
await page.goto('http://localhost:8099/dry-wash');
|
||||
const makeOrderText = page.getByText('Сделать заказ', { exact: true });
|
||||
await expect(makeOrderText).toBeVisible();
|
||||
} catch (error) {
|
||||
console.error('server not up', error);
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
|
||||
test('login', async ({ page }) => {
|
||||
await page.goto('http://localhost:8099/dry-wash/arm');
|
||||
await page.getByRole('textbox', { name: 'Username or email' }).click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Username or email' })
|
||||
.fill('237x237');
|
||||
await page.getByRole('textbox', { name: 'Password' }).click();
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill('');
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
await page.getByRole('heading', { name: 'Заказы' }).click();
|
||||
await page.getByRole('link', { name: 'Мастера' }).click();
|
||||
await page.getByRole('link', { name: 'Заказы' }).click();
|
||||
});
|
||||
@@ -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__ = '';
|
||||
@@ -26,6 +26,17 @@
|
||||
"dry-wash.order-create.form.washing-location-field.label": "Where is the car located?",
|
||||
"dry-wash.order-create.form.washing-location-field.placeholder": "Enter the address or select on the map",
|
||||
"dry-wash.order-create.form.washing-location-field.help": "For example, 55.754364, 48.743295 Universitetskaya Street, 1, Innopolis, Verkhneuslonsky district, Republic of Tatarstan (Tatarstan), 420500",
|
||||
"dry-wash.order-create.car-color-select.placeholder": "Input color",
|
||||
"dry-wash.order-create.car-color-select.custom": "Custom",
|
||||
"dry-wash.order-create.car-color-select.custom-label": "Custom:",
|
||||
"dry-wash.order-create.car-color-select.colors.white": "White",
|
||||
"dry-wash.order-create.car-color-select.colors.black": "Black",
|
||||
"dry-wash.order-create.car-color-select.colors.silver": "Silver",
|
||||
"dry-wash.order-create.car-color-select.colors.gray": "Gray",
|
||||
"dry-wash.order-create.car-color-select.colors.beige-brown": "Beige Brown",
|
||||
"dry-wash.order-create.car-color-select.colors.red": "Red",
|
||||
"dry-wash.order-create.car-color-select.colors.blue": "Blue",
|
||||
"dry-wash.order-create.car-color-select.colors.green": "Green",
|
||||
"dry-wash.order-create.car-body-select.placeholder": "Not specified",
|
||||
"dry-wash.order-create.car-body-select.options.sedan": "Sedan",
|
||||
"dry-wash.order-create.car-body-select.options.hatchback" : "Hatchback",
|
||||
@@ -50,6 +61,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: 14MB",
|
||||
"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",
|
||||
@@ -58,7 +75,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",
|
||||
@@ -105,5 +122,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": "Введите ФИО",
|
||||
@@ -75,6 +81,17 @@
|
||||
"dry-wash.order-create.form.washing-location-field.label": "Где находится автомобиль?",
|
||||
"dry-wash.order-create.form.washing-location-field.placeholder": "Введите адрес или выберите на карте",
|
||||
"dry-wash.order-create.form.washing-location-field.help": "Например, 55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500",
|
||||
"dry-wash.order-create.car-color-select.placeholder": "Введите цвет",
|
||||
"dry-wash.order-create.car-color-select.custom": "Другой",
|
||||
"dry-wash.order-create.car-color-select.custom-label": "Другой:",
|
||||
"dry-wash.order-create.car-color-select.colors.white": "Белый",
|
||||
"dry-wash.order-create.car-color-select.colors.black": "Черный",
|
||||
"dry-wash.order-create.car-color-select.colors.silver": "Серебристый",
|
||||
"dry-wash.order-create.car-color-select.colors.gray": "Серый",
|
||||
"dry-wash.order-create.car-color-select.colors.beige-brown": "Бежево-коричневый",
|
||||
"dry-wash.order-create.car-color-select.colors.red": "Красный",
|
||||
"dry-wash.order-create.car-color-select.colors.blue": "Синий",
|
||||
"dry-wash.order-create.car-color-select.colors.green": "Зеленый",
|
||||
"dry-wash.order-create.car-body-select.placeholder": "Не указан",
|
||||
"dry-wash.order-create.car-body-select.options.sedan": "Седан",
|
||||
"dry-wash.order-create.car-body-select.options.hatchback": "Хэтчбек",
|
||||
@@ -99,11 +116,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. Максимальный размер: 14МБ",
|
||||
"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": "Статус"
|
||||
}
|
||||
|
||||
30646
package-lock.json
generated
30646
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
145
package.json
145
package.json
@@ -1,72 +1,77 @@
|
||||
{
|
||||
"name": "dry-wash",
|
||||
"version": "0.5.0",
|
||||
"description": "<a id=\"readme-top\"></a>",
|
||||
"main": "./src/index.tsx",
|
||||
"scripts": {
|
||||
"test": "jest -u",
|
||||
"start": "brojs server --port=8099 --with-open-browser",
|
||||
"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",
|
||||
"preversion": "npm run eslint"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@brojs/cli": "^1.8.4",
|
||||
"@babel/core": "^7.26.7",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@chakra-ui/icons": "^2.2.4",
|
||||
"@chakra-ui/react": "^2.10.5",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@fontsource/open-sans": "^5.1.0",
|
||||
"@lottiefiles/react-lottie-player": "^3.5.4",
|
||||
"@pbe/react-yandex-maps": "^1.2.5",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"babel-jest": "^29.7.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.1",
|
||||
"framer-motion": "^6.2.8",
|
||||
"i18next": "^23.16.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fixed-jsdom": "^0.0.9",
|
||||
"keycloak-js": "^23.0.7",
|
||||
"msw": "^2.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-i18next": "^15.1.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-phone-number-input": "^3.4.9",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@stylistic/eslint-plugin": "^2.10.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"globals": "^15.11.0",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.12.2"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "./jest-preset-it/jest-preset.ts"
|
||||
}
|
||||
"name": "dry-wash",
|
||||
"version": "0.9.2",
|
||||
"description": "<a id=\"readme-top\"></a>",
|
||||
"main": "./src/index.tsx",
|
||||
"scripts": {
|
||||
"test": "jest -u",
|
||||
"start": "brojs server --port=8099 --with-open-browser",
|
||||
"build": "npm run clean && brojs build --dev",
|
||||
"build:prod": "npm run clean && brojs build",
|
||||
"clean": "rimraf dist",
|
||||
"eslint": "npx eslint src",
|
||||
"eslint:fix": "npx eslint src --fix",
|
||||
"preversion": "npm run eslint"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.7",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@brojs/cli": "^1.8.4",
|
||||
"@chakra-ui/icons": "^2.2.4",
|
||||
"@chakra-ui/react": "^2.10.5",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@fontsource/open-sans": "^5.1.0",
|
||||
"@lottiefiles/react-lottie-player": "^3.5.4",
|
||||
"@pbe/react-yandex-maps": "^1.2.5",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"babel-jest": "^29.7.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.1",
|
||||
"framer-motion": "^6.2.8",
|
||||
"i18next": "^23.16.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fixed-jsdom": "^0.0.9",
|
||||
"keycloak-js": "^23.0.7",
|
||||
"msw": "^2.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-i18next": "^15.1.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-phone-number-input": "^3.4.9",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"globals": "^15.11.0",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.12.2"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "./jest-preset-it/jest-preset.ts"
|
||||
}
|
||||
}
|
||||
|
||||
79
playwright.config.ts
Normal file
79
playwright.config.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
@@ -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,27 +1,92 @@
|
||||
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' }),
|
||||
getMasters: builder.query<Master[], { date: Date }>({
|
||||
query: ({ date }) => {
|
||||
const startDate = dayjs(date).startOf('day').toISOString();
|
||||
const endDate = dayjs(date).endOf('day').toISOString();
|
||||
return {
|
||||
url: '/arm/masters/list',
|
||||
method: 'POST',
|
||||
body: { startDate, endDate },
|
||||
};
|
||||
},
|
||||
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', 'Masters'],
|
||||
}),
|
||||
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',
|
||||
method: 'POST',
|
||||
body: master,
|
||||
}),
|
||||
invalidatesTags: ['Masters'],
|
||||
invalidatesTags: ['Masters', 'Orders'],
|
||||
}),
|
||||
deleteMaster: builder.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({
|
||||
url: `/arm/masters/${id}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
invalidatesTags: ['Masters', 'Orders'],
|
||||
}),
|
||||
updateMaster: builder.mutation<void, UpdateMasterPayload>({
|
||||
query: ({ id, name, phone }) => ({
|
||||
url: `/arm/masters/${id}`,
|
||||
method: 'PATCH',
|
||||
body: { name, phone },
|
||||
}),
|
||||
invalidatesTags: ['Masters', 'Orders'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetMastersQuery,
|
||||
useAddMasterMutation,
|
||||
useDeleteMasterMutation,
|
||||
useUpdateMasterMutation,
|
||||
useGetOrdersQuery,
|
||||
useUpdateOrdersMutation,
|
||||
} = api;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetOrder, CreateOrder } from "../../models/api";
|
||||
import { GetOrder, CreateOrder, UploadCarImage } from "../../models/api";
|
||||
|
||||
import { api } from "./api";
|
||||
import { extractBodyFromResponse, extractErrorMessageFromResponse } from "./utils";
|
||||
@@ -13,11 +13,19 @@ export const landingApi = api.injectEndpoints({
|
||||
createOrder: mutation<CreateOrder.Response, CreateOrder.Params>({
|
||||
query: ({ body }) => ({
|
||||
url: `/order/create`,
|
||||
params: { body },
|
||||
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,6 +1,6 @@
|
||||
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>) => {
|
||||
if (response.success) {
|
||||
@@ -8,8 +8,15 @@ export const extractBodyFromResponse = <Body>(response: BaseResponse<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,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>
|
||||
`;
|
||||
61
src/components/Header/Header.tsx
Normal file
61
src/components/Header/Header.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Box, Button, Heading, HStack, 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>
|
||||
)}
|
||||
{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'}
|
||||
>
|
||||
Карта заказов
|
||||
</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>
|
||||
|
||||
95
src/components/Map/Map.tsx
Normal file
95
src/components/Map/Map.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { useState } 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 dayjs from 'dayjs';
|
||||
|
||||
import { useGetOrdersQuery } from '../../__data__/service/api';
|
||||
import getCoordinates from '../../utils/getCoordinates';
|
||||
import DateNavigator from '../DateNavigator';
|
||||
|
||||
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, setCurrentDate] = useState(
|
||||
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>
|
||||
|
||||
<DateNavigator
|
||||
currentDate={currentDate}
|
||||
onPreviousDate={() =>
|
||||
setCurrentDate((prevDate) =>
|
||||
dayjs(prevDate).subtract(1, 'day').toDate(),
|
||||
)
|
||||
}
|
||||
onNextDate={() =>
|
||||
setCurrentDate((prevDate) => dayjs(prevDate).add(1, 'day').toDate())
|
||||
}
|
||||
/>
|
||||
|
||||
{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} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
@@ -10,16 +10,18 @@ import {
|
||||
Button,
|
||||
useDisclosure,
|
||||
Flex,
|
||||
useToast,
|
||||
Td,
|
||||
Text,
|
||||
Spinner,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
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';
|
||||
import DateNavigator from '../DateNavigator';
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
'name' as const,
|
||||
@@ -30,7 +32,9 @@ const TABLE_HEADERS = [
|
||||
|
||||
const Masters = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const toast = useToast();
|
||||
const showToast = useShowToast();
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.arm.master',
|
||||
});
|
||||
@@ -40,27 +44,34 @@ const Masters = () => {
|
||||
error,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = api.useGetMastersQuery();
|
||||
} = useGetMastersQuery({ date: currentDate });
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast({
|
||||
title: t('error.title'),
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
showToast(t('error.title'), 'error');
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<Box p='8'>
|
||||
<Flex justifyContent='space-between' alignItems='center' mb='5'>
|
||||
<Flex justifyContent='space-between' alignItems='baseline' mb='5'>
|
||||
<Heading size='lg'> {t('title')}</Heading>
|
||||
|
||||
<Button colorScheme='green' onClick={onOpen}>
|
||||
+ {t('add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<DateNavigator
|
||||
currentDate={currentDate}
|
||||
onPreviousDate={() =>
|
||||
setCurrentDate((prevDate) =>
|
||||
dayjs(prevDate).subtract(1, 'day').toDate(),
|
||||
)
|
||||
}
|
||||
onNextDate={() =>
|
||||
setCurrentDate((prevDate) => dayjs(prevDate).add(1, 'day').toDate())
|
||||
}
|
||||
/>
|
||||
<Table variant='simple' colorScheme='blackAlpha'>
|
||||
<Thead>
|
||||
<Tr>
|
||||
|
||||
@@ -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({ date: currentDate });
|
||||
|
||||
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';
|
||||
49
src/components/PriceCar/PriceCar.tsx
Normal file
49
src/components/PriceCar/PriceCar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
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, description }) => {
|
||||
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>
|
||||
)}
|
||||
<Text>{description}</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'>
|
||||
|
||||
@@ -21,9 +21,10 @@ import { CarBodySelectProps } from './types';
|
||||
export const CarBodySelect = forwardRef<HTMLInputElement, CarBodySelectProps>(
|
||||
function CarBodySelect(props, ref) {
|
||||
const handleOptionClick: UseRadioGroupProps['onChange'] = (value) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
props.onChange(value);
|
||||
props.onChange({
|
||||
target: { value },
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const { value, getRadioProps, getRootProps } = useRadioGroup({
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { forwardRef, useId } from 'react';
|
||||
import { Input, InputProps } from '@chakra-ui/react';
|
||||
|
||||
import { CAR_COLORS } from './helper';
|
||||
|
||||
export const CarColorInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
function CarColorInput(props, ref) {
|
||||
const listId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input ref={ref} list={listId} {...props} />
|
||||
<datalist id={listId}>
|
||||
{CAR_COLORS.map(({ code, name }) => (
|
||||
<option key={code} label={name} value={code}>{name}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// todo: add option color visual indication
|
||||
162
src/components/order-form/form/car-color/car-color-select.tsx
Normal file
162
src/components/order-form/form/car-color/car-color-select.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import {
|
||||
Input,
|
||||
Box,
|
||||
Stack,
|
||||
Text,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CAR_COLORS } from './helper';
|
||||
|
||||
interface CarColorSelectProps {
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
name?: string;
|
||||
isInvalid?: boolean;
|
||||
}
|
||||
|
||||
export const CarColorSelect = forwardRef<HTMLInputElement, CarColorSelectProps>(
|
||||
function CarColorSelect(props) {
|
||||
const [customColor, setCustomColor] = useState('');
|
||||
const [isCustom, setIsCustom] = useState(false);
|
||||
|
||||
const handleColorChange = (value: string) => {
|
||||
if (value === 'custom') {
|
||||
setIsCustom(true);
|
||||
return;
|
||||
}
|
||||
setIsCustom(false);
|
||||
props.onChange?.({
|
||||
target: { value },
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
};
|
||||
|
||||
const handleCustomColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setCustomColor(value);
|
||||
props.onChange?.({
|
||||
target: { value },
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
};
|
||||
|
||||
const { t } = useTranslation('~', {
|
||||
keyPrefix: 'dry-wash.order-create.car-color-select',
|
||||
});
|
||||
|
||||
const currentValue = isCustom ? 'custom' : props.value;
|
||||
|
||||
return (
|
||||
<Stack spacing={4} width="100%">
|
||||
<Flex gap={3} wrap="nowrap" overflowX="auto" pb={2}>
|
||||
{CAR_COLORS.map(({ name, code }) => (
|
||||
<Box
|
||||
key={name}
|
||||
flexShrink={0}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleColorChange(name)}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
gap={2}
|
||||
p={2}
|
||||
borderRadius="full"
|
||||
borderWidth="2px"
|
||||
borderColor={currentValue === name ? 'primary.500' : 'gray.200'}
|
||||
bg={currentValue === name ? 'primary.50' : 'white'}
|
||||
_hover={{
|
||||
borderColor: 'primary.500',
|
||||
bg: currentValue === name ? 'primary.50' : 'gray.50'
|
||||
}}
|
||||
minW={currentValue === name ? '120px' : 'auto'}
|
||||
h="48px"
|
||||
justify="center"
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<Box
|
||||
w="32px"
|
||||
h="32px"
|
||||
borderRadius="full"
|
||||
bg={code}
|
||||
border="1px"
|
||||
borderColor={currentValue === name ? 'primary.500' : 'gray.200'}
|
||||
transition="all 0.2s"
|
||||
boxShadow={currentValue === name ? 'sm' : 'none'}
|
||||
/>
|
||||
{currentValue === name && (
|
||||
<Text fontSize="xs" color="primary.700" fontWeight="medium">
|
||||
{t(`colors.${name}`)}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<Box
|
||||
flexShrink={0}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => handleColorChange('custom')}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
gap={2}
|
||||
p={2}
|
||||
borderRadius="full"
|
||||
borderWidth="2px"
|
||||
borderColor={isCustom ? 'primary.500' : 'gray.200'}
|
||||
bg={isCustom ? 'primary.50' : 'white'}
|
||||
_hover={{
|
||||
borderColor: 'primary.500',
|
||||
bg: isCustom ? 'primary.50' : 'gray.50'
|
||||
}}
|
||||
minW={isCustom ? '200px' : 'auto'}
|
||||
h="48px"
|
||||
justify="center"
|
||||
transition="all 0.2s"
|
||||
>
|
||||
{isCustom ? (
|
||||
<Flex gap={2} align="center">
|
||||
<Text fontSize="xs" color="primary.700" fontWeight="medium">
|
||||
{t('custom-label')}
|
||||
</Text>
|
||||
<Input
|
||||
size="sm"
|
||||
width="120px"
|
||||
value={customColor}
|
||||
onChange={handleCustomColorChange}
|
||||
placeholder={t('placeholder')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
borderColor="primary.200"
|
||||
_focus={{
|
||||
borderColor: 'primary.500',
|
||||
boxShadow: '0 0 0 1px var(--chakra-colors-primary-500)'
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex align="center" gap={2}>
|
||||
<Box
|
||||
w="32px"
|
||||
h="32px"
|
||||
borderRadius="full"
|
||||
bg="gray.100"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
transition="all 0.2s"
|
||||
/>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{t('custom')}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
export const CAR_COLORS: Record<'name' | 'code', string>[] = [
|
||||
export const CAR_COLORS = [
|
||||
{
|
||||
name: 'white',
|
||||
code: '#ffffff'
|
||||
@@ -31,4 +31,4 @@ export const CAR_COLORS: Record<'name' | 'code', string>[] = [
|
||||
name: 'green',
|
||||
code: '#078d51'
|
||||
},
|
||||
];
|
||||
] as const satisfies { name: string; code: string }[];
|
||||
@@ -1 +1 @@
|
||||
export { CarColorInput } from './car-color-input';
|
||||
export { CarColorSelect } from './car-color-select';
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Box, Flex, FormControl, FormLabel, VStack } from '@chakra-ui/react';
|
||||
|
||||
import { CarBodySelect } from './car-body';
|
||||
import { CarColorInput } from './car-color';
|
||||
import { CarNumberInput } from './car-number';
|
||||
import { FormInputField, FormControllerField } from './field';
|
||||
import { OrderFormProps, OrderFormValues } from './types';
|
||||
@@ -18,8 +17,9 @@ import {
|
||||
StringLocation,
|
||||
YMapsProvider,
|
||||
} from './location';
|
||||
import { CarColorSelect } from './car-color';
|
||||
|
||||
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
|
||||
@@ -72,7 +72,7 @@ export const OrderForm = ({ onSubmit, loading }: OrderFormProps) => {
|
||||
name='carColor'
|
||||
label={t('car-color-field.label')}
|
||||
errors={errors}
|
||||
Input={CarColorInput}
|
||||
Input={CarColorSelect}
|
||||
/>
|
||||
<FormInputField
|
||||
control={control}
|
||||
|
||||
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;
|
||||
@@ -9,7 +9,7 @@ export const isErrorMessage = (error: unknown): error is ErrorMessage => typeof
|
||||
|
||||
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,13 @@ 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;
|
||||
imageDescription?: 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>
|
||||
`;
|
||||
645
src/pages/__tests__/__snapshots__/masters.test.tsx.snap
Normal file
645
src/pages/__tests__/__snapshots__/masters.test.tsx.snap
Normal file
@@ -0,0 +1,645 @@
|
||||
// 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-s92abg"
|
||||
>
|
||||
<header
|
||||
class="css-106dwq4"
|
||||
>
|
||||
<div
|
||||
class="css-br9knx"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-8w8uga"
|
||||
>
|
||||
Сухой мастер
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1rafi8n"
|
||||
>
|
||||
<a
|
||||
class="chakra-button css-19byqlw"
|
||||
href="/order"
|
||||
>
|
||||
Заказы
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="chakra-divider css-zw0v9u"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-g11sl9"
|
||||
data-testid="master-button"
|
||||
href="/master"
|
||||
>
|
||||
Мастера
|
||||
</a>
|
||||
<a
|
||||
class="chakra-button css-19byqlw"
|
||||
href="/map"
|
||||
>
|
||||
Карта заказов
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<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>
|
||||
`;
|
||||
764
src/pages/__tests__/__snapshots__/order-create.test.tsx.snap
Normal file
764
src/pages/__tests__/__snapshots__/order-create.test.tsx.snap
Normal file
@@ -0,0 +1,764 @@
|
||||
// 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>
|
||||
<div
|
||||
class="chakra-stack css-uv9e93"
|
||||
>
|
||||
<div
|
||||
class="css-dbqfkc"
|
||||
>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-96lva5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-c58w4d"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-ltoa43"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-vqo9x6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-1lr2es4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-1wfunc4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-fg5oe6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-f0pfxe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="css-6su6fj"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="css-1nsxgdr"
|
||||
>
|
||||
<div
|
||||
class="css-1k9efnl"
|
||||
>
|
||||
<div
|
||||
class="css-r58uxc"
|
||||
/>
|
||||
<p
|
||||
class="chakra-text css-1xa8ojw"
|
||||
>
|
||||
Другой
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="chakra-form-control css-1kxonj9"
|
||||
role="group"
|
||||
>
|
||||
<label
|
||||
class="chakra-form__label css-g6pte"
|
||||
for="carBody"
|
||||
id="field-:r3:-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-:r3:"
|
||||
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-:r7:"
|
||||
class="chakra-popover__content css-1mvj5hv"
|
||||
id="popover-content-:r7:"
|
||||
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-:r7:"
|
||||
>
|
||||
<div
|
||||
class="css-124gwxm"
|
||||
role="radiogroup"
|
||||
>
|
||||
<div
|
||||
class="css-0"
|
||||
>
|
||||
<label
|
||||
class="css-4g6ai3"
|
||||
>
|
||||
<input
|
||||
aria-required="true"
|
||||
hidden=""
|
||||
id="radio-:r8:"
|
||||
name="radio-:r4:"
|
||||
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-:r9:"
|
||||
name="radio-:r4:"
|
||||
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-:ra:"
|
||||
name="radio-:r4:"
|
||||
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-:rb:"
|
||||
name="radio-:r4:"
|
||||
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-:rc:"
|
||||
name="radio-:r4:"
|
||||
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-:rd:"
|
||||
name="radio-:r4:"
|
||||
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-:re:"
|
||||
name="radio-:r4:"
|
||||
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-:rf:"
|
||||
name="radio-:r4:"
|
||||
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-:rg:"
|
||||
name="radio-:r4:"
|
||||
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-:rh:"
|
||||
name="radio-:r4:"
|
||||
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-:ri:"
|
||||
name="radio-:r4:"
|
||||
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-:rj:"
|
||||
id="field-:rj:-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-:rk:"
|
||||
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-:rl:"
|
||||
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-:rm:-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-:rm:-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>
|
||||
`;
|
||||
464
src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap
Normal file
464
src/pages/__tests__/__snapshots__/ordersList.test.tsx.snap
Normal file
@@ -0,0 +1,464 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Страница заказов должна корректно отображать список заказов после загрузки данных 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-s92abg"
|
||||
>
|
||||
<header
|
||||
class="css-106dwq4"
|
||||
>
|
||||
<div
|
||||
class="css-br9knx"
|
||||
>
|
||||
<h2
|
||||
class="chakra-heading css-8w8uga"
|
||||
>
|
||||
Сухой мастер
|
||||
</h2>
|
||||
<div
|
||||
class="chakra-stack css-1rafi8n"
|
||||
>
|
||||
<a
|
||||
class="chakra-button css-g11sl9"
|
||||
href="/auth/login"
|
||||
>
|
||||
Заказы
|
||||
</a>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="chakra-divider css-zw0v9u"
|
||||
/>
|
||||
<a
|
||||
class="chakra-button css-g11sl9"
|
||||
data-testid="master-button"
|
||||
href="/auth/login"
|
||||
>
|
||||
Мастера
|
||||
</a>
|
||||
<a
|
||||
class="chakra-button css-g11sl9"
|
||||
href="/auth/login"
|
||||
>
|
||||
Карта заказов
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<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"
|
||||
>
|
||||
09.03.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="cancelled"
|
||||
>
|
||||
Отменено
|
||||
</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"
|
||||
>
|
||||
<a
|
||||
class="chakra-button css-ez23ye"
|
||||
href="/auth/login/arm//auth/login?lat=55.78&lon=49.12¤tDate=Sun Mar 09 2025 11:23:09 GMT+0300 (Moscow Standard Time)"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M23.432,10.524C20.787,7.614,16.4,4.538,12,4.6,7.6,4.537,3.213,7.615.568,10.524a2.211,2.211,0,0,0,0,2.948C3.182,16.351,7.507,19.4,11.839,19.4h.308c4.347,0,8.671-3.049,11.288-5.929A2.21,2.21,0,0,0,23.432,10.524ZM7.4,12A4.6,4.6,0,1,1,12,16.6,4.6,4.6,0,0,1,7.4,12Z"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</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="cancelled"
|
||||
>
|
||||
Отменено
|
||||
</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"
|
||||
>
|
||||
<a
|
||||
class="chakra-button css-ez23ye"
|
||||
href="/auth/login/arm//auth/login?lat=55.78&lon=49.12¤tDate=Sun Mar 09 2025 11:23:09 GMT+0300 (Moscow Standard Time)"
|
||||
>
|
||||
<svg
|
||||
class="chakra-icon css-onkibi"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M23.432,10.524C20.787,7.614,16.4,4.538,12,4.6,7.6,4.537,3.213,7.615.568,10.524a2.211,2.211,0,0,0,0,2.948C3.182,16.351,7.507,19.4,11.839,19.4h.308c4.347,0,8.671-3.049,11.288-5.929A2.21,2.21,0,0,0,23.432,10.524ZM7.4,12A4.6,4.6,0,1,1,12,16.6,4.6,4.6,0,0,1,7.4,12Z"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</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>
|
||||
|
||||
@@ -38,12 +38,13 @@ const Page: FC = () => {
|
||||
<VStack w='full' h='full' alignItems='stretch' flexGrow={1}>
|
||||
{createOrderMutation.isUninitialized ? (
|
||||
<>
|
||||
<Heading textAlign='center' mt={4}>
|
||||
<Heading textAlign='center' mt={4} data-testid='heading'>
|
||||
{t('title')}
|
||||
</Heading>
|
||||
<OrderForm
|
||||
onSubmit={onOrderFormSubmit}
|
||||
loading={createOrderMutation.isLoading}
|
||||
data-testid='order-form'
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -21,6 +21,8 @@ import { Order } from '../../models/landing';
|
||||
import { landingApi } from '../../__data__/service/landing.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('~', {
|
||||
@@ -58,34 +60,55 @@ 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}
|
||||
description={order?.imageDescription}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
{isError && (
|
||||
<Alert status='error' alignItems='flex-start'>
|
||||
<Alert
|
||||
status='error'
|
||||
alignItems='flex-start'
|
||||
data-testid='error'
|
||||
>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>
|
||||
|
||||
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 {};
|
||||
|
||||
const [lat, lon] = location
|
||||
.split(',')
|
||||
.map((coord) => parseFloat(coord.trim()));
|
||||
|
||||
if (isNaN(lat) || isNaN(lon)) return {};
|
||||
|
||||
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', orderCreate: '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;
|
||||
@@ -15,28 +15,34 @@ router.get('/set/:name/:value', (req, res) => {
|
||||
router.get('/', (req, res) => {
|
||||
res.send(`<div>
|
||||
<fieldset>
|
||||
<legend>Мастера</legend>
|
||||
<legend>Мастера</legend>
|
||||
${generateRadioInput('masters', 'success')}
|
||||
${generateRadioInput('masters', 'error')}
|
||||
${generateRadioInput('masters', 'empty')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Заказы</legend>
|
||||
<legend>Заказы</legend>
|
||||
${generateRadioInput('orders', 'success')}
|
||||
${generateRadioInput('orders', 'error')}
|
||||
${generateRadioInput('orders', 'empty')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Лендинг - Сделать заказ</legend>
|
||||
<legend>Лендинг - Сделать заказ</legend>
|
||||
${generateRadioInput('orderCreate', 'success')}
|
||||
${generateRadioInput('orderCreate', 'error')}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Лендинг - Детали заказа</legend>
|
||||
<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>`);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ const commonError = { success: false, message: 'Что-то пошло не та
|
||||
|
||||
const sleep =
|
||||
(duration = 1000) =>
|
||||
(req, res, next) =>
|
||||
setTimeout(next, duration);
|
||||
(req, res, next) =>
|
||||
setTimeout(next, duration);
|
||||
|
||||
router.use(sleep());
|
||||
|
||||
router.get('/arm/masters', (req, res) => {
|
||||
router.post('/arm/masters/list', (req, res) => {
|
||||
res
|
||||
.status(/error/.test(STUBS.masters) ? 500 : 200)
|
||||
.send(
|
||||
@@ -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(
|
||||
@@ -98,6 +98,20 @@ router.post('/order/create', (req, res) => {
|
||||
);
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"success": false,
|
||||
"message": "Не удалось создать заказ"
|
||||
"error": "Не удалось создать заказ"
|
||||
}
|
||||
@@ -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