update goods
This commit is contained in:
@@ -0,0 +1,174 @@
|
|||||||
|
# Design: Доработка товара — удаление «под заказ», обязательные quantity и категория
|
||||||
|
|
||||||
|
**Дата:** 2026-05-15
|
||||||
|
**Статус:** На согласовании
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Упростить модель товара: убрать концепцию «под заказ», сделать количество и категорию обязательными полями. Категория «Не указано» остаётся технической заглушкой для переноса товаров при удалении категории, но не видна в каталоге и не выбирается при редактировании.
|
||||||
|
|
||||||
|
## Архитектура изменений
|
||||||
|
|
||||||
|
### 1. База данных (Prisma)
|
||||||
|
|
||||||
|
**Миграция:**
|
||||||
|
- Перед удалением полей: все товары с `inStock = false` получают `quantity = 0`
|
||||||
|
- Удалить поля `inStock` и `leadTimeDays` из модели `Product`
|
||||||
|
- Статус наличия определяется исключительно по `quantity`:
|
||||||
|
- `quantity > 0` → «В наличии»
|
||||||
|
- `quantity = 0` → «Нет в наличии»
|
||||||
|
|
||||||
|
**`server/prisma/schema.prisma`:**
|
||||||
|
```prisma
|
||||||
|
model Product {
|
||||||
|
// ... остальные поля без изменений ...
|
||||||
|
quantity Int @default(0)
|
||||||
|
// УДАЛЕНО: inStock Boolean @default(true)
|
||||||
|
// УДАЛЕНО: leadTimeDays Int?
|
||||||
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
|
||||||
|
categoryId String
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Сервер — валидация и CRUD
|
||||||
|
|
||||||
|
**`server/src/routes/api/admin-products.js`:**
|
||||||
|
|
||||||
|
**CREATE (POST):**
|
||||||
|
- `quantity` — required, `Int >= 0` (было nullable)
|
||||||
|
- `categoryId` — required (было: при пустом → авто-назначение «Не указано»)
|
||||||
|
- Удалить валидацию `leadTimeDays` при `!inStock`
|
||||||
|
- Удалить принудительную установку `quantity = 1` для «под заказ»
|
||||||
|
- Вернуть 400: `'Укажите категорию'` если `categoryId` отсутствует
|
||||||
|
|
||||||
|
**UPDATE (PATCH):**
|
||||||
|
- `quantity` — required, `Int >= 0` (было nullable)
|
||||||
|
- `categoryId` — required (было: при пустом → «Не указано»)
|
||||||
|
- Удалить логику очистки `leadTimeDays` при `inStock = true`
|
||||||
|
- Удалить принудительную установку `quantity = 1`
|
||||||
|
- Вернуть 400 при отсутствии `categoryId`
|
||||||
|
|
||||||
|
**JSON Schema:**
|
||||||
|
- `CREATE_PRODUCT_SCHEMA`: убрать `leadTimeDays`, сделать `quantity` required (убрать `nullable`)
|
||||||
|
- `PATCH_PRODUCT_SCHEMA`: убрать `leadTimeDays`, `quantity` — если передан, то `>= 0`
|
||||||
|
|
||||||
|
**`server/src/routes/api/public-catalog.js`:**
|
||||||
|
- Удалить ветку `availability === 'in_stock'` и `availability === 'made_to_order'`
|
||||||
|
- Фильтрация «в наличии» больше не нужна — все товары в каталоге
|
||||||
|
|
||||||
|
### 3. Клиент — админка (две страницы)
|
||||||
|
|
||||||
|
**`client/src/pages/admin/ui/AdminPage.tsx`** и **`client/src/pages/admin-products/ui/AdminProductsPage.tsx`:**
|
||||||
|
|
||||||
|
**FormState:**
|
||||||
|
- Удалить `inStock: boolean` и `leadTimeDays: string`
|
||||||
|
- `quantity: string` — без nullable-семантики
|
||||||
|
|
||||||
|
**UI:**
|
||||||
|
- Удалить Switch «В наличии / Под заказ»
|
||||||
|
- Удалить TextField «Срок исполнения, дней»
|
||||||
|
- TextField «Количество»:
|
||||||
|
- Без helper «Оставьте пустым...»
|
||||||
|
- Новый helper: «0 = нет в наличии»
|
||||||
|
- Валидация: не может быть пустым, `parseInt >= 0`
|
||||||
|
- Select «Категория»:
|
||||||
|
- Удалить `<MenuItem value="">` с «Не указано»
|
||||||
|
- Валидация: не даёт сохранить без выбранной категории
|
||||||
|
- Показать ошибку при попытке сохранить без категории
|
||||||
|
|
||||||
|
**Submit-валидация:**
|
||||||
|
- Удалить проверку `leadTimeDays` при `!inStock`
|
||||||
|
- Добавить проверку: `categoryId` не пустой → blocking error
|
||||||
|
- Добавить проверку: `quantity` не пустой → blocking error
|
||||||
|
|
||||||
|
### 4. Клиент — каталог
|
||||||
|
|
||||||
|
**`client/src/entities/product/ui/ProductCard.tsx`:**
|
||||||
|
- Удалить логику `'Под заказ · {leadTimeDays} дн.'`
|
||||||
|
- Новый статус:
|
||||||
|
- `quantity > 0` → «В наличии» (зелёный)
|
||||||
|
- `quantity === 0` → «Нет в наличии» (серый/red)
|
||||||
|
|
||||||
|
**`client/src/pages/product/ui/ProductPage.tsx`:**
|
||||||
|
- Удалить chip `'Под заказ · {leadTimeDays} дн.'`
|
||||||
|
- Удалить alert `'Этот товар изготавливается под заказ...'`
|
||||||
|
- Статус определяется по `quantity`
|
||||||
|
|
||||||
|
**`client/src/pages/checkout/ui/CheckoutPage.tsx`:**
|
||||||
|
- Удалить определение made-to-order товаров в корзине
|
||||||
|
- Удалить info alert о доставке после изготовления
|
||||||
|
|
||||||
|
### 5. Клиент — фильтры
|
||||||
|
|
||||||
|
**`client/src/pages/home/lib/use-product-filters.ts`:**
|
||||||
|
- Удалить `availability: 'all' | 'in_stock' | 'made_to_order'` из state
|
||||||
|
- Удалить `availability` из параметров `fetchPublicProducts()`
|
||||||
|
|
||||||
|
**`client/src/pages/home/ui/ProductFilters.tsx`:**
|
||||||
|
- Удалить `ToggleButtonGroup` с `'all'`, `'in_stock'`, `'made_to_order'`
|
||||||
|
- Удалить отображение категории «Не указано» из списка чипов (фильтр `cat.slug !== 'ne-ukazano'`)
|
||||||
|
|
||||||
|
### 6. Категория «Не указано» — что остаётся
|
||||||
|
|
||||||
|
| Где | Что происходит |
|
||||||
|
|---|---|
|
||||||
|
| `server/src/lib/default-category.js` | **Остаётся** — функция `getOrCreateUnspecifiedCategory()` |
|
||||||
|
| `server/src/index.js` | **Остаётся** — вызов при старте |
|
||||||
|
| `server/src/routes/api/admin-categories.js` | **Остаётся** — нельзя удалить/переименовать; при удалении категории товары переезжают в «Не указано» |
|
||||||
|
| Админка категорий | **Остаётся** — кнопка удаления заблокирована |
|
||||||
|
| Фильтры каталога | **Скрыта** — не показывается в чипах |
|
||||||
|
| Форма товара | **Скрыта** — не выбирается в Select |
|
||||||
|
|
||||||
|
## Статус товара — новая логика
|
||||||
|
|
||||||
|
```
|
||||||
|
quantity > 0 → «В наличии» (зелёный chip/badge)
|
||||||
|
quantity = 0 → «Нет в наличии» (серый chip/badge)
|
||||||
|
```
|
||||||
|
|
||||||
|
Никаких других статусов. Поле `inStock` больше не существует.
|
||||||
|
|
||||||
|
## Файлы для изменения
|
||||||
|
|
||||||
|
### Сервер
|
||||||
|
| Файл | Изменения |
|
||||||
|
|---|---|
|
||||||
|
| `server/prisma/schema.prisma` | Удалить `inStock`, `leadTimeDays` |
|
||||||
|
| `server/src/routes/api/admin-products.js` | Валидация, schema, убрать логику под заказ |
|
||||||
|
| `server/src/routes/api/public-catalog.js` | Убрать фильтр availability |
|
||||||
|
|
||||||
|
### Клиент
|
||||||
|
| Файл | Изменения |
|
||||||
|
|---|---|
|
||||||
|
| `client/src/pages/admin/ui/AdminPage.tsx` | FormState, UI, валидация |
|
||||||
|
| `client/src/pages/admin-products/ui/AdminProductsPage.tsx` | FormState, UI, валидация |
|
||||||
|
| `client/src/entities/product/ui/ProductCard.tsx` | Статус по quantity |
|
||||||
|
| `client/src/pages/product/ui/ProductPage.tsx` | Убрать под заказ UI |
|
||||||
|
| `client/src/pages/checkout/ui/CheckoutPage.tsx` | Убрать made-to-order detection |
|
||||||
|
| `client/src/pages/home/ui/ProductFilters.tsx` | Убрать availability toggle, скрыть «Не указано» |
|
||||||
|
| `client/src/pages/home/lib/use-product-filters.ts` | Убрать `availability` |
|
||||||
|
|
||||||
|
## Миграция данных
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// В Prisma migration:
|
||||||
|
// 1. UPDATE Product SET quantity = 0 WHERE inStock = false
|
||||||
|
// 2. ALTER TABLE Product DROP COLUMN inStock
|
||||||
|
// 3. ALTER TABLE Product DROP COLUMN leadTimeDays
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
**Сервер:**
|
||||||
|
- CREATE без categoryId → 400
|
||||||
|
- CREATE без quantity → 400
|
||||||
|
- CREATE с quantity = 0 → OK
|
||||||
|
- PATCH без categoryId → 400
|
||||||
|
- PATCH с quantity = 0 → OK
|
||||||
|
|
||||||
|
**Клиент:**
|
||||||
|
- Форма не сохраняется без категории
|
||||||
|
- Форма не сохраняется без количества
|
||||||
|
- Фильтры не содержат «Под заказ» и «Не указано»
|
||||||
|
- Карточка товара показывает «Нет в наличии» при quantity = 0
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
1476
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1616
|
||||||
@@ -14,7 +14,6 @@ export async function fetchPublicProducts(params?: {
|
|||||||
categorySlug?: string
|
categorySlug?: string
|
||||||
q?: string
|
q?: string
|
||||||
sort?: 'price_asc' | 'price_desc' | ''
|
sort?: 'price_asc' | 'price_desc' | ''
|
||||||
availability?: 'all' | 'in_stock' | 'made_to_order'
|
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
priceMinCents?: number
|
priceMinCents?: number
|
||||||
@@ -25,7 +24,6 @@ export async function fetchPublicProducts(params?: {
|
|||||||
categorySlug: params?.categorySlug || undefined,
|
categorySlug: params?.categorySlug || undefined,
|
||||||
q: params?.q || undefined,
|
q: params?.q || undefined,
|
||||||
sort: params?.sort || undefined,
|
sort: params?.sort || undefined,
|
||||||
availability: params?.availability || undefined,
|
|
||||||
page: params?.page || undefined,
|
page: params?.page || undefined,
|
||||||
pageSize: params?.pageSize || undefined,
|
pageSize: params?.pageSize || undefined,
|
||||||
priceMin: params?.priceMinCents ?? undefined,
|
priceMin: params?.priceMinCents ?? undefined,
|
||||||
@@ -55,16 +53,13 @@ export async function createProduct(body: {
|
|||||||
slug?: string
|
slug?: string
|
||||||
shortDescription?: string | null
|
shortDescription?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
quantity?: number | null
|
quantity: number
|
||||||
materials?: string[]
|
materials?: string[]
|
||||||
priceCents: number
|
priceCents: number
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
imageUrls?: string[]
|
imageUrls?: string[]
|
||||||
published: boolean
|
published: boolean
|
||||||
inStock?: boolean
|
categoryId: string
|
||||||
leadTimeDays?: number | null
|
|
||||||
/** Пустая строка / отсутствует — категория «Не указано» на сервере */
|
|
||||||
categoryId?: string
|
|
||||||
}): Promise<Product> {
|
}): Promise<Product> {
|
||||||
const { data } = await apiClient.post<Product>('admin/products', body)
|
const { data } = await apiClient.post<Product>('admin/products', body)
|
||||||
return data
|
return data
|
||||||
@@ -77,15 +72,13 @@ export async function updateProduct(
|
|||||||
slug: string
|
slug: string
|
||||||
shortDescription: string | null
|
shortDescription: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
quantity: number | null
|
quantity: number
|
||||||
materials: string[]
|
materials: string[]
|
||||||
priceCents: number
|
priceCents: number
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
imageUrls: string[]
|
imageUrls: string[]
|
||||||
published: boolean
|
published: boolean
|
||||||
inStock: boolean
|
categoryId: string
|
||||||
leadTimeDays: number | null
|
|
||||||
categoryId?: string
|
|
||||||
}>,
|
}>,
|
||||||
): Promise<Product> {
|
): Promise<Product> {
|
||||||
const { data } = await apiClient.patch<Product>(`admin/products/${id}`, body)
|
const { data } = await apiClient.patch<Product>(`admin/products/${id}`, body)
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ export type Product = {
|
|||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
imageUrls?: string[] // legacy-friendly (used only in admin payloads)
|
imageUrls?: string[] // legacy-friendly (used only in admin payloads)
|
||||||
published: boolean
|
published: boolean
|
||||||
inStock: boolean
|
|
||||||
leadTimeDays: number | null
|
|
||||||
categoryId: string
|
categoryId: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
|||||||
@@ -44,12 +44,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
|||||||
navigate(`/products/${product.id}`)
|
navigate(`/products/${product.id}`)
|
||||||
}, [navigate, product.id])
|
}, [navigate, product.id])
|
||||||
|
|
||||||
const stockLabel =
|
const stockLabel = product.quantity > 0 ? null : { label: 'Нет в наличии', color: 'default' as const }
|
||||||
product.inStock && product.quantity === 0
|
|
||||||
? { label: 'Нет в наличии', color: 'default' as const }
|
|
||||||
: !product.inStock
|
|
||||||
? { label: `Под заказ · ${product.leadTimeDays ?? '—'} дн.`, color: 'warning' as const }
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -132,7 +127,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
|||||||
label={stockLabel.label}
|
label={stockLabel.label}
|
||||||
size="small"
|
size="small"
|
||||||
color={stockLabel.color}
|
color={stockLabel.color}
|
||||||
variant={stockLabel.color === 'warning' ? 'outlined' : 'filled'}
|
variant="filled"
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
@@ -140,8 +135,8 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
backdropFilter: 'blur(4px)',
|
backdropFilter: 'blur(4px)',
|
||||||
bgcolor: stockLabel.color === 'default' ? 'rgba(0,0,0,0.55)' : undefined,
|
bgcolor: 'rgba(0,0,0,0.55)',
|
||||||
color: stockLabel.color === 'default' ? 'common.white' : undefined,
|
color: 'common.white',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function ToggleCartIcon(props: {
|
|||||||
<Tooltip title={tooltip}>
|
<Tooltip title={tooltip}>
|
||||||
<span>
|
<span>
|
||||||
<IconButton size={size} onClick={onClick} disabled={disabled || busy} aria-label={tooltip} type="button">
|
<IconButton size={size} onClick={onClick} disabled={disabled || busy} aria-label={tooltip} type="button">
|
||||||
{user ? (inCart ? <ShoppingCart fill="currentColor" /> : <ShoppingCart />) : <ShoppingCart />}
|
{user ? inCart ? <ShoppingCart fill="currentColor" /> : <ShoppingCart /> : <ShoppingCart />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function AdminCategoriesPage() {
|
|||||||
<TableCell>{c.sort}</TableCell>
|
<TableCell>{c.sort}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<EntityRowActions
|
<EntityRowActions
|
||||||
onEdit={() => openCategoryEdit(c)}
|
onEdit={c.slug === UNSPECIFIED_CATEGORY_SLUG ? undefined : () => openCategoryEdit(c)}
|
||||||
onDelete={c.slug === UNSPECIFIED_CATEGORY_SLUG ? undefined : () => setCategoryDeleteTarget(c)}
|
onDelete={c.slug === UNSPECIFIED_CATEGORY_SLUG ? undefined : () => setCategoryDeleteTarget(c)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ type FormState = {
|
|||||||
priceRub: string
|
priceRub: string
|
||||||
imageUrls: string[]
|
imageUrls: string[]
|
||||||
published: boolean
|
published: boolean
|
||||||
inStock: boolean
|
|
||||||
leadTimeDays: string
|
|
||||||
categoryId: string
|
categoryId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +59,11 @@ const emptyForm = (): FormState => ({
|
|||||||
slug: '',
|
slug: '',
|
||||||
shortDescription: '',
|
shortDescription: '',
|
||||||
description: '',
|
description: '',
|
||||||
quantity: '',
|
quantity: '0',
|
||||||
materials: '',
|
materials: '',
|
||||||
priceRub: '',
|
priceRub: '',
|
||||||
imageUrls: [],
|
imageUrls: [],
|
||||||
published: true,
|
published: true,
|
||||||
inStock: true,
|
|
||||||
leadTimeDays: '',
|
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,7 +79,6 @@ export function AdminProductsPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const titleValue = productForm.watch('title')
|
const titleValue = productForm.watch('title')
|
||||||
const inStockValue = productForm.watch('inStock')
|
|
||||||
|
|
||||||
const categoriesQuery = useQuery({
|
const categoriesQuery = useQuery({
|
||||||
queryKey: ['categories'],
|
queryKey: ['categories'],
|
||||||
@@ -118,13 +113,11 @@ export function AdminProductsPage() {
|
|||||||
slug: p.slug,
|
slug: p.slug,
|
||||||
shortDescription: p.shortDescription ?? '',
|
shortDescription: p.shortDescription ?? '',
|
||||||
description: p.description ?? '',
|
description: p.description ?? '',
|
||||||
quantity: p.quantity === null || p.quantity === undefined ? '' : String(p.quantity),
|
quantity: String(p.quantity),
|
||||||
materials: (p.materials ?? []).join(', '),
|
materials: (p.materials ?? []).join(', '),
|
||||||
priceRub: String(p.priceCents / 100),
|
priceRub: String(p.priceCents / 100),
|
||||||
imageUrls: urls,
|
imageUrls: urls,
|
||||||
published: p.published,
|
published: p.published,
|
||||||
inStock: p.inStock,
|
|
||||||
leadTimeDays: p.leadTimeDays ? String(p.leadTimeDays) : '',
|
|
||||||
categoryId: p.categoryId,
|
categoryId: p.categoryId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -134,14 +127,11 @@ export function AdminProductsPage() {
|
|||||||
const form = productForm.getValues()
|
const form = productForm.getValues()
|
||||||
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
||||||
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
||||||
const leadTimeDays = form.leadTimeDays.trim() ? Number(form.leadTimeDays) : null
|
if (!form.categoryId) throw new Error('Выберите категорию')
|
||||||
if (!form.inStock) {
|
const qty = form.quantity.trim()
|
||||||
if (!Number.isFinite(leadTimeDays) || !leadTimeDays || leadTimeDays <= 0) {
|
if (!qty) throw new Error('Укажите количество')
|
||||||
throw new Error('Если "под заказ", укажите срок исполнения (дней) > 0')
|
const qtyNum = Number(qty)
|
||||||
}
|
if (!Number.isFinite(qtyNum) || qtyNum < 0) throw new Error('Некорректное количество')
|
||||||
}
|
|
||||||
const qty = form.quantity.trim() ? Number(form.quantity) : null
|
|
||||||
if (qty !== null && (!Number.isFinite(qty) || qty < 0)) throw new Error('Некорректное количество')
|
|
||||||
const materials = form.materials
|
const materials = form.materials
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
@@ -151,13 +141,11 @@ export function AdminProductsPage() {
|
|||||||
slug: form.slug.trim() || undefined,
|
slug: form.slug.trim() || undefined,
|
||||||
shortDescription: form.shortDescription.trim() || null,
|
shortDescription: form.shortDescription.trim() || null,
|
||||||
description: form.description.trim() || null,
|
description: form.description.trim() || null,
|
||||||
quantity: qty === null ? null : Math.floor(qty),
|
quantity: Math.floor(qtyNum),
|
||||||
materials,
|
materials,
|
||||||
priceCents,
|
priceCents,
|
||||||
imageUrls: form.imageUrls,
|
imageUrls: form.imageUrls,
|
||||||
published: form.published,
|
published: form.published,
|
||||||
inStock: form.inStock,
|
|
||||||
leadTimeDays: form.inStock ? null : Math.round(leadTimeDays!),
|
|
||||||
categoryId: form.categoryId,
|
categoryId: form.categoryId,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -172,14 +160,11 @@ export function AdminProductsPage() {
|
|||||||
const form = productForm.getValues()
|
const form = productForm.getValues()
|
||||||
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
||||||
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
||||||
const leadTimeDays = form.leadTimeDays.trim() ? Number(form.leadTimeDays) : null
|
if (!form.categoryId) throw new Error('Выберите категорию')
|
||||||
if (!form.inStock) {
|
const qty = form.quantity.trim()
|
||||||
if (!Number.isFinite(leadTimeDays) || !leadTimeDays || leadTimeDays <= 0) {
|
if (!qty) throw new Error('Укажите количество')
|
||||||
throw new Error('Если "под заказ", укажите срок исполнения (дней) > 0')
|
const qtyNum = Number(qty)
|
||||||
}
|
if (!Number.isFinite(qtyNum) || qtyNum < 0) throw new Error('Некорректное количество')
|
||||||
}
|
|
||||||
const qty = form.quantity.trim() ? Number(form.quantity) : null
|
|
||||||
if (qty !== null && (!Number.isFinite(qty) || qty < 0)) throw new Error('Некорректное количество')
|
|
||||||
const materials = form.materials
|
const materials = form.materials
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
@@ -189,13 +174,11 @@ export function AdminProductsPage() {
|
|||||||
slug: form.slug.trim(),
|
slug: form.slug.trim(),
|
||||||
shortDescription: form.shortDescription.trim() || null,
|
shortDescription: form.shortDescription.trim() || null,
|
||||||
description: form.description.trim() || null,
|
description: form.description.trim() || null,
|
||||||
quantity: qty === null ? null : Math.floor(qty),
|
quantity: Math.floor(qtyNum),
|
||||||
materials,
|
materials,
|
||||||
priceCents,
|
priceCents,
|
||||||
imageUrls: form.imageUrls,
|
imageUrls: form.imageUrls,
|
||||||
published: form.published,
|
published: form.published,
|
||||||
inStock: form.inStock,
|
|
||||||
leadTimeDays: form.inStock ? null : Math.round(leadTimeDays!),
|
|
||||||
categoryId: form.categoryId,
|
categoryId: form.categoryId,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -364,13 +347,7 @@ export function AdminProductsPage() {
|
|||||||
control={productForm.control}
|
control={productForm.control}
|
||||||
name="quantity"
|
name="quantity"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField label="Количество" fullWidth {...field} inputMode="numeric" helperText="0 = нет в наличии" />
|
||||||
label="Количество"
|
|
||||||
fullWidth
|
|
||||||
{...field}
|
|
||||||
inputMode="numeric"
|
|
||||||
helperText="Оставьте пустым, если не хотите вести учёт"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -473,18 +450,16 @@ export function AdminProductsPage() {
|
|||||||
control={productForm.control}
|
control={productForm.control}
|
||||||
name="categoryId"
|
name="categoryId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth error={!field.value}>
|
||||||
<InputLabel id="cat-label">Категория</InputLabel>
|
<InputLabel id="cat-label">Категория</InputLabel>
|
||||||
<Select labelId="cat-label" label="Категория" {...field}>
|
<Select labelId="cat-label" label="Категория" {...field}>
|
||||||
<MenuItem value="">
|
|
||||||
<em>Не указано</em>
|
|
||||||
</MenuItem>
|
|
||||||
{(categoriesQuery.data ?? []).map((c: Category) => (
|
{(categoriesQuery.data ?? []).map((c: Category) => (
|
||||||
<MenuItem key={c.id} value={c.id}>
|
<MenuItem key={c.id} value={c.id}>
|
||||||
{c.name}
|
{c.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
{!field.value && <FormHelperText>Выберите категорию</FormHelperText>}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -498,23 +473,6 @@ export function AdminProductsPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={productForm.control}
|
|
||||||
name="inStock"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Switch checked={field.value} onChange={(_, v) => field.onChange(v)} />}
|
|
||||||
label={field.value ? 'В наличии' : 'Под заказ'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{!inStockValue && (
|
|
||||||
<Controller
|
|
||||||
control={productForm.control}
|
|
||||||
name="leadTimeDays"
|
|
||||||
render={({ field }) => <TextField label="Срок исполнения, дней" fullWidth {...field} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -522,7 +480,13 @@ export function AdminProductsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!titleValue.trim() || createMut.isPending || updateMut.isPending}
|
disabled={
|
||||||
|
!titleValue.trim() ||
|
||||||
|
!productForm.watch('categoryId') ||
|
||||||
|
!productForm.watch('quantity').trim() ||
|
||||||
|
createMut.isPending ||
|
||||||
|
updateMut.isPending
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{editing ? 'Сохранить' : 'Создать'}
|
{editing ? 'Сохранить' : 'Создать'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -59,8 +59,6 @@ type FormState = {
|
|||||||
priceRub: string
|
priceRub: string
|
||||||
imageUrls: string[]
|
imageUrls: string[]
|
||||||
published: boolean
|
published: boolean
|
||||||
inStock: boolean
|
|
||||||
leadTimeDays: string
|
|
||||||
categoryId: string
|
categoryId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,13 +67,11 @@ const emptyForm = (): FormState => ({
|
|||||||
slug: '',
|
slug: '',
|
||||||
shortDescription: '',
|
shortDescription: '',
|
||||||
description: '',
|
description: '',
|
||||||
quantity: '',
|
quantity: '0',
|
||||||
materials: '',
|
materials: '',
|
||||||
priceRub: '',
|
priceRub: '',
|
||||||
imageUrls: [],
|
imageUrls: [],
|
||||||
published: true,
|
published: true,
|
||||||
inStock: true,
|
|
||||||
leadTimeDays: '',
|
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -101,7 +97,6 @@ export function AdminPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const titleValue = productForm.watch('title')
|
const titleValue = productForm.watch('title')
|
||||||
const inStockValue = productForm.watch('inStock')
|
|
||||||
|
|
||||||
const categoriesQuery = useQuery({
|
const categoriesQuery = useQuery({
|
||||||
queryKey: ['categories'],
|
queryKey: ['categories'],
|
||||||
@@ -147,13 +142,11 @@ export function AdminPage() {
|
|||||||
slug: p.slug,
|
slug: p.slug,
|
||||||
shortDescription: p.shortDescription ?? '',
|
shortDescription: p.shortDescription ?? '',
|
||||||
description: p.description ?? '',
|
description: p.description ?? '',
|
||||||
quantity: p.quantity === null || p.quantity === undefined ? '' : String(p.quantity),
|
quantity: String(p.quantity),
|
||||||
materials: (p.materials ?? []).join(', '),
|
materials: (p.materials ?? []).join(', '),
|
||||||
priceRub: String(p.priceCents / 100),
|
priceRub: String(p.priceCents / 100),
|
||||||
imageUrls: urls,
|
imageUrls: urls,
|
||||||
published: p.published,
|
published: p.published,
|
||||||
inStock: p.inStock,
|
|
||||||
leadTimeDays: p.leadTimeDays ? String(p.leadTimeDays) : '',
|
|
||||||
categoryId: p.categoryId,
|
categoryId: p.categoryId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -163,14 +156,11 @@ export function AdminPage() {
|
|||||||
const form = productForm.getValues()
|
const form = productForm.getValues()
|
||||||
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
||||||
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
||||||
const leadTimeDays = form.leadTimeDays.trim() ? Number(form.leadTimeDays) : null
|
if (!form.categoryId) throw new Error('Выберите категорию')
|
||||||
if (!form.inStock) {
|
const qty = form.quantity.trim()
|
||||||
if (!Number.isFinite(leadTimeDays) || !leadTimeDays || leadTimeDays <= 0) {
|
if (!qty) throw new Error('Укажите количество')
|
||||||
throw new Error('Если "под заказ", укажите срок исполнения (дней) > 0')
|
const qtyNum = Number(qty)
|
||||||
}
|
if (!Number.isFinite(qtyNum) || qtyNum < 0) throw new Error('Некорректное количество')
|
||||||
}
|
|
||||||
const qty = form.quantity.trim() ? Number(form.quantity) : null
|
|
||||||
if (qty !== null && (!Number.isFinite(qty) || qty < 0)) throw new Error('Некорректное количество')
|
|
||||||
const materials = form.materials
|
const materials = form.materials
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
@@ -180,13 +170,11 @@ export function AdminPage() {
|
|||||||
slug: form.slug.trim() || undefined,
|
slug: form.slug.trim() || undefined,
|
||||||
shortDescription: form.shortDescription.trim() || null,
|
shortDescription: form.shortDescription.trim() || null,
|
||||||
description: form.description.trim() || null,
|
description: form.description.trim() || null,
|
||||||
quantity: qty === null ? null : Math.floor(qty),
|
quantity: Math.floor(qtyNum),
|
||||||
materials,
|
materials,
|
||||||
priceCents,
|
priceCents,
|
||||||
imageUrls: form.imageUrls,
|
imageUrls: form.imageUrls,
|
||||||
published: form.published,
|
published: form.published,
|
||||||
inStock: form.inStock,
|
|
||||||
leadTimeDays: form.inStock ? null : Math.round(leadTimeDays!),
|
|
||||||
categoryId: form.categoryId,
|
categoryId: form.categoryId,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -201,14 +189,11 @@ export function AdminPage() {
|
|||||||
const form = productForm.getValues()
|
const form = productForm.getValues()
|
||||||
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
const priceCents = Math.round(Number(form.priceRub.replace(',', '.')) * 100)
|
||||||
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
if (!Number.isFinite(priceCents) || priceCents < 0) throw new Error('Некорректная цена')
|
||||||
const leadTimeDays = form.leadTimeDays.trim() ? Number(form.leadTimeDays) : null
|
if (!form.categoryId) throw new Error('Выберите категорию')
|
||||||
if (!form.inStock) {
|
const qty = form.quantity.trim()
|
||||||
if (!Number.isFinite(leadTimeDays) || !leadTimeDays || leadTimeDays <= 0) {
|
if (!qty) throw new Error('Укажите количество')
|
||||||
throw new Error('Если "под заказ", укажите срок исполнения (дней) > 0')
|
const qtyNum = Number(qty)
|
||||||
}
|
if (!Number.isFinite(qtyNum) || qtyNum < 0) throw new Error('Некорректное количество')
|
||||||
}
|
|
||||||
const qty = form.quantity.trim() ? Number(form.quantity) : null
|
|
||||||
if (qty !== null && (!Number.isFinite(qty) || qty < 0)) throw new Error('Некорректное количество')
|
|
||||||
const materials = form.materials
|
const materials = form.materials
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
@@ -218,13 +203,11 @@ export function AdminPage() {
|
|||||||
slug: form.slug.trim(),
|
slug: form.slug.trim(),
|
||||||
shortDescription: form.shortDescription.trim() || null,
|
shortDescription: form.shortDescription.trim() || null,
|
||||||
description: form.description.trim() || null,
|
description: form.description.trim() || null,
|
||||||
quantity: qty === null ? null : Math.floor(qty),
|
quantity: Math.floor(qtyNum),
|
||||||
materials,
|
materials,
|
||||||
priceCents,
|
priceCents,
|
||||||
imageUrls: form.imageUrls,
|
imageUrls: form.imageUrls,
|
||||||
published: form.published,
|
published: form.published,
|
||||||
inStock: form.inStock,
|
|
||||||
leadTimeDays: form.inStock ? null : Math.round(leadTimeDays!),
|
|
||||||
categoryId: form.categoryId,
|
categoryId: form.categoryId,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -517,13 +500,7 @@ export function AdminPage() {
|
|||||||
control={productForm.control}
|
control={productForm.control}
|
||||||
name="quantity"
|
name="quantity"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField label="Количество" fullWidth {...field} inputMode="numeric" helperText="0 = нет в наличии" />
|
||||||
label="Количество"
|
|
||||||
fullWidth
|
|
||||||
{...field}
|
|
||||||
inputMode="numeric"
|
|
||||||
helperText="Оставьте пустым, если не хотите вести учёт"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -626,18 +603,16 @@ export function AdminPage() {
|
|||||||
control={productForm.control}
|
control={productForm.control}
|
||||||
name="categoryId"
|
name="categoryId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth error={!field.value}>
|
||||||
<InputLabel id="cat-label">Категория</InputLabel>
|
<InputLabel id="cat-label">Категория</InputLabel>
|
||||||
<Select labelId="cat-label" label="Категория" {...field}>
|
<Select labelId="cat-label" label="Категория" {...field}>
|
||||||
<MenuItem value="">
|
|
||||||
<em>Не указано</em>
|
|
||||||
</MenuItem>
|
|
||||||
{(categoriesQuery.data ?? []).map((c) => (
|
{(categoriesQuery.data ?? []).map((c) => (
|
||||||
<MenuItem key={c.id} value={c.id}>
|
<MenuItem key={c.id} value={c.id}>
|
||||||
{c.name}
|
{c.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
{!field.value && <FormHelperText>Выберите категорию</FormHelperText>}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -651,23 +626,6 @@ export function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={productForm.control}
|
|
||||||
name="inStock"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Switch checked={field.value} onChange={(_, v) => field.onChange(v)} />}
|
|
||||||
label={field.value ? 'В наличии' : 'Под заказ'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{!inStockValue && (
|
|
||||||
<Controller
|
|
||||||
control={productForm.control}
|
|
||||||
name="leadTimeDays"
|
|
||||||
render={({ field }) => <TextField label="Срок исполнения, дней" fullWidth {...field} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -675,7 +633,13 @@ export function AdminPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!titleValue.trim() || createMut.isPending || updateMut.isPending}
|
disabled={
|
||||||
|
!titleValue.trim() ||
|
||||||
|
!productForm.watch('categoryId') ||
|
||||||
|
!productForm.watch('quantity').trim() ||
|
||||||
|
createMut.isPending ||
|
||||||
|
updateMut.isPending
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{editing ? 'Сохранить' : 'Создать'}
|
{editing ? 'Сохранить' : 'Создать'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function CartPage() {
|
|||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{items.map((x) =>
|
{items.map((x) =>
|
||||||
(() => {
|
(() => {
|
||||||
const available = x.product.inStock ? x.product.quantity : 1
|
const available = x.product.quantity
|
||||||
const canInc = x.qty < available
|
const canInc = x.qty < available
|
||||||
const over = x.qty > available
|
const over = x.qty > available
|
||||||
return (
|
return (
|
||||||
@@ -83,9 +83,9 @@ export function CartPage() {
|
|||||||
<Typography color="text.secondary" variant="body2">
|
<Typography color="text.secondary" variant="body2">
|
||||||
{formatPriceRub(x.product.priceCents)} · {x.qty} шт. · Доступно: {available}
|
{formatPriceRub(x.product.priceCents)} · {x.qty} шт. · Доступно: {available}
|
||||||
</Typography>
|
</Typography>
|
||||||
{!x.product.inStock && (
|
{x.product.quantity === 0 && (
|
||||||
<Typography color="text.secondary" variant="caption">
|
<Typography color="text.secondary" variant="caption">
|
||||||
Под заказ — доставка после изготовления
|
Нет в наличии
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{over && (
|
{over && (
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ export function CheckoutPage() {
|
|||||||
const deliveryFeeCents = deliveryType === 'delivery' && items.length > 0 ? 50000 : 0
|
const deliveryFeeCents = deliveryType === 'delivery' && items.length > 0 ? 50000 : 0
|
||||||
const total = itemsSubtotalCents + deliveryFeeCents
|
const total = itemsSubtotalCents + deliveryFeeCents
|
||||||
const addresses = addressesQuery.data?.items ?? []
|
const addresses = addressesQuery.data?.items ?? []
|
||||||
const hasOverLimit = items.some((x) => x.qty > (x.product.inStock ? x.product.quantity : 1))
|
const hasOverLimit = items.some((x) => x.qty > x.product.quantity)
|
||||||
const hasMadeToOrder = items.some((x) => !x.product.inStock)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -105,7 +104,7 @@ export function CheckoutPage() {
|
|||||||
<Typography sx={{ fontWeight: 700, mb: 1 }}>Позиции</Typography>
|
<Typography sx={{ fontWeight: 700, mb: 1 }}>Позиции</Typography>
|
||||||
<Stack spacing={0.5}>
|
<Stack spacing={0.5}>
|
||||||
{items.map((x) => {
|
{items.map((x) => {
|
||||||
const available = x.product.inStock ? x.product.quantity : 1
|
const available = x.product.quantity
|
||||||
const over = x.qty > available
|
const over = x.qty > available
|
||||||
return (
|
return (
|
||||||
<Typography key={x.id} color={over ? 'error' : 'text.primary'}>
|
<Typography key={x.id} color={over ? 'error' : 'text.primary'}>
|
||||||
@@ -127,12 +126,6 @@ export function CheckoutPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasMadeToOrder && (
|
|
||||||
<Alert severity="info">
|
|
||||||
В заказе есть товары «под заказ». Доставка будет после изготовления (срок указан в карточке товара).
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormControl size="small" fullWidth>
|
<FormControl size="small" fullWidth>
|
||||||
<InputLabel id="delivery-type-label">Способ получения</InputLabel>
|
<InputLabel id="delivery-type-label">Способ получения</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export type UseProductFiltersResult = ReturnType<typeof useProductFilters>
|
|||||||
|
|
||||||
export function useProductFilters() {
|
export function useProductFilters() {
|
||||||
const [categorySlug, setCategorySlug] = useState<string>('')
|
const [categorySlug, setCategorySlug] = useState<string>('')
|
||||||
const [availability, setAvailability] = useState<'all' | 'in_stock' | 'made_to_order'>('all')
|
|
||||||
const [qInput, setQInput] = useState('')
|
const [qInput, setQInput] = useState('')
|
||||||
const [q, setQ] = useState('')
|
const [q, setQ] = useState('')
|
||||||
const [moreOpen, setMoreOpen] = useState(false)
|
const [moreOpen, setMoreOpen] = useState(false)
|
||||||
@@ -45,13 +44,6 @@ export function useProductFilters() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAvailabilityChange = (v: string) => {
|
|
||||||
if (v === 'all' || v === 'in_stock' || v === 'made_to_order') {
|
|
||||||
setAvailability(v)
|
|
||||||
setPage(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePriceMinChange = (v: string) => {
|
const handlePriceMinChange = (v: string) => {
|
||||||
setPriceMinRub(v)
|
setPriceMinRub(v)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@@ -68,7 +60,6 @@ export function useProductFilters() {
|
|||||||
|
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
setCategorySlug('')
|
setCategorySlug('')
|
||||||
setAvailability('all')
|
|
||||||
setQInput('')
|
setQInput('')
|
||||||
setSort('')
|
setSort('')
|
||||||
setPriceMinRub('')
|
setPriceMinRub('')
|
||||||
@@ -85,7 +76,6 @@ export function useProductFilters() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
categorySlug,
|
categorySlug,
|
||||||
availability,
|
|
||||||
qInput,
|
qInput,
|
||||||
q,
|
q,
|
||||||
moreOpen,
|
moreOpen,
|
||||||
@@ -101,7 +91,6 @@ export function useProductFilters() {
|
|||||||
handleCategoryChange,
|
handleCategoryChange,
|
||||||
handleSortChange,
|
handleSortChange,
|
||||||
handlePageSizeChange,
|
handlePageSizeChange,
|
||||||
handleAvailabilityChange,
|
|
||||||
handlePriceMinChange,
|
handlePriceMinChange,
|
||||||
handlePriceMaxChange,
|
handlePriceMaxChange,
|
||||||
handleCardScaleChange,
|
handleCardScaleChange,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export function HomePage() {
|
|||||||
'public',
|
'public',
|
||||||
{
|
{
|
||||||
categorySlug: filters.categorySlug || 'all',
|
categorySlug: filters.categorySlug || 'all',
|
||||||
availability: filters.availability,
|
|
||||||
q: filters.q,
|
q: filters.q,
|
||||||
sort: filters.sort,
|
sort: filters.sort,
|
||||||
page: filters.page,
|
page: filters.page,
|
||||||
@@ -45,7 +44,6 @@ export function HomePage() {
|
|||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
fetchPublicProducts({
|
fetchPublicProducts({
|
||||||
categorySlug: filters.categorySlug || undefined,
|
categorySlug: filters.categorySlug || undefined,
|
||||||
availability: filters.availability === 'all' ? undefined : filters.availability,
|
|
||||||
q: filters.q || undefined,
|
q: filters.q || undefined,
|
||||||
sort: filters.sort || '',
|
sort: filters.sort || '',
|
||||||
page: filters.page,
|
page: filters.page,
|
||||||
@@ -117,9 +115,7 @@ export function HomePage() {
|
|||||||
<ProductCard
|
<ProductCard
|
||||||
product={p}
|
product={p}
|
||||||
mediaHeight={mediaHeight}
|
mediaHeight={mediaHeight}
|
||||||
actions={
|
actions={!isAdmin && p.quantity > 0 ? <ToggleCartIcon productId={p.id} /> : undefined}
|
||||||
!isAdmin && !(p.inStock && p.quantity === 0) ? <ToggleCartIcon productId={p.id} /> : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type Props = UseProductFiltersResult & {
|
|||||||
|
|
||||||
export function ProductFilters({
|
export function ProductFilters({
|
||||||
categorySlug,
|
categorySlug,
|
||||||
availability,
|
|
||||||
qInput,
|
qInput,
|
||||||
moreOpen,
|
moreOpen,
|
||||||
sort,
|
sort,
|
||||||
@@ -41,19 +40,14 @@ export function ProductFilters({
|
|||||||
handleCategoryChange,
|
handleCategoryChange,
|
||||||
handleSortChange,
|
handleSortChange,
|
||||||
handlePageSizeChange,
|
handlePageSizeChange,
|
||||||
handleAvailabilityChange,
|
|
||||||
handlePriceMinChange,
|
handlePriceMinChange,
|
||||||
handlePriceMaxChange,
|
handlePriceMaxChange,
|
||||||
handleCardScaleChange,
|
handleCardScaleChange,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const categoriesForFilter = useMemo(() => {
|
const categoriesForFilter = useMemo(() => {
|
||||||
const list = categories ?? []
|
const list = (categories ?? []).filter((c) => c.slug !== 'ne-ukazano')
|
||||||
return [...list].sort((a, b) => {
|
return [...list].sort((a, b) => a.sort - b.sort || a.name.localeCompare(b.name, 'ru'))
|
||||||
if (a.slug === 'ne-ukazano') return 1
|
|
||||||
if (b.slug === 'ne-ukazano') return -1
|
|
||||||
return a.sort - b.sort || a.name.localeCompare(b.name, 'ru')
|
|
||||||
})
|
|
||||||
}, [categories])
|
}, [categories])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -124,26 +118,6 @@ export function ProductFilters({
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<ToggleButtonGroup
|
|
||||||
exclusive
|
|
||||||
size="small"
|
|
||||||
value={availability}
|
|
||||||
onChange={(_, v) => handleAvailabilityChange(v)}
|
|
||||||
sx={{
|
|
||||||
alignSelf: { xs: 'flex-start', sm: 'auto' },
|
|
||||||
'& .MuiToggleButton-root': { px: 1.5, fontWeight: 600, textTransform: 'none' },
|
|
||||||
'& .MuiToggleButton-root.Mui-selected': {
|
|
||||||
bgcolor: 'primary.main',
|
|
||||||
color: 'primary.contrastText',
|
|
||||||
'&:hover': { bgcolor: 'primary.dark' },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ToggleButton value="all">Все</ToggleButton>
|
|
||||||
<ToggleButton value="in_stock">В наличии</ToggleButton>
|
|
||||||
<ToggleButton value="made_to_order">Под заказ</ToggleButton>
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ export function ProductPage() {
|
|||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||||
{p.category?.name && <Chip label={p.category.name} />}
|
{p.category?.name && <Chip label={p.category.name} />}
|
||||||
<Chip label={p.inStock ? 'В наличии' : `Под заказ · ${p.leadTimeDays ?? '—'} дн.`} color="default" />
|
{p.quantity > 0 && <Chip label="В наличии" color="success" />}
|
||||||
|
{p.quantity === 0 && <Chip label="Нет в наличии" color="default" />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{(p.materials?.length ?? 0) > 0 && (
|
{(p.materials?.length ?? 0) > 0 && (
|
||||||
@@ -154,13 +155,7 @@ export function ProductPage() {
|
|||||||
{formatPriceRub(p.priceCents)}
|
{formatPriceRub(p.priceCents)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{!isAdmin && !(p.inStock && p.quantity === 0) ? <ToggleCartIcon productId={p.id} size="medium" /> : null}
|
{!isAdmin && p.quantity > 0 ? <ToggleCartIcon productId={p.id} size="medium" /> : null}
|
||||||
|
|
||||||
{!p.inStock && (
|
|
||||||
<Alert severity="info">
|
|
||||||
Этот товар изготавливается под заказ. Доставка будет после изготовления (~{p.leadTimeDays ?? '—'} дн.).
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{p.description ? (
|
{p.description ? (
|
||||||
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{p.description}</Typography>
|
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{p.description}</Typography>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- RedefineProductTable
|
||||||
|
-- Set quantity = 0 for made-to-order products before dropping inStock
|
||||||
|
UPDATE Product SET quantity = 0 WHERE inStock = 0;
|
||||||
|
|
||||||
|
-- Drop inStock and leadTimeDays columns
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Product" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"shortDescription" TEXT,
|
||||||
|
"description" TEXT,
|
||||||
|
"quantity" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"materials" TEXT NOT NULL DEFAULT '[]',
|
||||||
|
"priceCents" INTEGER NOT NULL,
|
||||||
|
"imageUrl" TEXT,
|
||||||
|
"published" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"categoryId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Product" ("createdAt", "description", "id", "imageUrl", "materials", "priceCents", "published", "quantity", "shortDescription", "slug", "title", "updatedAt", "categoryId") SELECT "createdAt", "description", "id", "imageUrl", "materials", "priceCents", "published", "quantity", "shortDescription", "slug", "title", "updatedAt", "categoryId" FROM "Product";
|
||||||
|
DROP TABLE "Product";
|
||||||
|
ALTER TABLE "new_Product" RENAME TO "Product";
|
||||||
|
CREATE UNIQUE INDEX "Product_slug_key" ON "Product"("slug");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
@@ -30,8 +30,6 @@ model Product {
|
|||||||
priceCents Int
|
priceCents Int
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
published Boolean @default(false)
|
published Boolean @default(false)
|
||||||
inStock Boolean @default(true)
|
|
||||||
leadTimeDays Int?
|
|
||||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
|
||||||
categoryId String
|
categoryId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getOrCreateUnspecifiedCategory } from '../../lib/default-category.js'
|
|
||||||
import { upsertGalleryImagesByUrls } from '../../lib/gallery.js'
|
import { upsertGalleryImagesByUrls } from '../../lib/gallery.js'
|
||||||
import { prisma } from '../../lib/prisma.js'
|
import { prisma } from '../../lib/prisma.js'
|
||||||
import {
|
import {
|
||||||
@@ -11,15 +10,13 @@ import { persistMultipartImages } from '../../lib/upload-images.js'
|
|||||||
const CREATE_PRODUCT_SCHEMA = {
|
const CREATE_PRODUCT_SCHEMA = {
|
||||||
body: {
|
body: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['title', 'priceCents'],
|
required: ['title', 'priceCents', 'quantity', 'categoryId'],
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: 'string', minLength: 1 },
|
title: { type: 'string', minLength: 1 },
|
||||||
slug: { type: 'string' },
|
slug: { type: 'string' },
|
||||||
categoryId: { type: 'string' },
|
categoryId: { type: 'string', minLength: 1 },
|
||||||
priceCents: { type: 'number', minimum: 0 },
|
priceCents: { type: 'number', minimum: 0 },
|
||||||
quantity: { type: 'number', minimum: 0, nullable: true },
|
quantity: { type: 'number', minimum: 0 },
|
||||||
inStock: { type: 'boolean' },
|
|
||||||
leadTimeDays: { type: 'number', minimum: 1, nullable: true },
|
|
||||||
shortDescription: { type: 'string', nullable: true },
|
shortDescription: { type: 'string', nullable: true },
|
||||||
description: { type: 'string', nullable: true },
|
description: { type: 'string', nullable: true },
|
||||||
materials: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
|
materials: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
|
||||||
@@ -36,11 +33,9 @@ const PATCH_PRODUCT_SCHEMA = {
|
|||||||
properties: {
|
properties: {
|
||||||
title: { type: 'string', minLength: 1 },
|
title: { type: 'string', minLength: 1 },
|
||||||
slug: { type: 'string' },
|
slug: { type: 'string' },
|
||||||
categoryId: { type: 'string' },
|
categoryId: { type: 'string', minLength: 1 },
|
||||||
priceCents: { type: 'number', minimum: 0 },
|
priceCents: { type: 'number', minimum: 0 },
|
||||||
quantity: { type: 'number', minimum: 0, nullable: true },
|
quantity: { type: 'number', minimum: 0 },
|
||||||
inStock: { type: 'boolean' },
|
|
||||||
leadTimeDays: { type: 'number', minimum: 1, nullable: true },
|
|
||||||
shortDescription: { type: 'string', nullable: true },
|
shortDescription: { type: 'string', nullable: true },
|
||||||
description: { type: 'string', nullable: true },
|
description: { type: 'string', nullable: true },
|
||||||
materials: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
|
materials: { anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }] },
|
||||||
@@ -101,52 +96,33 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const slug = String(body.slug ?? '').trim() || request.server.slugify(title) || `item-${Date.now()}`
|
const slug = String(body.slug ?? '').trim() || request.server.slugify(title) || `item-${Date.now()}`
|
||||||
let categoryId = String(body.categoryId ?? '').trim()
|
const categoryId = String(body.categoryId ?? '').trim()
|
||||||
if (!categoryId) {
|
if (!categoryId) {
|
||||||
categoryId = (await getOrCreateUnspecifiedCategory()).id
|
reply.code(400).send({ error: 'Укажите категорию' })
|
||||||
} else {
|
return
|
||||||
const cat = await prisma.category.findUnique({ where: { id: categoryId } })
|
}
|
||||||
if (!cat) {
|
const cat = await prisma.category.findUnique({ where: { id: categoryId } })
|
||||||
reply.code(400).send({ error: 'Категория не найдена' })
|
if (!cat) {
|
||||||
return
|
reply.code(400).send({ error: 'Категория не найдена' })
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
const priceCents = Number(body.priceCents)
|
const priceCents = Number(body.priceCents)
|
||||||
if (!Number.isFinite(priceCents) || priceCents < 0) {
|
if (!Number.isFinite(priceCents) || priceCents < 0) {
|
||||||
reply.code(400).send({ error: 'Некорректная цена (priceCents ≥ 0)' })
|
reply.code(400).send({ error: 'Некорректная цена (priceCents ≥ 0)' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const inStock = body.inStock === undefined || body.inStock === null ? true : Boolean(body.inStock)
|
|
||||||
const leadTimeDaysRaw = body.leadTimeDays
|
|
||||||
const leadTimeDays =
|
|
||||||
leadTimeDaysRaw === undefined || leadTimeDaysRaw === null || leadTimeDaysRaw === '' ? null : Number(leadTimeDaysRaw)
|
|
||||||
if (!inStock) {
|
|
||||||
if (!Number.isFinite(leadTimeDays) || leadTimeDays <= 0) {
|
|
||||||
reply.code(400).send({ error: 'Если "под заказ", укажите срок исполнения (дней) > 0' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const exists = await prisma.product.findUnique({ where: { slug } })
|
const exists = await prisma.product.findUnique({ where: { slug } })
|
||||||
if (exists) {
|
if (exists) {
|
||||||
reply.code(409).send({ error: 'Такой slug уже занят' })
|
reply.code(409).send({ error: 'Такой slug уже занят' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let quantity = 0
|
const n = Number(body.quantity)
|
||||||
if (!inStock) {
|
if (!Number.isFinite(n) || n < 0) {
|
||||||
quantity = 1
|
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
||||||
} else {
|
return
|
||||||
if (body.quantity === undefined || body.quantity === null || body.quantity === '') {
|
|
||||||
reply.code(400).send({ error: 'Укажите количество' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const n = Number(body.quantity)
|
|
||||||
if (!Number.isFinite(n) || n < 0) {
|
|
||||||
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
quantity = Math.floor(n)
|
|
||||||
}
|
}
|
||||||
|
const quantity = Math.floor(n)
|
||||||
|
|
||||||
const product = await prisma.product.create({
|
const product = await prisma.product.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -159,8 +135,6 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
priceCents: Math.round(priceCents),
|
priceCents: Math.round(priceCents),
|
||||||
imageUrl: body.imageUrl ? String(body.imageUrl) : null,
|
imageUrl: body.imageUrl ? String(body.imageUrl) : null,
|
||||||
published: Boolean(body.published),
|
published: Boolean(body.published),
|
||||||
inStock,
|
|
||||||
leadTimeDays: inStock ? null : Math.round(leadTimeDays),
|
|
||||||
categoryId,
|
categoryId,
|
||||||
images: Array.isArray(body.imageUrls)
|
images: Array.isArray(body.imageUrls)
|
||||||
? {
|
? {
|
||||||
@@ -209,12 +183,7 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
data.description = body.description ? String(body.description) : null
|
data.description = body.description ? String(body.description) : null
|
||||||
}
|
}
|
||||||
if (body.quantity !== undefined) {
|
if (body.quantity !== undefined) {
|
||||||
const v = body.quantity
|
const n = Number(body.quantity)
|
||||||
if (v === null || v === '') {
|
|
||||||
reply.code(400).send({ error: 'Укажите количество' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const n = Number(v)
|
|
||||||
if (!Number.isFinite(n) || n < 0) {
|
if (!Number.isFinite(n) || n < 0) {
|
||||||
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
reply.code(400).send({ error: 'Некорректное количество (quantity ≥ 0)' })
|
||||||
return
|
return
|
||||||
@@ -237,42 +206,17 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
}
|
}
|
||||||
if (body.published !== undefined) data.published = Boolean(body.published)
|
if (body.published !== undefined) data.published = Boolean(body.published)
|
||||||
if (body.categoryId !== undefined) {
|
if (body.categoryId !== undefined) {
|
||||||
const raw = body.categoryId
|
const cid = String(body.categoryId).trim()
|
||||||
if (raw === null || raw === '') {
|
if (!cid) {
|
||||||
data.categoryId = (await getOrCreateUnspecifiedCategory()).id
|
reply.code(400).send({ error: 'Укажите категорию' })
|
||||||
} else {
|
|
||||||
const cid = String(raw).trim()
|
|
||||||
const cat = await prisma.category.findUnique({ where: { id: cid } })
|
|
||||||
if (!cat) {
|
|
||||||
reply.code(400).send({ error: 'Категория не найдена' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.categoryId = cid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.inStock !== undefined) data.inStock = Boolean(body.inStock)
|
|
||||||
if (body.leadTimeDays !== undefined) {
|
|
||||||
const v = body.leadTimeDays
|
|
||||||
const n = v === null || v === '' ? null : Number(v)
|
|
||||||
if (n !== null && (!Number.isFinite(n) || n <= 0)) {
|
|
||||||
reply.code(400).send({ error: 'Срок исполнения должен быть числом дней > 0' })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.leadTimeDays = n === null ? null : Math.round(n)
|
const cat = await prisma.category.findUnique({ where: { id: cid } })
|
||||||
}
|
if (!cat) {
|
||||||
|
reply.code(400).send({ error: 'Категория не найдена' })
|
||||||
const nextInStock = data.inStock ?? existing.inStock
|
return
|
||||||
const nextLead = data.leadTimeDays ?? existing.leadTimeDays
|
}
|
||||||
if (!nextInStock && (!Number.isFinite(nextLead) || nextLead === null || nextLead <= 0)) {
|
data.categoryId = cid
|
||||||
reply.code(400).send({ error: 'Если "под заказ", укажите срок исполнения (дней) > 0' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (nextInStock && data.leadTimeDays !== undefined) {
|
|
||||||
data.leadTimeDays = null
|
|
||||||
}
|
|
||||||
if (!nextInStock) {
|
|
||||||
data.quantity = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const imagesUpdate =
|
const imagesUpdate =
|
||||||
@@ -312,4 +256,3 @@ export async function registerAdminProductRoutes(fastify) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const PUBLIC_PRODUCTS_QUERY_SCHEMA = {
|
|||||||
properties: {
|
properties: {
|
||||||
categorySlug: { type: 'string' },
|
categorySlug: { type: 'string' },
|
||||||
q: { type: 'string' },
|
q: { type: 'string' },
|
||||||
availability: { type: 'string', enum: ['all', 'in_stock', 'made_to_order'] },
|
|
||||||
sort: { type: 'string', enum: ['', 'price_asc', 'price_desc'] },
|
sort: { type: 'string', enum: ['', 'price_asc', 'price_desc'] },
|
||||||
page: { type: 'integer', minimum: 1 },
|
page: { type: 'integer', minimum: 1 },
|
||||||
pageSize: { type: 'integer', minimum: 1, maximum: 100 },
|
pageSize: { type: 'integer', minimum: 1, maximum: 100 },
|
||||||
@@ -84,8 +83,6 @@ export async function registerPublicCatalogRoutes(fastify) {
|
|||||||
const { categorySlug } = request.query
|
const { categorySlug } = request.query
|
||||||
const qRaw = request.query?.q
|
const qRaw = request.query?.q
|
||||||
const q = typeof qRaw === 'string' ? qRaw.trim() : ''
|
const q = typeof qRaw === 'string' ? qRaw.trim() : ''
|
||||||
const availabilityRaw = request.query?.availability
|
|
||||||
const availability = typeof availabilityRaw === 'string' ? availabilityRaw.trim() : ''
|
|
||||||
|
|
||||||
const sortRaw = request.query?.sort
|
const sortRaw = request.query?.sort
|
||||||
const sort = typeof sortRaw === 'string' ? sortRaw : ''
|
const sort = typeof sortRaw === 'string' ? sortRaw : ''
|
||||||
@@ -113,14 +110,6 @@ export async function registerPublicCatalogRoutes(fastify) {
|
|||||||
if (q) {
|
if (q) {
|
||||||
where.OR = [{ title: { contains: q } }, { shortDescription: { contains: q } }]
|
where.OR = [{ title: { contains: q } }, { shortDescription: { contains: q } }]
|
||||||
}
|
}
|
||||||
if (availability === 'in_stock') {
|
|
||||||
where.inStock = true
|
|
||||||
where.quantity = { gt: 0 }
|
|
||||||
} else if (availability === 'made_to_order') {
|
|
||||||
where.inStock = false
|
|
||||||
} else if (availability && availability !== 'all') {
|
|
||||||
return reply.code(400).send({ error: 'availability должен быть all | in_stock | made_to_order' })
|
|
||||||
}
|
|
||||||
const applyPriceFilter = !(priceMin !== null && priceMax !== null && priceMin === 0 && priceMax === 0)
|
const applyPriceFilter = !(priceMin !== null && priceMax !== null && priceMin === 0 && priceMax === 0)
|
||||||
|
|
||||||
if (applyPriceFilter && (priceMin !== null || priceMax !== null)) {
|
if (applyPriceFilter && (priceMin !== null || priceMax !== null)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user