пва
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
5700
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"type":"server-started","port":53669,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:53669","screen_dir":"/mnt/d/my_projects/shop/.superpowers/brainstorm/7074-1779609479/content","state_dir":"/mnt/d/my_projects/shop/.superpowers/brainstorm/7074-1779609479/state"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
7319
|
||||||
@@ -3,6 +3,7 @@ import Stack from '@mui/material/Stack'
|
|||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { DeliverySection } from './sections/DeliverySection'
|
import { DeliverySection } from './sections/DeliverySection'
|
||||||
import { HowToOrderSection } from './sections/HowToOrderSection'
|
import { HowToOrderSection } from './sections/HowToOrderSection'
|
||||||
|
import { OrderStatusesSection } from './sections/OrderStatusesSection'
|
||||||
import { PaymentSection } from './sections/PaymentSection'
|
import { PaymentSection } from './sections/PaymentSection'
|
||||||
import { ReturnsSection } from './sections/ReturnsSection'
|
import { ReturnsSection } from './sections/ReturnsSection'
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ export function InfoPage() {
|
|||||||
<HowToOrderSection />
|
<HowToOrderSection />
|
||||||
<DeliverySection />
|
<DeliverySection />
|
||||||
<PaymentSection />
|
<PaymentSection />
|
||||||
|
<OrderStatusesSection />
|
||||||
<ReturnsSection />
|
<ReturnsSection />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
import Grid from '@mui/material/Grid'
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Link from '@mui/material/Link'
|
||||||
import Paper from '@mui/material/Paper'
|
import Paper from '@mui/material/Paper'
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { Package, Store } from 'lucide-react'
|
import { Package, Store } from 'lucide-react'
|
||||||
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
|
import { DELIVERY_CARRIER_OPTIONS } from '@/shared/constants/delivery-carrier'
|
||||||
import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point'
|
import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point'
|
||||||
|
|
||||||
const deliveries = [
|
|
||||||
{
|
|
||||||
title: 'Самовывоз',
|
|
||||||
icon: <Store size={28} />,
|
|
||||||
lines: ['Бесплатно.', PICKUP_ADDRESS_FULL, 'Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Почта / Службы доставки',
|
|
||||||
icon: <Package size={28} />,
|
|
||||||
lines: [
|
|
||||||
'Отправка в другие города.',
|
|
||||||
'Каждому заказу присваивается трек-номер для отслеживания.',
|
|
||||||
'Стоимость рассчитывается по тарифу перевозчика при оформлении.',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function DeliverySection() {
|
export function DeliverySection() {
|
||||||
return (
|
return (
|
||||||
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
@@ -29,23 +15,51 @@ export function DeliverySection() {
|
|||||||
Доставка
|
Доставка
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{deliveries.map((d) => (
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
<Grid key={d.title} size={{ xs: 12, sm: 6, md: 4 }}>
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2, height: '100%' }}>
|
||||||
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2, height: '100%' }}>
|
<Stack spacing={1.5}>
|
||||||
<Stack spacing={1.5}>
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
<Store size={28} />
|
||||||
{d.icon}
|
<Typography variant="h6">Самовывоз</Typography>
|
||||||
<Typography variant="h6">{d.title}</Typography>
|
</Stack>
|
||||||
</Stack>
|
<Typography variant="body2" color="text.secondary">
|
||||||
{d.lines.map((line, i) => (
|
Бесплатно.
|
||||||
<Typography key={i} variant="body2" color="text.secondary">
|
</Typography>
|
||||||
{line}
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{PICKUP_ADDRESS_FULL}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.
|
||||||
|
</Typography>
|
||||||
|
<Link component={RouterLink} to="/about" variant="body2">
|
||||||
|
Посмотреть на карте
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2, height: '100%' }}>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
|
<Package size={28} />
|
||||||
|
<Typography variant="h6">Доставка по России</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Доступные службы доставки:
|
||||||
|
</Typography>
|
||||||
|
<Stack component="ul" spacing={0.5} sx={{ pl: 2, m: 0, listStyle: 'disc' }}>
|
||||||
|
{DELIVERY_CARRIER_OPTIONS.map((c) => (
|
||||||
|
<Typography key={c.code} component="li" variant="body2" color="text.secondary">
|
||||||
|
{c.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
<Typography variant="body2" color="text.secondary">
|
||||||
</Grid>
|
Стоимость рассчитывается по тарифу перевозчика. Админ скорректирует цену после оформления заказа.
|
||||||
))}
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Link from '@mui/material/Link'
|
||||||
import Paper from '@mui/material/Paper'
|
import Paper from '@mui/material/Paper'
|
||||||
import Step from '@mui/material/Step'
|
import Step from '@mui/material/Step'
|
||||||
import StepContent from '@mui/material/StepContent'
|
import StepContent from '@mui/material/StepContent'
|
||||||
import StepLabel from '@mui/material/StepLabel'
|
import StepLabel from '@mui/material/StepLabel'
|
||||||
import Stepper from '@mui/material/Stepper'
|
import Stepper from '@mui/material/Stepper'
|
||||||
|
import Tab from '@mui/material/Tab'
|
||||||
|
import Tabs from '@mui/material/Tabs'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { CheckCircle, ClipboardList, Mail, ShoppingCart, Truck } from 'lucide-react'
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
ClipboardList,
|
||||||
|
Clock,
|
||||||
|
CreditCard,
|
||||||
|
MapPin,
|
||||||
|
PackageOpen,
|
||||||
|
ShoppingCart,
|
||||||
|
Star,
|
||||||
|
Store,
|
||||||
|
Truck,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
|
import { PICKUP_ADDRESS_SHORT } from '@/shared/constants/pickup-point'
|
||||||
|
|
||||||
const steps = [
|
const commonSteps = [
|
||||||
{
|
{
|
||||||
label: 'Выберите товары',
|
label: 'Выберите товары',
|
||||||
icon: <ShoppingCart size={20} />,
|
icon: <ShoppingCart size={20} />,
|
||||||
@@ -17,39 +35,96 @@ const steps = [
|
|||||||
icon: <ClipboardList size={20} />,
|
icon: <ClipboardList size={20} />,
|
||||||
text: 'Перейдите в корзину и проверьте состав заказа: названия товаров, количество и итоговую сумму. Здесь же можно изменить количество или удалить позиции.',
|
text: 'Перейдите в корзину и проверьте состав заказа: названия товаров, количество и итоговую сумму. Здесь же можно изменить количество или удалить позиции.',
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const deliverySteps = [
|
||||||
{
|
{
|
||||||
label: 'Укажите контакты и адрес',
|
label: 'Укажите адрес доставки и получателя',
|
||||||
icon: <Mail size={20} />,
|
icon: <MapPin size={20} />,
|
||||||
text: 'Заполните имя, телефон и email для связи. Укажите адрес доставки — город, улицу, дом и квартиру. Эти данные нужны для расчёта стоимости и сроков.',
|
text: 'Заполните имя, телефон, email и адрес доставки. Если у вас уже есть сохранённые адреса — выберите из списка или добавьте новый в личном кабинете.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Выберите доставку и оплату',
|
label: 'Выберите способ доставки',
|
||||||
icon: <Truck size={20} />,
|
icon: <Truck size={20} />,
|
||||||
text: 'Выберите способ доставки: самовывоз, курьер или почта/СДЭК. Затем укажите способ оплаты: картой онлайн или при получении.',
|
text: 'Доступны: Почта России, Озон ПВЗ, Яндекс ПВЗ, 5Post, WB ПВЗ. После оформления админ рассчитает точную стоимость доставки и скорректирует цену заказа.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Подтвердите заказ',
|
label: 'Оплатите заказ',
|
||||||
icon: <CheckCircle size={20} />,
|
icon: <CreditCard size={20} />,
|
||||||
text: 'Проверьте все данные ещё раз и нажмите «Оформить заказ». После этого мастер получит уведомление и начнёт подготовку вашего изделия.',
|
text: 'Онлайн-оплата через ЮKassa — банковские карты и СБП. Перенаправление на защищённую платёжную страницу.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Получите заказ',
|
||||||
|
icon: <PackageOpen size={20} />,
|
||||||
|
text: 'После отправки вы получите трек-номер для отслеживания. Следите за статусом заказа в личном кабинете.',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const pickupSteps = [
|
||||||
|
{
|
||||||
|
label: 'Подтвердите заказ',
|
||||||
|
icon: <CheckCircle size={20} />,
|
||||||
|
text: 'Выберите способ оплаты: онлайн через ЮKassa (карты, СБП) или при получении (наличные / карта).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Согласуйте время получения',
|
||||||
|
icon: <Clock size={20} />,
|
||||||
|
text: 'Админ свяжется с вами, чтобы договориться об удобном времени выдачи заказа.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Получите заказ',
|
||||||
|
icon: <Store size={20} />,
|
||||||
|
text: `Адрес: ${PICKUP_ADDRESS_SHORT}. Перед визитом согласуем время, чтобы заказ точно был готов.`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function BranchStepper({ steps }: { steps: typeof deliverySteps }) {
|
||||||
|
return (
|
||||||
|
<Stepper orientation="vertical" activeStep={-1}>
|
||||||
|
{steps.map((step) => (
|
||||||
|
<Step key={step.label} completed={false}>
|
||||||
|
<StepLabel slots={{ stepIcon: () => step.icon }}>{step.label}</StepLabel>
|
||||||
|
<StepContent>
|
||||||
|
<Typography color="text.secondary">{step.text}</Typography>
|
||||||
|
</StepContent>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function HowToOrderSection() {
|
export function HowToOrderSection() {
|
||||||
|
const [tab, setTab] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
<Typography variant="h5" gutterBottom>
|
<Typography variant="h5" gutterBottom>
|
||||||
Как оформить заказ
|
Как оформить заказ
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stepper orientation="vertical" activeStep={-1}>
|
<BranchStepper steps={commonSteps} />
|
||||||
{steps.map((step) => (
|
<Box sx={{ mt: 2, mb: 1 }}>
|
||||||
<Step key={step.label} completed={false}>
|
<Tabs value={tab} onChange={(_, v) => setTab(v)} variant="fullWidth">
|
||||||
<StepLabel slots={{ stepIcon: () => step.icon }}>{step.label}</StepLabel>
|
<Tab label="Доставка" />
|
||||||
<StepContent>
|
<Tab label="Самовывоз" />
|
||||||
<Typography color="text.secondary">{step.text}</Typography>
|
</Tabs>
|
||||||
</StepContent>
|
</Box>
|
||||||
</Step>
|
{tab === 0 && <BranchStepper steps={deliverySteps} />}
|
||||||
))}
|
{tab === 1 && (
|
||||||
</Stepper>
|
<Box>
|
||||||
|
<BranchStepper steps={pickupSteps} />
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
<Link component={RouterLink} to="/about">
|
||||||
|
Посмотреть на карте
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box sx={{ mt: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Star size={18} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
После получения заказа вы можете оставить отзыв в личном кабинете.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import Paper from '@mui/material/Paper'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { Banknote, CheckCircle, Package, PackageCheck, PackageSearch, Store, XCircle } from 'lucide-react'
|
||||||
|
import { ORDER_STATUS_DATA, type StatusIconName } from '@/shared/lib/order-status-data'
|
||||||
|
|
||||||
|
const iconMap: Record<StatusIconName, ReactElement> = {
|
||||||
|
banknote: <Banknote size={18} />,
|
||||||
|
'check-circle': <CheckCircle size={18} />,
|
||||||
|
'package-search': <PackageSearch size={18} />,
|
||||||
|
package: <Package size={18} />,
|
||||||
|
'package-check': <PackageCheck size={18} />,
|
||||||
|
store: <Store size={18} />,
|
||||||
|
'x-circle': <XCircle size={18} />,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrderStatusesSection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Статусы заказа
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Текущий статус отображается в личном кабинете. Вот что означает каждый из них:
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{ORDER_STATUS_DATA.map((s) => (
|
||||||
|
<Stack key={s.code} direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
|
||||||
|
<Chip
|
||||||
|
icon={iconMap[s.iconName]}
|
||||||
|
label={s.label}
|
||||||
|
color={s.color}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ minWidth: 180, flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ pt: 0.3 }}>
|
||||||
|
{s.description}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,13 +9,13 @@ import { Banknote, CreditCard } from 'lucide-react'
|
|||||||
const methods = [
|
const methods = [
|
||||||
{
|
{
|
||||||
icon: <CreditCard size={22} />,
|
icon: <CreditCard size={22} />,
|
||||||
primary: 'Банковская карта онлайн',
|
primary: 'Онлайн-оплата через ЮKassa (карты, СБП)',
|
||||||
secondary: 'Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.',
|
secondary: 'Оплата после подтверждения заказа админом. Перенаправление на защищённую платёжную страницу ЮKassa.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Banknote size={22} />,
|
icon: <Banknote size={22} />,
|
||||||
primary: 'Оплата при получении',
|
primary: 'Оплата при получении',
|
||||||
secondary: 'Оплата наличными или картой при получении заказа.',
|
secondary: 'Наличными или картой при самовывозе.',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export function PaymentSection() {
|
|||||||
Оплата
|
Оплата
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
Оплата происходит после подтверждения заказа мастером. Вы получите уведомление, когда заказ будет подтверждён и
|
Оплата происходит после подтверждения заказа админом. Вы получите уведомление, когда заказ будет подтверждён и
|
||||||
готов к оплате.
|
готов к оплате.
|
||||||
</Typography>
|
</Typography>
|
||||||
<List disablePadding>
|
<List disablePadding>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { deliveryCarrierLabelRu } from '@/shared/constants/delivery-carrier'
|
|||||||
import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point'
|
import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point'
|
||||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||||
import { parseOrderAddressSnapshot } from '@/shared/lib/order-address-snapshot'
|
import { parseOrderAddressSnapshot } from '@/shared/lib/order-address-snapshot'
|
||||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
import { OrderStatusChip } from '@/shared/ui/OrderStatusChip'
|
||||||
|
|
||||||
export function OrderDetailPage() {
|
export function OrderDetailPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -130,7 +130,14 @@ export function OrderDetailPage() {
|
|||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2, alignItems: { sm: 'center' } }}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2, alignItems: { sm: 'center' } }}>
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Typography variant="h4">Заказ #{order.id.slice(-6)}</Typography>
|
<Typography variant="h4">Заказ #{order.id.slice(-6)}</Typography>
|
||||||
<Typography color="text.secondary">Статус: {orderStatusLabelRu(order.status)}</Typography>
|
<OrderStatusChip
|
||||||
|
status={order.status}
|
||||||
|
tooltipOverride={
|
||||||
|
order.status === 'PENDING_PAYMENT' && order.deliveryType === 'delivery' && !order.deliveryFeeLocked
|
||||||
|
? 'Оплата происходит после подтверждения заказа админом. Вы получите уведомление, когда заказ будет подтверждён и готов к оплате.'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button component={RouterLink} to="/me/orders" variant="outlined">
|
<Button component={RouterLink} to="/me/orders" variant="outlined">
|
||||||
К списку
|
К списку
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ export const PICKUP_COORDINATES = { lat: 58.09898000206914, lng: 57.813169680997
|
|||||||
/** Полная строка адреса для текстовых блоков. */
|
/** Полная строка адреса для текстовых блоков. */
|
||||||
export const PICKUP_ADDRESS_FULL =
|
export const PICKUP_ADDRESS_FULL =
|
||||||
'618909, Россия, Пермский край, Лысьвенский муниципальный округ, Лысьва, улица Мира, 34'
|
'618909, Россия, Пермский край, Лысьвенский муниципальный округ, Лысьва, улица Мира, 34'
|
||||||
|
|
||||||
|
/** Короткий адрес для компактных блоков. */
|
||||||
|
export const PICKUP_ADDRESS_SHORT = 'Лысьва, ул. Мира, 34'
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
export type StatusColor = 'warning' | 'success' | 'info' | 'error'
|
||||||
|
export type StatusIconName = 'banknote' | 'check-circle' | 'package-search' | 'package' | 'package-check' | 'store' | 'x-circle'
|
||||||
|
|
||||||
|
export interface OrderStatusData {
|
||||||
|
code: string
|
||||||
|
label: string
|
||||||
|
iconName: StatusIconName
|
||||||
|
color: StatusColor
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ORDER_STATUS_DATA: ReadonlyArray<OrderStatusData> = [
|
||||||
|
{
|
||||||
|
code: 'PENDING_PAYMENT',
|
||||||
|
label: 'Ожидает оплаты',
|
||||||
|
iconName: 'banknote',
|
||||||
|
color: 'warning',
|
||||||
|
description:
|
||||||
|
'Заказ оформлен и подтверждён администратором. Оплатите онлайн через ЮKassa или дождитесь получения (для самовывоза с оплатой при получении).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'PAID',
|
||||||
|
label: 'Оплачен',
|
||||||
|
iconName: 'check-circle',
|
||||||
|
color: 'success',
|
||||||
|
description: 'Оплата получена. Админ скоро возьмёт заказ в работу.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'IN_PROGRESS',
|
||||||
|
label: 'Подготовка к отправке',
|
||||||
|
iconName: 'package-search',
|
||||||
|
color: 'info',
|
||||||
|
description: 'Админ готовит заказ к отправке или выдаче. Скоро статус обновится.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'SHIPPED',
|
||||||
|
label: 'Отправлен',
|
||||||
|
iconName: 'package',
|
||||||
|
color: 'info',
|
||||||
|
description: 'Заказ передан в службу доставки. Трек-номер для отслеживания(при наличии) будет указан в сообщении админа.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'READY_FOR_PICKUP',
|
||||||
|
label: 'Готов к получению',
|
||||||
|
iconName: 'store',
|
||||||
|
color: 'success',
|
||||||
|
description: 'Заказ готов к самовывозу. Приезжайте в согласованное время.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'DONE',
|
||||||
|
label: 'Завершён',
|
||||||
|
iconName: 'package-check',
|
||||||
|
color: 'success',
|
||||||
|
description: 'Заказ получен. Вы можете оставить отзыв в личном кабинете.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'CANCELLED',
|
||||||
|
label: 'Отменён',
|
||||||
|
iconName: 'x-circle',
|
||||||
|
color: 'error',
|
||||||
|
description: 'Заказ отменён. Если оплата была произведена, средства вернутся на карту.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getOrderStatusData(code: string): OrderStatusData | undefined {
|
||||||
|
return ORDER_STATUS_DATA.find((s) => s.code === code)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ export function orderStatusLabelRu(code: string): string {
|
|||||||
DRAFT: 'Черновик',
|
DRAFT: 'Черновик',
|
||||||
PENDING_PAYMENT: 'Ожидает оплаты',
|
PENDING_PAYMENT: 'Ожидает оплаты',
|
||||||
PAID: 'Оплачен',
|
PAID: 'Оплачен',
|
||||||
IN_PROGRESS: 'В работе',
|
IN_PROGRESS: 'Подготовка к отправке',
|
||||||
SHIPPED: 'Отправлен',
|
SHIPPED: 'Отправлен',
|
||||||
READY_FOR_PICKUP: 'Готово к получению',
|
READY_FOR_PICKUP: 'Готово к получению',
|
||||||
DONE: 'Завершён',
|
DONE: 'Завершён',
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
|
import { Banknote, CheckCircle, Package, PackageCheck, PackageSearch, Store, XCircle } from 'lucide-react'
|
||||||
|
import { getOrderStatusData, type StatusIconName } from '@/shared/lib/order-status-data'
|
||||||
|
|
||||||
|
const iconMap: Record<StatusIconName, ReactElement> = {
|
||||||
|
banknote: <Banknote size={18} />,
|
||||||
|
'check-circle': <CheckCircle size={18} />,
|
||||||
|
'package-search': <PackageSearch size={18} />,
|
||||||
|
package: <Package size={18} />,
|
||||||
|
'package-check': <PackageCheck size={18} />,
|
||||||
|
store: <Store size={18} />,
|
||||||
|
'x-circle': <XCircle size={18} />,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderStatusChipProps {
|
||||||
|
status: string
|
||||||
|
tooltipOverride?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrderStatusChip({ status, tooltipOverride }: OrderStatusChipProps) {
|
||||||
|
const data = getOrderStatusData(status)
|
||||||
|
const label = data?.label ?? status
|
||||||
|
const color = data?.color ?? 'default'
|
||||||
|
const icon = data ? iconMap[data.iconName] : undefined
|
||||||
|
const tooltip = tooltipOverride ?? data?.description ?? ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={tooltip} arrow placement="top">
|
||||||
|
<Chip icon={icon} label={label} color={color as 'default'} size="small" variant="outlined" />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Редизайн страницы /info — Спецификация
|
||||||
|
|
||||||
|
**Дата:** 2026-05-24
|
||||||
|
**Страница:** `/info` (Информация для покупателей)
|
||||||
|
**Подход:** Минимальные правки — обновить контент существующих секций, не меняя структуру страницы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Секция «Как оформить заказ» (HowToOrderSection)
|
||||||
|
|
||||||
|
MUI Stepper с ветвлением на шаге 3. Два сценария: доставка и самовывоз.
|
||||||
|
|
||||||
|
### Общие шаги (1–2):
|
||||||
|
|
||||||
|
| Шаг | Название | Описание |
|
||||||
|
|-----|----------|----------|
|
||||||
|
| 1 | Выберите товары | Найдите нужное в каталоге, добавьте в корзину |
|
||||||
|
| 2 | Проверьте корзину | Убедитесь, что состав и количество верны |
|
||||||
|
|
||||||
|
### Ветвление на шаге 3:
|
||||||
|
|
||||||
|
#### Доставка:
|
||||||
|
|
||||||
|
| Шаг | Название | Описание |
|
||||||
|
|-----|----------|----------|
|
||||||
|
| 3 | Укажите адрес доставки и получателя | Имя, телефон, email, адрес (или добавьте новый в личном кабинете) |
|
||||||
|
| 4 | Выберите способ доставки | Почта России, Озон ПВЗ, Яндекс ПВЗ, 5Post, WB ПВЗ. Дождитесь расчёта стоимости доставки — мастер скорректирует цену |
|
||||||
|
| 5 | Оплатите заказ | Онлайн через ЮKassa (карты, СБП) |
|
||||||
|
| 6 | Получите заказ | Отслеживайте по трек-номеру |
|
||||||
|
|
||||||
|
#### Самовывоз:
|
||||||
|
|
||||||
|
| Шаг | Название | Описание |
|
||||||
|
|-----|----------|----------|
|
||||||
|
| 3 | Подтвердите заказ | Выберите оплату: онлайн через ЮKassa или при получении |
|
||||||
|
| 4 | Согласуйте время получения | Мастер свяжется с вами, чтобы договориться о времени |
|
||||||
|
| 5 | Получите заказ | Адрес: ул. Мира, 34 (ссылка на /about — карта) |
|
||||||
|
|
||||||
|
### Финальный шаг (после получения):
|
||||||
|
После получения заказа можно оставить отзыв.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Секция «Доставка» (DeliverySection)
|
||||||
|
|
||||||
|
Две карточки:
|
||||||
|
|
||||||
|
### Самовывоз
|
||||||
|
- Бесплатно
|
||||||
|
- Адрес: 618909, Россия, Пермский край, Лысьвенский муниципальный округ, Лысьва, улица Мира, 34
|
||||||
|
- Ссылка на /about (карта)
|
||||||
|
- Перед визитом согласуем время — чтобы заказ точно был готов к выдаче
|
||||||
|
|
||||||
|
### Доставка по России
|
||||||
|
Перечислить все 5 служб:
|
||||||
|
- Почта России
|
||||||
|
- Озон доставка (пункт выдачи)
|
||||||
|
- Яндекс доставка (пункт выдачи)
|
||||||
|
- 5Post (пункт выдачи)
|
||||||
|
- WB доставка (пункт выдачи)
|
||||||
|
- Каждому заказу присваивается трек-номер для отслеживания
|
||||||
|
- Стоимость рассчитывается по тарифу перевозчика, мастер скорректирует цену после оформления
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Секция «Оплата» (PaymentSection)
|
||||||
|
|
||||||
|
Два способа:
|
||||||
|
|
||||||
|
| Способ | Описание |
|
||||||
|
|--------|----------|
|
||||||
|
| Онлайн-оплата через ЮKassa (карты, СБП) | Оплата после подтверждения заказа мастером. Перенаправление на защищённую платёжную страницу ЮKassa |
|
||||||
|
| Оплата при получении | Наличными или картой при самовывозе |
|
||||||
|
|
||||||
|
Примечание: оплата происходит после подтверждения заказа мастером.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Секция «Возврат» (ReturnsSection)
|
||||||
|
|
||||||
|
Без изменений — текущий контент остаётся.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Файлы для изменения
|
||||||
|
|
||||||
|
| Файл | Что менять |
|
||||||
|
|------|-----------|
|
||||||
|
| `client/src/pages/info/ui/sections/HowToOrderSection.tsx` | Переписать stepper с ветвлением |
|
||||||
|
| `client/src/pages/info/ui/sections/DeliverySection.tsx` | Перечислить carriers, добавить ссылку на /about |
|
||||||
|
| `client/src/pages/info/ui/sections/PaymentSection.tsx` | Заменить «Банковская карта онлайн» на «ЮKassa (карты, СБП)» |
|
||||||
Binary file not shown.
@@ -10,10 +10,10 @@ export function renderOrderStatusChangedTg({ orderId, oldStatus, newStatus }) {
|
|||||||
DRAFT: 'Черновик',
|
DRAFT: 'Черновик',
|
||||||
PENDING_PAYMENT: 'Ожидает оплаты',
|
PENDING_PAYMENT: 'Ожидает оплаты',
|
||||||
PAID: 'Оплачен',
|
PAID: 'Оплачен',
|
||||||
IN_PROGRESS: 'В работе',
|
IN_PROGRESS: 'Подготовка к отправке',
|
||||||
READY_FOR_PICKUP: 'Готов к выдаче',
|
READY_FOR_PICKUP: 'Готов к выдаче',
|
||||||
SHIPPED: 'Отправлен',
|
SHIPPED: 'Отправлен',
|
||||||
DONE: 'Выполнен',
|
DONE: 'Завершён',
|
||||||
CANCELLED: 'Отменён',
|
CANCELLED: 'Отменён',
|
||||||
}
|
}
|
||||||
return `🔄 Заказ #${orderId.slice(0, 8)}\n${labels[oldStatus] || oldStatus} → <b>${labels[newStatus] || newStatus}</b>`
|
return `🔄 Заказ #${orderId.slice(0, 8)}\n${labels[oldStatus] || oldStatus} → <b>${labels[newStatus] || newStatus}</b>`
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export const ORDER_STATUSES = Object.freeze([
|
export const ORDER_STATUSES = Object.freeze([
|
||||||
'DRAFT',
|
"DRAFT",
|
||||||
'PENDING_PAYMENT',
|
"PENDING_PAYMENT",
|
||||||
'PAID',
|
"PAID",
|
||||||
'IN_PROGRESS',
|
"IN_PROGRESS",
|
||||||
'SHIPPED',
|
"SHIPPED",
|
||||||
'READY_FOR_PICKUP',
|
"READY_FOR_PICKUP",
|
||||||
'DONE',
|
"DONE",
|
||||||
'CANCELLED',
|
"CANCELLED",
|
||||||
])
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Допустимые переходы статусов, доступные админу.
|
* Допустимые переходы статусов, доступные админу.
|
||||||
@@ -15,24 +15,24 @@ export const ORDER_STATUSES = Object.freeze([
|
|||||||
* Для IN_PROGRESS: объект с ключами по deliveryType.
|
* Для IN_PROGRESS: объект с ключами по deliveryType.
|
||||||
*/
|
*/
|
||||||
export const ADMIN_ORDER_TRANSITIONS = Object.freeze({
|
export const ADMIN_ORDER_TRANSITIONS = Object.freeze({
|
||||||
DRAFT: ['PENDING_PAYMENT', 'CANCELLED'],
|
DRAFT: ["PENDING_PAYMENT", "CANCELLED"],
|
||||||
PENDING_PAYMENT: ['PAID', 'CANCELLED'],
|
PENDING_PAYMENT: ["PAID", "CANCELLED"],
|
||||||
PAID: ['IN_PROGRESS', 'CANCELLED'],
|
PAID: ["IN_PROGRESS", "CANCELLED"],
|
||||||
IN_PROGRESS: Object.freeze({
|
IN_PROGRESS: Object.freeze({
|
||||||
delivery: ['SHIPPED', 'CANCELLED'],
|
delivery: ["SHIPPED", "CANCELLED"],
|
||||||
pickup: ['READY_FOR_PICKUP', 'CANCELLED'],
|
pickup: ["READY_FOR_PICKUP", "CANCELLED"],
|
||||||
}),
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export function getNextAdminStatuses(from, deliveryType) {
|
export function getNextAdminStatuses(from, deliveryType) {
|
||||||
const transition = ADMIN_ORDER_TRANSITIONS[from]
|
const transition = ADMIN_ORDER_TRANSITIONS[from];
|
||||||
if (!transition) return []
|
if (!transition) return [];
|
||||||
if (Array.isArray(transition)) return [...transition]
|
if (Array.isArray(transition)) return [...transition];
|
||||||
return transition[deliveryType] ? [...transition[deliveryType]] : []
|
return transition[deliveryType] ? [...transition[deliveryType]] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canTransitionAdminOrderStatus(order, next) {
|
export function canTransitionAdminOrderStatus(order, next) {
|
||||||
const from = order.status
|
const from = order.status;
|
||||||
if (from === next) return true
|
if (from === next) return true;
|
||||||
return getNextAdminStatuses(from, order.deliveryType).includes(next)
|
return getNextAdminStatuses(from, order.deliveryType).includes(next);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user