test commit
This commit is contained in:
@@ -2,13 +2,11 @@ import { lazy, Suspense } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { MainLayout } from '@/app/layout/MainLayout'
|
||||
import { AboutPage } from '@/pages/about'
|
||||
// import { AdminLayoutPage } from '@/pages/admin-layout'
|
||||
import { AuthCallbackPage, AuthPage } from '@/pages/auth'
|
||||
import { CartPage } from '@/pages/cart'
|
||||
import { CheckoutPage } from '@/pages/checkout'
|
||||
import { HomePage } from '@/pages/home'
|
||||
import { InfoPage } from '@/pages/info'
|
||||
// import { MeLayoutPage } from '@/pages/me'
|
||||
import { NotFoundPage } from '@/pages/not-found'
|
||||
import { PrivacyPolicyPage } from '@/pages/privacy-policy'
|
||||
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/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
||||
{ 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 (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Оповещения
|
||||
Уведомления
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||
Настройка оповещений администратора.
|
||||
Настройка уведомлений администратора.
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -57,7 +57,7 @@ export function MeLayoutPage() {
|
||||
{ to: '/me/messages', label: 'Сообщения', icon: <MessageCircle /> },
|
||||
{ to: '/me/settings', label: 'Настройки', icon: <Settings /> },
|
||||
{ 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 (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Оповещения
|
||||
Уведомления
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||
Настройте, какие уведомления вы хотите получать на почту.
|
||||
@@ -78,7 +78,7 @@ export function NotificationsPage() {
|
||||
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 }}>
|
||||
Включите, чтобы получать уведомления о заказах на почту.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import Paper from '@mui/material/Paper'
|
||||
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 : '')
|
||||
|
||||
@@ -138,8 +138,8 @@ const sections = [
|
||||
`ИНН: ${OP_INN}`,
|
||||
`ОГРН: ${OP_OGRN}`,
|
||||
`Адрес: ${OP_ADDR}`,
|
||||
`Телефон: +7 (900) 000-00-00`, // TODO: заменить на реальный номер телефона
|
||||
`Email: ${STORE_EMAIL}`, // TODO: заменить на реальный email при настройке STORE_EMAIL
|
||||
`Телефон: ${STORE_PHONE}`,
|
||||
`Email: ${STORE_EMAIL}`,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -6,14 +6,20 @@ import {
|
||||
import { prisma } from '../../lib/prisma.js'
|
||||
|
||||
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({
|
||||
orderBy: [{ sort: 'asc' }, { name: 'asc' }],
|
||||
})
|
||||
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) => {
|
||||
try {
|
||||
const body = request.body ?? {}
|
||||
const name = String(body.name ?? '').trim()
|
||||
if (!name) {
|
||||
@@ -39,9 +45,14 @@ export async function registerAdminCategoryRoutes(fastify) {
|
||||
},
|
||||
})
|
||||
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) => {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const body = request.body ?? {}
|
||||
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 })
|
||||
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) => {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const existing = await prisma.category.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
@@ -117,5 +133,9 @@ export async function registerAdminCategoryRoutes(fastify) {
|
||||
prisma.category.delete({ where: { id } }),
|
||||
])
|
||||
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
|
||||
|
||||
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({
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
include: { galleryImage: true },
|
||||
@@ -15,9 +16,14 @@ export async function registerCatalogSliderRoutes(fastify) {
|
||||
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({
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
include: { galleryImage: true },
|
||||
@@ -30,9 +36,14 @@ export async function registerCatalogSliderRoutes(fastify) {
|
||||
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) => {
|
||||
try {
|
||||
const body = request.body ?? {}
|
||||
const rawSlides = body.slides
|
||||
if (!Array.isArray(rawSlides)) {
|
||||
@@ -87,5 +98,9 @@ export async function registerCatalogSliderRoutes(fastify) {
|
||||
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 avatar = avatarRaw === null || avatarRaw === undefined ? undefined : String(avatarRaw).trim()
|
||||
const avatarTypeRaw = request.body?.avatarType
|
||||
const avatarType =
|
||||
avatarTypeRaw === null || avatarTypeRaw === undefined ? undefined : String(avatarTypeRaw).trim()
|
||||
const avatarType = avatarTypeRaw === null || avatarTypeRaw === undefined ? undefined : String(avatarTypeRaw).trim()
|
||||
const avatarStyleRaw = request.body?.avatarStyle
|
||||
const avatarStyle =
|
||||
avatarStyleRaw === null || avatarStyleRaw === undefined ? undefined : String(avatarStyleRaw).trim()
|
||||
|
||||
@@ -45,16 +45,22 @@ function validateAddressPayload(body, reply) {
|
||||
}
|
||||
|
||||
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 items = await prisma.shippingAddress.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
|
||||
})
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const validated = validateAddressPayload(request.body, reply)
|
||||
if (!validated) return
|
||||
@@ -73,9 +79,14 @@ export async function registerUserAddressRoutes(fastify) {
|
||||
})
|
||||
})
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||
@@ -123,7 +134,8 @@ export async function registerUserAddressRoutes(fastify) {
|
||||
|
||||
if (body.lat !== undefined) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -149,9 +161,14 @@ export async function registerUserAddressRoutes(fastify) {
|
||||
})
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||
@@ -159,9 +176,14 @@ export async function registerUserAddressRoutes(fastify) {
|
||||
|
||||
await prisma.shippingAddress.delete({ where: { id } })
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||
@@ -173,5 +195,9 @@ export async function registerUserAddressRoutes(fastify) {
|
||||
})
|
||||
|
||||
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'
|
||||
|
||||
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 items = await prisma.cartItem.findMany({
|
||||
where: { userId },
|
||||
@@ -15,9 +16,14 @@ export async function registerUserCartRoutes(fastify) {
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const productId = String(request.body?.productId || '').trim()
|
||||
const qtyRaw = request.body?.qty
|
||||
@@ -40,9 +46,14 @@ export async function registerUserCartRoutes(fastify) {
|
||||
create: { userId, productId, qty: nextQty },
|
||||
})
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
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 } })
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const existing = await prisma.cartItem.findFirst({ where: { id, userId } })
|
||||
if (!existing) return reply.code(404).send({ error: 'Позиция корзины не найдена' })
|
||||
await prisma.cartItem.delete({ where: { id } })
|
||||
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 }
|
||||
|
||||
const orderIds = orders.map((o) => o.id)
|
||||
const readStates = await prisma.userOrderMessageReadState.findMany({
|
||||
where: { userId },
|
||||
})
|
||||
const lastReadByOrder = new Map(readStates.map((r) => [r.orderId, r.lastReadAt]))
|
||||
|
||||
let count = 0
|
||||
for (const o of orders) {
|
||||
const lastRead = lastReadByOrder.get(o.id) ?? new Date(0)
|
||||
const n = await prisma.orderMessage.count({
|
||||
where: {
|
||||
orderId: o.id,
|
||||
authorType: 'admin',
|
||||
createdAt: { gt: lastRead },
|
||||
},
|
||||
const adminMessages = await prisma.orderMessage.findMany({
|
||||
where: { orderId: { in: orderIds }, authorType: 'admin' },
|
||||
select: { orderId: true, createdAt: true },
|
||||
})
|
||||
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 }
|
||||
})
|
||||
@@ -86,25 +85,32 @@ export async function registerUserMessageRoutes(fastify) {
|
||||
})
|
||||
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 = []
|
||||
for (const o of orders) {
|
||||
const lastMsg = o.messages[0]
|
||||
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({
|
||||
orderId: o.id,
|
||||
status: o.status,
|
||||
deliveryType: o.deliveryType,
|
||||
lastMessageAt: lastMsg.createdAt,
|
||||
preview: lastMsg.text.length > 280 ? `${lastMsg.text.slice(0, 277)}…` : lastMsg.text,
|
||||
unreadCount,
|
||||
unreadCount: unreadCounts.get(o.id) ?? 0,
|
||||
})
|
||||
}
|
||||
return { items }
|
||||
|
||||
@@ -176,11 +176,12 @@ export async function registerUserOrderRoutes(fastify) {
|
||||
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 orders = await prisma.order.findMany({
|
||||
where: { userId },
|
||||
include: { items: true },
|
||||
include: { items: { select: { qty: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
return {
|
||||
@@ -194,9 +195,14 @@ export async function registerUserOrderRoutes(fastify) {
|
||||
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) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
const order = await prisma.order.findFirst({
|
||||
@@ -205,6 +211,10 @@ export async function registerUserOrderRoutes(fastify) {
|
||||
})
|
||||
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
|
||||
return { item: order }
|
||||
} catch (err) {
|
||||
request.log.error(err)
|
||||
return reply.code(500).send({ error: 'Не удалось загрузить заказ' })
|
||||
}
|
||||
})
|
||||
|
||||
fastify.get(
|
||||
@@ -251,6 +261,7 @@ export async function registerUserOrderRoutes(fastify) {
|
||||
'/api/me/orders/:id/confirm-received',
|
||||
{ preHandler: [fastify.authenticate] },
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const userId = request.user.sub
|
||||
const { id } = request.params
|
||||
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' } })
|
||||
return { ok: true, status: 'DONE' }
|
||||
} catch (err) {
|
||||
request.log.error(err)
|
||||
return reply.code(500).send({ error: 'Не удалось подтвердить получение' })
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user