feat: cena pre appkacov ({cena_app}), tag cenovky pre appkacov, doplnkove grafiky ako stitky
- premenna {cena_app} v cenovke a pole Pre appkacov v produkte (ked ju sablona pouziva)
- tag Appka v sablone; import da cenovku pre appkacov produktom s Cenou pre appkacov,
bezny produkt ju nikdy nedostane
- stlpec Doplnkove grafiky sa pri importe zmeni na stitky na fotke
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
11392722e2
commit
bd67fc7020
8 files changed
+114
-34
No files matched your search
@@ -1023,6 +1023,8 @@ export default function Home() {
|
||||
currentCardPriceMain={targetProduct?.cardPriceMain || undefined}
|
||||
currentCardPriceSub={targetProduct?.cardPriceSub || undefined}
|
||||
currentCardDiscount={targetProduct?.cardDiscount || undefined}
|
||||
currentAppPriceMain={targetProduct?.appPriceMain || undefined}
|
||||
currentAppPriceSub={targetProduct?.appPriceSub || undefined}
|
||||
/>
|
||||
|
||||
{/* Modál Moje newslettre (Ukladanie, duplikácia, vytváranie a prepínanie) */}
|
||||
|
||||
@@ -108,6 +108,7 @@ const TOKENS: Array<{ token: string; label: string }> = [
|
||||
{ token: '{zlava}', label: 'Zľava' },
|
||||
{ token: '{cena_s_kartou}', label: 'Cena s kartou' },
|
||||
{ token: '{zlava_s_kartou}', label: 'Zľava s kartou' },
|
||||
{ token: '{cena_app}', label: 'Cena v appke' },
|
||||
{ token: '{jednotkova_cena}', label: 'Jedn. cena' },
|
||||
{ token: '{mena}', label: '€' }
|
||||
];
|
||||
@@ -118,6 +119,7 @@ const QUICK_LAYERS: Array<{ label: string; text: string; fontSize: number }> = [
|
||||
{ label: 'Zľava', text: '-{zlava}', fontSize: 14 },
|
||||
{ label: 'Cena s kartou', text: '{cena_s_kartou}', fontSize: 40 },
|
||||
{ label: 'Zľava s kartou', text: '-{zlava_s_kartou}', fontSize: 14 },
|
||||
{ label: 'Cena v appke', text: '{cena_app}', fontSize: 40 },
|
||||
{ label: 'Text', text: 'AKCIA', fontSize: 16 }
|
||||
];
|
||||
|
||||
@@ -174,9 +176,14 @@ const ImportTagsEditor: React.FC<{ tags: BadgeImportTags; onChange: (tags: Badge
|
||||
<option value="">Bez pôvodu</option>
|
||||
<option value="SK">SK (slovenský produkt)</option>
|
||||
</select>
|
||||
<span className="text-[10px] font-semibold text-gray-600">Appka</span>
|
||||
<select value={tags.app ? 'app' : ''} onChange={(e) => onChange({ ...tags, app: e.target.value === 'app' || undefined })} className={cls}>
|
||||
<option value="">Bežná cenovka</option>
|
||||
<option value="app">Pre appkáčov (cena v aplikácii)</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 leading-snug">
|
||||
Pri importe ZIP-u sa šablóna priradí produktom podľa stĺpcov Zľava grafická (prázdna = základná, s výkričníkom podľa stĺpca Výkričník), Farba cenovky a Pôvod.
|
||||
Pri importe ZIP-u sa šablóna priradí produktom podľa stĺpcov Zľava grafická (prázdna = základná, s výkričníkom podľa stĺpca Výkričník), Farba cenovky a Pôvod. Cenovka pre appkáčov sa dá produktom, ktoré majú vyplnenú Cenu pre appkáčov.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -270,6 +277,8 @@ interface BadgeEditorModalProps {
|
||||
currentCardPriceMain?: string;
|
||||
currentCardPriceSub?: string;
|
||||
currentCardDiscount?: string;
|
||||
currentAppPriceMain?: string;
|
||||
currentAppPriceSub?: string;
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
@@ -290,7 +299,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
currentUnitPrice = '1kg = 4,91 €',
|
||||
currentCardPriceMain = '1',
|
||||
currentCardPriceSub = '49',
|
||||
currentCardDiscount = '30%'
|
||||
currentCardDiscount = '30%',
|
||||
currentAppPriceMain,
|
||||
currentAppPriceSub
|
||||
}) => {
|
||||
const [badges, setBadges] = useState<BadgeTemplate[]>([]);
|
||||
const [badge, setBadge] = useState<BadgeTemplate | null>(null);
|
||||
@@ -324,7 +335,9 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
unitPrice: currentUnitPrice,
|
||||
cardPriceMain: testPrice?.main || currentCardPriceMain,
|
||||
cardPriceSub: testPrice?.sub || currentCardPriceSub,
|
||||
cardDiscount: currentCardDiscount
|
||||
cardDiscount: currentCardDiscount,
|
||||
appPriceMain: testPrice?.main || currentAppPriceMain || currentPriceMain,
|
||||
appPriceSub: testPrice?.sub || currentAppPriceSub || currentPriceSub
|
||||
};
|
||||
|
||||
// --- načítanie šablón -----------------------------------------------------
|
||||
@@ -659,7 +672,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const hasCents = !!layer && /\{(cena|cena_s_kartou|centy)\}/.test(layer.text || '');
|
||||
const hasCents = !!layer && /\{(cena|cena_s_kartou|cena_app|centy)\}/.test(layer.text || '');
|
||||
const fontName = layer?.fontFamily || 'Arial Narrow';
|
||||
const isKnownFont = FONTS.some((f) => f.name === fontName);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/
|
||||
import { ChevronDown, ChevronUp, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
||||
import { ImageUploadInput } from './ImageUploadInput';
|
||||
import { pickAlt } from '@/lib/altText';
|
||||
import { DEFAULT_STORES_OFFSET_X, DEFAULT_STORES_OFFSET_Y, DEFAULT_STORES_SIZE, MAIN_PHOTO, PRODUCT_BG_PRESETS, badgeWidthFor, photoStack, productCardAlt } from '@/lib/productCard';
|
||||
import { DEFAULT_STORES_OFFSET_X, DEFAULT_STORES_OFFSET_Y, DEFAULT_STORES_SIZE, MAIN_PHOTO, PRODUCT_BG_PRESETS, badgeWidthFor, photoStack, productCardAlt, usesAppPrice } from '@/lib/productCard';
|
||||
|
||||
export { PRODUCT_BG_PRESETS };
|
||||
|
||||
@@ -16,6 +16,7 @@ type Variant = 'standard' | 'loyalty';
|
||||
type TextKey =
|
||||
| 'priceMain' | 'priceSub' | 'discountText'
|
||||
| 'cardPriceMain' | 'cardPriceSub' | 'cardDiscount' | 'cardUnitPrice'
|
||||
| 'appPriceMain' | 'appPriceSub'
|
||||
| 'noCardPriceMain' | 'noCardPriceSub' | 'noCardDiscount' | 'noCardUnitPrice';
|
||||
type Patch = Record<string, unknown>;
|
||||
|
||||
@@ -274,7 +275,7 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
||||
|
||||
const text = (key: TextKey) => p[key] as string | undefined;
|
||||
|
||||
const priceRow = (label: string, mainKey: TextKey, subKey: TextKey, discountKey: TextKey, unitKey?: TextKey) => (
|
||||
const priceRow = (label: string, mainKey: TextKey, subKey: TextKey, discountKey?: TextKey, unitKey?: TextKey) => (
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-semibold text-gray-600">{label}</span>
|
||||
<div className={`grid ${unitKey ? 'grid-cols-[1fr_1fr_1fr]' : 'grid-cols-3'} gap-1.5`}>
|
||||
@@ -284,9 +285,11 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
||||
<Field label="Centy">
|
||||
<TextInput value={text(subKey)} onChange={set(subKey)} bold />
|
||||
</Field>
|
||||
<Field label="Zľava">
|
||||
<TextInput value={text(discountKey)} onChange={set(discountKey)} placeholder="25%" />
|
||||
</Field>
|
||||
{discountKey && (
|
||||
<Field label="Zľava">
|
||||
<TextInput value={text(discountKey)} onChange={set(discountKey)} placeholder="25%" />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
{unitKey && (
|
||||
<Field label="Jednotková cena">
|
||||
@@ -450,6 +453,7 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
||||
<>
|
||||
{priceRow('Cena', 'priceMain', 'priceSub', 'discountText')}
|
||||
{usesCardPrice(p.badgeCustomTemplate) && priceRow('S kartou', 'cardPriceMain', 'cardPriceSub', 'cardDiscount')}
|
||||
{usesAppPrice(p.badgeCustomTemplate) && priceRow('Pre appkáčov', 'appPriceMain', 'appPriceSub')}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -310,12 +310,18 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||||
EAN {p.ean}
|
||||
{p.unitPrice && ` · ${p.unitPrice}`}
|
||||
{p.deposit && ` · záloha ${p.deposit} €`}
|
||||
{p.appPrice != null && ` · v appke ${p.appPrice.toFixed(2).replace('.', ',')} €`}
|
||||
{stores && ` · ${stores}`}
|
||||
</p>
|
||||
{p.extras.length > 0 && (
|
||||
<p className="text-[11px] text-gray-500 truncate">
|
||||
štítok na fotke: <span className="font-bold text-red-700">{p.extras.join(' · ')}</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[11px] truncate">
|
||||
{p.badge && p.badgeMatch === 'origin' ? (
|
||||
{p.badge && p.badgeMissing.length ? (
|
||||
<span className="text-amber-700">
|
||||
cenovka: {p.badge.name} – šablóna „{p.badgeLabel}“ so SK chýba, použije sa variant bez pôvodu
|
||||
cenovka: {p.badge.name} – chýba {p.badgeMissing.join(' a ')} pre „{p.badgeLabel}“
|
||||
</span>
|
||||
) : p.badge ? (
|
||||
<span className="text-emerald-700">cenovka: {p.badge.name}</span>
|
||||
|
||||
@@ -69,10 +69,12 @@ export interface BadgeProductData {
|
||||
cardPriceMain?: string; // cena s kartou → {cena_s_kartou}
|
||||
cardPriceSub?: string;
|
||||
cardDiscount?: string; // zľava s kartou → {zlava_s_kartou}
|
||||
appPriceMain?: string; // cena pre appkáčov → {cena_app}
|
||||
appPriceSub?: string;
|
||||
}
|
||||
|
||||
// Premenné ceny s centami v hornom indexe: {cena} a {cena_s_kartou}
|
||||
export const BADGE_PRICE_TOKEN_RE = /\{(cena|cena_s_kartou)\}/;
|
||||
// Premenné ceny s centami v hornom indexe: {cena}, {cena_s_kartou} a {cena_app}
|
||||
export const BADGE_PRICE_TOKEN_RE = /\{(cena|cena_s_kartou|cena_app)\}/;
|
||||
|
||||
// Nahradenie jednoduchých textových premenných (bez horného indexu)
|
||||
export function replaceBadgeTextVars(text: string, product: BadgeProductData): string {
|
||||
@@ -89,6 +91,9 @@ export function badgePriceParts(token: string, product: BadgeProductData): [stri
|
||||
if (token === 'cena_s_kartou') {
|
||||
return [product.cardPriceMain || '', product.cardPriceSub || ''];
|
||||
}
|
||||
if (token === 'cena_app') {
|
||||
return [product.appPriceMain || '', product.appPriceSub || ''];
|
||||
}
|
||||
return [product.priceMain || '0', product.priceSub || '00'];
|
||||
}
|
||||
|
||||
|
||||
+61
-19
@@ -1,7 +1,7 @@
|
||||
// Import ZIP-u s podkladmi z Databázy produktov: tabuľka podklady_*.xlsx (jeden riadok = jeden produkt)
|
||||
// a fotky v priečinkoch <EAN>/web/*.png.
|
||||
import JSZip from 'jszip';
|
||||
import { BADGE_IMPORT_TYPES, BadgeColor, BadgeTemplate, ProductItem } from '@/types/newsletter';
|
||||
import { BADGE_IMPORT_TYPES, BadgeColor, BadgeTemplate, ProductItem, ProductLabel } from '@/types/newsletter';
|
||||
import { findBadgeByCode } from './badgeUtils';
|
||||
import { CARD_BG_BY_BADGE_COLOR } from './productCard';
|
||||
|
||||
@@ -10,14 +10,16 @@ export interface PodkladyProduct {
|
||||
title: string;
|
||||
price: number | null; // akciová cena
|
||||
cardPrice: number | null; // cena s vernostnou kartou
|
||||
appPrice: number | null; // cena pre appkáčov
|
||||
unitPrice: string; // napr. "1kg = 5,39 €"
|
||||
discount: string; // napr. "30%"
|
||||
badgeLabel: string; // napr. "SUPER CENA | červená | SK" – z čoho sa hľadá šablóna cenovky
|
||||
badge?: BadgeTemplate;
|
||||
badgeMatch?: 'exact' | 'origin'; // origin = našla sa šablóna s iným pôvodom (napr. bez SK)
|
||||
badgeMissing: string[]; // čo šablóne chýba voči riadku (napr. „SK variant“, „variant pre appkáčov“)
|
||||
color?: BadgeColor; // farba cenovky → pozadie karty
|
||||
stores: ProductItem['stores'];
|
||||
deposit: string; // napr. "0,15"
|
||||
extras: string[]; // doplnkové grafiky → štítky na fotke (napr. „PODIEL MÄSA 90 %“)
|
||||
photo?: Blob;
|
||||
photoName: string;
|
||||
}
|
||||
@@ -39,24 +41,51 @@ function toBadgeColor(value: string): BadgeColor | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Šablóna cenovky podľa tagov (Typ + Farba + Pôvod). SK produkt bez SK šablóny dostane variant bez pôvodu,
|
||||
// opačne nie – zahraničný produkt nesmie dostať cenovku so slovenskou vlajkou.
|
||||
// Šablóna cenovky podľa tagov (Typ + Farba + Pôvod + Appka). Náhradný variant sa berie len smerom,
|
||||
// ktorý nič nepridá: SK produkt môže dostať cenovku bez vlajky a produkt s cenou pre appkáčov bežnú cenovku,
|
||||
// ale zahraničný produkt nikdy cenovku so slovenskou vlajkou a bežný produkt nikdy cenovku pre appkáčov.
|
||||
function findBadgeByTags(
|
||||
badges: BadgeTemplate[],
|
||||
type: string,
|
||||
color: BadgeColor | undefined,
|
||||
origin: string
|
||||
): { badge: BadgeTemplate; match: 'exact' | 'origin' } | undefined {
|
||||
const sk = norm(origin) === 'sk' ? 'SK' : '';
|
||||
const candidates = badges.filter(
|
||||
(b) => b.importTags?.type && norm(b.importTags.type) === norm(type) && (!b.importTags.color || b.importTags.color === color)
|
||||
);
|
||||
// presná farba má prednosť pred „hocijakou“
|
||||
const rank = (b: BadgeTemplate) => (b.importTags?.color ? 0 : 1);
|
||||
const exact = candidates.filter((b) => (b.importTags?.origin || '') === sk).sort((a, b) => rank(a) - rank(b))[0];
|
||||
if (exact) return { badge: exact, match: 'exact' };
|
||||
const other = sk ? candidates.sort((a, b) => rank(a) - rank(b))[0] : undefined;
|
||||
return other ? { badge: other, match: 'origin' } : undefined;
|
||||
origin: string,
|
||||
app: boolean
|
||||
): { badge: BadgeTemplate; missing: string[] } | undefined {
|
||||
const sk = norm(origin) === 'sk';
|
||||
const candidates = badges.filter((b) => {
|
||||
const t = b.importTags;
|
||||
if (!t?.type || norm(t.type) !== norm(type)) return false;
|
||||
if (t.color && t.color !== color) return false;
|
||||
if (t.origin === 'SK' && !sk) return false;
|
||||
if (t.app && !app) return false;
|
||||
return true;
|
||||
});
|
||||
// najlepšia zhoda: rovnaký pôvod, rovnaká appka, presná farba
|
||||
const score = (b: BadgeTemplate) =>
|
||||
((b.importTags?.origin === 'SK') === sk ? 0 : 4) + (!!b.importTags?.app === app ? 0 : 2) + (b.importTags?.color ? 0 : 1);
|
||||
const best = candidates.sort((a, b) => score(a) - score(b))[0];
|
||||
if (!best) return undefined;
|
||||
const missing = [
|
||||
sk && best.importTags?.origin !== 'SK' ? 'SK variant' : '',
|
||||
app && !best.importTags?.app ? 'variant pre appkáčov' : ''
|
||||
].filter(Boolean);
|
||||
return { badge: best, missing };
|
||||
}
|
||||
|
||||
// Doplnková grafika → štítok na fotke; dlhší text sa zalomí na dva riadky
|
||||
function extraLabel(text: string, idx: number): ProductLabel {
|
||||
const words = text.split(' ');
|
||||
let label = text;
|
||||
if (text.length > 16 && words.length > 1) {
|
||||
let best = 1;
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const a = words.slice(0, i).join(' ').length;
|
||||
const bestA = words.slice(0, best).join(' ').length;
|
||||
if (Math.abs(a - text.length / 2) < Math.abs(bestA - text.length / 2)) best = i;
|
||||
}
|
||||
label = `${words.slice(0, best).join(' ')}<br>${words.slice(best).join(' ')}`;
|
||||
}
|
||||
return { id: `lbl-${Date.now()}-${idx}`, text: label, x: 28, y: 14 + idx * 14, size: 11, bg: '#e2001a', color: '#ffffff', rotate: 0 };
|
||||
}
|
||||
|
||||
// ---------- jednoduché čítanie .xlsx (prvý hárok) ----------
|
||||
@@ -136,6 +165,8 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom
|
||||
titleAlt: col('Názov produktu'),
|
||||
price: col('Akciová cena (leták)', 'Cena (xlsx podklady)'),
|
||||
cardPrice: col('Cena s vernostnou kartou'),
|
||||
appPrice: col('Cena pre appkáčov'),
|
||||
extras: col('Doplnkové grafiky'),
|
||||
unitPrice: col('Jednotková cena'),
|
||||
discount: col('Zľava %'),
|
||||
badge: col('Zľava grafická'),
|
||||
@@ -166,7 +197,8 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom
|
||||
// prázdna „Zľava grafická“ = základná cenovka (s výkričníkom podľa stĺpca Výkričník)
|
||||
const type = graphic || (norm(get(row, c.exclaim)) === 'ano' ? 'ZÁKLADNÁ !' : 'ZÁKLADNÁ');
|
||||
const tries = [[graphic, colorText, origin], [graphic, colorText], [graphic]].map((parts) => parts.filter(Boolean).join(' | '));
|
||||
const byTags = findBadgeByTags(badges, type, color, origin);
|
||||
const appPrice = toNumber(get(row, c.appPrice));
|
||||
const byTags = findBadgeByTags(badges, type, color, origin, appPrice != null);
|
||||
const byName = !byTags && graphic ? tries.map((t) => findBadgeByCode(badges, t)).find(Boolean) : undefined;
|
||||
|
||||
const formats = norm(get(row, c.formats));
|
||||
@@ -178,16 +210,23 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom
|
||||
title: get(row, c.title) || get(row, c.titleAlt) || ean,
|
||||
price: toNumber(get(row, c.price)),
|
||||
cardPrice: toNumber(get(row, c.cardPrice)),
|
||||
appPrice,
|
||||
unitPrice: formatUnitPrice(get(row, c.unitPrice)),
|
||||
discount: discount ? `${Math.round(discount)}%` : '',
|
||||
badgeLabel: [BADGE_IMPORT_TYPES.find((t) => t.value === type)?.label || type, colorText, origin].filter(Boolean).join(' | '),
|
||||
badgeLabel: [BADGE_IMPORT_TYPES.find((t) => t.value === type)?.label || type, colorText, origin, appPrice != null ? 'appka' : '']
|
||||
.filter(Boolean)
|
||||
.join(' | '),
|
||||
badge: byTags?.badge || byName,
|
||||
badgeMatch: byTags ? byTags.match : byName ? 'exact' : undefined,
|
||||
badgeMissing: byTags?.missing || [],
|
||||
color,
|
||||
stores: formats
|
||||
? { potraviny: formats.includes('potraviny'), supermarket: formats.includes('supermarket'), tempo: formats.includes('tempo') }
|
||||
: { potraviny: true, supermarket: true, tempo: true },
|
||||
deposit: deposit ? formatEur(deposit) : '',
|
||||
extras: get(row, c.extras)
|
||||
.split(/[;\n]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
photo: photoFile ? await photoFile.async('blob') : undefined,
|
||||
photoName: photoFile ? photoFile.name.split('/').pop()! : ''
|
||||
});
|
||||
@@ -199,6 +238,7 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom
|
||||
export function podkladyToProductItem(p: PodkladyProduct, imageUrl: string, idx: number): ProductItem {
|
||||
const price = splitPrice(p.price ?? 0);
|
||||
const card = p.cardPrice != null ? splitPrice(p.cardPrice) : null;
|
||||
const app = p.appPrice != null ? splitPrice(p.appPrice) : null;
|
||||
return {
|
||||
...(p.badge ? { badgeCustomTemplate: p.badge, badgeId: p.badge.id } : {}),
|
||||
id: `zip-${Date.now()}-${idx}`,
|
||||
@@ -209,6 +249,8 @@ export function podkladyToProductItem(p: PodkladyProduct, imageUrl: string, idx:
|
||||
unitPrice: p.unitPrice,
|
||||
discountText: p.discount,
|
||||
...(card ? { cardPriceMain: card.main, cardPriceSub: card.sub } : {}),
|
||||
...(app ? { appPriceMain: app.main, appPriceSub: app.sub } : {}),
|
||||
...(p.extras.length ? { labels: p.extras.map(extraLabel) } : {}),
|
||||
note: p.deposit ? `+ záloha ${p.deposit} €` : '',
|
||||
badgeText: 'zľava',
|
||||
stores: p.stores,
|
||||
|
||||
@@ -205,7 +205,7 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string {
|
||||
p.customBadgePngUrl, badgeWidthFor(p, ctx.variant),
|
||||
t ? [t.svgRaw, t.layersConfig, t.width] : null,
|
||||
p.priceMain, p.priceSub, p.discountText, p.badgeText, p.unitPrice,
|
||||
p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice,
|
||||
p.cardPriceMain, p.cardPriceSub, p.cardDiscount, p.cardUnitPrice, p.appPriceMain, p.appPriceSub,
|
||||
p.noCardPriceMain, p.noCardPriceSub, p.noCardDiscount, p.noCardUnitPrice,
|
||||
p.stores, p.storesOffsetX ?? DEFAULT_STORES_OFFSET_X, p.storesOffsetY ?? DEFAULT_STORES_OFFSET_Y, p.storesSize ?? DEFAULT_STORES_SIZE,
|
||||
p.labels || [], p.extraPhotos || [], photoStack(p)
|
||||
@@ -339,6 +339,9 @@ function renderVisualPreview(p: CardProduct, ctx: CardContext, key: string): str
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Šablóna cenovky zobrazuje cenu pre appkáčov
|
||||
export const usesAppPrice = (t?: BadgeTemplate) => !!t && JSON.stringify(t.layersConfig || []).includes('{cena_app}');
|
||||
|
||||
// Alt text obrázka karty: názov, gramáž a cena (ako v šablóne); ručne zadaný alt má prednosť
|
||||
export function productCardAlt(p: CardProduct, variant: CardVariant): string {
|
||||
const name = [pickAlt(p.title), p.quantity].filter(Boolean).join(' ');
|
||||
@@ -355,9 +358,11 @@ export function productCardAlt(p: CardProduct, variant: CardVariant): string {
|
||||
} else {
|
||||
const main = price(p.priceMain, p.priceSub);
|
||||
const card = price(p.cardPriceMain, p.cardPriceSub);
|
||||
const app = usesAppPrice(p.badgeCustomTemplate) ? price(p.appPriceMain, p.appPriceSub) : '';
|
||||
priceText = [
|
||||
main ? `${main}${p.discountText ? `, zľava ${pct(p.discountText)}` : ''}` : '',
|
||||
card ? `s kartou ${card}${p.cardDiscount ? ` (zľava ${pct(p.cardDiscount)})` : ''}` : ''
|
||||
card ? `s kartou ${card}${p.cardDiscount ? ` (zľava ${pct(p.cardDiscount)})` : ''}` : '',
|
||||
app ? `v aplikácii ${app}` : ''
|
||||
].filter(Boolean).join(', ');
|
||||
}
|
||||
return pickAlt(p.imageAlt, [name, priceText].filter(Boolean).join(' – '), 'Produkt');
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface ProductItem {
|
||||
cardPriceMain?: string; // cena s kartou (vernostná), celé eurá
|
||||
cardPriceSub?: string; // cena s kartou, centy
|
||||
cardDiscount?: string; // zľava s kartou, napr. "30%"
|
||||
appPriceMain?: string; // cena pre appkáčov (v aplikácii), celé eurá → {cena_app}
|
||||
appPriceSub?: string; // cena pre appkáčov, centy
|
||||
badgeText: string; // napr. "AKCIA" alebo "SUPER CENA"
|
||||
badgeId?: string; // priradená cenovka z databázy
|
||||
badgeCustomTemplate?: BadgeTemplate; // Uložená dynamická šablóna cenovky (bez fixnej ceny)
|
||||
@@ -257,4 +259,5 @@ export interface BadgeImportTags {
|
||||
type?: string; // druh cenovky – jeden z BADGE_IMPORT_TYPES (SUPER CENA, SUPER VÝHODNÉ, ZÁKLADNÁ, ZÁKLADNÁ !)
|
||||
color?: BadgeColor; // „Farba cenovky“
|
||||
origin?: 'SK' | ''; // „Pôvod“: SK alebo bez pôvodu
|
||||
app?: boolean; // cenovka pre appkáčov – pre produkty s „Cenou pre appkáčov“
|
||||
}
|
||||
Reference in new issue
Block a user