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

+22 -23
View File
@@ -7,7 +7,7 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { BadgeTemplate, BadgeLayer } from '@/types/newsletter';
import { supabase } from '@/lib/supabase';
import { renderCleanBadgeSvg, BadgeProductData } from '@/lib/badgeUtils';
import { renderCleanBadgeSvg, BadgeProductData, BadgeRow, badgeFromRow, normalizeBadgeCode } from '@/lib/badgeUtils';
import {
Tag,
Plus,
@@ -45,6 +45,7 @@ function toBadgeDbPayload(badge: BadgeTemplate) {
svg_raw: badge.svgRaw,
layers_config: {
layers: badge.layersConfig,
code: badge.code ? normalizeBadgeCode(badge.code) : null,
width: badge.width || 110,
align: badge.align || 'center',
paddingTop: badge.paddingTop ?? 4,
@@ -53,27 +54,6 @@ function toBadgeDbPayload(badge: BadgeTemplate) {
};
}
interface BadgeRow {
id: string;
name: string;
svg_raw: string | null;
layers_config: BadgeLayer[] | { layers?: BadgeLayer[]; width?: number; align?: BadgeTemplate['align']; paddingTop?: number; paddingBottom?: number } | null;
}
function fromBadgeRow(d: BadgeRow): BadgeTemplate {
const cfg = d.layers_config && !Array.isArray(d.layers_config) ? d.layers_config : null;
return {
id: d.id,
name: d.name,
svgRaw: d.svg_raw || '',
layersConfig: cfg ? cfg.layers || [] : (d.layers_config as BadgeLayer[]) || [],
width: cfg?.width || 110,
align: cfg?.align || 'center',
paddingTop: cfg?.paddingTop ?? 4,
paddingBottom: cfg?.paddingBottom ?? 4
};
}
function saveLocal(list: BadgeTemplate[]) {
try {
localStorage.setItem(LOCAL_KEY, JSON.stringify(list));
@@ -294,6 +274,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const fileRef = useRef<HTMLInputElement>(null);
const isDirty = !!badge && JSON.stringify(badge) !== savedJson;
// Kód pre import musí byť jedinečný
const codeTaken =
!!badge?.code &&
badges.some((b) => b.id !== badge.id && b.code && normalizeBadgeCode(b.code) === normalizeBadgeCode(badge.code));
const layer = badge?.layersConfig.find((l) => l.id === selectedId) || null;
const vb = useMemo(() => viewBoxOf(badge?.svgRaw), [badge?.svgRaw]);
const canvasWidth = Math.round(300 * zoom);
@@ -324,7 +308,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
let loaded: BadgeTemplate[] = [];
try {
const { data } = await supabase.from('badges').select('*').order('created_at', { ascending: false });
loaded = ((data as BadgeRow[]) || []).map(fromBadgeRow);
loaded = ((data as BadgeRow[]) || []).map(badgeFromRow);
} catch {
// bez databázy použijeme kópiu v prehliadači
}
@@ -663,6 +647,21 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
title="Názov šablóny"
/>
)}
{badge && (
<label className="flex items-center gap-1 shrink-0" title="Kód pre hromadný import: v stĺpci cenovky v CSV/JSON/XML zadajte tento kód">
<span className="text-[10px] font-bold text-gray-500">Kód</span>
<input
value={badge.code || ''}
onChange={(e) => commit({ ...badge, code: e.target.value.toUpperCase() }, false)}
onBlur={() => badge.code && commit({ ...badge, code: normalizeBadgeCode(badge.code) }, false)}
placeholder={normalizeBadgeCode(badge.name) || 'KOD'}
className={`w-32 text-[11px] font-mono uppercase border rounded-lg px-1.5 py-1 ${
codeTaken ? 'border-red-400 bg-red-50' : 'border-gray-200'
}`}
/>
{codeTaken && <span className="text-[10px] font-bold text-red-600">už používa iná šablóna</span>}
</label>
)}
{isDirty && <span className="text-[10px] font-bold text-amber-600 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5">Neuložené</span>}
{savedFlash && <span className="text-[10px] font-bold text-emerald-600 flex items-center gap-1"><Check className="w-3 h-3" /> Uložené</span>}
</div>