diff --git a/locales/en.json b/locales/en.json index 17394ab..edf5854 100644 --- a/locales/en.json +++ b/locales/en.json @@ -38,6 +38,15 @@ "dry-wash.order-create.car-body-select.options.sports-car" : "Sports-car", "dry-wash.order-create.car-body-select.options.other": "Other", "dry-wash.order-create.form.submit-button.label": "Submit", + "dry-wash.order-view.title": "Your order", + "dry-wash.order-view.error.title": "Error", + "dry-wash.order-view.fetch.error": "Failed to fetch the details of order #{{number}}", + "dry-wash.order-view.details.title": "Order #{{number}}", + "dry-wash.order-view.details.owner": "Owner", + "dry-wash.order-view.details.car": "Car", + "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.arm.master.add": "Add", "dry-wash.arm.order.title": "Orders", "dry-wash.arm.order.table.empty": "Table empty", diff --git a/locales/ru.json b/locales/ru.json index 775f6b5..3964f13 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -61,17 +61,26 @@ "dry-wash.order-create.form.washing-location-field.help": "Например, 55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500", "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" : "Хэтчбек", - "dry-wash.order-create.car-body-select.options.crossover" : "Кроссовер", - "dry-wash.order-create.car-body-select.options.suv" : "Внедорожник", - "dry-wash.order-create.car-body-select.options.station-wagon" : "Универсал", - "dry-wash.order-create.car-body-select.options.coupe" : "Купе", - "dry-wash.order-create.car-body-select.options.minivan" : "Минивэн", - "dry-wash.order-create.car-body-select.options.pickup" : "Пикап", - "dry-wash.order-create.car-body-select.options.liftback" : "Лифтбек", - "dry-wash.order-create.car-body-select.options.sports-car" : "Спорткар", + "dry-wash.order-create.car-body-select.options.hatchback": "Хэтчбек", + "dry-wash.order-create.car-body-select.options.crossover": "Кроссовер", + "dry-wash.order-create.car-body-select.options.suv": "Внедорожник", + "dry-wash.order-create.car-body-select.options.station-wagon": "Универсал", + "dry-wash.order-create.car-body-select.options.coupe": "Купе", + "dry-wash.order-create.car-body-select.options.minivan": "Минивэн", + "dry-wash.order-create.car-body-select.options.pickup": "Пикап", + "dry-wash.order-create.car-body-select.options.liftback": "Лифтбек", + "dry-wash.order-create.car-body-select.options.sports-car": "Спорткар", "dry-wash.order-create.car-body-select.options.other": "Другой", "dry-wash.order-create.form.submit-button.label": "Отправить", + "dry-wash.order-view.title": "Ваш заказ", + "dry-wash.order-view.error.title": "Ошибка", + "dry-wash.order-view.fetch.error": "Не удалось загрузить детали заказа №{{number}}", + "dry-wash.order-view.details.title": "Заказ №{{number}}", + "dry-wash.order-view.details.owner": "Владелец", + "dry-wash.order-view.details.car": "Автомобиль", + "dry-wash.order-view.details.location": "Где", + "dry-wash.order-view.details.datetime-range": "Когда", + "dry-wash.order-view.details.alert": "С вами свяжется оператор насчет оплаты по указанному номеру телефона", "dry-wash.notFound.title": "Страница не найдена", "dry-wash.notFound.description": "К сожалению, запрашиваемая вами страница не существует.", "dry-wash.notFound.button.back": "Вернуться на главную", diff --git a/src/api/landing.ts b/src/api/landing.ts new file mode 100644 index 0000000..1ea8bf4 --- /dev/null +++ b/src/api/landing.ts @@ -0,0 +1,25 @@ +import { getConfigValue } from '@brojs/cli'; + +import { Order } from '../models/landing'; + +enum LandingEndpoints { + ORDER_VIEW = '/order' +} + +const LandingService = () => { + const endpoint = getConfigValue('dry-wash.api'); + + const fetchOrder = async (orderId: Order.Id) => { + const response = await fetch(`${endpoint}${LandingEndpoints.ORDER_VIEW}/${orderId}`); + + if (!response.ok) { + throw new Error(`Failed to fetch order: ${response.status}`); + } + + return await response.json(); + }; + + return { fetchOrder }; +}; + +export { LandingService, LandingEndpoints }; diff --git a/src/components/order-view/details/index.ts b/src/components/order-view/details/index.ts new file mode 100644 index 0000000..8e1c3ce --- /dev/null +++ b/src/components/order-view/details/index.ts @@ -0,0 +1 @@ +export { OrderDetails } from './order-details'; \ No newline at end of file diff --git a/src/components/order-view/details/order-details.tsx b/src/components/order-view/details/order-details.tsx new file mode 100644 index 0000000..515c567 --- /dev/null +++ b/src/components/order-view/details/order-details.tsx @@ -0,0 +1,99 @@ +import React, { FC } from 'react'; +import { + Alert, + AlertIcon, + Heading, + HStack, + UnorderedList, + VStack, + ListItem, + Text, +} from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; + +import { Order } from '../../../models/landing'; +import { formatDatetime } from '../../../lib'; +import { carBodySelectOptions } from '../../order-form/form/car-body/helper'; + +import { OrderStatus } from './status'; + +type OrderDetailsProps = Order.View; + +export const OrderDetails: FC = ({ + id, + status, + phone, + carNumber, + carBody, + carColor, + location, + datetimeBegin, + datetimeEnd, +}) => { + const { t } = useTranslation('~', { + keyPrefix: 'dry-wash.order-view.details', + }); + const { t: tCarBody } = useTranslation('~', { + keyPrefix: 'dry-wash.order-create.car-body-select.options', + }); + + return ( + + + + {t('title', { number: id })} + + + + + {[ + { + label: t('owner'), + value: phone, + }, + { + label: t('car'), + value: [ + carNumber, + tCarBody( + `${carBodySelectOptions.find(({ value }) => value === carBody)?.labelTKey}`, + ), + carColor, + ] + .filter((v) => v) + .join(', '), + }, + { + label: t('location'), + value: location, + }, + { + label: t('datetime-range'), + value: [ + formatDatetime(datetimeBegin), + formatDatetime(datetimeEnd), + ].join(' - '), + }, + ].map(({ label, value }, i) => ( + + {label}:{' '} + + {value} + + + ))} + + + + {t('alert')} + + + ); +}; + +// todo: add i18n for date & time diff --git a/src/components/order-view/details/status/index.ts b/src/components/order-view/details/status/index.ts new file mode 100644 index 0000000..4efae81 --- /dev/null +++ b/src/components/order-view/details/status/index.ts @@ -0,0 +1 @@ +export { OrderStatus } from './order-status'; \ No newline at end of file diff --git a/src/components/order-view/details/status/order-status.tsx b/src/components/order-view/details/status/order-status.tsx new file mode 100644 index 0000000..cc40c90 --- /dev/null +++ b/src/components/order-view/details/status/order-status.tsx @@ -0,0 +1,49 @@ +import { Tag, TagProps } from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TFunction } from 'i18next'; + +import { Order } from '../../../../models/landing'; + +const getPropsByStatus = ( + status: Order.Status, + t: TFunction<'~', 'dry-wash.arm.order.status'>, +): TagProps => { + switch (status) { + case 'canceled': + return { + colorScheme: 'red', + children: t('canceled'), + }; + case 'progress': + return { + colorScheme: 'yellow', + children: t('progress'), + }; + case 'pending': + return { + colorScheme: 'yellow', + children: t('pending'), + }; + case 'working': + return { + colorScheme: 'yellow', + children: t('working'), + }; + case 'complete': + return { + colorScheme: 'green', + children: t('complete'), + }; + } +}; + +export const OrderStatus: FC<{ value: Order.Status }> = ({ value }) => { + const { t } = useTranslation('~', { + keyPrefix: 'dry-wash.arm.order.status', + }); + + const props = getPropsByStatus(value, t); + + return ; +}; diff --git a/src/components/order-view/index.ts b/src/components/order-view/index.ts new file mode 100644 index 0000000..e9d0758 --- /dev/null +++ b/src/components/order-view/index.ts @@ -0,0 +1 @@ +export * from './details'; \ No newline at end of file diff --git a/src/lib/date-helpers.ts b/src/lib/date-helpers.ts index 1a3270f..305ddf4 100644 --- a/src/lib/date-helpers.ts +++ b/src/lib/date-helpers.ts @@ -10,3 +10,7 @@ export const getTimeSlot = (start: string, end: string) => { end: endDay, }); }; + +export const formatDatetime = (datetime: string) => { + return dayjs(datetime).format('HH:mm DD.MM.YYYY'); +}; \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts index fcdac2d..dc82bac 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,2 @@ +export * from './date-helpers'; export * from './types'; \ No newline at end of file diff --git a/src/mocks/landing/index.ts b/src/mocks/landing/index.ts index 0c76edd..facfc18 100644 --- a/src/mocks/landing/index.ts +++ b/src/mocks/landing/index.ts @@ -1,2 +1 @@ -export * as mockOrder from './order'; export * as mockReview from './review'; \ No newline at end of file diff --git a/src/mocks/landing/order.ts b/src/mocks/landing/order.ts deleted file mode 100644 index 8c14c81..0000000 --- a/src/mocks/landing/order.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Order } from "../../models"; - -export const orders: Order.View[] = [ - { id: 'id1' }, - { id: 'id2' }, - { id: 'id3' }, -]; \ No newline at end of file diff --git a/src/models/api/index.ts b/src/models/api/index.ts new file mode 100644 index 0000000..e27d5ad --- /dev/null +++ b/src/models/api/index.ts @@ -0,0 +1 @@ +export * from './order-view'; \ No newline at end of file diff --git a/src/models/api/order-view.ts b/src/models/api/order-view.ts new file mode 100644 index 0000000..0c33f0c --- /dev/null +++ b/src/models/api/order-view.ts @@ -0,0 +1,14 @@ +import { Order } from "../landing"; + +export type FetchOrderQueryResponse = { + id: string; + orderDate: string; + carNumber: string; + carBody: number; + carColor?: string; + startWashTime: string; + endWashTime: string; + status: Order.Status; + phone: string; + location: string; +}; \ No newline at end of file diff --git a/src/models/landing/order.ts b/src/models/landing/order.ts index d286a90..8a8eeef 100644 --- a/src/models/landing/order.ts +++ b/src/models/landing/order.ts @@ -2,6 +2,12 @@ import { Car, Customer, Washing } from "."; export type Id = string; +export type Status = 'pending' | + 'progress' | + 'working' | + 'canceled' | + 'complete'; + export type Create = { customer: { phone: Customer.PhoneNumber, @@ -20,4 +26,13 @@ export type Create = { export type View = { id: Id; + orderDate: string, + status: Status, + phone: Customer.PhoneNumber; + carNumber: Car.RegistrationNumber; + carBody: Car.BodyStyle; + carColor?: Car.Color; + location: Washing.Location; + datetimeBegin: Washing.AvailableBeginDateTime; + datetimeEnd: Washing.AvailableEndDateTime; }; \ No newline at end of file diff --git a/src/pages/order-view/helper.tsx b/src/pages/order-view/helper.tsx new file mode 100644 index 0000000..13f9513 --- /dev/null +++ b/src/pages/order-view/helper.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; + +import { LandingService } from '../../api/landing'; +import { Order } from '../../models/landing'; +import { FetchOrderQueryResponse } from '../../models/api'; + +export const useFetchOrderDetails = ({ + orderId, +}: { + orderId: Order.View['id']; +}) => { + const { fetchOrder } = LandingService(); + + const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + + try { + const data = await fetchOrder(orderId); + setData(data.body); + } catch (error) { + setError(error.message); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, []); + + return { + isLoading, + data, + error, + }; +}; diff --git a/src/pages/order-view/index.tsx b/src/pages/order-view/index.tsx index d032472..5ac4bd0 100644 --- a/src/pages/order-view/index.tsx +++ b/src/pages/order-view/index.tsx @@ -1,7 +1,75 @@ -import React from "react"; +import React, { FC, useEffect } from 'react'; +import { HStack, Spinner, useToast } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { Container, Heading, VStack } from '@chakra-ui/react'; +import { useParams } from 'react-router-dom'; -const Page = () => { - return

Order view

; +import { LandingThemeProvider } from '../../containers'; +import { OrderDetails } from '../../components/order-view'; + +import { useFetchOrderDetails } from './helper'; + +const Page: FC = () => { + const { t } = useTranslation('~', { + keyPrefix: 'dry-wash.order-view', + }); + + const { orderId } = useParams(); + + const { isLoading, data, error } = useFetchOrderDetails({ orderId }); + + const toast = useToast(); + useEffect(() => { + if (error) { + toast({ + title: t('error.title'), + description: t('fetch.error'), + status: 'error', + duration: 5000, + isClosable: true, + position: 'bottom-right', + }); + } + }, [error]); + + return ( + + + + + {t('title')} + + {isLoading ? ( + + + + ) : ( + data && ( + + ) + )} + + + + ); }; export default Page; diff --git a/stubs/api/admin.js b/stubs/api/admin.js index e8c5a07..2f9e4eb 100644 --- a/stubs/api/admin.js +++ b/stubs/api/admin.js @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-require-imports */ const router = require('express').Router(); -const STUBS = { masters: 'success', orders: 'success' }; +const STUBS = { masters: 'success', orders: 'success', orderView: 'success' }; router.get('/set/:name/:value', (req, res) => { const { name, value } = req.params; @@ -24,6 +24,11 @@ router.get('/', (req, res) => { ${generateRadioInput('orders', 'success')} ${generateRadioInput('orders', 'error')} +
+ Лендинг - Детали заказа + ${generateRadioInput('orderView', 'success')} + ${generateRadioInput('orderView', 'error')} +
`); }); diff --git a/stubs/api/index.js b/stubs/api/index.js index 8fe4716..771f3dd 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -33,6 +33,19 @@ router.get('/arm/orders', (req, res) => { ); }); +router.get('/order/:orderId', ({ params }, res) => { + const { orderId } = params; + const stubName = `${orderId}-${STUBS.orderView}`; + + res + .status(/error/.test(stubName) ? 500 : 200) + .send( + /^error$/.test(stubName) + ? commonError + : require(`../json/landing-order-view/${stubName}.json`), + ); +}); + router.use('/admin', require('./admin')); module.exports = router; diff --git a/stubs/json/landing-order-view/1-success.json b/stubs/json/landing-order-view/1-success.json new file mode 100644 index 0000000..4e09761 --- /dev/null +++ b/stubs/json/landing-order-view/1-success.json @@ -0,0 +1,14 @@ +{ + "success": true, + "body": { + "id": "order1", + "orderDate": "2024-11-24T08:41:46.366Z", + "status": "progress", + "carNumber": "A123BC", + "carBody": 1, + "startWashTime": "2024-11-24T10:30:00.000Z", + "endWashTime": "2024-11-24T16:30:00.000Z", + "phone": "79001234563", + "location": "55.754364, 48.743295 Университетская улица, 1, Иннополис, Верхнеуслонский район, Республика Татарстан (Татарстан), 420500" + } +} \ No newline at end of file