feat: hromadny import produktov zo ZIP-u s podkladmi (xlsx + fotky)
Tabulka z Databazy produktov sa precita bez mapovania stlpcov, fotky <EAN>/web/*.png sa zmensia na 1000 px a nahraju. Cenovka sa hlada podla Zlava graficka | Farba cenovky | Povod. Importovane produkty preberu rozlozenie karty z prveho produktu v sekcii. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48fd82a77d
commit
9b1f7b60cf
3 files changed
+303
-6
No files matched your search
+14
-1
@@ -357,11 +357,24 @@ export default function Home() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rozloženie karty (cenovka, štítky predajní, farba) prevezmú nové produkty z prvého produktu v sekcii
|
||||||
|
const first: ProductItem | undefined = target.data.products?.[0];
|
||||||
|
const layoutKeys = [
|
||||||
|
'badgeWidth', 'badgeAlign', 'badgeLayout', 'badgePaddingTop', 'badgePaddingBottom',
|
||||||
|
'storesOffsetX', 'storesOffsetY', 'storesSize', 'cardBgColor'
|
||||||
|
] as const;
|
||||||
|
const withLayout = imported.map((p) => {
|
||||||
|
if (!first) return p;
|
||||||
|
const inherited: Partial<ProductItem> = {};
|
||||||
|
for (const k of layoutKeys) if (p[k] === undefined && first[k] !== undefined) Object.assign(inherited, { [k]: first[k] });
|
||||||
|
return { ...inherited, ...p };
|
||||||
|
});
|
||||||
|
|
||||||
const updated = {
|
const updated = {
|
||||||
...target,
|
...target,
|
||||||
data: {
|
data: {
|
||||||
...target.data,
|
...target.data,
|
||||||
products: [...(target.data.products || []), ...imported]
|
products: [...(target.data.products || []), ...withLayout]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
handleUpdateModule(updated);
|
handleUpdateModule(updated);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { BadgeTemplate, ProductItem } from '@/types/newsletter';
|
|||||||
import { findBadgeByCode, badgeCode } from '@/lib/badgeUtils';
|
import { findBadgeByCode, badgeCode } from '@/lib/badgeUtils';
|
||||||
import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
|
import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { PodkladyProduct, podkladyToProductItem, readPodkladyZip, splitPrice } from '@/lib/podkladyZip';
|
||||||
|
import { shrinkImage, uploadFile } from '@/lib/upload';
|
||||||
|
|
||||||
interface ImportModalProps {
|
interface ImportModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -50,6 +52,10 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|||||||
const [presetName, setPresetName] = useState('');
|
const [presetName, setPresetName] = useState('');
|
||||||
const [savedPresets, setSavedPresets] = useState<any[]>([]);
|
const [savedPresets, setSavedPresets] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// ZIP s podkladmi z Databázy produktov (tabuľka + fotky) – pevný formát, bez mapovania stĺpcov
|
||||||
|
const [zip, setZip] = useState<{ products: (PodkladyProduct & { thumb: string })[]; info: string } | null>(null);
|
||||||
|
const [zipProgress, setZipProgress] = useState<string | null>(null);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -59,6 +65,19 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|||||||
|
|
||||||
const ext = uploaded.name.split('.').pop()?.toLowerCase();
|
const ext = uploaded.name.split('.').pop()?.toLowerCase();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
setZip(null);
|
||||||
|
|
||||||
|
if (ext === 'zip') {
|
||||||
|
setColumns([]);
|
||||||
|
setParsedRows([]);
|
||||||
|
readPodkladyZip(uploaded, availableBadges)
|
||||||
|
.then(({ products, info }) => {
|
||||||
|
if (!products.length) throw new Error('V tabuľke sa nenašiel žiadny produkt.');
|
||||||
|
setZip({ products: products.map((p) => ({ ...p, thumb: p.photo ? URL.createObjectURL(p.photo) : '' })), info });
|
||||||
|
})
|
||||||
|
.catch((err: Error) => alert(`ZIP sa nepodarilo načítať: ${err.message}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ext === 'csv') {
|
if (ext === 'csv') {
|
||||||
setFileType('csv');
|
setFileType('csv');
|
||||||
@@ -135,7 +154,39 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|||||||
setMapping(newMapping);
|
setMapping(newMapping);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Import zo ZIP-u: fotky sa zmenšia (max 1000 px) a nahrajú, potom sa produkty pridajú do sekcie
|
||||||
|
const applyZipImport = async () => {
|
||||||
|
if (!zip) return;
|
||||||
|
const items: ProductItem[] = [];
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < zip.products.length; i++) {
|
||||||
|
const p = zip.products[i];
|
||||||
|
setZipProgress(`Nahrávam fotky ${i + 1}/${zip.products.length}…`);
|
||||||
|
let imageUrl = '';
|
||||||
|
if (p.photo) {
|
||||||
|
const small = await shrinkImage(new File([p.photo], p.photoName || `${p.ean}.png`, { type: p.photo.type || 'image/png' }));
|
||||||
|
imageUrl = await uploadFile(small, small.name);
|
||||||
|
}
|
||||||
|
items.push(podkladyToProductItem(p, imageUrl, i));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Import sa nepodaril: ${(err as Error).message}`);
|
||||||
|
setZipProgress(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setZipProgress(null);
|
||||||
|
zip.products.forEach((p) => p.thumb && URL.revokeObjectURL(p.thumb));
|
||||||
|
setZip(null);
|
||||||
|
setFile(null);
|
||||||
|
onImportProducts(items);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const applyImport = () => {
|
const applyImport = () => {
|
||||||
|
if (zip) {
|
||||||
|
applyZipImport();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!mapping.title) {
|
if (!mapping.title) {
|
||||||
alert('Prosím vyberte stĺpec pre Názov produktu.');
|
alert('Prosím vyberte stĺpec pre Názov produktu.');
|
||||||
return;
|
return;
|
||||||
@@ -211,7 +262,7 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|||||||
<div className="border-2 border-dashed border-gray-300 hover:border-red-500 rounded-xl p-6 text-center cursor-pointer transition-colors bg-gray-50/50">
|
<div className="border-2 border-dashed border-gray-300 hover:border-red-500 rounded-xl p-6 text-center cursor-pointer transition-colors bg-gray-50/50">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv,.json,.xml"
|
accept=".csv,.json,.xml,.zip"
|
||||||
onChange={handleFileUpload}
|
onChange={handleFileUpload}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
id="file-upload-input"
|
id="file-upload-input"
|
||||||
@@ -219,12 +270,68 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|||||||
<label htmlFor="file-upload-input" className="cursor-pointer">
|
<label htmlFor="file-upload-input" className="cursor-pointer">
|
||||||
<FileText className="w-10 h-10 text-gray-400 mx-auto mb-2" />
|
<FileText className="w-10 h-10 text-gray-400 mx-auto mb-2" />
|
||||||
<p className="font-semibold text-gray-700">
|
<p className="font-semibold text-gray-700">
|
||||||
{file ? file.name : 'Kliknite sem alebo pretiahnite CSV, JSON alebo XML'}
|
{file ? file.name : 'Kliknite sem a vyberte ZIP s podkladmi, CSV, JSON alebo XML'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
Podporované formáty: .zip (podklady z Databázy produktov – tabuľka aj fotky), .csv, .json, .xml
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 mt-1">Podporované formáty: .csv, .json, .xml</p>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ZIP s podkladmi: náhľad produktov */}
|
||||||
|
{zip && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-baseline justify-between gap-3">
|
||||||
|
<h3 className="text-sm font-bold text-gray-800 uppercase tracking-wider">Produkty v ZIP-e ({zip.products.length})</h3>
|
||||||
|
{zip.info && <span className="text-[11px] text-gray-500 truncate">{zip.info}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="border border-gray-200 rounded-xl divide-y divide-gray-100 overflow-hidden">
|
||||||
|
{zip.products.map((p) => {
|
||||||
|
const price = p.price != null ? splitPrice(p.price) : null;
|
||||||
|
const stores = [p.stores.potraviny && 'P', p.stores.supermarket && 'S', p.stores.tempo && 'T'].filter(Boolean).join(' ');
|
||||||
|
return (
|
||||||
|
<div key={p.ean} className="flex items-center gap-3 px-3 py-2 bg-white">
|
||||||
|
<span className="w-12 h-12 rounded-lg bg-gray-50 border border-gray-200 overflow-hidden flex items-center justify-center shrink-0">
|
||||||
|
{p.thumb ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img src={p.thumb} alt="" className="w-full h-full object-contain" />
|
||||||
|
) : (
|
||||||
|
<span className="text-[9px] text-red-600 font-bold text-center leading-tight">bez fotky</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-bold text-gray-900 truncate">{p.title}</p>
|
||||||
|
<p className="text-[11px] text-gray-500 truncate">
|
||||||
|
EAN {p.ean}
|
||||||
|
{p.unitPrice && ` · ${p.unitPrice}`}
|
||||||
|
{p.deposit && ` · záloha ${p.deposit} €`}
|
||||||
|
{stores && ` · ${stores}`}
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] truncate">
|
||||||
|
{p.badge ? (
|
||||||
|
<span className="text-emerald-700">cenovka: {p.badge.name}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-amber-700">
|
||||||
|
{p.badgeLabel ? `cenovka „${p.badgeLabel}“ nie je medzi šablónami – použije sa predvolená` : 'bez cenovky – použije sa predvolená'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<p className="text-sm font-black text-red-600">{price ? `${price.main},${price.sub} €` : '—'}</p>
|
||||||
|
{p.discount && <p className="text-[11px] font-bold text-gray-600">-{p.discount}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-gray-500">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mapping Table */}
|
{/* Mapping Table */}
|
||||||
{columns.length > 0 && (
|
{columns.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -363,11 +470,11 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={applyImport}
|
onClick={applyImport}
|
||||||
disabled={parsedRows.length === 0}
|
disabled={(parsedRows.length === 0 && !zip) || !!zipProgress}
|
||||||
className="px-5 py-2 text-sm font-bold bg-red-600 text-white rounded-xl hover:bg-red-700 disabled:opacity-50 transition-colors flex items-center gap-2"
|
className="px-5 py-2 text-sm font-bold bg-red-600 text-white rounded-xl hover:bg-red-700 disabled:opacity-50 transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
Importovať do kampane
|
{zipProgress || (zip ? `Importovať ${zip.products.length} produktov` : 'Importovať do kampane')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Import ZIP-u s podkladmi z Databázy produktov: tabuľka podklady_*.xlsx (jeden riadok = jeden produkt)
|
||||||
|
// a fotky v priečinkoch <EAN>/web/*.png.
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
import { BadgeTemplate, ProductItem } from '@/types/newsletter';
|
||||||
|
import { findBadgeByCode } from './badgeUtils';
|
||||||
|
|
||||||
|
export interface PodkladyProduct {
|
||||||
|
ean: string;
|
||||||
|
title: string;
|
||||||
|
price: number | null; // akciová cena
|
||||||
|
cardPrice: number | null; // cena s vernostnou kartou
|
||||||
|
unitPrice: string; // napr. "1kg = 5,39 €"
|
||||||
|
discount: string; // napr. "30%"
|
||||||
|
badgeLabel: string; // napr. "SUPER CENA | červená | SK" – z čoho sa hľadá šablóna cenovky
|
||||||
|
badge?: BadgeTemplate;
|
||||||
|
stores: ProductItem['stores'];
|
||||||
|
deposit: string; // napr. "0,15"
|
||||||
|
photo?: Blob;
|
||||||
|
photoName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const norm = (s: string) =>
|
||||||
|
s
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[̀-ͯ]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// ---------- jednoduché čítanie .xlsx (prvý hárok) ----------
|
||||||
|
function colIndex(ref: string): number {
|
||||||
|
const letters = ref.replace(/\d+/g, '');
|
||||||
|
let n = 0;
|
||||||
|
for (const ch of letters) n = n * 26 + (ch.charCodeAt(0) - 64);
|
||||||
|
return n - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readXlsx(data: ArrayBuffer): Promise<string[][]> {
|
||||||
|
const xlsx = await JSZip.loadAsync(data);
|
||||||
|
const parse = async (path: string) => {
|
||||||
|
const text = await xlsx.file(path)?.async('string');
|
||||||
|
return text ? new DOMParser().parseFromString(text, 'application/xml') : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shared: string[] = [];
|
||||||
|
const sst = await parse('xl/sharedStrings.xml');
|
||||||
|
sst?.querySelectorAll('si').forEach((si) => {
|
||||||
|
shared.push([...si.getElementsByTagName('t')].map((t) => t.textContent || '').join(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheetPath =
|
||||||
|
xlsx.file('xl/worksheets/sheet1.xml') ? 'xl/worksheets/sheet1.xml' : Object.keys(xlsx.files).find((p) => /^xl\/worksheets\/[^/]+\.xml$/.test(p));
|
||||||
|
const sheet = sheetPath ? await parse(sheetPath) : null;
|
||||||
|
if (!sheet) throw new Error('V tabuľke sa nenašiel hárok.');
|
||||||
|
|
||||||
|
const rows: string[][] = [];
|
||||||
|
sheet.querySelectorAll('sheetData > row').forEach((row) => {
|
||||||
|
const cells: string[] = [];
|
||||||
|
row.querySelectorAll('c').forEach((c) => {
|
||||||
|
const type = c.getAttribute('t');
|
||||||
|
const v = c.getElementsByTagName('v')[0]?.textContent ?? '';
|
||||||
|
let value = v;
|
||||||
|
if (type === 's') value = shared[Number(v)] ?? '';
|
||||||
|
else if (type === 'inlineStr') value = [...c.getElementsByTagName('t')].map((t) => t.textContent || '').join('');
|
||||||
|
cells[colIndex(c.getAttribute('r') || 'A')] = value;
|
||||||
|
});
|
||||||
|
rows.push(Array.from(cells, (x) => x ?? ''));
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- prevody hodnôt ----------
|
||||||
|
function toNumber(value: string): number | null {
|
||||||
|
const n = parseFloat(String(value || '').replace(/\s/g, '').replace(',', '.'));
|
||||||
|
return Number.isFinite(n) ? n : 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitPrice(n: number): { main: string; sub: string } {
|
||||||
|
const [main, sub] = n.toFixed(2).split('.');
|
||||||
|
return { main, sub };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- načítanie ZIP-u ----------
|
||||||
|
export async function readPodkladyZip(file: File, badges: BadgeTemplate[]): Promise<{ products: PodkladyProduct[]; info: string }> {
|
||||||
|
const zip = await JSZip.loadAsync(file);
|
||||||
|
const files = Object.values(zip.files).filter((f) => !f.dir && !f.name.startsWith('__MACOSX/'));
|
||||||
|
const table = files.find((f) => /\.xlsx$/i.test(f.name));
|
||||||
|
if (!table) throw new Error('V ZIP-e chýba tabuľka .xlsx s podkladmi.');
|
||||||
|
const info = (await files.find((f) => /_info\.txt$/i.test(f.name))?.async('string'))?.trim() || '';
|
||||||
|
|
||||||
|
const [header = [], ...rows] = await readXlsx(await table.async('arraybuffer'));
|
||||||
|
const col = (...names: string[]) => header.findIndex((h) => names.some((n) => norm(h) === norm(n)));
|
||||||
|
const c = {
|
||||||
|
ean: col('EAN'),
|
||||||
|
title: col('Názov v letáku'),
|
||||||
|
titleAlt: col('Názov produktu'),
|
||||||
|
price: col('Akciová cena (leták)', 'Cena (xlsx podklady)'),
|
||||||
|
cardPrice: col('Cena s vernostnou kartou'),
|
||||||
|
unitPrice: col('Jednotková cena'),
|
||||||
|
discount: col('Zľava %'),
|
||||||
|
badge: col('Zľava grafická'),
|
||||||
|
color: col('Farba cenovky'),
|
||||||
|
origin: col('Pôvod'),
|
||||||
|
formats: col('Formát'),
|
||||||
|
deposit: col('Zálohovanie')
|
||||||
|
};
|
||||||
|
if (c.ean < 0) throw new Error('Tabuľka nemá stĺpec EAN – nejde o podklady z Databázy produktov?');
|
||||||
|
const get = (row: string[], i: number) => (i >= 0 ? String(row[i] ?? '').trim() : '');
|
||||||
|
|
||||||
|
const images = files.filter((f) => /\.(png|jpe?g|webp)$/i.test(f.name));
|
||||||
|
const products: PodkladyProduct[] = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
const ean = get(row, c.ean);
|
||||||
|
if (!ean) continue;
|
||||||
|
|
||||||
|
// fotka: <EAN>/web/… (prípadne hocijaký obrázok v priečinku <EAN>/)
|
||||||
|
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
|
||||||
|
const graphic = get(row, c.badge);
|
||||||
|
const color = get(row, c.color);
|
||||||
|
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 formats = norm(get(row, c.formats));
|
||||||
|
const deposit = toNumber(get(row, c.deposit));
|
||||||
|
const discount = toNumber(get(row, c.discount));
|
||||||
|
|
||||||
|
products.push({
|
||||||
|
ean,
|
||||||
|
title: get(row, c.title) || get(row, c.titleAlt) || ean,
|
||||||
|
price: toNumber(get(row, c.price)),
|
||||||
|
cardPrice: toNumber(get(row, c.cardPrice)),
|
||||||
|
unitPrice: formatUnitPrice(get(row, c.unitPrice)),
|
||||||
|
discount: discount ? `${Math.round(discount)}%` : '',
|
||||||
|
badgeLabel: tries[0],
|
||||||
|
badge,
|
||||||
|
stores: formats
|
||||||
|
? { potraviny: formats.includes('potraviny'), supermarket: formats.includes('supermarket'), tempo: formats.includes('tempo') }
|
||||||
|
: { potraviny: true, supermarket: true, tempo: true },
|
||||||
|
deposit: deposit ? formatEur(deposit) : '',
|
||||||
|
photo: photoFile ? await photoFile.async('blob') : undefined,
|
||||||
|
photoName: photoFile ? photoFile.name.split('/').pop()! : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { products, info };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produkt pre kampaň (fotka už nahratá – imageUrl)
|
||||||
|
export function podkladyToProductItem(p: PodkladyProduct, imageUrl: string, idx: number): ProductItem {
|
||||||
|
const price = splitPrice(p.price ?? 0);
|
||||||
|
const card = p.cardPrice != null ? splitPrice(p.cardPrice) : null;
|
||||||
|
return {
|
||||||
|
...(p.badge ? { badgeCustomTemplate: p.badge, badgeId: p.badge.id } : {}),
|
||||||
|
id: `zip-${Date.now()}-${idx}`,
|
||||||
|
title: p.title,
|
||||||
|
imageUrl,
|
||||||
|
priceMain: price.main,
|
||||||
|
priceSub: price.sub,
|
||||||
|
unitPrice: p.unitPrice,
|
||||||
|
discountText: p.discount,
|
||||||
|
...(card ? { cardPriceMain: card.main, cardPriceSub: card.sub } : {}),
|
||||||
|
note: p.deposit ? `+ záloha ${p.deposit} €` : '',
|
||||||
|
badgeText: 'zľava',
|
||||||
|
stores: p.stores
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user