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:
1 parent
e2e9a82094
commit
14358b228f
2 files changed
+63
-8
No files matched your search
@@ -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
@@ -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 } : {}),
|
||||
|
||||
Reference in new issue
Block a user