Compare commits

...

2 Commits

Author SHA1 Message Date
sdarbinyan
ffde301181 dosc changed 2026-02-20 11:01:15 +04:00
sdarbinyan
f7919f6ca7 integration 2026-02-20 10:43:47 +04:00
11 changed files with 87 additions and 1 deletions

4
API.md
View File

@@ -229,6 +229,7 @@ Response 200:
"priority": 1,
"quantity": 50,
"price": 1299,
"discount": 10,
"currency": "USD",
"imgs": ["https://...", "https://..."],
"tags": ["new", "featured"],
@@ -285,6 +286,7 @@ Body:
"priority": 10,
"quantity": 100,
"price": 999,
"discount": 0, // 0100 (percentage off price)
"currency": "USD", // USD | EUR | RUB | GBP | UAH
"imgs": ["https://..."],
"tags": ["new"],
@@ -318,6 +320,7 @@ Body: (any subset of fields)
{
"name": "Updated Name",
"price": 899,
"discount": 15,
"quantity": 80,
"visible": false
}
@@ -389,6 +392,7 @@ Response 201:
- `currency` supported values: `USD`, `EUR`, `RUB`, `GBP`, `UAH`.
- `badges`: optional string array. Predefined values with UI colors: `new`, `sale`, `exclusive`, `hot`, `limited`, `bestseller`, `featured`. Custom strings are also allowed.
- `imgs`: always send the **complete** array on update, not individual images.
- `discount`: integer `0``100` representing a percentage discount. `0` means no discount. The discounted price is calculated as `price * (1 - discount / 100)`.
- `description`: array of `{ key, value }` pairs - free-form attributes per item.
- `translations`: optional object keyed by language code (`"ru"`, `"en"`, etc.) — each value may contain `name`, `simpleDescription`, `description[]`. The marketplace frontend should use these when rendering in the corresponding language, falling back to the default fields if a translation is absent.
- Auto-save from the backoffice fires `PATCH` with a single field every ~500 ms.

View File

@@ -238,6 +238,7 @@ Query-параметры:
"priority": 1,
"quantity": 50,
"price": 1299,
"discount": 10,
"currency": "USD",
"imgs": ["https://...", "https://..."],
"tags": ["new", "featured"],
@@ -297,6 +298,7 @@ POST /api/subcategories/:subcategoryId/items
"priority": 10,
"quantity": 100,
"price": 999,
"discount": 0, // 0100 (процент скидки)
"currency": "USD", // USD | EUR | RUB | GBP | UAH
"imgs": ["https://..."],
"tags": ["new"],
@@ -330,6 +332,7 @@ PATCH /api/items/:itemId
{
"name": "Новое название",
"price": 899,
"discount": 15,
"quantity": 80,
"visible": false,
"translations": {
@@ -446,6 +449,7 @@ GET /api/subcategories/:subcategoryId/items?lang=ru&page=1
- Поддерживаемые значения `currency`: `USD`, `EUR`, `RUB`, `GBP`, `UAH`.
- `badges`: необязательный массив строк. Стандартные значения с цветами в интерфейсе: `new`, `sale`, `exclusive`, `hot`, `limited`, `bestseller`, `featured`. Свои строки тоже допустимы.
- `imgs`: при обновлении всегда передавай **полный** массив, не отдельные изображения.
- `discount`: целое число `0``100` — процент скидки. `0` означает отсутствие скидки. Цена со скидкой вычисляется как `price * (1 - discount / 100)`.
- `description`: массив пар `{ key, value }` — свободные атрибуты товара.
- Автосохранение из бэкофиса отправляет `PATCH` с одним полем каждые ~500 мс.

View File

@@ -53,6 +53,7 @@ export const TRANSLATIONS: Record<string, Record<string, string>> = {
// --- Item basic fields ---
ITEM_NAME: 'Item Name',
PRICE: 'Price',
DISCOUNT: 'Discount (%)',
QUANTITY: 'Quantity',
CURRENCY: 'Currency',
SIMPLE_DESCRIPTION: 'Simple Description',
@@ -169,6 +170,7 @@ export const TRANSLATIONS: Record<string, Record<string, string>> = {
// --- Item basic fields ---
ITEM_NAME: 'Название товара',
PRICE: 'Цена',
DISCOUNT: 'Скидка (%)',
QUANTITY: 'Количество',
CURRENCY: 'Валюта',
SIMPLE_DESCRIPTION: 'Краткое описание',

View File

@@ -14,6 +14,7 @@ export interface Item {
priority: number;
quantity: number;
price: number;
discount: number;
currency: string;
imgs: string[];
tags: string[];

View File

@@ -107,6 +107,25 @@
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>{{ 'DISCOUNT' | translate }}</mat-label>
<input
matInput
type="number"
step="1"
[(ngModel)]="item()!.discount"
(blur)="onFieldChange('discount', item()!.discount)"
min="0"
max="100"
placeholder="0">
<span matSuffix>%</span>
@if (item()!.discount < 0 || item()!.discount > 100) {
<mat-error>Discount must be 0100%</mat-error>
}
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'SIMPLE_DESCRIPTION' | translate }}</mat-label>
<textarea

View File

@@ -72,7 +72,13 @@
<h1 class="item-name">{{ item.name }}</h1>
<div class="price-row">
@if (item.discount > 0) {
<span class="price-old">{{ item.price | number:'1.2-2' }} {{ item.currency }}</span>
<span class="price">{{ item.price * (1 - item.discount / 100) | number:'1.2-2' }} {{ item.currency }}</span>
<span class="discount-tag">-{{ item.discount }}%</span>
} @else {
<span class="price">{{ item.price | number:'1.2-2' }} {{ item.currency }}</span>
}
@if (item.quantity > 0) {
<span class="in-stock">
<mat-icon>check_circle</mat-icon>

View File

@@ -195,6 +195,22 @@
color: #1976d2;
}
.price-old {
font-size: 1.1rem;
color: #999;
text-decoration: line-through;
}
.discount-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
background: #e53935;
color: #fff;
font-size: 0.85rem;
font-weight: 600;
}
.in-stock, .out-of-stock {
display: flex;
align-items: center;

View File

@@ -98,6 +98,9 @@
<div class="item-details">
<span class="price">{{ item.price }} {{ item.currency }}</span>
@if (item.discount > 0) {
<span class="discount-chip">-{{ item.discount }}%</span>
}
<span class="quantity">{{ 'QTY' | translate }}: {{ item.quantity }}</span>
</div>

View File

@@ -226,6 +226,16 @@
color: #1976d2;
}
.discount-chip {
display: inline-block;
padding: 1px 6px;
border-radius: 4px;
background: #e53935;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
}
.quantity {
color: #666;
}

View File

@@ -90,6 +90,7 @@ export class MockDataService {
priority: 1,
quantity: 50,
price: 1299,
discount: 0,
currency: 'USD',
imgs: [
'https://via.placeholder.com/600x400?text=iPhone+Front',
@@ -121,6 +122,7 @@ export class MockDataService {
priority: 2,
quantity: 35,
price: 1199,
discount: 10,
currency: 'USD',
imgs: ['https://via.placeholder.com/600x400?text=Samsung+S24'],
tags: ['new', 'android'],
@@ -140,6 +142,7 @@ export class MockDataService {
priority: 3,
quantity: 20,
price: 999,
discount: 15,
currency: 'USD',
imgs: ['https://via.placeholder.com/600x400?text=Pixel+8'],
tags: ['sale', 'android', 'ai'],
@@ -158,6 +161,7 @@ export class MockDataService {
priority: 1,
quantity: 15,
price: 2499,
discount: 0,
currency: 'USD',
imgs: ['https://via.placeholder.com/600x400?text=MacBook'],
tags: ['featured', 'professional'],
@@ -177,6 +181,7 @@ export class MockDataService {
priority: 2,
quantity: 0,
price: 1799,
discount: 5,
currency: 'USD',
imgs: ['https://via.placeholder.com/600x400?text=Dell+XPS'],
tags: ['out-of-stock'],
@@ -200,6 +205,7 @@ export class MockDataService {
priority: i,
quantity: Math.floor(Math.random() * 100),
price: Math.floor(Math.random() * 1000) + 100,
discount: Math.random() > 0.7 ? Math.floor(Math.random() * 30) + 5 : 0,
currency: 'USD',
imgs: [`https://via.placeholder.com/600x400?text=Product+${i}`],
tags: ['test'],
@@ -404,6 +410,7 @@ export class MockDataService {
priority: data.priority || 99,
quantity: data.quantity || 0,
price: data.price || 0,
discount: data.discount || 0,
currency: data.currency || 'USD',
imgs: data.imgs || [],
tags: data.tags || [],

View File

@@ -54,6 +54,15 @@ export class ValidationService {
return null;
}
validateDiscount(value: any): string | null {
if (value === undefined || value === null || value === '') return null;
const num = Number(value);
if (isNaN(num)) return 'Discount must be a number';
if (num < 0) return 'Discount cannot be negative';
if (num > 100) return 'Discount cannot exceed 100%';
return null;
}
validateUrl(value: string): string | null {
if (!value || value.trim().length === 0) {
return null; // Optional field
@@ -131,6 +140,11 @@ export class ValidationService {
if (quantityError) errors['quantity'] = quantityError;
}
if (item['discount'] !== undefined) {
const discountError = this.validateDiscount(item['discount']);
if (discountError) errors['discount'] = discountError;
}
if (item['currency'] !== undefined) {
const currencyError = this.validateCurrency(item['currency']);
if (currencyError) errors['currency'] = currencyError;