Merge branch 'туц_fixes'
This commit is contained in:
@@ -19,7 +19,7 @@ export function MainLayout({ children }: PropsWithChildren) {
|
|||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', minWidth: '500px' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', minWidth: 0, overflowX: 'hidden' }}>
|
||||||
<ScrollOnNavigate />
|
<ScrollOnNavigate />
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { MainLayout } from '../MainLayout'
|
||||||
|
|
||||||
|
vi.mock('@/app/layout/AppHeader', () => ({
|
||||||
|
AppHeader: () => <header>Шапка</header>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/shared/ui/CookieConsentBanner', () => ({
|
||||||
|
CookieConsentBanner: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/shared/ui/DemoBanner', () => ({
|
||||||
|
DemoBanner: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/shared/ui/ScrollOnNavigate', () => ({
|
||||||
|
ScrollOnNavigate: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/shared/ui/ScrollToTop', () => ({
|
||||||
|
ScrollToTop: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('MainLayout', () => {
|
||||||
|
it('не задает фиксированную минимальную ширину, которая ломает мобильный экран', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MainLayout>Контент</MainLayout>
|
||||||
|
</MemoryRouter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(container.firstElementChild).not.toHaveStyle({ minWidth: '500px' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export type { FormState } from './model/types'
|
export type { FormState } from './model/types'
|
||||||
export { emptyForm } from './lib/use-product-form-helpers'
|
export { emptyForm, isValidProductPriceRub, isValidProductQuantity } from './lib/use-product-form-helpers'
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { isValidProductPriceRub, isValidProductQuantity } from '../use-product-form-helpers'
|
||||||
|
|
||||||
|
describe('product form helpers', () => {
|
||||||
|
it('принимает корректную цену в рублях', () => {
|
||||||
|
expect(isValidProductPriceRub('1200')).toBe(true)
|
||||||
|
expect(isValidProductPriceRub('1200,50')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('отклоняет пустую или некорректную цену', () => {
|
||||||
|
expect(isValidProductPriceRub('')).toBe(false)
|
||||||
|
expect(isValidProductPriceRub('0')).toBe(false)
|
||||||
|
expect(isValidProductPriceRub('1200,555')).toBe(false)
|
||||||
|
expect(isValidProductPriceRub('1,2,3')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('принимает только целое количество от 0 до 10', () => {
|
||||||
|
expect(isValidProductQuantity('0')).toBe(true)
|
||||||
|
expect(isValidProductQuantity('10')).toBe(true)
|
||||||
|
expect(isValidProductQuantity('')).toBe(false)
|
||||||
|
expect(isValidProductQuantity('11')).toBe(false)
|
||||||
|
expect(isValidProductQuantity('1.5')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,3 +12,19 @@ export const emptyForm = (): FormState => ({
|
|||||||
published: true,
|
published: true,
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function isValidProductPriceRub(value: string): boolean {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!/^\d+([,.]\d{1,2})?$/.test(trimmed)) return false
|
||||||
|
|
||||||
|
const priceRub = Number(trimmed.replace(',', '.'))
|
||||||
|
return Number.isFinite(priceRub) && priceRub > 0 && priceRub <= 10_000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidProductQuantity(value: string): boolean {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return false
|
||||||
|
|
||||||
|
const quantity = Number(trimmed)
|
||||||
|
return Number.isInteger(quantity) && quantity >= 0 && quantity <= 10
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Typography from '@mui/material/Typography'
|
|||||||
import { Controller, type UseFormReturn } from 'react-hook-form'
|
import { Controller, type UseFormReturn } from 'react-hook-form'
|
||||||
import type { Category } from '@/entities/product/model/types'
|
import type { Category } from '@/entities/product/model/types'
|
||||||
import { OptimizedImage } from '@/shared/ui/OptimizedImage'
|
import { OptimizedImage } from '@/shared/ui/OptimizedImage'
|
||||||
|
import { isValidProductPriceRub, isValidProductQuantity } from '../lib/use-product-form-helpers'
|
||||||
import type { FormState } from '../model/types'
|
import type { FormState } from '../model/types'
|
||||||
|
|
||||||
export function ProductFormFields({
|
export function ProductFormFields({
|
||||||
@@ -31,7 +32,17 @@ export function ProductFormFields({
|
|||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="title"
|
name="title"
|
||||||
render={({ field }) => <TextField label="Название" fullWidth required {...field} />}
|
rules={{ required: 'Укажите название' }}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
label="Название"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
{...field}
|
||||||
|
helperText={fieldState.error?.message}
|
||||||
|
error={!!fieldState.error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -73,11 +84,8 @@ export function ProductFormFields({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="quantity"
|
name="quantity"
|
||||||
rules={{
|
rules={{
|
||||||
validate: (v) => {
|
required: 'Укажите количество',
|
||||||
const n = Number(v)
|
validate: (v) => isValidProductQuantity(v) || 'Целое число от 0 до 10',
|
||||||
if (!Number.isInteger(n) || n < 0 || n > 10) return 'Целое число от 0 до 10'
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -99,13 +107,7 @@ export function ProductFormFields({
|
|||||||
name="priceRub"
|
name="priceRub"
|
||||||
rules={{
|
rules={{
|
||||||
required: 'Укажите цену',
|
required: 'Укажите цену',
|
||||||
validate: (v) => {
|
validate: (v) => isValidProductPriceRub(v) || 'Цена должна быть от 0,01 до 10 000 ₽, максимум 2 знака',
|
||||||
const n = Number(v.replace(',', '.'))
|
|
||||||
if (!Number.isFinite(n) || n <= 0) return 'Цена должна быть больше 0'
|
|
||||||
if (n > 10_000) return 'Цена не может превышать 10 000 ₽'
|
|
||||||
if (!Number.isInteger(Math.round(n * 100))) return 'Не более 2 знаков после запятой'
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -194,6 +196,7 @@ export function ProductFormFields({
|
|||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="categoryId"
|
name="categoryId"
|
||||||
|
rules={{ required: 'Выберите категорию' }}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth error={!field.value}>
|
<FormControl fullWidth error={!field.value}>
|
||||||
<InputLabel id="cat-label">Категория</InputLabel>
|
<InputLabel id="cat-label">Категория</InputLabel>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import TableHead from '@mui/material/TableHead'
|
|||||||
import TableRow from '@mui/material/TableRow'
|
import TableRow from '@mui/material/TableRow'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm, useWatch } from 'react-hook-form'
|
||||||
import {
|
import {
|
||||||
createProduct,
|
createProduct,
|
||||||
deleteProduct,
|
deleteProduct,
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from '@/entities/product/api/admin-product-api'
|
} from '@/entities/product/api/admin-product-api'
|
||||||
import { fetchCategories } from '@/entities/product/api/product-api'
|
import { fetchCategories } from '@/entities/product/api/product-api'
|
||||||
import type { Product } from '@/entities/product/model/types'
|
import type { Product } from '@/entities/product/model/types'
|
||||||
import { emptyForm, type FormState } from '@/features/product-form'
|
import { emptyForm, isValidProductPriceRub, isValidProductQuantity, type FormState } from '@/features/product-form'
|
||||||
import { GalleryImagePicker } from '@/features/product-form/ui/GalleryImagePicker'
|
import { GalleryImagePicker } from '@/features/product-form/ui/GalleryImagePicker'
|
||||||
import { ProductFormFields } from '@/features/product-form/ui/ProductFormFields'
|
import { ProductFormFields } from '@/features/product-form/ui/ProductFormFields'
|
||||||
import { formatPriceRub } from '@/shared/lib/format-price'
|
import { formatPriceRub } from '@/shared/lib/format-price'
|
||||||
@@ -42,7 +42,15 @@ export function AdminProductsPage() {
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
})
|
})
|
||||||
|
|
||||||
const titleValue = productForm.watch('title')
|
const [titleValue, categoryIdValue, quantityValue, priceRubValue, imageUrlsValue] = useWatch({
|
||||||
|
control: productForm.control,
|
||||||
|
name: ['title', 'categoryId', 'quantity', 'priceRub', 'imageUrls'],
|
||||||
|
})
|
||||||
|
const canSubmit =
|
||||||
|
titleValue.trim().length > 0 &&
|
||||||
|
categoryIdValue.trim().length > 0 &&
|
||||||
|
isValidProductQuantity(quantityValue) &&
|
||||||
|
isValidProductPriceRub(priceRubValue)
|
||||||
|
|
||||||
const categoriesQuery = useQuery({
|
const categoriesQuery = useQuery({
|
||||||
queryKey: ['categories'],
|
queryKey: ['categories'],
|
||||||
@@ -246,15 +254,7 @@ export function AdminProductsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={
|
disabled={!canSubmit || createMut.isPending || updateMut.isPending}
|
||||||
!titleValue.trim() ||
|
|
||||||
!productForm.watch('categoryId') ||
|
|
||||||
!productForm.watch('quantity').trim() ||
|
|
||||||
!productForm.watch('priceRub').trim() ||
|
|
||||||
!productForm.formState.isValid ||
|
|
||||||
createMut.isPending ||
|
|
||||||
updateMut.isPending
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{editing ? 'Сохранить' : 'Создать'}
|
{editing ? 'Сохранить' : 'Создать'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -265,7 +265,7 @@ export function AdminProductsPage() {
|
|||||||
open={galleryPickOpen}
|
open={galleryPickOpen}
|
||||||
onClose={() => setGalleryPickOpen(false)}
|
onClose={() => setGalleryPickOpen(false)}
|
||||||
onSelect={handleGallerySelect}
|
onSelect={handleGallerySelect}
|
||||||
currentUrls={productForm.watch('imageUrls')}
|
currentUrls={imageUrlsValue}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import {
|
||||||
|
createProduct,
|
||||||
|
deleteProduct,
|
||||||
|
fetchAdminProducts,
|
||||||
|
updateProduct,
|
||||||
|
} from '@/entities/product/api/admin-product-api'
|
||||||
|
import { fetchCategories } from '@/entities/product/api/product-api'
|
||||||
|
import type { Category } from '@/entities/product/model/types'
|
||||||
|
import { AdminProductsPage } from '../AdminProductsPage'
|
||||||
|
|
||||||
|
vi.mock('@/entities/product/api/admin-product-api', () => ({
|
||||||
|
createProduct: vi.fn(),
|
||||||
|
deleteProduct: vi.fn(),
|
||||||
|
fetchAdminProducts: vi.fn(),
|
||||||
|
updateProduct: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/entities/product/api/product-api', () => ({
|
||||||
|
fetchCategories: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const category: Category = {
|
||||||
|
id: 'cat-1',
|
||||||
|
name: 'Игрушки',
|
||||||
|
slug: 'igrushki',
|
||||||
|
sort: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPage() {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AdminProductsPage />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AdminProductsPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(fetchCategories).mockResolvedValue([category])
|
||||||
|
vi.mocked(fetchAdminProducts).mockResolvedValue([])
|
||||||
|
vi.mocked(createProduct).mockResolvedValue({} as never)
|
||||||
|
vi.mocked(updateProduct).mockResolvedValue({} as never)
|
||||||
|
vi.mocked(deleteProduct).mockResolvedValue(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('разрешает создать товар после ввода цены и не сбрасывает фокус поля цены', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Новый товар' }))
|
||||||
|
await user.type(screen.getByLabelText(/Название/), 'Мишка ручной работы')
|
||||||
|
await user.click(screen.getByLabelText(/Категория/))
|
||||||
|
await user.click(await screen.findByRole('option', { name: 'Игрушки' }))
|
||||||
|
|
||||||
|
const priceInput = screen.getByLabelText(/Цена, ₽/)
|
||||||
|
await user.click(priceInput)
|
||||||
|
await user.type(priceInput, '1200')
|
||||||
|
|
||||||
|
expect(priceInput).toHaveFocus()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('button', { name: 'Создать' })).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Binary file not shown.
@@ -0,0 +1,96 @@
|
|||||||
|
import jwt from '@fastify/jwt'
|
||||||
|
import Fastify from 'fastify'
|
||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { prisma } from '../../../lib/prisma.js'
|
||||||
|
import { mapProductForApi, parseMaterialsInput, slugify } from '../_product-helpers.js'
|
||||||
|
import { registerAdminProductRoutes } from '../admin-products.js'
|
||||||
|
|
||||||
|
const JWT_SECRET = 'test-secret'
|
||||||
|
const ADMIN_EMAIL = `admin-products-${Date.now()}@example.com`
|
||||||
|
|
||||||
|
let app
|
||||||
|
let adminUser
|
||||||
|
let category
|
||||||
|
|
||||||
|
async function signToken(user) {
|
||||||
|
return app.jwt.sign({ sub: user.id, email: user.email })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const fastify = Fastify({ logger: false })
|
||||||
|
await fastify.register(jwt, { secret: JWT_SECRET })
|
||||||
|
fastify.decorate('verifyAdmin', async (request, reply) => {
|
||||||
|
try {
|
||||||
|
await request.jwtVerify()
|
||||||
|
} catch {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
if (request.user.email !== ADMIN_EMAIL) {
|
||||||
|
return reply.code(401).send({ error: 'Admin only' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fastify.decorate('slugify', slugify)
|
||||||
|
fastify.decorate('parseMaterialsInput', parseMaterialsInput)
|
||||||
|
fastify.decorate('mapProductForApi', mapProductForApi)
|
||||||
|
await registerAdminProductRoutes(fastify)
|
||||||
|
await fastify.ready()
|
||||||
|
return fastify
|
||||||
|
}
|
||||||
|
|
||||||
|
function productData(overrides = {}) {
|
||||||
|
return {
|
||||||
|
title: 'Медведь',
|
||||||
|
slug: `bear-${Date.now()}`,
|
||||||
|
priceCents: 120000,
|
||||||
|
quantity: 1,
|
||||||
|
categoryId: category.id,
|
||||||
|
published: true,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('admin product routes', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await prisma.product.deleteMany({ where: { category: { slug: { startsWith: 'admin-products-test-' } } } })
|
||||||
|
await prisma.category.deleteMany({ where: { slug: { startsWith: 'admin-products-test-' } } })
|
||||||
|
await prisma.user.deleteMany({ where: { email: ADMIN_EMAIL } })
|
||||||
|
|
||||||
|
adminUser = await prisma.user.create({ data: { email: ADMIN_EMAIL } })
|
||||||
|
category = await prisma.category.create({
|
||||||
|
data: {
|
||||||
|
name: 'Тестовая категория',
|
||||||
|
slug: `admin-products-test-${Date.now()}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await prisma.product.deleteMany({ where: { categoryId: category.id } })
|
||||||
|
app = await buildApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.product.deleteMany({ where: { categoryId: category.id } })
|
||||||
|
await prisma.category.delete({ where: { id: category.id } })
|
||||||
|
await prisma.user.delete({ where: { id: adminUser.id } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('генерирует уникальный slug при создании товара с повторяющимся названием без ручного slug', async () => {
|
||||||
|
await prisma.product.create({ data: productData({ title: 'Bear', slug: 'bear' }) })
|
||||||
|
const token = await signToken(adminUser)
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/admin/products',
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
payload: productData({ title: 'Bear', slug: undefined }),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(201)
|
||||||
|
expect(res.json().slug).toBe('bear-2')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -40,6 +40,19 @@ const PATCH_PRODUCT_SCHEMA = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildUniqueProductSlug(baseSlug) {
|
||||||
|
const base = String(baseSlug || '').trim()
|
||||||
|
let candidate = base
|
||||||
|
let suffix = 2
|
||||||
|
|
||||||
|
while (await prisma.product.findUnique({ where: { slug: candidate } })) {
|
||||||
|
candidate = `${base}-${suffix}`
|
||||||
|
suffix += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
export async function registerAdminProductRoutes(fastify) {
|
export async function registerAdminProductRoutes(fastify) {
|
||||||
fastify.get('/api/admin/products', { preHandler: [fastify.verifyAdmin] }, async (request) => {
|
fastify.get('/api/admin/products', { preHandler: [fastify.verifyAdmin] }, async (request) => {
|
||||||
const items = await prisma.product.findMany({
|
const items = await prisma.product.findMany({
|
||||||
@@ -59,7 +72,9 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
reply.code(400).send({ error: 'Укажите название' })
|
reply.code(400).send({ error: 'Укажите название' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const slug = String(body.slug ?? '').trim() || request.server.slugify(title) || `item-${Date.now()}`
|
const requestedSlug = String(body.slug ?? '').trim()
|
||||||
|
const slugBase = requestedSlug || request.server.slugify(title) || `item-${Date.now()}`
|
||||||
|
const slug = requestedSlug ? slugBase : await buildUniqueProductSlug(slugBase)
|
||||||
const categoryId = String(body.categoryId ?? '').trim()
|
const categoryId = String(body.categoryId ?? '').trim()
|
||||||
if (!categoryId) {
|
if (!categoryId) {
|
||||||
reply.code(400).send({ error: 'Укажите категорию' })
|
reply.code(400).send({ error: 'Укажите категорию' })
|
||||||
@@ -79,7 +94,7 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
reply.code(400).send({ error: 'Цена не может превышать 10 000 ₽' })
|
reply.code(400).send({ error: 'Цена не может превышать 10 000 ₽' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const exists = await prisma.product.findUnique({ where: { slug } })
|
const exists = requestedSlug ? await prisma.product.findUnique({ where: { slug } }) : null
|
||||||
if (exists) {
|
if (exists) {
|
||||||
reply.code(409).send({ error: 'Такой slug уже занят' })
|
reply.code(409).send({ error: 'Такой slug уже занят' })
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user