feat: kod sablony cenovky pre hromadny import

- sablona cenovky ma kod (napr. SUPER_CENA), nastavuje sa v hlavicke editora
  cenoviek; kontrola jedinecnosti, ulozi sa v layers_config (bez zmeny DB)
- import CSV/JSON/XML: novy stlpec Cenovka (kod sablony), automaticky
  rozpoznany (cenovka/badge/sablona); pouzije sa kod alebo nazov sablony,
  bez ohladu na velke pismena a diakritiku
- nahlad importu ukazuje priradenu sablonu, neznamy kod nahlasi upozornenie
- spolocne nacitanie sablon z DB (badgeFromRow) pre editor aj stranku

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 12:41:39 +02:00
1 parent 6d73660cac
commit 332ab833e9
5 files changed
+138 -41

No files matched your search

+54 -4
View File
@@ -3,7 +3,8 @@
import React, { useState } from 'react';
import Papa from 'papaparse';
import { XMLParser } from 'fast-xml-parser';
import { ProductItem } from '@/types/newsletter';
import { BadgeTemplate, ProductItem } from '@/types/newsletter';
import { findBadgeByCode, normalizeBadgeCode } from '@/lib/badgeUtils';
import { Upload, Check, AlertCircle, FileText, X } from 'lucide-react';
import { supabase } from '@/lib/supabase';
@@ -11,12 +12,14 @@ interface ImportModalProps {
isOpen: boolean;
onClose: () => void;
onImportProducts: (products: ProductItem[]) => void;
availableBadges?: BadgeTemplate[]; // šablóny cenoviek – stĺpec s kódom určí, ktorú použiť
}
export const ProductImportModal: React.FC<ImportModalProps> = ({
isOpen,
onClose,
onImportProducts
onImportProducts,
availableBadges = []
}) => {
const [file, setFile] = useState<File | null>(null);
const [fileType, setFileType] = useState<'csv' | 'json' | 'xml'>('csv');
@@ -32,6 +35,7 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
discountText: string;
imageUrl: string;
validity: string;
badgeCode: string;
}>({
title: '',
priceMain: '',
@@ -39,7 +43,8 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
unitPrice: '',
discountText: '',
imageUrl: '',
validity: ''
validity: '',
badgeCode: ''
});
const [presetName, setPresetName] = useState('');
@@ -125,6 +130,7 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
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);
};
@@ -148,7 +154,11 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
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',
@@ -163,6 +173,20 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
};
});
// 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();
};
@@ -268,6 +292,25 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
</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((b) => normalizeBadgeCode(b.code || b.name)).join(', ')
: 'zatiaľ žiadne šablóny'}
{' '}(kód nastavíte v Editore cenoviek; funguje aj názov šablóny)
</p>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">URL obrázka</label>
<select
@@ -290,7 +333,14 @@ export const ProductImportModal: React.FC<ImportModalProps> = ({
<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="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>