feat: add order-view (#9) #51
| @ -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", | ||||
|  | ||||
| @ -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": "Вернуться на главную", | ||||
|  | ||||
							
								
								
									
										25
									
								
								src/api/landing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/api/landing.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| import { getConfigValue } from '@brojs/cli'; | ||||
| 
 | ||||
| import { Order } from '../models/landing'; | ||||
| 
 | ||||
| enum LandingEndpoints { | ||||
|   ORDER_VIEW = '/order' | ||||
| } | ||||
| 
 | ||||
| const LandingService = () => { | ||||
|   const endpoint = getConfigValue('dry-wash.api'); | ||||
| 
 | ||||
|   const fetchOrder = async (orderId: Order.Id) => { | ||||
|     const response = await fetch(`${endpoint}${LandingEndpoints.ORDER_VIEW}/${orderId}`); | ||||
| 
 | ||||
|     if (!response.ok) { | ||||
|       throw new Error(`Failed to fetch order: ${response.status}`); | ||||
|     } | ||||
| 
 | ||||
|     return await response.json(); | ||||
|   }; | ||||
| 
 | ||||
|   return { fetchOrder }; | ||||
| }; | ||||
| 
 | ||||
| export { LandingService, LandingEndpoints }; | ||||
							
								
								
									
										1
									
								
								src/components/order-view/details/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/order-view/details/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| export { OrderDetails } from './order-details'; | ||||
							
								
								
									
										99
									
								
								src/components/order-view/details/order-details.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/components/order-view/details/order-details.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<OrderDetailsProps> = ({ | ||||
|   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 ( | ||||
|     <VStack p={4} alignItems='flex-start' gap={4}> | ||||
|       <HStack | ||||
|         width='full' | ||||
|         flexWrap='wrap' | ||||
|         justifyContent='space-between' | ||||
|         gap={2} | ||||
|       > | ||||
|         <Heading as='h2' size='lg'> | ||||
|           {t('title', { number: id })} | ||||
|         </Heading> | ||||
|         <OrderStatus value={status} /> | ||||
|       </HStack> | ||||
|       <UnorderedList styleType='none'> | ||||
|         {[ | ||||
|           { | ||||
|             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) => ( | ||||
|           <ListItem key={i}> | ||||
|             {label}:{' '} | ||||
|             <Text as='span' color='primary.500' fontWeight='bold'> | ||||
|               {value} | ||||
|             </Text> | ||||
|           </ListItem> | ||||
|         ))} | ||||
|       </UnorderedList> | ||||
|       <Alert status='info' alignItems='flex-start'> | ||||
|         <AlertIcon /> | ||||
|         {t('alert')} | ||||
|       </Alert> | ||||
|     </VStack> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // todo: add i18n for date & time
 | ||||
							
								
								
									
										1
									
								
								src/components/order-view/details/status/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/order-view/details/status/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| export { OrderStatus } from './order-status'; | ||||
							
								
								
									
										49
									
								
								src/components/order-view/details/status/order-status.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/components/order-view/details/status/order-status.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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 <Tag variant='solid' {...props} />; | ||||
| }; | ||||
							
								
								
									
										1
									
								
								src/components/order-view/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/order-view/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| export * from './details'; | ||||
| @ -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'); | ||||
| }; | ||||
| @ -1 +1,2 @@ | ||||
| export * from './date-helpers'; | ||||
| export * from './types'; | ||||
| @ -1,2 +1 @@ | ||||
| export * as mockOrder from './order'; | ||||
| export * as mockReview from './review'; | ||||
| @ -1,7 +0,0 @@ | ||||
| import { Order } from "../../models"; | ||||
| 
 | ||||
| export const orders: Order.View[] = [ | ||||
|   { id: 'id1' }, | ||||
|   { id: 'id2' }, | ||||
|   { id: 'id3' }, | ||||
| ]; | ||||
							
								
								
									
										1
									
								
								src/models/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/models/api/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| export * from './order-view'; | ||||
							
								
								
									
										14
									
								
								src/models/api/order-view.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/models/api/order-view.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| import { Order } from "../landing"; | ||||
| 
 | ||||
| export type FetchOrderQueryResponse = { | ||||
|   id: string; | ||||
|   orderDate: string; | ||||
|   carNumber: string; | ||||
|   carBody: number; | ||||
|   carColor?: string; | ||||
|   startWashTime: string; | ||||
|   endWashTime: string; | ||||
|   status: Order.Status; | ||||
|   phone: string; | ||||
|   location: string; | ||||
| }; | ||||
| @ -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; | ||||
| }; | ||||
							
								
								
									
										40
									
								
								src/pages/order-view/helper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/pages/order-view/helper.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| 
 | ||||
| import { LandingService } from '../../api/landing'; | ||||
| import { Order } from '../../models/landing'; | ||||
| import { FetchOrderQueryResponse } from '../../models/api'; | ||||
| 
 | ||||
| export const useFetchOrderDetails = ({ | ||||
|   orderId, | ||||
| }: { | ||||
|   orderId: Order.View['id']; | ||||
| }) => { | ||||
|   const { fetchOrder } = LandingService(); | ||||
| 
 | ||||
|   const [data, setData] = useState<FetchOrderQueryResponse>(); | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const fetchData = async () => { | ||||
|       setIsLoading(true); | ||||
| 
 | ||||
|       try { | ||||
|         const data = await fetchOrder(orderId); | ||||
|         setData(data.body); | ||||
|       } catch (error) { | ||||
|         setError(error.message); | ||||
|       } finally { | ||||
|         setIsLoading(false); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     fetchData(); | ||||
|   }, []); | ||||
| 
 | ||||
|   return { | ||||
|     isLoading, | ||||
|     data, | ||||
|     error, | ||||
|   }; | ||||
| }; | ||||
| @ -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 <h1>Order view</h1>; | ||||
| 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 ( | ||||
|     <LandingThemeProvider> | ||||
|       <Container | ||||
|         w='full' | ||||
|         maxWidth='container.xl' | ||||
|         minH='100vh' | ||||
|         padding={0} | ||||
|         bg='white' | ||||
|         centerContent | ||||
|       > | ||||
|         <VStack w='full' h='full' alignItems='stretch' flexGrow={1}> | ||||
|           <Heading textAlign='center' mt={4}> | ||||
|             {t('title')} | ||||
|           </Heading> | ||||
|           {isLoading ? ( | ||||
|             <HStack width='full' justifyContent='center'> | ||||
|               <Spinner size='lg' /> | ||||
|             </HStack> | ||||
|           ) : ( | ||||
|             data && ( | ||||
|               <OrderDetails | ||||
|                 id={data.id} | ||||
|                 orderDate={data.orderDate} | ||||
|                 status={data.status} | ||||
|                 phone={data.phone} | ||||
|                 carNumber={data.carNumber} | ||||
|                 carBody={data.carBody} | ||||
|                 carColor={data.carColor} | ||||
|                 location={data.location} | ||||
|                 datetimeBegin={data.startWashTime} | ||||
|                 datetimeEnd={data.endWashTime} | ||||
|               /> | ||||
|             ) | ||||
|           )} | ||||
|         </VStack> | ||||
|       </Container> | ||||
|     </LandingThemeProvider> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Page; | ||||
|  | ||||
| @ -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')} | ||||
|     </fieldset> | ||||
|     <fieldset>  | ||||
|     <legend>Лендинг - Детали заказа</legend> | ||||
|       ${generateRadioInput('orderView', 'success')} | ||||
|       ${generateRadioInput('orderView', 'error')} | ||||
|     </fieldset> | ||||
| </div>`); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										14
									
								
								stubs/json/landing-order-view/1-success.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								stubs/json/landing-order-view/1-success.json
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user