From 08fb233da0e1de956857990d585b777a3c725ce3 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Wed, 21 Oct 2026 14:04:48 +0200 Subject: [PATCH] feat: tagy sablon cenoviek pre import podkladov, pozadie karty podla farby cenovky Sablona ma tagy Typ (SUPER CENA / SUPER VYHODNE), Farba (cervena / oranzova / zelena) a Povod (SK / bez povodu); import ZIP-u podla nich vyberie cenovku. SK produkt bez SK sablony dostane variant bez povodu, opacne nie. Pribudla oranzova predvolba pozadia karty. Co-Authored-By: Claude Opus 5.5 --- src/components/editor/BadgeEditorModal.tsx | 44 ++++++++++++++++- src/components/editor/ConfigPanel.tsx | 4 +- src/components/editor/ProductEditor.tsx | 11 +---- src/components/editor/ProductImportModal.tsx | 17 +++++-- src/lib/badgeUtils.ts | 6 ++- src/lib/podkladyZip.ts | 52 +++++++++++++++++--- src/lib/productCard.ts | 18 +++++++ src/types/newsletter.ts | 9 ++++ 8 files changed, 135 insertions(+), 26 deletions(-) diff --git a/src/components/editor/BadgeEditorModal.tsx b/src/components/editor/BadgeEditorModal.tsx index 64989c8..70ef2f8 100644 --- a/src/components/editor/BadgeEditorModal.tsx +++ b/src/components/editor/BadgeEditorModal.tsx @@ -5,7 +5,7 @@ // v súradniciach viewBoxu. Klávesnica: šípky posun (Shift = viac), Delete zmaže vrstvu, Ctrl+Z / Ctrl+Y späť / znova, // Ctrl+S uloží. import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { BadgeTemplate, BadgeLayer } from '@/types/newsletter'; +import { BadgeTemplate, BadgeLayer, BadgeImportTags, BadgeColor } from '@/types/newsletter'; import { supabase } from '@/lib/supabase'; import { renderCleanBadgeSvg, BadgeProductData, BadgeRow, badgeFromRow, badgeCode } from '@/lib/badgeUtils'; import { @@ -48,7 +48,8 @@ function toBadgeDbPayload(badge: BadgeTemplate) { width: badge.width || 110, align: badge.align || 'center', paddingTop: badge.paddingTop ?? 4, - paddingBottom: badge.paddingBottom ?? 4 + paddingBottom: badge.paddingBottom ?? 4, + importTags: badge.importTags } }; } @@ -147,6 +148,41 @@ function loadFont(layer: BadgeLayer) { // Malé UI prvky // --------------------------------------------------------------------------- +// Tagy šablóny pre import podkladov: ktorým riadkom tabuľky (Zľava grafická + Farba cenovky + Pôvod) patrí +const BADGE_TYPES = ['SUPER CENA', 'SUPER VÝHODNÉ']; +const ImportTagsEditor: React.FC<{ tags: BadgeImportTags; onChange: (tags: BadgeImportTags) => void }> = ({ tags, onChange }) => { + const cls = 'w-full text-[11px] border border-gray-300 rounded-lg px-2 py-1.5 bg-white'; + return ( +
+
+ Typ + + Farba + + Pôvod + +
+

+ Pri importe ZIP-u sa šablóna priradí produktom so stĺpcami Zľava grafická, Farba cenovky a Pôvod podľa týchto tagov. +

+
+ ); +}; + const Section: React.FC<{ title: string; action?: React.ReactNode; children: React.ReactNode }> = ({ title, action, children }) => (
@@ -776,6 +812,10 @@ export const BadgeEditorModal: React.FC = ({
{badge && ( <> +
+ commit({ ...badge, importTags })} /> +
+
{/* Rýchle predvoľby */} -
+
{PRODUCT_BG_PRESETS.map((preset) => { const currentBg = (module.data.cardBgColor || '#feeaea').toLowerCase(); const isSelected = currentBg === preset.color.toLowerCase(); @@ -792,7 +792,7 @@ export const ConfigPanel: React.FC = ({
{/* Rýchle predvoľby */} -
+
{PRODUCT_BG_PRESETS.map((preset) => { const currentBg = (module.data.cardBgColor || '#e6e1d9').toLowerCase(); const isSelected = currentBg === preset.color.toLowerCase(); diff --git a/src/components/editor/ProductEditor.tsx b/src/components/editor/ProductEditor.tsx index c8432bf..e6c3acb 100644 --- a/src/components/editor/ProductEditor.tsx +++ b/src/components/editor/ProductEditor.tsx @@ -7,16 +7,9 @@ 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, 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 } from '@/lib/productCard'; -export const PRODUCT_BG_PRESETS = [ - { label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' }, - { label: 'Žltá', sub: 'Domáce', color: '#fef0c7', border: '#e6d49a' }, - { label: 'Hnedá', sub: 'Gril', color: '#dcb085', border: '#c49a6c' }, - { label: 'Sivá', sub: 'Neutrálna', color: '#e6e1d9', border: '#d5cfc5' }, - { label: 'Zelená', sub: 'Bio', color: '#e3f0e4', border: '#c8e6c9' }, - { label: 'Biela', sub: 'Čistá', color: '#ffffff', border: '#e5e7eb' } -]; +export { PRODUCT_BG_PRESETS }; type Variant = 'standard' | 'loyalty'; // Textové polia cien produktu (bežné aj vernostné) diff --git a/src/components/editor/ProductImportModal.tsx b/src/components/editor/ProductImportModal.tsx index 71f4997..cf4396c 100644 --- a/src/components/editor/ProductImportModal.tsx +++ b/src/components/editor/ProductImportModal.tsx @@ -8,6 +8,7 @@ import { findBadgeByCode, badgeCode } from '@/lib/badgeUtils'; import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react'; import { supabase } from '@/lib/supabase'; import { PodkladyProduct, podkladyToProductItem, readPodkladyZip, splitPrice } from '@/lib/podkladyZip'; +import { CARD_BG_BY_BADGE_COLOR } from '@/lib/productCard'; import { shrinkImage, uploadFile } from '@/lib/upload'; interface ImportModalProps { @@ -291,7 +292,11 @@ export const ProductImportModal: React.FC = ({ const stores = [p.stores.potraviny && 'P', p.stores.supermarket && 'S', p.stores.tempo && 'T'].filter(Boolean).join(' '); return (
- + {p.thumb ? ( // eslint-disable-next-line @next/next/no-img-element @@ -308,7 +313,11 @@ export const ProductImportModal: React.FC = ({ {stores && ` · ${stores}`}

- {p.badge ? ( + {p.badge && p.badgeMatch === 'origin' ? ( + + cenovka: {p.badge.name} – šablóna „{p.badgeLabel}“ so SK chýba, použije sa variant bez pôvodu + + ) : p.badge ? ( cenovka: {p.badge.name} ) : ( @@ -326,8 +335,8 @@ export const ProductImportModal: React.FC = ({ })}

- Fotky sa pri importe zmenšia na najviac 1000 px. Rozloženie karty (veľkosť a poloha cenovky, štítky predajní) sa prevezme z prvého - produktu v sekcii. + Fotky sa pri importe zmenšia na najviac 1000 px, pozadie karty sa nastaví podľa farby cenovky. Rozloženie karty (veľkosť a poloha + cenovky, štítky predajní) sa prevezme z prvého produktu v sekcii.

)} diff --git a/src/lib/badgeUtils.ts b/src/lib/badgeUtils.ts index 74e474c..28602c5 100644 --- a/src/lib/badgeUtils.ts +++ b/src/lib/badgeUtils.ts @@ -1,4 +1,4 @@ -import { BadgeTemplate, BadgeLayer } from '@/types/newsletter'; +import { BadgeTemplate, BadgeLayer, BadgeImportTags } from '@/types/newsletter'; // Vymazané všetky preddefinované šablóny — v systéme sú výhradne vlastné cenovky vytvorené používateľom export const DEFAULT_BADGE_TEMPLATES: BadgeTemplate[] = []; @@ -19,6 +19,7 @@ export interface BadgeRow { align?: BadgeTemplate['align']; paddingTop?: number; paddingBottom?: number; + importTags?: BadgeImportTags; } | null; } @@ -34,7 +35,8 @@ export function badgeFromRow(d: BadgeRow): BadgeTemplate { width: cfg?.width || 110, align: cfg?.align || 'center', paddingTop: cfg?.paddingTop ?? 4, - paddingBottom: cfg?.paddingBottom ?? 4 + paddingBottom: cfg?.paddingBottom ?? 4, + importTags: cfg?.importTags }; } diff --git a/src/lib/podkladyZip.ts b/src/lib/podkladyZip.ts index 5c6df64..40f6fd5 100644 --- a/src/lib/podkladyZip.ts +++ b/src/lib/podkladyZip.ts @@ -1,8 +1,9 @@ // Import ZIP-u s podkladmi z Databázy produktov: tabuľka podklady_*.xlsx (jeden riadok = jeden produkt) // a fotky v priečinkoch /web/*.png. import JSZip from 'jszip'; -import { BadgeTemplate, ProductItem } from '@/types/newsletter'; +import { BadgeColor, BadgeTemplate, ProductItem } from '@/types/newsletter'; import { findBadgeByCode } from './badgeUtils'; +import { CARD_BG_BY_BADGE_COLOR } from './productCard'; export interface PodkladyProduct { ean: string; @@ -13,6 +14,8 @@ export interface PodkladyProduct { 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) + color?: BadgeColor; // farba cenovky → pozadie karty stores: ProductItem['stores']; deposit: string; // napr. "0,15" photo?: Blob; @@ -27,6 +30,36 @@ const norm = (s: string) => .replace(/\s+/g, ' ') .trim(); +// „červená“ → cervena +function toBadgeColor(value: string): BadgeColor | undefined { + const n = norm(value); + if (n.startsWith('cerven')) return 'cervena'; + if (n.startsWith('oranz')) return 'oranzova'; + if (n.startsWith('zelen')) return 'zelena'; + 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. +function findBadgeByTags( + badges: BadgeTemplate[], + graphic: string, + color: BadgeColor | undefined, + origin: string +): { badge: BadgeTemplate; match: 'exact' | 'origin' } | undefined { + if (!graphic) return undefined; + const sk = norm(origin) === 'sk' ? 'SK' : ''; + const candidates = badges.filter( + (b) => b.importTags?.type && norm(b.importTags.type) === norm(graphic) && (!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; +} + // ---------- jednoduché čítanie .xlsx (prvý hárok) ---------- function colIndex(ref: string): number { const letters = ref.replace(/\d+/g, ''); @@ -125,12 +158,14 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom const own = images.filter((f) => f.name.split('/')[0] === ean); const photoFile = own.find((f) => f.name.includes('/web/')) || own[0]; - // šablóna cenovky: "Super cena | Červená | SK" → skúsime s pôvodom, bez neho, len grafiku + // šablóna cenovky: podľa tagov šablóny, inak podľa názvu „Super cena | Červená | SK“ const graphic = get(row, c.badge); - const color = get(row, c.color); + const colorText = get(row, c.color); + const color = toBadgeColor(colorText); const origin = get(row, c.origin); - const tries = [[graphic, color, origin], [graphic, color], [graphic]].map((parts) => parts.filter(Boolean).join(' | ')); - const badge = graphic ? tries.map((t) => findBadgeByCode(badges, t)).find(Boolean) : undefined; + const tries = [[graphic, colorText, origin], [graphic, colorText], [graphic]].map((parts) => parts.filter(Boolean).join(' | ')); + const byTags = findBadgeByTags(badges, graphic, color, origin); + const byName = !byTags && graphic ? tries.map((t) => findBadgeByCode(badges, t)).find(Boolean) : undefined; const formats = norm(get(row, c.formats)); const deposit = toNumber(get(row, c.deposit)); @@ -144,7 +179,9 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom unitPrice: formatUnitPrice(get(row, c.unitPrice)), discount: discount ? `${Math.round(discount)}%` : '', badgeLabel: tries[0], - badge, + badge: byTags?.badge || byName, + badgeMatch: byTags ? byTags.match : byName ? 'exact' : undefined, + color, stores: formats ? { potraviny: formats.includes('potraviny'), supermarket: formats.includes('supermarket'), tempo: formats.includes('tempo') } : { potraviny: true, supermarket: true, tempo: true }, @@ -172,6 +209,7 @@ export function podkladyToProductItem(p: PodkladyProduct, imageUrl: string, idx: ...(card ? { cardPriceMain: card.main, cardPriceSub: card.sub } : {}), note: p.deposit ? `+ záloha ${p.deposit} €` : '', badgeText: 'zľava', - stores: p.stores + stores: p.stores, + ...(p.color ? { cardBgColor: CARD_BG_BY_BADGE_COLOR[p.color] } : {}) }; } diff --git a/src/lib/productCard.ts b/src/lib/productCard.ts index 00044e8..ad3d3e7 100644 --- a/src/lib/productCard.ts +++ b/src/lib/productCard.ts @@ -224,6 +224,24 @@ export function cardVisualKey(p: CardProduct, ctx: CardContext): string { // Poradie fotiek v karte odzadu dopredu ('main' = hlavná fotka). Uložené poradie sa doplní o nové fotky // (dopredu) a očistí o zmazané; bez uloženého poradia platí staršie nastavenie „za hlavnou fotkou“. export const MAIN_PHOTO = 'main'; + +// Predvoľby farby pozadia karty produktu (oranžová, červená a zelená zodpovedajú farbám cenoviek v podkladoch) +export const PRODUCT_BG_PRESETS = [ + { label: 'Červená', sub: 'Fantasticky', color: '#feeaea', border: '#f5d4d4' }, + { label: 'Oranžová', sub: 'Super výhodné', color: '#fdebd8', border: '#f3d2ae' }, + { label: 'Žltá', sub: 'Domáce', color: '#fef0c7', border: '#e6d49a' }, + { label: 'Hnedá', sub: 'Gril', color: '#dcb085', border: '#c49a6c' }, + { label: 'Sivá', sub: 'Neutrálna', color: '#e6e1d9', border: '#d5cfc5' }, + { label: 'Zelená', sub: 'Bio', color: '#e3f0e4', border: '#c8e6c9' }, + { label: 'Biela', sub: 'Čistá', color: '#ffffff', border: '#e5e7eb' } +]; + +// Farba cenovky z podkladov → pozadie karty +export const CARD_BG_BY_BADGE_COLOR: Record<'cervena' | 'oranzova' | 'zelena', string> = { + cervena: '#feeaea', + oranzova: '#fdebd8', + zelena: '#e3f0e4' +}; export function photoStack(p: Pick): string[] { const extras = p.extraPhotos || []; const ids = new Set([MAIN_PHOTO, ...extras.map((f) => f.id)]); diff --git a/src/types/newsletter.ts b/src/types/newsletter.ts index 7e63122..54265c2 100644 --- a/src/types/newsletter.ts +++ b/src/types/newsletter.ts @@ -240,4 +240,13 @@ export interface BadgeTemplate { align?: 'left' | 'center' | 'right'; // Zarovnanie (vľavo, stred, vpravo) paddingTop?: number; paddingBottom?: number; + importTags?: BadgeImportTags; // podľa čoho sa šablóna vyberie pri importe podkladov (ZIP) +} + +// Tagy šablóny cenovky pre import podkladov – zodpovedajú stĺpcom tabuľky z Databázy produktov +export type BadgeColor = 'cervena' | 'oranzova' | 'zelena'; +export interface BadgeImportTags { + type?: string; // „Zľava grafická“, napr. SUPER CENA, SUPER VÝHODNÉ + color?: BadgeColor; // „Farba cenovky“ + origin?: 'SK' | ''; // „Pôvod“: SK alebo bez pôvodu }