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>
507 lines
23 KiB
TypeScript
507 lines
23 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState } from 'react';
|
||
import Papa from 'papaparse';
|
||
import { XMLParser } from 'fast-xml-parser';
|
||
import { BadgeTemplate, ProductItem } from '@/types/newsletter';
|
||
import { findBadgeByCode, badgeCode } from '@/lib/badgeUtils';
|
||
import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
|
||
import { supabase } from '@/lib/supabase';
|
||
import { PodkladyProduct, podkladyToProductItem, readPodkladyZip, splitPrice } from '@/lib/podkladyZip';
|
||
import { CARD_BG_BY_BADGE_COLOR } from '@/lib/productCard';
|
||
import { shrinkImage, uploadFile } from '@/lib/upload';
|
||
|
||
interface ImportModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onImportProducts: (products: ProductItem[], options?: { replace?: boolean }) => void;
|
||
availableBadges?: BadgeTemplate[]; // šablóny cenoviek – stĺpec s kódom určí, ktorú použiť
|
||
}
|
||
|
||
export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
onImportProducts,
|
||
availableBadges = []
|
||
}) => {
|
||
const [file, setFile] = useState<File | null>(null);
|
||
const [fileType, setFileType] = useState<'csv' | 'json' | 'xml'>('csv');
|
||
const [parsedRows, setParsedRows] = useState<any[]>([]);
|
||
const [columns, setColumns] = useState<string[]>([]);
|
||
|
||
// Mapovanie stĺpcov
|
||
const [mapping, setMapping] = useState<{
|
||
title: string;
|
||
priceMain: string;
|
||
priceSub: string;
|
||
unitPrice: string;
|
||
discountText: string;
|
||
imageUrl: string;
|
||
validity: string;
|
||
badgeCode: string;
|
||
}>({
|
||
title: '',
|
||
priceMain: '',
|
||
priceSub: '',
|
||
unitPrice: '',
|
||
discountText: '',
|
||
imageUrl: '',
|
||
validity: '',
|
||
badgeCode: ''
|
||
});
|
||
|
||
const [presetName, setPresetName] = useState('');
|
||
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);
|
||
const [replaceExisting, setReplaceExisting] = useState(false); // vymazať doterajšie produkty sekcie
|
||
|
||
if (!isOpen) return null;
|
||
|
||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const uploaded = e.target.files?.[0];
|
||
if (!uploaded) return;
|
||
setFile(uploaded);
|
||
|
||
const ext = uploaded.name.split('.').pop()?.toLowerCase();
|
||
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') {
|
||
setFileType('csv');
|
||
reader.onload = (event) => {
|
||
const text = event.target?.result as string;
|
||
Papa.parse(text, {
|
||
header: true,
|
||
skipEmptyLines: true,
|
||
complete: (results) => {
|
||
if (results.data.length > 0) {
|
||
setParsedRows(results.data);
|
||
const headers = Object.keys(results.data[0] as object);
|
||
setColumns(headers);
|
||
autoGuessMapping(headers);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
reader.readAsText(uploaded);
|
||
} else if (ext === 'json') {
|
||
setFileType('json');
|
||
reader.onload = (event) => {
|
||
try {
|
||
const json = JSON.parse(event.target?.result as string);
|
||
const arrayData = Array.isArray(json) ? json : json.products || [json];
|
||
setParsedRows(arrayData);
|
||
if (arrayData.length > 0) {
|
||
const headers = Object.keys(arrayData[0]);
|
||
setColumns(headers);
|
||
autoGuessMapping(headers);
|
||
}
|
||
} catch (err) {
|
||
alert('Chyba pri parsovaní JSON súboru.');
|
||
}
|
||
};
|
||
reader.readAsText(uploaded);
|
||
} else if (ext === 'xml') {
|
||
setFileType('xml');
|
||
reader.onload = (event) => {
|
||
try {
|
||
const parser = new XMLParser({ ignoreAttributes: false });
|
||
const jsonObj = parser.parse(event.target?.result as string);
|
||
// Hľadáme pole produktov
|
||
const possibleArray =
|
||
jsonObj.products?.product || jsonObj.items?.item || Object.values(jsonObj)[0];
|
||
const arrayData = Array.isArray(possibleArray) ? possibleArray : [possibleArray];
|
||
setParsedRows(arrayData);
|
||
if (arrayData.length > 0) {
|
||
const headers = Object.keys(arrayData[0]);
|
||
setColumns(headers);
|
||
autoGuessMapping(headers);
|
||
}
|
||
} catch (err) {
|
||
alert('Chyba pri parsovaní XML súboru.');
|
||
}
|
||
};
|
||
reader.readAsText(uploaded);
|
||
}
|
||
};
|
||
|
||
const autoGuessMapping = (headers: string[]) => {
|
||
const newMapping = { ...mapping };
|
||
headers.forEach((h) => {
|
||
const lower = h.toLowerCase();
|
||
if (lower.includes('nazov') || lower.includes('title') || lower.includes('name')) newMapping.title = h;
|
||
if (lower.includes('hlavna') || lower.includes('euro') || lower.includes('price_main')) newMapping.priceMain = h;
|
||
if (lower.includes('cent') || lower.includes('sub') || lower.includes('price_sub')) newMapping.priceSub = h;
|
||
if (lower.includes('merna') || lower.includes('unit') || lower.includes('kg')) newMapping.unitPrice = h;
|
||
if (lower.includes('zlava') || lower.includes('discount')) newMapping.discountText = h;
|
||
if (lower.includes('obrazok') || lower.includes('image') || lower.includes('foto')) newMapping.imageUrl = h;
|
||
if (lower.includes('platnost') || lower.includes('valid')) newMapping.validity = h;
|
||
if (lower.includes('cenovk') || lower.includes('badge') || lower.includes('sablon') || lower.includes('template')) newMapping.badgeCode = h;
|
||
});
|
||
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, { replace: replaceExisting });
|
||
onClose();
|
||
};
|
||
|
||
const applyImport = () => {
|
||
if (zip) {
|
||
applyZipImport();
|
||
return;
|
||
}
|
||
if (!mapping.title) {
|
||
alert('Prosím vyberte stĺpec pre Názov produktu.');
|
||
return;
|
||
}
|
||
|
||
const importedProducts: ProductItem[] = parsedRows.map((row, idx) => {
|
||
let pMain = String(row[mapping.priceMain] || '0').trim();
|
||
let pSub = String(row[mapping.priceSub] || '00').trim();
|
||
|
||
// Ak je cena v jednom stĺpci napr. "1.99" alebo "1,99"
|
||
if (!mapping.priceSub && mapping.priceMain) {
|
||
const fullPrice = String(row[mapping.priceMain] || '0').replace(',', '.');
|
||
const parts = fullPrice.split('.');
|
||
pMain = parts[0] || '0';
|
||
pSub = parts[1] || '00';
|
||
if (pSub.length === 1) pSub += '0';
|
||
}
|
||
|
||
// Šablóna cenovky podľa kódu (alebo názvu) zo súboru
|
||
const badge = mapping.badgeCode ? findBadgeByCode(availableBadges, row[mapping.badgeCode]) : undefined;
|
||
|
||
return {
|
||
...(badge ? { badgeCustomTemplate: badge, badgeId: badge.id, badgeWidth: badge.width ?? 110 } : {}),
|
||
id: `imported-${Date.now()}-${idx}`,
|
||
title: String(row[mapping.title] || 'Nový produkt'),
|
||
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] || '',
|
||
discountText: row[mapping.discountText] ? `${row[mapping.discountText]}%` : '',
|
||
badgeText: 'zľava',
|
||
cardBgColor: '#feeaea',
|
||
stores: { potraviny: true, supermarket: true, tempo: true },
|
||
validityOverride: row[mapping.validity] || undefined
|
||
};
|
||
});
|
||
|
||
// Kódy cenoviek, ku ktorým sa nenašla šablóna
|
||
if (mapping.badgeCode) {
|
||
const unknown = [
|
||
...new Set(
|
||
parsedRows
|
||
.map((row) => String(row[mapping.badgeCode] || '').trim())
|
||
.filter((v) => v && !findBadgeByCode(availableBadges, v))
|
||
)
|
||
];
|
||
if (unknown.length) {
|
||
alert(`Pre tieto kódy cenoviek sa nenašla šablóna (produkty dostanú predvolenú cenovku):\n${unknown.join(', ')}`);
|
||
}
|
||
}
|
||
|
||
onImportProducts(importedProducts);
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs flex items-center justify-center p-4">
|
||
<div className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||
{/* Header */}
|
||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Upload className="w-5 h-5 text-red-600" />
|
||
<h2 className="text-lg font-bold text-gray-900">Hromadný import produktov</h2>
|
||
</div>
|
||
<button onClick={onClose} className="p-1 text-gray-400 hover:text-gray-600 rounded-lg">
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="p-6 overflow-y-auto space-y-6">
|
||
{/* File Upload Box */}
|
||
<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
|
||
type="file"
|
||
accept=".csv,.json,.xml,.zip"
|
||
onChange={handleFileUpload}
|
||
className="hidden"
|
||
id="file-upload-input"
|
||
/>
|
||
<label htmlFor="file-upload-input" className="cursor-pointer">
|
||
<FileText className="w-10 h-10 text-gray-400 mx-auto mb-2" />
|
||
<p className="font-semibold text-gray-700">
|
||
{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>
|
||
</label>
|
||
</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"
|
||
style={p.color ? { backgroundColor: CARD_BG_BY_BADGE_COLOR[p.color] } : undefined}
|
||
title={p.color ? 'Pozadie karty podľa farby cenovky' : undefined}
|
||
>
|
||
{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 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}`}
|
||
{p.deposit && ` · záloha ${p.deposit} €`}
|
||
{p.appPrice != null && ` · v appke ${p.appPrice.toFixed(2).replace('.', ',')} €`}
|
||
{stores && ` · ${stores}`}
|
||
</p>
|
||
{p.extras.length > 0 && (
|
||
<p className="text-[11px] text-gray-500 truncate">
|
||
štítok na fotke: <span className="font-bold text-red-700">{p.extras.join(' · ')}</span>
|
||
</p>
|
||
)}
|
||
<p className="text-[11px] truncate">
|
||
{p.badge && p.badgeMissing.length ? (
|
||
<span className="text-amber-700">
|
||
cenovka: {p.badge.name} – chýba {p.badgeMissing.join(' a ')} pre „{p.badgeLabel}“
|
||
</span>
|
||
) : 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>
|
||
<label className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-xs font-semibold text-gray-700 cursor-pointer">
|
||
<input type="checkbox" checked={replaceExisting} onChange={(e) => setReplaceExisting(e.target.checked)} className="w-3.5 h-3.5 accent-red-600" />
|
||
Vymazať doterajšie produkty v sekcii (nahradiť novými zo ZIP-u)
|
||
</label>
|
||
<p className="text-[11px] text-gray-500">
|
||
Fotky sa pri importe zmenšia na najviac 1000 px, pozadie karty sa nastaví podľa farby cenovky. Rozloženie karty (veľkosť a poloha
|
||
cenovky, štítky predajní) sa prevezme z prvého produktu v sekcii.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Mapping Table */}
|
||
{columns.length > 0 && (
|
||
<div className="space-y-4">
|
||
<h3 className="text-sm font-bold text-gray-800 uppercase tracking-wider">
|
||
Vizuálny Data Mapper (Párovanie stĺpcov)
|
||
</h3>
|
||
<div className="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-200">
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">Názov produktu *</label>
|
||
<select
|
||
value={mapping.title}
|
||
onChange={(e) => setMapping({ ...mapping, title: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Vyberte stĺpec --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">Hlavná cena (Eurá)</label>
|
||
<select
|
||
value={mapping.priceMain}
|
||
onChange={(e) => setMapping({ ...mapping, priceMain: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Vyberte stĺpec --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">Centy (podružná cena)</label>
|
||
<select
|
||
value={mapping.priceSub}
|
||
onChange={(e) => setMapping({ ...mapping, priceSub: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Vyberte stĺpec (voliteľné) --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">Merná cena (napr. 1kg = 4,91 €)</label>
|
||
<select
|
||
value={mapping.unitPrice}
|
||
onChange={(e) => setMapping({ ...mapping, unitPrice: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Vyberte stĺpec --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">Zľava %</label>
|
||
<select
|
||
value={mapping.discountText}
|
||
onChange={(e) => setMapping({ ...mapping, discountText: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Vyberte stĺpec --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="col-span-2">
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">Cenovka (kód šablóny)</label>
|
||
<select
|
||
value={mapping.badgeCode}
|
||
onChange={(e) => setMapping({ ...mapping, badgeCode: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Bez cenovky zo súboru (predvolená) --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
<p className="text-[11px] text-gray-500 mt-1">
|
||
Kódy šablón:{' '}
|
||
{availableBadges.length
|
||
? availableBadges.map(badgeCode).join(', ')
|
||
: 'zatiaľ žiadne šablóny'}
|
||
{' '}(kód vzniká z názvu šablóny; v súbore môže byť kód aj presný názov)
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-semibold text-gray-600 mb-1">URL obrázka</label>
|
||
<select
|
||
value={mapping.imageUrl}
|
||
onChange={(e) => setMapping({ ...mapping, imageUrl: e.target.value })}
|
||
className="w-full text-sm border border-gray-300 rounded-lg p-2 bg-white"
|
||
>
|
||
<option value="">-- Vyberte stĺpec --</option>
|
||
{columns.map((c) => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Živý náhľad prvých 2 riadkov */}
|
||
{parsedRows.length > 0 && (
|
||
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
||
<div className="bg-gray-100 px-4 py-2 text-xs font-bold text-gray-700">
|
||
Živý náhľad prvých položiek ({parsedRows.length} nájdených záznamov)
|
||
</div>
|
||
<div className="p-3 text-xs space-y-2 bg-white">
|
||
{parsedRows.slice(0, 2).map((row, idx) => (
|
||
<div key={idx} className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||
<span className="font-semibold text-gray-800">
|
||
{row[mapping.title] || 'Bez názvu'}
|
||
{mapping.badgeCode && (
|
||
<span className="ml-2 text-[10px] font-medium text-gray-500">
|
||
cenovka: {findBadgeByCode(availableBadges, row[mapping.badgeCode])?.name || `? (${row[mapping.badgeCode] || 'prázdne'})`}
|
||
</span>
|
||
)}
|
||
</span>
|
||
<span className="text-red-600 font-bold">
|
||
{row[mapping.priceMain] || '0'},{row[mapping.priceSub] || '00'} €
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex items-center justify-between">
|
||
<button
|
||
onClick={onClose}
|
||
className="px-4 py-2 text-sm font-semibold text-gray-600 hover:text-gray-900"
|
||
>
|
||
Zrušiť
|
||
</button>
|
||
<button
|
||
onClick={applyImport}
|
||
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"
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
{zipProgress || (zip ? `Importovať ${zip.products.length} produktov` : 'Importovať do kampane')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|