test commit
This commit is contained in:
@@ -2,13 +2,11 @@ import { lazy, Suspense } from 'react'
|
|||||||
import { Route, Routes } from 'react-router-dom'
|
import { Route, Routes } from 'react-router-dom'
|
||||||
import { MainLayout } from '@/app/layout/MainLayout'
|
import { MainLayout } from '@/app/layout/MainLayout'
|
||||||
import { AboutPage } from '@/pages/about'
|
import { AboutPage } from '@/pages/about'
|
||||||
// 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'
|
||||||
import { CheckoutPage } from '@/pages/checkout'
|
import { CheckoutPage } from '@/pages/checkout'
|
||||||
import { HomePage } from '@/pages/home'
|
import { HomePage } from '@/pages/home'
|
||||||
import { InfoPage } from '@/pages/info'
|
import { InfoPage } from '@/pages/info'
|
||||||
// import { MeLayoutPage } from '@/pages/me'
|
|
||||||
import { NotFoundPage } from '@/pages/not-found'
|
import { NotFoundPage } from '@/pages/not-found'
|
||||||
import { PrivacyPolicyPage } from '@/pages/privacy-policy'
|
import { PrivacyPolicyPage } from '@/pages/privacy-policy'
|
||||||
import { ProductPage } from '@/pages/product'
|
import { ProductPage } from '@/pages/product'
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type { CartItem } from './model/types'
|
||||||
|
export { fetchMyCart, addToCart, setCartQty, removeCartItem } from './api/cart-api'
|
||||||
|
export type { CartResponse } from './api/cart-api'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
fetchUserNotificationSettings,
|
||||||
|
updateUserNotificationSettings,
|
||||||
|
fetchAdminNotificationSettings,
|
||||||
|
updateAdminNotificationSettings,
|
||||||
|
} from './api/notifications-api'
|
||||||
|
export type { UserNotificationSettings, AdminNotificationSettings } from './api/notifications-api'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export {
|
||||||
|
fetchMyOrders,
|
||||||
|
createOrder,
|
||||||
|
confirmOrderReceived,
|
||||||
|
fetchMyOrder,
|
||||||
|
fetchOrderReviewEligibility,
|
||||||
|
} from './api/order-api'
|
||||||
|
export { createOrderPayment, getOrderPaymentStatus, postOrderMessage } from './api/order-api'
|
||||||
|
export type { OrderListResponse, OrderDetailResponse } from './api/order-api'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { fetchPublicProducts, fetchPublicProduct, fetchCategories } from './api/product-api'
|
||||||
|
export type { PublicProductsResponse } from './api/product-api'
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export {
|
||||||
|
postProductReview,
|
||||||
|
uploadReviewImage,
|
||||||
|
fetchLatestApprovedReviews,
|
||||||
|
fetchPublicProductReviews,
|
||||||
|
} from './api/reviews-api'
|
||||||
|
export type {
|
||||||
|
PublicReviewFeedItem,
|
||||||
|
PublicReviewsLatestResponse,
|
||||||
|
PublicProductReviewItem,
|
||||||
|
PublicProductReviewsResponse,
|
||||||
|
} from './api/reviews-api'
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type { AdminUser, ShippingAddress } from './model/types'
|
||||||
|
export { fetchAdminUsers, createAdminUser, updateAdminUser, deleteAdminUser } from './api/user-api'
|
||||||
|
export type { AdminUsersListResponse } from './api/user-api'
|
||||||
|
export {
|
||||||
|
fetchMyAddresses,
|
||||||
|
createMyAddress,
|
||||||
|
updateMyAddress,
|
||||||
|
deleteMyAddress,
|
||||||
|
setMyAddressDefault,
|
||||||
|
} from './api/address-api'
|
||||||
@@ -60,7 +60,7 @@ export function AdminLayoutPage() {
|
|||||||
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
|
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
|
||||||
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
||||||
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
|
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
|
||||||
{ to: '/admin/notifications', label: 'Оповещения', icon: <Bell /> },
|
{ to: '/admin/notifications', label: 'Уведомления', icon: <Bell /> },
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ export function AdminNotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Оповещения
|
Уведомления
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||||
Настройка оповещений администратора.
|
Настройка уведомлений администратора.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function MeLayoutPage() {
|
|||||||
{ to: '/me/messages', label: 'Сообщения', icon: <MessageCircle /> },
|
{ to: '/me/messages', label: 'Сообщения', icon: <MessageCircle /> },
|
||||||
{ to: '/me/settings', label: 'Настройки', icon: <Settings /> },
|
{ to: '/me/settings', label: 'Настройки', icon: <Settings /> },
|
||||||
{ to: '/me/addresses', label: 'Адреса доставки', icon: <MapPin /> },
|
{ to: '/me/addresses', label: 'Адреса доставки', icon: <MapPin /> },
|
||||||
{ to: '/me/notifications', label: 'Оповещения', icon: <Bell /> },
|
{ to: '/me/notifications', label: 'Уведомления', icon: <Bell /> },
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Divider from '@mui/material/Divider'
|
|
||||||
import Stack from '@mui/material/Stack'
|
|
||||||
import TextField from '@mui/material/TextField'
|
|
||||||
import Typography from '@mui/material/Typography'
|
|
||||||
import { useUnit } from 'effector-react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import {
|
|
||||||
$requestEmailChangeCodeError,
|
|
||||||
$updateProfileError,
|
|
||||||
$user,
|
|
||||||
$verifyEmailChangeError,
|
|
||||||
requestEmailChangeCodeFx,
|
|
||||||
updateProfileFx,
|
|
||||||
verifyEmailChangeFx,
|
|
||||||
} from '@/shared/model/auth'
|
|
||||||
import type { AxiosError } from 'axios'
|
|
||||||
|
|
||||||
function getApiErrorMessage(error: unknown): string | null {
|
|
||||||
const e = error as AxiosError<{ error?: string }>
|
|
||||||
const msg = e?.response?.data?.error
|
|
||||||
return msg ? String(msg) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MePage() {
|
|
||||||
const user = useUnit($user)
|
|
||||||
const pendingEmailReq = useUnit(requestEmailChangeCodeFx.pending)
|
|
||||||
const pendingEmailVerify = useUnit(verifyEmailChangeFx.pending)
|
|
||||||
const pendingProfile = useUnit(updateProfileFx.pending)
|
|
||||||
const errorEmailReq = useUnit($requestEmailChangeCodeError)
|
|
||||||
const errorProfile = useUnit($updateProfileError)
|
|
||||||
const errorEmailVerify = useUnit($verifyEmailChangeError)
|
|
||||||
|
|
||||||
const emailForm = useForm<{ newEmail: string; code: string }>({
|
|
||||||
defaultValues: { newEmail: '', code: '' },
|
|
||||||
mode: 'onChange',
|
|
||||||
})
|
|
||||||
|
|
||||||
const profileForm = useForm<{ displayName: string }>({
|
|
||||||
defaultValues: { displayName: user?.displayName ? String(user.displayName) : '' },
|
|
||||||
mode: 'onChange',
|
|
||||||
})
|
|
||||||
|
|
||||||
const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify)
|
|
||||||
const profileErrorMsg = getApiErrorMessage(errorProfile)
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h4" gutterBottom>
|
|
||||||
Профиль
|
|
||||||
</Typography>
|
|
||||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
|
||||||
Текущая почта: <b>{user.email}</b>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{emailErrorMsg && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{emailErrorMsg}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{profileErrorMsg && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{profileErrorMsg}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack spacing={3} sx={{ maxWidth: 560 }}>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Имя / ник
|
|
||||||
</Typography>
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<TextField
|
|
||||||
label="Имя или ник"
|
|
||||||
helperText="До 40 символов"
|
|
||||||
slotProps={{ htmlInput: { maxLength: 40 } }}
|
|
||||||
{...profileForm.register('displayName')}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
disabled={pendingProfile}
|
|
||||||
onClick={() => {
|
|
||||||
const raw = profileForm.getValues('displayName')
|
|
||||||
const name = raw.trim()
|
|
||||||
updateProfileFx({ displayName: name.length ? name : null })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Сохранить имя
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Смена почты
|
|
||||||
</Typography>
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<TextField label="Новая почта" {...emailForm.register('newEmail')} />
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
disabled={!emailForm.watch('newEmail') || pendingEmailReq}
|
|
||||||
onClick={() => requestEmailChangeCodeFx(emailForm.getValues('newEmail').trim())}
|
|
||||||
>
|
|
||||||
Отправить код на новую почту
|
|
||||||
</Button>
|
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
|
||||||
<TextField label="Код (6 цифр)" inputMode="numeric" {...emailForm.register('code')} />
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
disabled={emailForm.watch('code').trim().length !== 6 || pendingEmailVerify}
|
|
||||||
onClick={() =>
|
|
||||||
verifyEmailChangeFx({
|
|
||||||
newEmail: emailForm.getValues('newEmail').trim(),
|
|
||||||
code: emailForm.getValues('code').trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Подтвердить
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -57,7 +57,7 @@ export function NotificationsPage() {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Оповещения
|
Уведомления
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||||
Настройте, какие уведомления вы хотите получать на почту.
|
Настройте, какие уведомления вы хотите получать на почту.
|
||||||
@@ -78,7 +78,7 @@ export function NotificationsPage() {
|
|||||||
onChange={(e) => handleToggle('globalEnabled', e.target.checked)}
|
onChange={(e) => handleToggle('globalEnabled', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={<Typography sx={{ fontWeight: 600 }}>Получать оповещения</Typography>}
|
label={<Typography sx={{ fontWeight: 600 }}>Получать уведомления</Typography>}
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ ml: 4 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ ml: 4 }}>
|
||||||
Включите, чтобы получать уведомления о заказах на почту.
|
Включите, чтобы получать уведомления о заказах на почту.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Paper from '@mui/material/Paper'
|
import Paper from '@mui/material/Paper'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { STORE_EMAIL, STORE_PUBLIC_SITE_URL } from '@/shared/config'
|
import { STORE_EMAIL, STORE_PHONE, STORE_PUBLIC_SITE_URL } from '@/shared/config'
|
||||||
|
|
||||||
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
const SITE_URL = STORE_PUBLIC_SITE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
||||||
|
|
||||||
@@ -138,8 +138,8 @@ const sections = [
|
|||||||
`ИНН: ${OP_INN}`,
|
`ИНН: ${OP_INN}`,
|
||||||
`ОГРН: ${OP_OGRN}`,
|
`ОГРН: ${OP_OGRN}`,
|
||||||
`Адрес: ${OP_ADDR}`,
|
`Адрес: ${OP_ADDR}`,
|
||||||
`Телефон: +7 (900) 000-00-00`, // TODO: заменить на реальный номер телефона
|
`Телефон: ${STORE_PHONE}`,
|
||||||
`Email: ${STORE_EMAIL}`, // TODO: заменить на реальный email при настройке STORE_EMAIL
|
`Email: ${STORE_EMAIL}`,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
@@ -6,14 +6,20 @@ import {
|
|||||||
import { prisma } from '../../lib/prisma.js'
|
import { prisma } from '../../lib/prisma.js'
|
||||||
|
|
||||||
export async function registerAdminCategoryRoutes(fastify) {
|
export async function registerAdminCategoryRoutes(fastify) {
|
||||||
fastify.get('/api/admin/categories', { preHandler: [fastify.verifyAdmin] }, async () => {
|
fastify.get('/api/admin/categories', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const items = await prisma.category.findMany({
|
const items = await prisma.category.findMany({
|
||||||
orderBy: [{ sort: 'asc' }, { name: 'asc' }],
|
orderBy: [{ sort: 'asc' }, { name: 'asc' }],
|
||||||
})
|
})
|
||||||
return { items }
|
return { items }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить категории' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/api/admin/categories', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
fastify.post('/api/admin/categories', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const body = request.body ?? {}
|
const body = request.body ?? {}
|
||||||
const name = String(body.name ?? '').trim()
|
const name = String(body.name ?? '').trim()
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -39,9 +45,14 @@ export async function registerAdminCategoryRoutes(fastify) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
reply.code(201).send(category)
|
reply.code(201).send(category)
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось создать категорию' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.patch('/api/admin/categories/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
fastify.patch('/api/admin/categories/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const body = request.body ?? {}
|
const body = request.body ?? {}
|
||||||
const existing = await prisma.category.findUnique({ where: { id } })
|
const existing = await prisma.category.findUnique({ where: { id } })
|
||||||
@@ -94,9 +105,14 @@ export async function registerAdminCategoryRoutes(fastify) {
|
|||||||
|
|
||||||
const updated = await prisma.category.update({ where: { id }, data })
|
const updated = await prisma.category.update({ where: { id }, data })
|
||||||
return updated
|
return updated
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось обновить категорию' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.delete('/api/admin/categories/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
fastify.delete('/api/admin/categories/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const existing = await prisma.category.findUnique({ where: { id } })
|
const existing = await prisma.category.findUnique({ where: { id } })
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -117,5 +133,9 @@ export async function registerAdminCategoryRoutes(fastify) {
|
|||||||
prisma.category.delete({ where: { id } }),
|
prisma.category.delete({ where: { id } }),
|
||||||
])
|
])
|
||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось удалить категорию' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { prisma } from '../../lib/prisma.js'
|
|||||||
const MAX_SLIDES = 20
|
const MAX_SLIDES = 20
|
||||||
|
|
||||||
export async function registerCatalogSliderRoutes(fastify) {
|
export async function registerCatalogSliderRoutes(fastify) {
|
||||||
fastify.get('/api/catalog-slider', async () => {
|
fastify.get('/api/catalog-slider', async (request, reply) => {
|
||||||
|
try {
|
||||||
const slides = await prisma.catalogSliderSlide.findMany({
|
const slides = await prisma.catalogSliderSlide.findMany({
|
||||||
orderBy: { sortOrder: 'asc' },
|
orderBy: { sortOrder: 'asc' },
|
||||||
include: { galleryImage: true },
|
include: { galleryImage: true },
|
||||||
@@ -15,9 +16,14 @@ export async function registerCatalogSliderRoutes(fastify) {
|
|||||||
caption: s.caption,
|
caption: s.caption,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить слайдер' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.get('/api/admin/catalog-slider', { preHandler: [fastify.verifyAdmin] }, async () => {
|
fastify.get('/api/admin/catalog-slider', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const slides = await prisma.catalogSliderSlide.findMany({
|
const slides = await prisma.catalogSliderSlide.findMany({
|
||||||
orderBy: { sortOrder: 'asc' },
|
orderBy: { sortOrder: 'asc' },
|
||||||
include: { galleryImage: true },
|
include: { galleryImage: true },
|
||||||
@@ -30,9 +36,14 @@ export async function registerCatalogSliderRoutes(fastify) {
|
|||||||
caption: s.caption,
|
caption: s.caption,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить слайдер' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.put('/api/admin/catalog-slider', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
fastify.put('/api/admin/catalog-slider', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const body = request.body ?? {}
|
const body = request.body ?? {}
|
||||||
const rawSlides = body.slides
|
const rawSlides = body.slides
|
||||||
if (!Array.isArray(rawSlides)) {
|
if (!Array.isArray(rawSlides)) {
|
||||||
@@ -87,5 +98,9 @@ export async function registerCatalogSliderRoutes(fastify) {
|
|||||||
caption: s.caption,
|
caption: s.caption,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось обновить слайдер' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const avatarRaw = request.body?.avatar
|
const avatarRaw = request.body?.avatar
|
||||||
const avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
const avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
||||||
const avatarTypeRaw = request.body?.avatarType
|
const avatarTypeRaw = request.body?.avatarType
|
||||||
const avatarType =
|
const avatarType = avatarTypeRaw === null || avatarTypeRaw === undefined ? undefined : String(avatarTypeRaw).trim()
|
||||||
avatarTypeRaw === null || avatarTypeRaw === undefined ? undefined : String(avatarTypeRaw).trim()
|
|
||||||
const avatarStyleRaw = request.body?.avatarStyle
|
const avatarStyleRaw = request.body?.avatarStyle
|
||||||
const avatarStyle =
|
const avatarStyle =
|
||||||
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
||||||
|
|||||||
@@ -45,16 +45,22 @@ function validateAddressPayload(body, reply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function registerUserAddressRoutes(fastify) {
|
export async function registerUserAddressRoutes(fastify) {
|
||||||
fastify.get('/api/me/addresses', { preHandler: [fastify.authenticate] }, async (request) => {
|
fastify.get('/api/me/addresses', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const items = await prisma.shippingAddress.findMany({
|
const items = await prisma.shippingAddress.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
|
orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
|
||||||
})
|
})
|
||||||
return { items }
|
return { items }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить адреса' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/api/me/addresses', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.post('/api/me/addresses', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const validated = validateAddressPayload(request.body, reply)
|
const validated = validateAddressPayload(request.body, reply)
|
||||||
if (!validated) return
|
if (!validated) return
|
||||||
@@ -73,9 +79,14 @@ export async function registerUserAddressRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
return reply.code(201).send({ item: created })
|
return reply.code(201).send({ item: created })
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось создать адрес' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.patch('/api/me/addresses/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.patch('/api/me/addresses/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||||
@@ -123,7 +134,8 @@ export async function registerUserAddressRoutes(fastify) {
|
|||||||
|
|
||||||
if (body.lat !== undefined) {
|
if (body.lat !== undefined) {
|
||||||
const lat = Number(body.lat)
|
const lat = Number(body.lat)
|
||||||
if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' })
|
if (!Number.isFinite(lat) || lat < -90 || lat > 90)
|
||||||
|
return reply.code(400).send({ error: 'Некорректная широта' })
|
||||||
data.lat = lat
|
data.lat = lat
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,9 +161,14 @@ export async function registerUserAddressRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return { item: updated }
|
return { item: updated }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось обновить адрес' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.delete('/api/me/addresses/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.delete('/api/me/addresses/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||||
@@ -159,9 +176,14 @@ export async function registerUserAddressRoutes(fastify) {
|
|||||||
|
|
||||||
await prisma.shippingAddress.delete({ where: { id } })
|
await prisma.shippingAddress.delete({ where: { id } })
|
||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось удалить адрес' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/api/me/addresses/:id/default', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.post('/api/me/addresses/:id/default', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||||
@@ -173,5 +195,9 @@ export async function registerUserAddressRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return { item: updated }
|
return { item: updated }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось установить адрес по умолчанию' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { prisma } from '../lib/prisma.js'
|
import { prisma } from '../lib/prisma.js'
|
||||||
|
|
||||||
export async function registerUserCartRoutes(fastify) {
|
export async function registerUserCartRoutes(fastify) {
|
||||||
fastify.get('/api/me/cart', { preHandler: [fastify.authenticate] }, async (request) => {
|
fastify.get('/api/me/cart', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const items = await prisma.cartItem.findMany({
|
const items = await prisma.cartItem.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
@@ -15,9 +16,14 @@ export async function registerUserCartRoutes(fastify) {
|
|||||||
product: x.product,
|
product: x.product,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить корзину' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/api/me/cart/items', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.post('/api/me/cart/items', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const productId = String(request.body?.productId || '').trim()
|
const productId = String(request.body?.productId || '').trim()
|
||||||
const qtyRaw = request.body?.qty
|
const qtyRaw = request.body?.qty
|
||||||
@@ -40,9 +46,14 @@ export async function registerUserCartRoutes(fastify) {
|
|||||||
create: { userId, productId, qty: nextQty },
|
create: { userId, productId, qty: nextQty },
|
||||||
})
|
})
|
||||||
return reply.code(201).send({ item })
|
return reply.code(201).send({ item })
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось добавить в корзину' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.patch('/api/me/cart/items/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.patch('/api/me/cart/items/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const qtyRaw = request.body?.qty
|
const qtyRaw = request.body?.qty
|
||||||
@@ -63,14 +74,23 @@ export async function registerUserCartRoutes(fastify) {
|
|||||||
|
|
||||||
const updated = await prisma.cartItem.update({ where: { id }, data: { qty: nextQty } })
|
const updated = await prisma.cartItem.update({ where: { id }, data: { qty: nextQty } })
|
||||||
return { item: updated }
|
return { item: updated }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось обновить количество' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.delete('/api/me/cart/items/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.delete('/api/me/cart/items/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const existing = await prisma.cartItem.findFirst({ where: { id, userId } })
|
const existing = await prisma.cartItem.findFirst({ where: { id, userId } })
|
||||||
if (!existing) return reply.code(404).send({ error: 'Позиция корзины не найдена' })
|
if (!existing) return reply.code(404).send({ error: 'Позиция корзины не найдена' })
|
||||||
await prisma.cartItem.delete({ where: { id } })
|
await prisma.cartItem.delete({ where: { id } })
|
||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось удалить из корзины' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,22 +44,21 @@ export async function registerUserMessageRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
if (orders.length === 0) return { count: 0 }
|
if (orders.length === 0) return { count: 0 }
|
||||||
|
|
||||||
|
const orderIds = orders.map((o) => o.id)
|
||||||
const readStates = await prisma.userOrderMessageReadState.findMany({
|
const readStates = await prisma.userOrderMessageReadState.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
})
|
})
|
||||||
const lastReadByOrder = new Map(readStates.map((r) => [r.orderId, r.lastReadAt]))
|
const lastReadByOrder = new Map(readStates.map((r) => [r.orderId, r.lastReadAt]))
|
||||||
|
|
||||||
let count = 0
|
const adminMessages = await prisma.orderMessage.findMany({
|
||||||
for (const o of orders) {
|
where: { orderId: { in: orderIds }, authorType: 'admin' },
|
||||||
const lastRead = lastReadByOrder.get(o.id) ?? new Date(0)
|
select: { orderId: true, createdAt: true },
|
||||||
const n = await prisma.orderMessage.count({
|
|
||||||
where: {
|
|
||||||
orderId: o.id,
|
|
||||||
authorType: 'admin',
|
|
||||||
createdAt: { gt: lastRead },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
count += n
|
|
||||||
|
let count = 0
|
||||||
|
for (const msg of adminMessages) {
|
||||||
|
const lastRead = lastReadByOrder.get(msg.orderId) ?? new Date(0)
|
||||||
|
if (msg.createdAt > lastRead) count++
|
||||||
}
|
}
|
||||||
return { count }
|
return { count }
|
||||||
})
|
})
|
||||||
@@ -86,25 +85,32 @@ export async function registerUserMessageRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
const lastReadByOrder = new Map(readStates.map((r) => [r.orderId, r.lastReadAt]))
|
const lastReadByOrder = new Map(readStates.map((r) => [r.orderId, r.lastReadAt]))
|
||||||
|
|
||||||
|
const orderIds = orders.map((o) => o.id)
|
||||||
|
const unreadCounts = new Map()
|
||||||
|
if (orderIds.length > 0) {
|
||||||
|
const adminMessages = await prisma.orderMessage.findMany({
|
||||||
|
where: { orderId: { in: orderIds }, authorType: 'admin' },
|
||||||
|
select: { orderId: true, createdAt: true },
|
||||||
|
})
|
||||||
|
for (const msg of adminMessages) {
|
||||||
|
const lastRead = lastReadByOrder.get(msg.orderId) ?? new Date(0)
|
||||||
|
if (msg.createdAt > lastRead) {
|
||||||
|
unreadCounts.set(msg.orderId, (unreadCounts.get(msg.orderId) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const items = []
|
const items = []
|
||||||
for (const o of orders) {
|
for (const o of orders) {
|
||||||
const lastMsg = o.messages[0]
|
const lastMsg = o.messages[0]
|
||||||
if (!lastMsg) continue
|
if (!lastMsg) continue
|
||||||
const lastRead = lastReadByOrder.get(o.id) ?? new Date(0)
|
|
||||||
const unreadCount = await prisma.orderMessage.count({
|
|
||||||
where: {
|
|
||||||
orderId: o.id,
|
|
||||||
authorType: 'admin',
|
|
||||||
createdAt: { gt: lastRead },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
items.push({
|
items.push({
|
||||||
orderId: o.id,
|
orderId: o.id,
|
||||||
status: o.status,
|
status: o.status,
|
||||||
deliveryType: o.deliveryType,
|
deliveryType: o.deliveryType,
|
||||||
lastMessageAt: lastMsg.createdAt,
|
lastMessageAt: lastMsg.createdAt,
|
||||||
preview: lastMsg.text.length > 280 ? `${lastMsg.text.slice(0, 277)}…` : lastMsg.text,
|
preview: lastMsg.text.length > 280 ? `${lastMsg.text.slice(0, 277)}…` : lastMsg.text,
|
||||||
unreadCount,
|
unreadCount: unreadCounts.get(o.id) ?? 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return { items }
|
return { items }
|
||||||
|
|||||||
@@ -176,11 +176,12 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
return reply.code(201).send({ orderId: created.id })
|
return reply.code(201).send({ orderId: created.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.get('/api/me/orders', { preHandler: [fastify.authenticate] }, async (request) => {
|
fastify.get('/api/me/orders', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const orders = await prisma.order.findMany({
|
const orders = await prisma.order.findMany({
|
||||||
where: { userId },
|
where: { userId },
|
||||||
include: { items: true },
|
include: { items: { select: { qty: true } } },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
@@ -194,9 +195,14 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
itemsCount: o.items.reduce((s, i) => s + i.qty, 0),
|
itemsCount: o.items.reduce((s, i) => s + i.qty, 0),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить заказы' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.get('/api/me/orders/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
fastify.get('/api/me/orders/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const order = await prisma.order.findFirst({
|
const order = await prisma.order.findFirst({
|
||||||
@@ -205,6 +211,10 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
|
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
|
||||||
return { item: order }
|
return { item: order }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось загрузить заказ' })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
@@ -251,6 +261,7 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
'/api/me/orders/:id/confirm-received',
|
'/api/me/orders/:id/confirm-received',
|
||||||
{ preHandler: [fastify.authenticate] },
|
{ preHandler: [fastify.authenticate] },
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
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 } })
|
||||||
@@ -264,6 +275,10 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
|
|
||||||
await prisma.order.update({ where: { id }, data: { status: 'DONE' } })
|
await prisma.order.update({ where: { id }, data: { status: 'DONE' } })
|
||||||
return { ok: true, status: 'DONE' }
|
return { ok: true, status: 'DONE' }
|
||||||
|
} catch (err) {
|
||||||
|
request.log.error(err)
|
||||||
|
return reply.code(500).send({ error: 'Не удалось подтвердить получение' })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user