327 lines
13 KiB
TypeScript
327 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import Papa from 'papaparse';
|
|
import { XMLParser } from 'fast-xml-parser';
|
|
import { ProductItem } from '@/types/newsletter';
|
|
import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
|
|
import { supabase } from '@/lib/supabase';
|
|
|
|
interface ImportModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onImportProducts: (products: ProductItem[]) => void;
|
|
}
|
|
|
|
export const ProductImportModal: React.FC<ImportModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onImportProducts
|
|
}) => {
|
|
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;
|
|
}>({
|
|
title: '',
|
|
priceMain: '',
|
|
priceSub: '',
|
|
unitPrice: '',
|
|
discountText: '',
|
|
imageUrl: '',
|
|
validity: ''
|
|
});
|
|
|
|
const [presetName, setPresetName] = useState('');
|
|
const [savedPresets, setSavedPresets] = useState<any[]>([]);
|
|
|
|
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();
|
|
|
|
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;
|
|
});
|
|
setMapping(newMapping);
|
|
};
|
|
|
|
const applyImport = () => {
|
|
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';
|
|
}
|
|
|
|
return {
|
|
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
|
|
};
|
|
});
|
|
|
|
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"
|
|
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 alebo pretiahnite CSV, JSON alebo XML'}
|
|
</p>
|
|
<p className="text-xs text-gray-400 mt-1">Podporované formáty: .csv, .json, .xml</p>
|
|
</label>
|
|
</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>
|
|
<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'}</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}
|
|
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
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|