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
@@ -7,6 +7,8 @@ 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 { shrinkImage, uploadFile } from '@/lib/upload';
|
||||
|
||||
interface ImportModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -50,6 +52,10 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||||
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);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -59,6 +65,19 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||||
|
||||
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');
|
||||
@@ -135,7 +154,39 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||||
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 = () => {
|
||||
if (zip) {
|
||||
applyZipImport();
|
||||
return;
|
||||
}
|
||||
if (!mapping.title) {
|
||||
alert('Prosím vyberte stĺpec pre Názov produktu.');
|
||||
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">
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,.json,.xml"
|
||||
accept=".csv,.json,.xml,.zip"
|
||||
onChange={handleFileUpload}
|
||||
className="hidden"
|
||||
id="file-upload-input"
|
||||
@@ -219,12 +270,68 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||||
<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 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 className="text-xs text-gray-400 mt-1">Podporované formáty: .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">
|
||||
{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 */}
|
||||
{columns.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
@@ -363,11 +470,11 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
Importovať do kampane
|
||||
{zipProgress || (zip ? `Importovať ${zip.products.length} produktov` : 'Importovať do kampane')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user