fix: jednotkova cena v tvare 'jednotkova cena 7,75 EUR/kg' aj v existujucich produktoch

- normalizeUnitPrice prevedie stare zapisy '1kg = 7,75 EUR' pri nacitani
  newslettera, v CSV importe a v predvolenych produktoch
- jednotkova cena pod nazvom sa neopakuje, ked ju ma priamo cenovka
  (vernostna karta, appkaci)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-22 10:46:42 +02:00
1 parent 9635b89e7c
commit d778ed345c
6 files changed
+54 -23

No files matched your search

+13 -4
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate } from '@/types/newsletter';
import { NewsletterCampaign, NewsletterModule, ProductItem, BadgeTemplate, CardProduct } from '@/types/newsletter';
import { DEFAULT_CAMPAIGN, SUPABASE_ASSETS_BASE, ensureValidityBar } from '@/lib/defaultCampaign';
import { renderEmailHtml, renderSingleModuleHtml, getCampaignValidity } from '@/lib/emailRenderer';
import { supabase } from '@/lib/supabase';
@@ -15,6 +15,7 @@ import { SavedCampaignsModal } from '@/components/editor/SavedCampaignsModal';
import { NameDialog } from '@/components/editor/NameDialog';
import JSZip from 'jszip';
import { prepareCardImages } from '@/lib/cardImages';
import { normalizeUnitPrice } from '@/lib/unitPrice';
import { useCampaignReview } from '@/components/review/useCampaignReview';
import { ReviewModal } from '@/components/review/ReviewModal';
import { AnnotationLayer, useModuleLayout } from '@/components/review/AnnotationLayer';
@@ -88,7 +89,15 @@ export default function Home() {
useEffect(() => {
if (!availableBadges.length) return;
const byId = new Map(availableBadges.map((b) => [b.id, b]));
const update = (p: ProductItem, def: BadgeTemplate | undefined): ProductItem => {
const UNIT_KEYS = ['unitPrice', 'cardUnitPrice', 'noCardUnitPrice', 'appUnitPrice'] as const;
const update = (input: ProductItem, def: BadgeTemplate | undefined): ProductItem => {
// jednotkové ceny v jednotnom tvare „jednotková cena 7,75 EUR/kg“ (staršie produkty mali „1kg = 7,75 €“)
let p = input;
for (const k of UNIT_KEYS) {
const cur = (p as CardProduct)[k];
const next = cur ? normalizeUnitPrice(cur) : cur;
if (next !== cur) p = { ...p, [k]: next };
}
if (p.customBadgePngUrl) return p;
if (!p.badgeCustomTemplate) return def ? { ...p, badgeCustomTemplate: def, badgeId: def.id, badgeWidth: def.width } : p;
const fresh = byId.get(p.badgeCustomTemplate.id);
@@ -346,7 +355,7 @@ export default function Home() {
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_5.png`,
priceMain: '2',
priceSub: '49',
unitPrice: '1kg = 9,90 €',
unitPrice: 'jednotková cena 9,90 EUR/kg',
discountText: '15%',
badgeText: 'zľava',
cardBgColor: '#feeaea',
@@ -1050,7 +1059,7 @@ export default function Home() {
currentDiscount={targetProduct?.discountText || targetProduct?.cardDiscount || '25%'}
currentPriceMain={targetProduct?.priceMain || targetProduct?.cardPriceMain || '1'}
currentPriceSub={targetProduct?.priceSub || targetProduct?.cardPriceSub || '99'}
currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || '1kg = 4,91 €'}
currentUnitPrice={targetProduct?.unitPrice || targetProduct?.cardUnitPrice || 'jednotková cena 4,91 EUR/kg'}
currentCardPriceMain={targetProduct?.cardPriceMain || undefined}
currentCardPriceSub={targetProduct?.cardPriceSub || undefined}
currentCardDiscount={targetProduct?.cardDiscount || undefined}
+1 -1
View File
@@ -315,7 +315,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
currentDiscount = '25%',
currentPriceMain = '1',
currentPriceSub = '99',
currentUnitPrice = '1kg = 4,91 €',
currentUnitPrice = 'jednotková cena 4,91 EUR/kg',
currentCardPriceMain = '1',
currentCardPriceSub = '49',
currentCardDiscount = '30%',
+3 -2
View File
@@ -9,6 +9,7 @@ import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
import { supabase } from '@/lib/supabase';
import { PodkladyProduct, pickLoyaltyBadge, podkladyToLoyaltyItem, podkladyToProductItem, readPodkladyZip, splitPrice } from '@/lib/podkladyZip';
import { CARD_BG_BY_BADGE_COLOR } from '@/lib/productCard';
import { normalizeUnitPrice } from '@/lib/unitPrice';
import { shrinkImage, uploadFile } from '@/lib/upload';
interface ImportModalProps {
@@ -222,7 +223,7 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
imageUrl: row[mapping.imageUrl] || 'https://dqexxfspkylqttrtxsww.supabase.co/storage/v1/object/public/newsletter-assets/produkt_1.png',
priceMain: pMain,
priceSub: pSub,
unitPrice: row[mapping.unitPrice] || '',
unitPrice: normalizeUnitPrice(String(row[mapping.unitPrice] || '')),
discountText: row[mapping.discountText] ? `${row[mapping.discountText]}%` : '',
badgeText: 'zľava',
cardBgColor: '#feeaea',
@@ -416,7 +417,7 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">Merná cena (napr. 1kg = 4,91 €)</label>
<label className="block text-xs font-semibold text-gray-600 mb-1">Jednotková cena (napr. 4,91 EUR/kg)</label>
<select
value={mapping.unitPrice}
onChange={(e) => setMapping({ ...mapping, unitPrice: e.target.value })}
+9 -9
View File
@@ -108,7 +108,7 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_1.png`,
priceMain: '1',
priceSub: '99',
unitPrice: '1kg = 4,91 €',
unitPrice: 'jednotková cena 4,91 EUR/kg',
discountText: '23%',
badgeText: 'zľava',
cardBgColor: '#feeaea',
@@ -121,7 +121,7 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_2.png`,
priceMain: '0',
priceSub: '89',
unitPrice: '1kg = 8,90 €',
unitPrice: 'jednotková cena 8,90 EUR/kg',
discountText: '36%',
badgeText: 'zľava',
cardBgColor: '#feeaea',
@@ -151,7 +151,7 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_3.png`,
priceMain: '1',
priceSub: '05',
unitPrice: '1kg = 0,53 €',
unitPrice: 'jednotková cena 0,53 EUR/kg',
discountText: '34%',
badgeText: 'zľava',
cardBgColor: '#fef0c7',
@@ -195,7 +195,7 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_5.png`,
priceMain: '3',
priceSub: '99',
unitPrice: '1kg = 3,99 €',
unitPrice: 'jednotková cena 3,99 EUR/kg',
discountText: '',
badgeText: 'Skvelá cena',
cardBgColor: '#dcb085',
@@ -208,7 +208,7 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
imageUrl: `${SUPABASE_ASSETS_BASE}produkt_6.png`,
priceMain: '1',
priceSub: '99',
unitPrice: '1kg = 7,96 €',
unitPrice: 'jednotková cena 7,96 EUR/kg',
discountText: '38%',
badgeText: 'zľava',
cardBgColor: '#dcb085',
@@ -238,12 +238,12 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
noCardDiscount: '21%',
noCardPriceMain: '0',
noCardPriceSub: '79',
noCardUnitPrice: '1kg = 9,88 €',
noCardUnitPrice: 'jednotková cena 9,88 EUR/kg',
// S kartou
cardDiscount: '38%',
cardPriceMain: '0',
cardPriceSub: '62',
cardUnitPrice: '1kg = 7,75 €',
cardUnitPrice: 'jednotková cena 7,75 EUR/kg',
stores: { potraviny: true, supermarket: true, tempo: true },
showSlovakFlag: true
},
@@ -255,12 +255,12 @@ export const DEFAULT_CAMPAIGN: NewsletterCampaign = {
noCardDiscount: '23%',
noCardPriceMain: '0',
noCardPriceSub: '45',
noCardUnitPrice: '1kg = 11,25 €',
noCardUnitPrice: 'jednotková cena 11,25 EUR/kg',
// S kartou
cardDiscount: '40%',
cardPriceMain: '0',
cardPriceSub: '35',
cardUnitPrice: '1kg = 8,75 €',
cardUnitPrice: 'jednotková cena 8,75 EUR/kg',
stores: { potraviny: true, supermarket: true, tempo: true },
showSlovakFlag: true
}
+3 -7
View File
@@ -409,13 +409,9 @@ function renderVisualExport(p: CardProduct, ctx: CardContext, key: string): stri
// Texty pod vizuálom: názov 15px/700 + gramáž 15px/400, jednotková cena 11px #555, poznámka a platnosť 10px #888
const TEXT_FONT = "font-family: 'Poppins', Helvetica, Arial, sans-serif;";
function renderTexts(p: CardProduct, ctx: CardContext): string {
// Pri predvolenej vernostnej cenovke sú jednotkové ceny priamo na cenovke
const unitPrice =
ctx.variant === 'loyalty'
? p.badgeCustomTemplate || p.customBadgePngUrl
? p.cardUnitPrice || p.unitPrice
: ''
: p.unitPrice;
// Jednotková cena pod názvom, len ak ju nemá priamo cenovka (predvolená vernostná, šablóny s {jednotkova_cena…})
const badgeHasUnit = !p.customBadgePngUrl && (!p.badgeCustomTemplate ? ctx.variant === 'loyalty' : JSON.stringify(p.badgeCustomTemplate.layersConfig || []).includes('{jednotkova_cena'));
const unitPrice = badgeHasUnit ? '' : ctx.variant === 'loyalty' ? p.cardUnitPrice || p.unitPrice : p.unitPrice;
const validity = p.validityOverride || ctx.validity;
const small = (text: string) =>
`<div class="prod-unit force-text-dark" style="margin-top: 2px; ${TEXT_FONT} font-size: 10px; line-height: 1.3; color: #888888;">${escapeWithBreaks(text)}</div>`;
+25
View File
@@ -0,0 +1,25 @@
// Jednotná podoba jednotkovej ceny: "jednotková cena 7,75 EUR/kg"
// Prevedie staršie zápisy ("1kg = 7,75 €", "1 l = 0,79 €") aj zápis z podkladov ("7,75 EUR/kg").
// Iný text (napr. ručne napísaný) ostane bez zmeny.
export function normalizeUnitPrice(value: string | undefined, fallbackUnit = ''): string {
const v = (value || '').trim();
if (!v) return '';
const num = (s: string) => {
const n = parseFloat(s.replace(/\s/g, '').replace(',', '.'));
return Number.isFinite(n) ? n.toFixed(2).replace('.', ',') : null;
};
// "1kg = 7,75 €", "1 kg = 7,75 EUR"
let m = v.match(/^1\s*([a-zA-Zá-ž.]+)\s*=\s*([\d\s.,]+?)\s*(?:€|EUR)$/i);
if (m) {
const n = num(m[2]);
return n ? `jednotková cena ${n} EUR/${m[1]}` : v;
}
// "7,75 EUR/kg", "jednotková cena 7,75 EUR/kg", "7,75 €/kg", "7,75" (jednotka z fallbackUnit)
m = v.match(/^(?:jednotková cena\s*)?([\d\s.,]+?)\s*(?:EUR|€)?\s*(?:\/\s*(.+))?$/i);
if (m) {
const n = num(m[1]);
const unit = (m[2] || fallbackUnit).trim();
return n ? `jednotková cena ${n} EUR${unit ? `/${unit}` : ''}` : v;
}
return v;
}