base commit
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { MainLayout } from '@/app/layout/MainLayout'
|
import { MainLayout } from '@/app/layout/MainLayout'
|
||||||
import { AppProviders } from '@/app/providers/AppProviders'
|
import { AppProviders } from '@/app/providers/AppProviders'
|
||||||
|
import { AboutPage } from '@/pages/about'
|
||||||
import { AdminLayoutPage } from '@/pages/admin-layout'
|
import { AdminLayoutPage } from '@/pages/admin-layout'
|
||||||
import { AuthCallbackPage, AuthPage } from '@/pages/auth'
|
import { AuthCallbackPage, AuthPage } from '@/pages/auth'
|
||||||
import { CartPage } from '@/pages/cart'
|
import { CartPage } from '@/pages/cart'
|
||||||
@@ -22,6 +23,7 @@ export function App() {
|
|||||||
<Route path="/auth/callback" element={<AuthCallbackPage />} />
|
<Route path="/auth/callback" element={<AuthCallbackPage />} />
|
||||||
<Route path="/cart" element={<CartPage />} />
|
<Route path="/cart" element={<CartPage />} />
|
||||||
<Route path="/checkout" element={<CheckoutPage />} />
|
<Route path="/checkout" element={<CheckoutPage />} />
|
||||||
|
<Route path="/about" element={<AboutPage />} />
|
||||||
<Route path="/info" element={<InfoPage />} />
|
<Route path="/info" element={<InfoPage />} />
|
||||||
<Route path="/me/*" element={<MeLayoutPage />} />
|
<Route path="/me/*" element={<MeLayoutPage />} />
|
||||||
<Route path="/products/:id" element={<ProductPage />} />
|
<Route path="/products/:id" element={<ProductPage />} />
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type NavItem = { label: string; to: string }
|
|||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ label: 'Каталог', to: '/' },
|
{ label: 'Каталог', to: '/' },
|
||||||
|
{ label: 'О нас', to: '/about' },
|
||||||
{ label: 'О покупке', to: '/info' },
|
{ label: 'О покупке', to: '/info' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function MainLayout({ children }: PropsWithChildren) {
|
|||||||
<Container maxWidth="lg">
|
<Container maxWidth="lg">
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid size={{ xs: 12, sm: 4 }}>
|
<Grid size={{ xs: 12, sm: 4 }}>
|
||||||
<Typography variant="subtitle1" fontWeight={700} gutterBottom>
|
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700 }}>
|
||||||
Магазин
|
Магазин
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
@@ -50,10 +50,13 @@ export function MainLayout({ children }: PropsWithChildren) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4 }}>
|
<Grid size={{ xs: 12, sm: 4 }}>
|
||||||
<Typography variant="subtitle1" fontWeight={700} gutterBottom>
|
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700 }}>
|
||||||
Покупателям
|
Покупателям
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
|
<Link component={RouterLink} to="/about" color="inherit" underline="hover" variant="body2">
|
||||||
|
О нас и самовывоз
|
||||||
|
</Link>
|
||||||
<Link component={RouterLink} to="/me" color="inherit" underline="hover" variant="body2">
|
<Link component={RouterLink} to="/me" color="inherit" underline="hover" variant="body2">
|
||||||
Личный кабинет
|
Личный кабинет
|
||||||
</Link>
|
</Link>
|
||||||
@@ -63,7 +66,7 @@ export function MainLayout({ children }: PropsWithChildren) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, sm: 4 }}>
|
<Grid size={{ xs: 12, sm: 4 }}>
|
||||||
<Typography variant="subtitle1" fontWeight={700} gutterBottom>
|
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 700 }}>
|
||||||
Контакты
|
Контакты
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={0.75}>
|
<Stack spacing={0.75}>
|
||||||
@@ -86,7 +89,11 @@ export function MainLayout({ children }: PropsWithChildren) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
<Typography variant="caption" color="text.secondary" display="block" textAlign={{ xs: 'left', sm: 'center' }}>
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ display: 'block', textAlign: { xs: 'left', sm: 'center' } }}
|
||||||
|
>
|
||||||
© {year} {STORE_NAME}. Сделано для демонстрации возможностей витрины.
|
© {year} {STORE_NAME}. Сделано для демонстрации возможностей витрины.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type AdminOrderListItem = {
|
|||||||
id: string
|
id: string
|
||||||
status: string
|
status: string
|
||||||
deliveryType: 'delivery' | 'pickup'
|
deliveryType: 'delivery' | 'pickup'
|
||||||
|
paymentMethod?: 'online' | 'on_pickup'
|
||||||
totalCents: number
|
totalCents: number
|
||||||
currency: string
|
currency: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
@@ -24,6 +25,7 @@ export type AdminOrderDetailResponse = {
|
|||||||
id: string
|
id: string
|
||||||
status: string
|
status: string
|
||||||
deliveryType: 'delivery' | 'pickup'
|
deliveryType: 'delivery' | 'pickup'
|
||||||
|
paymentMethod?: 'online' | 'on_pickup'
|
||||||
itemsSubtotalCents: number
|
itemsSubtotalCents: number
|
||||||
deliveryFeeCents: number
|
deliveryFeeCents: number
|
||||||
totalCents: number
|
totalCents: number
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ export type OrderListItem = {
|
|||||||
|
|
||||||
export type OrderListResponse = { items: OrderListItem[] }
|
export type OrderListResponse = { items: OrderListItem[] }
|
||||||
|
|
||||||
|
export type OrderPaymentMethod = 'online' | 'on_pickup'
|
||||||
|
|
||||||
export type OrderDetailResponse = {
|
export type OrderDetailResponse = {
|
||||||
item: {
|
item: {
|
||||||
id: string
|
id: string
|
||||||
status: string
|
status: string
|
||||||
deliveryType: 'delivery' | 'pickup'
|
deliveryType: 'delivery' | 'pickup'
|
||||||
|
paymentMethod?: OrderPaymentMethod
|
||||||
itemsSubtotalCents: number
|
itemsSubtotalCents: number
|
||||||
deliveryFeeCents: number
|
deliveryFeeCents: number
|
||||||
totalCents: number
|
totalCents: number
|
||||||
@@ -43,6 +46,7 @@ export type OrderDetailResponse = {
|
|||||||
|
|
||||||
export async function createOrder(body: {
|
export async function createOrder(body: {
|
||||||
deliveryType: 'delivery' | 'pickup'
|
deliveryType: 'delivery' | 'pickup'
|
||||||
|
paymentMethod?: OrderPaymentMethod
|
||||||
addressId?: string | null
|
addressId?: string | null
|
||||||
comment?: string | null
|
comment?: string | null
|
||||||
}): Promise<{ orderId: string }> {
|
}): Promise<{ orderId: string }> {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AboutPage } from './ui/AboutPage'
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Paper from '@mui/material/Paper'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import * as maplibregl from 'maplibre-gl'
|
||||||
|
import Map, { Marker } from 'react-map-gl/maplibre'
|
||||||
|
import { PICKUP_ADDRESS_FULL, PICKUP_COORDINATES } from '@/shared/constants/pickup-point'
|
||||||
|
|
||||||
|
const rasterStyle = {
|
||||||
|
version: 8 as const,
|
||||||
|
sources: {
|
||||||
|
osm: {
|
||||||
|
type: 'raster' as const,
|
||||||
|
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{ id: 'osm', type: 'raster' as const, source: 'osm' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AboutPage() {
|
||||||
|
const { lat, lng } = PICKUP_COORDINATES
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
О нас
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Магазин изделий ручной работы. Мы отвечаем за качество и сроки изготовления всего, что вы видите в каталоге.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Контакты и самовывоз
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ mb: 1 }}>
|
||||||
|
Забрать заказ можно по адресу самовывоза (координаты указаны на карте ниже):
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ whiteSpace: 'pre-wrap', fontWeight: 600 }}>{PICKUP_ADDRESS_FULL}</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2" sx={{ mt: 1 }}>
|
||||||
|
Перед визитом мы свяжемся с вами и согласуем время — чтобы заказ точно был готов к выдаче.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: 380,
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Map
|
||||||
|
mapLib={maplibregl}
|
||||||
|
initialViewState={{ latitude: lat, longitude: lng, zoom: 16 }}
|
||||||
|
style={{ width: '100%', height: 380 }}
|
||||||
|
mapStyle={rasterStyle}
|
||||||
|
scrollZoom={false}
|
||||||
|
dragRotate={false}
|
||||||
|
dragPan={false}
|
||||||
|
doubleClickZoom={false}
|
||||||
|
keyboard={false}
|
||||||
|
touchZoomRotate={false}
|
||||||
|
>
|
||||||
|
<Marker longitude={lng} latitude={lat} anchor="bottom">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: 2,
|
||||||
|
borderColor: 'background.paper',
|
||||||
|
boxShadow: 3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
</Map>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import { formatPriceRub } from '@/shared/lib/format-price'
|
|||||||
import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status'
|
import { groupOrdersByStatus } from '@/shared/lib/group-orders-by-status'
|
||||||
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
||||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||||
|
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||||
|
|
||||||
@@ -207,6 +208,10 @@ export function AdminOrdersPage() {
|
|||||||
#{detail.id.slice(-8)} · {detail.user.email} · {orderStatusLabelRu(detail.status)} ·{' '}
|
#{detail.id.slice(-8)} · {detail.user.email} · {orderStatusLabelRu(detail.status)} ·{' '}
|
||||||
{formatPriceRub(detail.totalCents)}
|
{formatPriceRub(detail.totalCents)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Получение: {detail.deliveryType === 'pickup' ? 'самовывоз' : 'доставка'}
|
||||||
|
{(detail.paymentMethod ?? 'online') === 'on_pickup' ? ' · оплата при получении' : ' · онлайн-оплата'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: { sm: 'center' } }}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: { sm: 'center' } }}>
|
||||||
<FormControl size="small" sx={{ minWidth: 240 }}>
|
<FormControl size="small" sx={{ minWidth: 240 }}>
|
||||||
@@ -240,25 +245,13 @@ export function AdminOrdersPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={1} sx={{ mb: 1 }}>
|
<Stack spacing={1} sx={{ mb: 1 }}>
|
||||||
{detail.messages.map((m) => (
|
{detail.messages.map((m) => (
|
||||||
<Box
|
<ChatMessageBubble key={m.id} authorType={m.authorType === 'admin' ? 'user' : 'admin'}>
|
||||||
key={m.id}
|
|
||||||
sx={{
|
|
||||||
p: 1.25,
|
|
||||||
border: 1,
|
|
||||||
borderColor: 'divider',
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: m.authorType === 'admin' ? 'primary.50' : 'grey.100',
|
|
||||||
alignSelf: m.authorType === 'admin' ? 'flex-end' : 'flex-start',
|
|
||||||
width: 'fit-content',
|
|
||||||
maxWidth: '85%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{m.authorType === 'admin' ? 'Админ (вы)' : 'Пользователь'} ·{' '}
|
{m.authorType === 'admin' ? 'Админ (вы)' : 'Пользователь'} ·{' '}
|
||||||
{new Date(m.createdAt).toLocaleString()}
|
{new Date(m.createdAt).toLocaleString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<RichTextMessageContent value={m.text} tone="chat" />
|
<RichTextMessageContent value={m.text} tone="chat" />
|
||||||
</Box>
|
</ChatMessageBubble>
|
||||||
))}
|
))}
|
||||||
{detail.messages.length === 0 && <Typography color="text.secondary">Нет сообщений.</Typography>}
|
{detail.messages.length === 0 && <Typography color="text.secondary">Нет сообщений.</Typography>}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Divider from '@mui/material/Divider'
|
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
@@ -11,7 +10,6 @@ import { useUnit } from 'effector-react'
|
|||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { apiClient } from '@/shared/api/client'
|
import { apiClient } from '@/shared/api/client'
|
||||||
import { oauthAuthorizeUrl } from '@/shared/lib/oauth-authorize-url'
|
|
||||||
import { $user, tokenSet } from '@/shared/model/auth'
|
import { $user, tokenSet } from '@/shared/model/auth'
|
||||||
|
|
||||||
type AuthResponse = { token: string; user: { id: string; email: string; name?: string | null; phone?: string | null } }
|
type AuthResponse = { token: string; user: { id: string; email: string; name?: string | null; phone?: string | null } }
|
||||||
@@ -93,21 +91,9 @@ export function AuthPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack spacing={2} sx={{ maxWidth: 520 }}>
|
<Stack spacing={2} sx={{ maxWidth: 520 }}>
|
||||||
<Typography variant="subtitle1">Быстрый вход</Typography>
|
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
|
|
||||||
<Button component="a" href={oauthAuthorizeUrl('vk')} variant="outlined" fullWidth>
|
|
||||||
Войти через VK
|
|
||||||
</Button>
|
|
||||||
<Button component="a" href={oauthAuthorizeUrl('yandex')} variant="outlined" fullWidth>
|
|
||||||
Войти через Яндекс
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Divider>или по email</Divider>
|
|
||||||
|
|
||||||
<TextField label="Email" {...register('email')} fullWidth />
|
<TextField label="Email" {...register('email')} fullWidth />
|
||||||
|
|
||||||
<Typography variant="h6">Вариант 1: Email + код</Typography>
|
<Typography variant="h6">Email + код</Typography>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||||
<Button variant="outlined" onClick={() => requestCode.mutate()} disabled={!email || requestCode.isPending}>
|
<Button variant="outlined" onClick={() => requestCode.mutate()} disabled={!email || requestCode.isPending}>
|
||||||
Отправить код
|
Отправить код
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import FormControl from '@mui/material/FormControl'
|
import FormControl from '@mui/material/FormControl'
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
import InputLabel from '@mui/material/InputLabel'
|
import InputLabel from '@mui/material/InputLabel'
|
||||||
|
import Link from '@mui/material/Link'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import Radio from '@mui/material/Radio'
|
||||||
|
import RadioGroup from '@mui/material/RadioGroup'
|
||||||
import Select from '@mui/material/Select'
|
import Select from '@mui/material/Select'
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
@@ -23,6 +27,7 @@ export function CheckoutPage() {
|
|||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery')
|
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery')
|
||||||
|
const [pickupPayment, setPickupPayment] = useState<'online' | 'on_pickup'>('online')
|
||||||
const [addressId, setAddressId] = useState('')
|
const [addressId, setAddressId] = useState('')
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
|
|
||||||
@@ -45,6 +50,7 @@ export function CheckoutPage() {
|
|||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
createOrder({
|
createOrder({
|
||||||
deliveryType,
|
deliveryType,
|
||||||
|
paymentMethod: deliveryType === 'delivery' ? 'online' : pickupPayment,
|
||||||
addressId: deliveryType === 'delivery' ? selectedAddressId : null,
|
addressId: deliveryType === 'delivery' ? selectedAddressId : null,
|
||||||
comment: comment.trim() || null,
|
comment: comment.trim() || null,
|
||||||
}),
|
}),
|
||||||
@@ -134,7 +140,10 @@ export function CheckoutPage() {
|
|||||||
value={deliveryType}
|
value={deliveryType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v = String(e.target.value)
|
const v = String(e.target.value)
|
||||||
if (v === 'delivery' || v === 'pickup') setDeliveryType(v)
|
if (v === 'delivery' || v === 'pickup') {
|
||||||
|
setDeliveryType(v)
|
||||||
|
if (v === 'delivery') setPickupPayment('online')
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value="delivery">Доставка</MenuItem>
|
<MenuItem value="delivery">Доставка</MenuItem>
|
||||||
@@ -183,7 +192,35 @@ export function CheckoutPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{deliveryType === 'pickup' && (
|
{deliveryType === 'pickup' && (
|
||||||
<Alert severity="info">Самовывоз: адрес доставки не нужен. Мы свяжемся с вами для согласования.</Alert>
|
<>
|
||||||
|
<Alert severity="info" sx={{ mb: 0 }}>
|
||||||
|
Самовывоз: адрес доставки не нужен. Адрес точки выдачи указан на странице{' '}
|
||||||
|
<Link component={RouterLink} to="/about" underline="hover" sx={{ fontWeight: 700 }}>
|
||||||
|
О нас
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Alert>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" gutterBottom sx={{ mt: 0.5 }}>
|
||||||
|
Оплата
|
||||||
|
</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
value={pickupPayment}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = String(e.target.value)
|
||||||
|
if (v === 'online' || v === 'on_pickup') setPickupPayment(v)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControlLabel value="online" control={<Radio size="small" />} label="Онлайн-оплата" />
|
||||||
|
<FormControlLabel value="on_pickup" control={<Radio size="small" />} label="Оплатить при получении" />
|
||||||
|
</RadioGroup>
|
||||||
|
{pickupPayment === 'on_pickup' && (
|
||||||
|
<Alert severity="info" sx={{ mt: 1 }}>
|
||||||
|
Оплатите заказ наличными или картой при выдаче на самовывозе.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -371,12 +371,7 @@ export function HomePage() {
|
|||||||
product={p}
|
product={p}
|
||||||
mediaHeight={mediaHeight}
|
mediaHeight={mediaHeight}
|
||||||
actions={
|
actions={
|
||||||
!isAdmin ? (
|
!isAdmin && !(p.inStock && p.quantity === 0) ? <ToggleCartIcon productId={p.id} /> : undefined
|
||||||
<ToggleCartIcon
|
|
||||||
productId={p.id}
|
|
||||||
disabledReason={p.inStock && p.quantity === 0 ? 'Нет в наличии' : null}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Link as RouterLink } from 'react-router-dom'
|
|||||||
import { fetchMyOrder, postOrderMessage } from '@/entities/order/api/order-api'
|
import { fetchMyOrder, postOrderMessage } from '@/entities/order/api/order-api'
|
||||||
import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
import { fetchMyConversations, markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
||||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||||
|
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||||
|
|
||||||
@@ -114,13 +115,15 @@ export function MessagesPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
secondaryTypographyProps={{
|
slotProps={{
|
||||||
sx: {
|
secondary: {
|
||||||
mt: 0.5,
|
sx: {
|
||||||
overflow: 'hidden',
|
mt: 0.5,
|
||||||
display: '-webkit-box',
|
overflow: 'hidden',
|
||||||
WebkitLineClamp: 2,
|
display: '-webkit-box',
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
secondary={c.preview}
|
secondary={c.preview}
|
||||||
@@ -160,24 +163,12 @@ export function MessagesPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack spacing={1} sx={{ mb: 2, maxHeight: 360, overflow: 'auto' }}>
|
<Stack spacing={1} sx={{ mb: 2, maxHeight: 360, overflow: 'auto' }}>
|
||||||
{order.messages.map((m) => (
|
{order.messages.map((m) => (
|
||||||
<Box
|
<ChatMessageBubble key={m.id} authorType={m.authorType === 'admin' ? 'admin' : 'user'}>
|
||||||
key={m.id}
|
|
||||||
sx={{
|
|
||||||
p: 1.25,
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: m.authorType === 'admin' ? 'grey.100' : 'primary.50',
|
|
||||||
border: 1,
|
|
||||||
borderColor: 'divider',
|
|
||||||
alignSelf: m.authorType === 'admin' ? 'flex-start' : 'flex-end',
|
|
||||||
width: 'fit-content',
|
|
||||||
maxWidth: '85%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
|
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<RichTextMessageContent value={m.text} tone="chat" />
|
<RichTextMessageContent value={m.text} tone="chat" />
|
||||||
</Box>
|
</ChatMessageBubble>
|
||||||
))}
|
))}
|
||||||
{order.messages.length === 0 && <Typography color="text.secondary">Нет сообщений.</Typography>}
|
{order.messages.length === 0 && <Typography color="text.secondary">Нет сообщений.</Typography>}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import DialogActions from '@mui/material/DialogActions'
|
|||||||
import DialogContent from '@mui/material/DialogContent'
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Divider from '@mui/material/Divider'
|
import Divider from '@mui/material/Divider'
|
||||||
|
import Link from '@mui/material/Link'
|
||||||
import Rating from '@mui/material/Rating'
|
import Rating from '@mui/material/Rating'
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
@@ -22,8 +23,10 @@ import {
|
|||||||
} from '@/entities/order/api/order-api'
|
} from '@/entities/order/api/order-api'
|
||||||
import { postProductReview, uploadReviewImage } from '@/entities/product/api/reviews-api'
|
import { postProductReview, uploadReviewImage } from '@/entities/product/api/reviews-api'
|
||||||
import { markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
import { markOrderMessagesRead } from '@/entities/user/api/messages-api'
|
||||||
|
import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point'
|
||||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||||
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
||||||
|
import { ChatMessageBubble } from '@/shared/ui/ChatMessageBubble'
|
||||||
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
import { RichTextMessageContent } from '@/shared/ui/RichTextMessageContent'
|
||||||
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
import { RichTextMessageEditor } from '@/shared/ui/RichTextMessageEditor'
|
||||||
|
|
||||||
@@ -96,6 +99,7 @@ export function OrderDetailPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const order = orderQuery.data?.item
|
const order = orderQuery.data?.item
|
||||||
|
const payOnPickup = (order?.paymentMethod ?? 'online') === 'on_pickup'
|
||||||
const canSendMessage = text.replace(/<[^>]*>/g, ' ').trim().length > 0
|
const canSendMessage = text.replace(/<[^>]*>/g, ' ').trim().length > 0
|
||||||
|
|
||||||
const eligibilityQuery = useQuery({
|
const eligibilityQuery = useQuery({
|
||||||
@@ -199,6 +203,7 @@ export function OrderDetailPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
||||||
Способ: {order.deliveryType === 'pickup' ? 'Самовывоз' : 'Доставка'}
|
Способ: {order.deliveryType === 'pickup' ? 'Самовывоз' : 'Доставка'}
|
||||||
|
{order.deliveryType === 'pickup' && <> · оплата: {payOnPickup ? 'при получении' : 'онлайн'}</>}
|
||||||
</Typography>
|
</Typography>
|
||||||
{order.deliveryType === 'delivery' && (
|
{order.deliveryType === 'delivery' && (
|
||||||
<>
|
<>
|
||||||
@@ -220,9 +225,21 @@ export function OrderDetailPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{order.deliveryType === 'pickup' && (
|
{order.deliveryType === 'pickup' && (
|
||||||
<Typography color="text.secondary">
|
<Stack spacing={0.75}>
|
||||||
Адрес доставки не требуется. Мы свяжемся для согласования самовывоза.
|
<Typography color="text.secondary" variant="body2">
|
||||||
</Typography>
|
Адрес самовывоза и карта — на странице{' '}
|
||||||
|
<Link component={RouterLink} to="/about" underline="hover">
|
||||||
|
О нас
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{PICKUP_ADDRESS_FULL}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2">
|
||||||
|
Заберите заказ точно ко времени, которое согласуем по телефону или в чате заказа.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{order.comment && (
|
{order.comment && (
|
||||||
<Typography color="text.secondary" variant="body2" sx={{ mt: 1 }}>
|
<Typography color="text.secondary" variant="body2" sx={{ mt: 1 }}>
|
||||||
@@ -235,25 +252,33 @@ export function OrderDetailPage() {
|
|||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Оплата
|
Оплата
|
||||||
</Typography>
|
</Typography>
|
||||||
{order.status === 'PENDING_PAYMENT' && (
|
{payOnPickup ? (
|
||||||
<>
|
|
||||||
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
|
||||||
Пока это заглушка. После нажатия заказ перейдёт в статус «Проверка оплаты».
|
|
||||||
</Typography>
|
|
||||||
<Button variant="contained" onClick={() => payMut.mutate()} disabled={payMut.isPending}>
|
|
||||||
Оплатить (заглушка)
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{order.status === 'PAYMENT_VERIFICATION' && (
|
|
||||||
<Typography color="info.main" variant="body2">
|
|
||||||
Оплата отправлена на проверку. Мы проверим поступление и обновим статус.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{!['PENDING_PAYMENT', 'PAYMENT_VERIFICATION'].includes(order.status) && (
|
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography color="text.secondary" variant="body2">
|
||||||
На этом этапе действий по оплате в этом блоке не требуется.
|
Оплата при получении на точке самовывоза (наличные или карта — по договорённости).
|
||||||
</Typography>
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{order.status === 'PENDING_PAYMENT' && (
|
||||||
|
<>
|
||||||
|
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
|
||||||
|
Пока это заглушка. После нажатия заказ перейдёт в статус «Проверка оплаты».
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" onClick={() => payMut.mutate()} disabled={payMut.isPending}>
|
||||||
|
Оплатить (заглушка)
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{order.status === 'PAYMENT_VERIFICATION' && (
|
||||||
|
<Typography color="info.main" variant="body2">
|
||||||
|
Оплата отправлена на проверку. Мы проверим поступление и обновим статус.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{!['PENDING_PAYMENT', 'PAYMENT_VERIFICATION'].includes(order.status) && (
|
||||||
|
<Typography color="text.secondary" variant="body2">
|
||||||
|
На этом этапе действий по оплате в этом блоке не требуется.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -316,24 +341,12 @@ export function OrderDetailPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={1} sx={{ mb: 2 }}>
|
<Stack spacing={1} sx={{ mb: 2 }}>
|
||||||
{order.messages.map((m) => (
|
{order.messages.map((m) => (
|
||||||
<Box
|
<ChatMessageBubble key={m.id} authorType={m.authorType === 'admin' ? 'admin' : 'user'}>
|
||||||
key={m.id}
|
|
||||||
sx={{
|
|
||||||
p: 1.25,
|
|
||||||
borderRadius: 2,
|
|
||||||
bgcolor: m.authorType === 'admin' ? 'grey.100' : 'primary.50',
|
|
||||||
border: 1,
|
|
||||||
borderColor: 'divider',
|
|
||||||
alignSelf: m.authorType === 'admin' ? 'flex-start' : 'flex-end',
|
|
||||||
width: 'fit-content',
|
|
||||||
maxWidth: '85%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
|
{m.authorType === 'admin' ? 'Админ' : 'Вы'} · {new Date(m.createdAt).toLocaleString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<RichTextMessageContent value={m.text} tone="chat" />
|
<RichTextMessageContent value={m.text} tone="chat" />
|
||||||
</Box>
|
</ChatMessageBubble>
|
||||||
))}
|
))}
|
||||||
{order.messages.length === 0 && <Typography color="text.secondary">Пока сообщений нет.</Typography>}
|
{order.messages.length === 0 && <Typography color="text.secondary">Пока сообщений нет.</Typography>}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export function ProductPage() {
|
|||||||
{formatPriceRub(p.priceCents)}
|
{formatPriceRub(p.priceCents)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{!isAdmin && <ToggleCartIcon productId={p.id} size="medium" />}
|
{!isAdmin && !(p.inStock && p.quantity === 0) ? <ToggleCartIcon productId={p.id} size="medium" /> : null}
|
||||||
|
|
||||||
{!p.inStock && (
|
{!p.inStock && (
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** Точка самовывоза (координаты центра участка ул. Мира по данным OSM/Nominatim). */
|
||||||
|
export const PICKUP_COORDINATES = { lat: 58.0994284, lng: 57.803296 }
|
||||||
|
|
||||||
|
/** Полная строка адреса для текстовых блоков. */
|
||||||
|
export const PICKUP_ADDRESS_FULL =
|
||||||
|
'34, улица Мира, Лысьва, Лысьвенский муниципальный округ, Пермский край, Приволжский федеральный округ, 618909, Россия'
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import { alpha } from '@mui/material/styles'
|
||||||
|
|
||||||
|
type Author = 'admin' | 'user'
|
||||||
|
|
||||||
|
export function ChatMessageBubble(props: { authorType: Author; children: ReactNode }) {
|
||||||
|
const { authorType, children } = props
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 1.25,
|
||||||
|
borderRadius: 2,
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
alignSelf: authorType === 'admin' ? 'flex-start' : 'flex-end',
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: '85%',
|
||||||
|
color: 'text.primary',
|
||||||
|
bgcolor: (theme) =>
|
||||||
|
authorType === 'admin'
|
||||||
|
? alpha(theme.palette.grey[500], theme.palette.mode === 'dark' ? 0.28 : 0.14)
|
||||||
|
: alpha(theme.palette.primary.main, theme.palette.mode === 'dark' ? 0.28 : 0.1),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,16 +21,18 @@ export function RichTextMessageContent({ value, tone = 'default' }: RichTextMess
|
|||||||
if (!editor) return
|
if (!editor) return
|
||||||
const normalizedValue = value.trim() ? value : '<p></p>'
|
const normalizedValue = value.trim() ? value : '<p></p>'
|
||||||
if (editor.getHTML() === normalizedValue) return
|
if (editor.getHTML() === normalizedValue) return
|
||||||
editor.commands.setContent(normalizedValue, false)
|
editor.commands.setContent(normalizedValue, { emitUpdate: false })
|
||||||
}, [editor, value])
|
}, [editor, value])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
...(tone === 'chat' ? { color: 'text.primary' } : {}),
|
||||||
'& .ProseMirror': {
|
'& .ProseMirror': {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
|
...(tone === 'chat' ? { color: 'inherit' } : {}),
|
||||||
...(tone === 'review'
|
...(tone === 'review'
|
||||||
? {
|
? {
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function RichTextMessageEditor({
|
|||||||
if (!editor) return
|
if (!editor) return
|
||||||
const normalizedValue = value.trim() ? value : '<p></p>'
|
const normalizedValue = value.trim() ? value : '<p></p>'
|
||||||
if (editor.getHTML() === normalizedValue) return
|
if (editor.getHTML() === normalizedValue) return
|
||||||
editor.commands.setContent(normalizedValue, false)
|
editor.commands.setContent(normalizedValue, { emitUpdate: false })
|
||||||
}, [editor, value])
|
}, [editor, value])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Order" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
||||||
|
"deliveryType" TEXT NOT NULL DEFAULT 'delivery',
|
||||||
|
"paymentMethod" TEXT NOT NULL DEFAULT 'online',
|
||||||
|
"itemsSubtotalCents" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"deliveryFeeCents" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"totalCents" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"currency" TEXT NOT NULL DEFAULT 'RUB',
|
||||||
|
"addressSnapshotJson" TEXT,
|
||||||
|
"comment" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Order" ("addressSnapshotJson", "comment", "createdAt", "currency", "deliveryFeeCents", "deliveryType", "id", "itemsSubtotalCents", "status", "totalCents", "updatedAt", "userId") SELECT "addressSnapshotJson", "comment", "createdAt", "currency", "deliveryFeeCents", "deliveryType", "id", "itemsSubtotalCents", "status", "totalCents", "updatedAt", "userId" FROM "Order";
|
||||||
|
DROP TABLE "Order";
|
||||||
|
ALTER TABLE "new_Order" RENAME TO "Order";
|
||||||
|
CREATE INDEX "Order_userId_createdAt_idx" ON "Order"("userId", "createdAt");
|
||||||
|
CREATE INDEX "Order_status_updatedAt_idx" ON "Order"("status", "updatedAt");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -109,6 +109,8 @@ model Order {
|
|||||||
status String @default("DRAFT")
|
status String @default("DRAFT")
|
||||||
/// 'delivery' | 'pickup'
|
/// 'delivery' | 'pickup'
|
||||||
deliveryType String @default("delivery")
|
deliveryType String @default("delivery")
|
||||||
|
/// 'online' | 'on_pickup' — способ расчёта для заказа
|
||||||
|
paymentMethod String @default("online")
|
||||||
itemsSubtotalCents Int @default(0)
|
itemsSubtotalCents Int @default(0)
|
||||||
deliveryFeeCents Int @default(0)
|
deliveryFeeCents Int @default(0)
|
||||||
totalCents Int @default(0)
|
totalCents Int @default(0)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export async function registerAdminOrderRoutes(fastify) {
|
|||||||
id: o.id,
|
id: o.id,
|
||||||
status: o.status,
|
status: o.status,
|
||||||
deliveryType: o.deliveryType,
|
deliveryType: o.deliveryType,
|
||||||
|
paymentMethod: o.paymentMethod,
|
||||||
totalCents: o.totalCents,
|
totalCents: o.totalCents,
|
||||||
currency: o.currency,
|
currency: o.currency,
|
||||||
createdAt: o.createdAt,
|
createdAt: o.createdAt,
|
||||||
|
|||||||
@@ -420,10 +420,23 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const commentRaw = request.body?.comment
|
const commentRaw = request.body?.comment
|
||||||
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
||||||
|
|
||||||
|
const paymentMethodRaw = request.body?.paymentMethod
|
||||||
|
const paymentMethod =
|
||||||
|
paymentMethodRaw === undefined || paymentMethodRaw === null || paymentMethodRaw === ''
|
||||||
|
? 'online'
|
||||||
|
: String(paymentMethodRaw).trim()
|
||||||
|
if (paymentMethod !== 'online' && paymentMethod !== 'on_pickup') {
|
||||||
|
return reply.code(400).send({ error: 'paymentMethod должен быть online | on_pickup' })
|
||||||
|
}
|
||||||
|
|
||||||
if (deliveryType !== 'delivery' && deliveryType !== 'pickup') {
|
if (deliveryType !== 'delivery' && deliveryType !== 'pickup') {
|
||||||
return reply.code(400).send({ error: 'deliveryType должен быть delivery | pickup' })
|
return reply.code(400).send({ error: 'deliveryType должен быть delivery | pickup' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paymentMethod === 'on_pickup' && deliveryType !== 'pickup') {
|
||||||
|
return reply.code(400).send({ error: 'Оплата при получении доступна только для самовывоза' })
|
||||||
|
}
|
||||||
|
|
||||||
let address = null
|
let address = null
|
||||||
if (deliveryType === 'delivery') {
|
if (deliveryType === 'delivery') {
|
||||||
if (!addressId) return reply.code(400).send({ error: 'Выберите адрес доставки' })
|
if (!addressId) return reply.code(400).send({ error: 'Выберите адрес доставки' })
|
||||||
@@ -472,6 +485,8 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
lng: address.lng,
|
lng: address.lng,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initialStatus = paymentMethod === 'on_pickup' ? 'IN_PROGRESS' : 'PENDING_PAYMENT'
|
||||||
|
|
||||||
let created
|
let created
|
||||||
try {
|
try {
|
||||||
created = await prisma.$transaction(async (tx) => {
|
created = await prisma.$transaction(async (tx) => {
|
||||||
@@ -491,8 +506,9 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const order = await tx.order.create({
|
const order = await tx.order.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
status: 'PENDING_PAYMENT',
|
status: initialStatus,
|
||||||
deliveryType,
|
deliveryType,
|
||||||
|
paymentMethod,
|
||||||
itemsSubtotalCents,
|
itemsSubtotalCents,
|
||||||
deliveryFeeCents,
|
deliveryFeeCents,
|
||||||
totalCents,
|
totalCents,
|
||||||
@@ -678,6 +694,12 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const order = await prisma.order.findFirst({ where: { id, userId } })
|
const order = await prisma.order.findFirst({ where: { id, userId } })
|
||||||
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
|
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
|
||||||
|
|
||||||
|
const paymentMethod = order.paymentMethod ?? 'online'
|
||||||
|
if (paymentMethod === 'on_pickup') {
|
||||||
|
return reply.code(409).send({ error: 'Для этого заказа оплата при получении — кнопка оплаты не нужна.' })
|
||||||
|
}
|
||||||
|
|
||||||
let nextStatus = order.status
|
let nextStatus = order.status
|
||||||
if (order.status === 'DRAFT') {
|
if (order.status === 'DRAFT') {
|
||||||
await prisma.order.update({ where: { id }, data: { status: 'PENDING_PAYMENT' } })
|
await prisma.order.update({ where: { id }, data: { status: 'PENDING_PAYMENT' } })
|
||||||
|
|||||||
Reference in New Issue
Block a user