From 14358b228f29c8390a19afd28afdee571a3779d4 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Wed, 21 Oct 2026 16:26:25 +0200 Subject: [PATCH] feat: import podkladov - jednotkova cena v tvare 'jednotkova cena 8,56 EUR/l', gramaz oddelena z nazvu Mnozstvo sa odoberie z konca nazvu v letaku (aj 'od 225 g do 250 g', '3 x 50 g 1 bal.'); co je pred mnozstvom zo stlpca Mnozstvo, ostava v nazve. Overene na 432 riadkoch podkladov. Co-Authored-By: Claude Opus 5.5 --- src/components/editor/ProductImportModal.tsx | 5 +- src/lib/podkladyZip.ts | 66 +++++++++++++++++--- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/components/editor/ProductImportModal.tsx b/src/components/editor/ProductImportModal.tsx index 13a01c0..1029c93 100644 --- a/src/components/editor/ProductImportModal.tsx +++ b/src/components/editor/ProductImportModal.tsx @@ -306,7 +306,10 @@ export const ProductImportModal: React.FC = ({ )}
-

{p.title}

+

+ {p.title} + {p.quantity && {p.quantity}} +

EAN {p.ean} {p.unitPrice && ` · ${p.unitPrice}`} diff --git a/src/lib/podkladyZip.ts b/src/lib/podkladyZip.ts index 5cdd218..2640d7a 100644 --- a/src/lib/podkladyZip.ts +++ b/src/lib/podkladyZip.ts @@ -11,7 +11,8 @@ export interface PodkladyProduct { 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 €" + unitPrice: string; // napr. "jednotková cena 5,39 EUR/kg" + quantity: string; // gramáž / množstvo, napr. "1 kg" discount: string; // napr. "30%" badgeLabel: string; // napr. "SUPER CENA | červená | SK" – z čoho sa hľadá šablóna cenovky badge?: BadgeTemplate; @@ -138,10 +139,58 @@ function toNumber(value: string): number | null { const formatEur = (n: number) => n.toFixed(2).replace('.', ','); -// "5,39 EUR/kg" → "1kg = 5,39 €" (formát ako v ostatných kartách) -function formatUnitPrice(value: string): string { - const m = value.trim().match(/^([\d.,]+)\s*(?:EUR|€)\s*\/\s*(\S+)$/i); - return m ? `1${m[2]} = ${m[1]} €` : value.trim(); +// Jednotková cena: "5,39 EUR/kg" → "jednotková cena 5,39 EUR/kg" +// (bez jednotky v podkladoch sa doplní z Množstvo MJ: g → kg, ml → l) +export function formatUnitPrice(value: string, quantityUnit = ''): string { + const v = value.trim(); + if (!v) return ''; + const m = v.match(/^(?:jednotková cena\s*)?([\d\s.,]+?)\s*(?:EUR|€)?\s*(?:\/\s*(.+))?$/i); + const n = m ? toNumber(m[1]) : null; + if (!m || n == null) return v; + const unit = (m[2] || { g: 'kg', kg: 'kg', ml: 'l', l: 'l', ks: 'ks' }[quantityUnit.trim().toLowerCase()] || '').trim(); + return `jednotková cena ${formatEur(n)} EUR${unit ? `/${unit}` : ''}`; +} + +// Gramáž / množstvo na konci názvu z letáku: "Ryža guľatozrnná 1 kg", "Cereálie 2 druhy od 225 g do 250 g", +// "Vrecia na smeti 60 l 15 ks 1 bal.". Z konca názvu sa odoberú údaje o množstve; ak je medzi nimi množstvo +// zo stĺpca Množstvo, oddelí sa od neho (to pred ním patrí k názvu, napr. „60 l“ pri vreciach). +const UNIT = String.raw`(?:kg|g|mg|l|ml|cl|dl|ks|bal\.?|balenie|pracích dávok|pracie dávky|pracia dávka|PD|m|cm|mm|tbl\.?|kapsúl|plátkov|rolí|rolky|vreciek)`; +const NUM = String.raw`\d+(?:[.,]\d+)?`; +const AMOUNT = String.raw`(?:(?:${NUM}\s?[x×]\s?)?${NUM}\s?${UNIT}(?![\p{L}]))`; +const TAIL = new RegExp(String.raw`(?:^|\s)((?:od\s+${AMOUNT}\s+do\s+${AMOUNT})|${AMOUNT}|${NUM}\s?[x×]\s?${AMOUNT})\s*$`, 'iu'); + +export function splitQuantity(title: string, qty = '', qtyUnit = ''): { title: string; quantity: string } { + let rest = title.trim(); + const parts: string[] = []; + for (let m = rest.match(TAIL); m; m = rest.match(TAIL)) { + parts.unshift(m[1]); + rest = rest.slice(0, m.index).trim(); + } + if (!parts.length) { + // v názve nie je množstvo – doplníme ho zo stĺpcov Množstvo + Množstvo MJ + const n = toNumber(qty); + return { title: rest, quantity: n != null && qtyUnit ? `${String(n).replace('.', ',')} ${qtyUnit.trim()}` : '' }; + } + // množstvo zo stĺpca (napr. 15 ks) – časti pred ním sú súčasť názvu + const n = toNumber(qty); + const key = n != null && qtyUnit ? `${n}|${qtyUnit.trim().toLowerCase()}` : ''; + const idx = key ? parts.findIndex((p) => amountKeys(p).includes(key)) : -1; + const keep = idx > 0 ? parts.slice(0, idx) : []; + return { title: [rest, ...keep].filter(Boolean).join(' '), quantity: parts.slice(Math.max(idx, 0)).join(' ') }; +} + +// "0,555 l" → ["0.555|l", "555|ml"] – aby sa dalo porovnať s množstvom zo stĺpcov +function amountKeys(text: string): string[] { + const m = text.match(new RegExp(String.raw`(${NUM})\s?(kg|g|l|ml|ks)\b`, 'i')); + if (!m) return []; + const n = toNumber(m[1])!; + const u = m[2].toLowerCase(); + const keys = [`${n}|${u}`]; + if (u === 'kg') keys.push(`${+(n * 1000).toFixed(3)}|g`); + if (u === 'g') keys.push(`${+(n / 1000).toFixed(6)}|kg`); + if (u === 'l') keys.push(`${+(n * 1000).toFixed(3)}|ml`); + if (u === 'ml') keys.push(`${+(n / 1000).toFixed(6)}|l`); + return keys; } export function splitPrice(n: number): { main: string; sub: string } { @@ -163,6 +212,8 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom ean: col('EAN'), title: col('Názov v letáku'), titleAlt: col('Názov produktu'), + qty: col('Množstvo'), + qtyUnit: col('Množstvo MJ'), price: col('Akciová cena (leták)', 'Cena (xlsx podklady)'), cardPrice: col('Cena s vernostnou kartou'), appPrice: col('Cena pre appkáčov'), @@ -207,11 +258,11 @@ export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Prom products.push({ ean, - title: get(row, c.title) || get(row, c.titleAlt) || ean, + ...splitQuantity(get(row, c.title) || get(row, c.titleAlt) || ean, get(row, c.qty), get(row, c.qtyUnit)), price: toNumber(get(row, c.price)), cardPrice: toNumber(get(row, c.cardPrice)), appPrice, - unitPrice: formatUnitPrice(get(row, c.unitPrice)), + unitPrice: formatUnitPrice(get(row, c.unitPrice), get(row, c.qtyUnit)), discount: discount ? `${Math.round(discount)}%` : '', badgeLabel: [BADGE_IMPORT_TYPES.find((t) => t.value === type)?.label || type, colorText, origin, appPrice != null ? 'appka' : ''] .filter(Boolean) @@ -246,6 +297,7 @@ export function podkladyToProductItem(p: PodkladyProduct, imageUrl: string, idx: imageUrl, priceMain: price.main, priceSub: price.sub, + quantity: p.quantity, unitPrice: p.unitPrice, discountText: p.discount, ...(card ? { cardPriceMain: card.main, cardPriceSub: card.sub } : {}),