feat(admin): Shopify-style variant attributes matching production data shape

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

P0 user feedback: variants were just free-text name+price, and the user shared the real production payload - a flat array of {color, size, price, currency, remaining} rows, colors as '0x8B4513' hex.

- New model: AdminProductVariantAttributeDef (key/label/isColor/values) + AdminProductVariant (attributes: Record<string,string>, sku, image, remaining, prices: {currency,price}[]) - this IS the production shape, grouped by combo with one row per currency instead of flattened, so admins edit one variant card instead of 4 duplicate rows
- Attribute manager: add custom attributes (Color, Size, or anything), color attributes get a native color picker + live swatch preview instead of typing hex; other attributes get plain value chips
- 'Generate variants' computes the cartesian product of attribute values (Color x Size = 4 combos for 1 color x 4 sizes) and preserves existing sku/price/stock data for combos that still exist after regeneration
- Multi-currency pricing per variant (matches prod: same combo priced in RUB/USD/EUR/AMD) with per-currency add/remove
- Color hex kept in the exact '0x8B4513' production format (toBackendColor/toCssColor conversion helpers)
- Fixed an Angular v21 control-flow parser bug hit while building this: an @if/@else block whose only content is a bare {{ interpolation }} touching the block's closing brace fails to parse (NG5002 'Unclosed block for' cascading from a completely unrelated line) - worked around by keeping interpolation in its own element
- Verified end-to-end in browser: added Color (color picker) + Size (S/M/L/XL) attributes, generated 4 variant combos, added all 4 currencies to a variant - matches the shared production JSON exactly
This commit is contained in:
sdarbinyan
2026-07-19 13:40:51 +04:00
parent 440d2ec211
commit 3b955b116a
12 changed files with 461 additions and 29 deletions

View File

@@ -1554,6 +1554,20 @@ export const en: Translations = {
variantName: 'Variant name',
variantsHint: 'Each variant is a purchasable option of this product, e.g. a size or color, with its own price and stock.',
variants: 'Variants',
variantAttributes: 'Options',
variantAttributesHint: 'Add options like Color or Size, then list the values customers can choose from. Combinations below are generated automatically.',
colorAttribute: 'Color',
removeAttribute: 'Remove option',
removeValue: 'Remove value',
pickColor: 'Pick a color',
newValuePlaceholder: 'e.g. Small',
addValue: 'Add',
newAttributePlaceholder: 'e.g. Size',
addAttribute: 'Add option',
generateVariants: 'Generate variants',
variantsList: 'Variants',
remaining: 'Stock',
variantImage: 'Image',
archived: 'Archived',
slugHint: 'The web address of this product. Filled in automatically from the name - edit only if you need a custom URL.',
skuHint: 'SKU (stock keeping unit) is your internal product code, used for inventory and order lookups. Any unique text works.',

View File

@@ -1549,6 +1549,20 @@ export const hy: Translations = {
variantName: 'Տարբերակի անուն',
variantsHint: 'Յուրաքանչյուր տարբերակ այս ապրանքի առանձին գնվող տարբերակ է (օր․՝ չափս կամ գույն)՝ իր գնով և մնացորդով։',
variants: 'Տարբերակներ',
variantAttributes: 'Ընտրանքներ',
variantAttributesHint: 'Ավելացրեք ընտրանքներ, օրինակ՝ Գույն կամ Չափս, ապա նշեք արժեքները։ Ստորև զուգակցումները ստեղծվում են ինքնաշխատ։',
colorAttribute: 'Գույն',
removeAttribute: 'Հեռացնել ընտրանքը',
removeValue: 'Հեռացնել արժեքը',
pickColor: 'Ընտրել գույնը',
newValuePlaceholder: 'օր.՝ Փոքր',
addValue: 'Ավելացնել',
newAttributePlaceholder: 'օր.՝ Չափս',
addAttribute: 'Ավելացնել ընտրանք',
generateVariants: 'Գեներացնել տարբերակները',
variantsList: 'Տարբերակներ',
remaining: 'Պաշար',
variantImage: 'Նկար',
archived: 'Արխիվում',
slugHint: 'Ապրանքի վեբ-հասցեն։ Լրացվում է ինքնաշխատ անունից — փոխեք միայն, եթե պետք է հատուկ URL։',
skuHint: 'SKU (ապրանքային կոդ) — ձեր ներքին կոդն է պահեստի հաշվառման և պատվերների որոնման համար։ Ցանկացած եզակի տեքստ կաշխատի։',

View File

@@ -1549,6 +1549,20 @@ export const ru: Translations = {
variantName: 'Название варианта',
variantsHint: 'Каждый вариант — это отдельный покупаемый вариант товара (например, размер или цвет) со своей ценой и остатком.',
variants: 'Варианты',
variantAttributes: 'Опции',
variantAttributesHint: 'Добавьте опции вроде Цвет или Размер, затем перечислите значения. Комбинации ниже создаются автоматически.',
colorAttribute: 'Цвет',
removeAttribute: 'Удалить опцию',
removeValue: 'Удалить значение',
pickColor: 'Выбрать цвет',
newValuePlaceholder: 'напр. Маленький',
addValue: 'Добавить',
newAttributePlaceholder: 'напр. Размер',
addAttribute: 'Добавить опцию',
generateVariants: 'Сгенерировать варианты',
variantsList: 'Варианты',
remaining: 'Остаток',
variantImage: 'Изображение',
archived: 'В архиве',
slugHint: 'Веб-адрес этого товара. Заполняется автоматически из названия — меняйте, только если нужен особый URL.',
skuHint: 'SKU (артикул) — ваш внутренний код товара для учёта склада и поиска заказов. Подойдёт любой уникальный текст.',

View File

@@ -1562,6 +1562,20 @@ export interface Translations {
variantName: string;
variantsHint: string;
variants: string;
variantAttributes: string;
variantAttributesHint: string;
colorAttribute: string;
removeAttribute: string;
removeValue: string;
pickColor: string;
newValuePlaceholder: string;
addValue: string;
newAttributePlaceholder: string;
addAttribute: string;
generateVariants: string;
variantsList: string;
remaining: string;
variantImage: string;
archived: string;
slugHint: string;
skuHint: string;