diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3c35d6f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "i18n-ally.localesPaths": [ + "locales" + ] +} \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 4844afd..e4d705b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,7 @@ pipeline { agent { docker { - image 'node:20' + image 'node:22' } } @@ -30,25 +30,21 @@ pipeline { } } - stage('checks') { - parallel { - stage('eslint') { - steps { - sh 'npm run eslint' - } - } + stage('eslint') { + steps { + sh 'npm run eslint' + } + } - stage('test') { - steps { - sh 'npm run test' - } - } + stage('test') { + steps { + sh 'npm run test' + } + } - stage('build') { - steps { - sh 'npm run build' - } - } + stage('build') { + steps { + sh 'npm run build' } } diff --git a/bro.config.js b/bro.config.js index 4ab3829..e471644 100644 --- a/bro.config.js +++ b/bro.config.js @@ -21,7 +21,16 @@ module.exports = { features: { '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" + } }, }, config: { diff --git a/locales/en.json b/locales/en.json index db39a54..155e992 100644 --- a/locales/en.json +++ b/locales/en.json @@ -50,6 +50,12 @@ "dry-wash.order-view.details.location": "Where", "dry-wash.order-view.details.datetime-range": "When", "dry-wash.order-view.details.alert": "The operator will contact you about the payment at the specified phone number", + "dry-wash.order-view.upload-car-image.field.label": "Upload a photo of your car, and our service will quickly calculate the pre-order price!", + "dry-wash.order-view.upload-car-image.field.help": "Allowed formats: .jpg, .png. Maximum size: 5MB", + "dry-wash.order-view.upload-car-image.file-input.placeholder": "Upload a file", + "dry-wash.order-view.upload-car-image.file-input.button": "Upload", + "dry-wash.order-view.upload-car-image-query.success.title": "The car image is successfully uploaded", + "dry-wash.order-view.upload-car-image-query.error.title": "Failed to upload the car image", "dry-wash.arm.master.add": "Add", "dry-wash.arm.order.title": "Orders", "dry-wash.arm.order.table.empty": "Table empty", @@ -58,7 +64,7 @@ "dry-wash.arm.order.status.complete": "Completed", "dry-wash.arm.order.status.pending": "Pending", "dry-wash.arm.order.status.working": "Working", - "dry-wash.arm.order.status.canceled": "Canceled", + "dry-wash.arm.order.status.cancelled": "Canceled", "dry-wash.arm.order.status.placeholder": "Select status", "dry-wash.arm.order.master.placeholder": "Select master", "dry-wash.arm.order.table.header.carNumber": "Car Number", diff --git a/locales/ru.json b/locales/ru.json index 15586fb..ed6718d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Номер машины", @@ -105,6 +105,12 @@ "dry-wash.order-view.details.location": "Где", "dry-wash.order-view.details.datetime-range": "Когда", "dry-wash.order-view.details.alert": "С вами свяжется оператор насчет оплаты по указанному номеру телефона", + "dry-wash.order-view.upload-car-image.field.label": "Загрузите фото вашего автомобиля, и наш сервис быстро рассчитает предварительную стоимость заказа!", + "dry-wash.order-view.upload-car-image.field.help": "Допустимые форматы: .jpg, .png. Максимальный размер: 5МБ", + "dry-wash.order-view.upload-car-image.file-input.placeholder": "Загрузите файл", + "dry-wash.order-view.upload-car-image.file-input.button": "Загрузить", + "dry-wash.order-view.upload-car-image-query.success.title": "Изображение автомобиля успешно загружено", + "dry-wash.order-view.upload-car-image-query.error.title": "Не удалось загрузить изображение автомобиля", "dry-wash.notFound.title": "Страница не найдена", "dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.", "dry-wash.notFound.button.back": "Вернуться на главную", diff --git a/package-lock.json b/package-lock.json index 3e160a7..f4c5ea4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dry-wash", - "version": "0.8.0", + "version": "0.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dry-wash", - "version": "0.8.0", + "version": "0.9.1", "license": "ISC", "dependencies": { "@babel/core": "^7.26.7", diff --git a/package.json b/package.json index ed10c91..c767522 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dry-wash", - "version": "0.8.0", + "version": "0.9.1", "description": "", "main": "./src/index.tsx", "scripts": { @@ -9,8 +9,8 @@ "build": "npm run clean && brojs build --dev", "build:prod": "npm run clean && brojs build", "clean": "rimraf dist", - "eslint": "npx eslint .", - "eslint:fix": "npx eslint . --fix", + "eslint": "npx eslint src", + "eslint:fix": "npx eslint src --fix", "preversion": "npm run eslint" }, "keywords": [], diff --git a/src/__data__/features.ts b/src/__data__/features.ts index 4366694..48ca7d2 100644 --- a/src/__data__/features.ts +++ b/src/__data__/features.ts @@ -11,5 +11,8 @@ export const FEATURE = { return interval; } } + }, + carImageUpload: { + isOn: Boolean(features?.['car-img-upload']) } }; \ No newline at end of file diff --git a/src/__data__/service/landing.api.ts b/src/__data__/service/landing.api.ts index 7f84aef..9372cf8 100644 --- a/src/__data__/service/landing.api.ts +++ b/src/__data__/service/landing.api.ts @@ -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"; @@ -19,5 +19,13 @@ export const landingApi = api.injectEndpoints({ transformResponse: extractBodyFromResponse, transformErrorResponse: extractErrorMessageFromResponse, }), + uploadCarImage: mutation({ + query: ({ orderId, body }) => ({ + url: `/order/${orderId}/upload-car-img`, + body, + method: 'POST' + }), + transformErrorResponse: extractErrorMessageFromResponse, + }), }) }); diff --git a/src/__data__/service/utils.ts b/src/__data__/service/utils.ts index 2ffe971..f55bb9c 100644 --- a/src/__data__/service/utils.ts +++ b/src/__data__/service/utils.ts @@ -13,9 +13,10 @@ export const extractErrorMessageFromResponse = ({ }: FetchBaseQueryError) => { if ( typeof data === 'object' && - 'message' in data && - typeof data.message === 'string' + data !== null && + 'error' in data && + typeof data.error === 'string' ) { - return data.message; + return data.error; } }; diff --git a/src/__data__/store.ts b/src/__data__/store.ts index dabb855..7fe0918 100644 --- a/src/__data__/store.ts +++ b/src/__data__/store.ts @@ -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; diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx index d0fc96a..6385951 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -28,7 +28,6 @@ class ErrorBoundary extends Component { componentDidCatch(error: Error, errorInfo: ErrorInfo): void { console.error('Error caught by ErrorBoundary:', error, errorInfo); - console.error('4545'); this.setState({ error, errorInfo }); } diff --git a/src/components/ErrorBoundary/__tests__/ErrorBoundary.test.tsx b/src/components/ErrorBoundary/__tests__/ErrorBoundary.test.tsx index a194bcd..db19d52 100644 --- a/src/components/ErrorBoundary/__tests__/ErrorBoundary.test.tsx +++ b/src/components/ErrorBoundary/__tests__/ErrorBoundary.test.tsx @@ -24,6 +24,10 @@ jest.mock('@brojs/cli', () => { describe('ErrorBoundary', () => { it('должен отобразить запасной UI при ошибке', async () => { + // Подавляем вывод ошибки в консоль во время теста + const consoleSpy = jest.spyOn(console, 'error'); + consoleSpy.mockImplementation(() => {}); + const { container } = render( @@ -39,7 +43,9 @@ describe('ErrorBoundary', () => { ); expect(button).not.toBeNull(); - expect(container).toMatchSnapshot(); + + // Восстанавливаем console.error после теста + consoleSpy.mockRestore(); }); }); diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000..9adba6b --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,51 @@ +import { Box, Button, Heading, HStack, Divider, Flex } from '@chakra-ui/react'; +import React from 'react'; +import { useLocation, Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import { URLs } from '../../__data__/urls'; + +const Header = () => { + const location = useLocation(); + const isActive = (keyword: string) => location.pathname.includes(keyword); + + const { t } = useTranslation('~', { + keyPrefix: 'dry-wash.arm.master.sideBar', + }); + + return ( + + + + {t('title')} + + + {URLs.armOrder.isOn && ( + + )} + + {URLs.armMaster.isOn && ( + + )} + + + + ); +}; + +export default Header; diff --git a/src/components/Header/index.ts b/src/components/Header/index.ts new file mode 100644 index 0000000..579f1ac --- /dev/null +++ b/src/components/Header/index.ts @@ -0,0 +1 @@ +export { default } from './Header'; diff --git a/src/components/LayoutArm/LayoutArm.tsx b/src/components/LayoutArm/LayoutArm.tsx index aebba1a..6c04037 100644 --- a/src/components/LayoutArm/LayoutArm.tsx +++ b/src/components/LayoutArm/LayoutArm.tsx @@ -2,10 +2,10 @@ 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'; const LayoutArm = () => { let defaultRedirect = null; @@ -17,8 +17,8 @@ const LayoutArm = () => { } return ( - - + +
} /> diff --git a/src/components/OrderItem/OrderItem.tsx b/src/components/OrderItem/OrderItem.tsx index bba3c82..e3fd853 100644 --- a/src/components/OrderItem/OrderItem.tsx +++ b/src/components/OrderItem/OrderItem.tsx @@ -2,16 +2,17 @@ import React, { ChangeEvent, useState } from 'react'; import { Td, Tr, Link, Select } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; +import { ViewIcon } from '@chakra-ui/icons'; import { getTimeSlot } from '../../lib'; import { useUpdateOrdersMutation } from '../../__data__/service/api'; import { OrderArm, Status, statuses } from '../../models/api'; +import PopoverTemplate from '../PopoverTemplate'; const statusColors: Record = { pending: 'yellow.100', progress: 'blue.100', - working: 'orange.100', - canceled: 'red.100', + cancelled: 'red.100', complete: 'green.100', }; @@ -34,7 +35,7 @@ const OrderItem = ({ const [statusSelect, setStatus] = useState(status); const bgColor = statusColors[statusSelect]; - const [masterSelect, setMaster] = useState(master?.name); + const [masterSelectId, setMasterSelectId] = useState(master); const handelChangeMasters = (e: ChangeEvent) => { const masterName = e.target.value; @@ -43,7 +44,7 @@ const OrderItem = ({ ); if (selectedMaster) { - setMaster(masterName); + setMasterSelectId(selectedMaster.id); updateOrders({ id, master: selectedMaster.id }); } else { console.error('Master not found'); @@ -56,6 +57,10 @@ const OrderItem = ({ setStatus(status); }; + const masterSelectChange = allMasters.find( + (master) => master.id === masterSelectId, + ); + return ( {carNumber} @@ -79,7 +84,7 @@ const OrderItem = ({ { + onChange(event.target.files[0]); + handleSubmit(onSubmit)(); + }} + type='file' + hidden + /> + + + + ); + }} + /> + {errors.carImg?.message} + {t('field.help')} + + + ); +}); diff --git a/src/components/order-view/car-img/helper.ts b/src/components/order-view/car-img/helper.ts new file mode 100644 index 0000000..61c32e5 --- /dev/null +++ b/src/components/order-view/car-img/helper.ts @@ -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]); +}; \ No newline at end of file diff --git a/src/components/order-view/car-img/index.ts b/src/components/order-view/car-img/index.ts new file mode 100644 index 0000000..4a829cf --- /dev/null +++ b/src/components/order-view/car-img/index.ts @@ -0,0 +1 @@ +export { CarImageForm } from './car-img-form'; diff --git a/src/components/order-view/details/order-details.tsx b/src/components/order-view/details/order-details.tsx index 62c8563..9411404 100644 --- a/src/components/order-view/details/order-details.tsx +++ b/src/components/order-view/details/order-details.tsx @@ -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,7 +43,7 @@ export const OrderDetails: FC = ({ location, startWashTime, endWashTime, - ...props + created }) => { const { t } = useTranslation('~', { keyPrefix: 'dry-wash.order-view.details', @@ -51,7 +53,7 @@ export const OrderDetails: FC = ({ }); return ( - + <> = ({ gap={2} > - {t('title', { number: orderNumber })} + {t('title', { number: orderNumber })} ({dayjs(created).format('LLLL')}) @@ -105,7 +107,7 @@ export const OrderDetails: FC = ({ {t('alert')} - + ); }; diff --git a/src/models/api/common.ts b/src/models/api/common.ts index 4bde4dd..16d573f 100644 --- a/src/models/api/common.ts +++ b/src/models/api/common.ts @@ -9,7 +9,7 @@ export const isErrorMessage = (error: unknown): error is ErrorMessage => typeof type ErrorResponse = { success: false; - message: ErrorMessage; + error: ErrorMessage; }; export type BaseResponse = SuccessResponse | ErrorResponse; diff --git a/src/models/api/order.ts b/src/models/api/order.ts index dabe6f9..012e2ba 100644 --- a/src/models/api/order.ts +++ b/src/models/api/order.ts @@ -21,14 +21,24 @@ export namespace CreateOrder { }; } +export namespace UploadCarImage { + export type Response = void; + export type Params = { + orderId: Order.Id; + /** + * @example { file: File } + */ + body: FormData; + }; +} + type GetArrItemType = ArrType extends Array ? ItemType : never; export const statuses = [ 'pending' as const, 'progress' as const, - 'working' as const, - 'canceled' as const, + 'cancelled' as const, 'complete' as const, ]; @@ -42,7 +52,7 @@ export type OrderArm = { status?: GetArrItemType; phone?: string; location?: string; - master: Master; + master: string | []; notes: ''; allMasters: Master[]; id: string; diff --git a/src/pages/__tests__/__snapshots__/masters.test.tsx.snap b/src/pages/__tests__/__snapshots__/masters.test.tsx.snap index 276475c..fc26ae6 100644 --- a/src/pages/__tests__/__snapshots__/masters.test.tsx.snap +++ b/src/pages/__tests__/__snapshots__/masters.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Master Page render 1`] = ` +exports[`Master Page should display master list and show details when master button is clicked 1`] = `
Заказ №{{number}} + ( + Sunday, January 19, 2025 5:04 PM + ) `; -exports[`Order View page, initial load shows order details loading 1`] = ` +exports[`Страница просмотра заказа отображает индикатор загрузки деталей заказа 1`] = `
`; -exports[`Order View page, initial load shows order error 1`] = ` +exports[`Страница просмотра заказа отображает ошибку при некорректном ID заказа 1`] = `
- 16.02.2025 + 23.02.2025