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 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 16:26:25 +02:00
1 parent e2e9a82094
commit 14358b228f
2 files changed
+63 -8

No files matched your search

+4 -1
View File
@@ -306,7 +306,10 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
)}
</span>
<div className="flex-1 min-w-0">
<p className="text-xs font-bold text-gray-900 truncate">{p.title}</p>
<p className="text-xs text-gray-900 truncate">
<span className="font-bold">{p.title}</span>
{p.quantity && <span className="ml-1 rounded bg-gray-100 px-1 text-gray-600">{p.quantity}</span>}
</p>
<p className="text-[11px] text-gray-500 truncate">
EAN {p.ean}
{p.unitPrice && ` · ${p.unitPrice}`}
+59 -7
View File
@@ -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 } : {}),