feat: filter roletky cenoviek v produkte (typ, farba, povod, appka), zoradenie podla nazvu

Aktualna sablona ostava v zozname aj ked ju filter skryje alebo bola zmazana.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 16:38:24 +02:00
1 parent 2a0b1f37ba
commit c8543b4d9b
1 file changed
+95 -3
+95 -3
View File
@@ -2,8 +2,8 @@
// Editor jedného produktu (bežné aj vernostné produkty) – zbalený po produktoch, len to, čo treba:
// texty, fotka (zoom/posun), cenovka (šablóna, veľkosť, ceny), dostupnosť a ďalšie možnosti.
import React, { useEffect, useRef } from 'react';
import { BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/newsletter';
import React, { useEffect, useRef, useState } from 'react';
import { BADGE_IMPORT_TYPES, BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/newsletter';
import { ChevronDown, ChevronUp, Trash2, Pencil, Globe, Layers } from 'lucide-react';
import { ImageUploadInput } from './ImageUploadInput';
import { pickAlt } from '@/lib/altText';
@@ -99,6 +99,84 @@ const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => (
</button>
);
// Filter šablón cenoviek v produkte podľa tagov (typ, farba, pôvod, appka); prázdne = všetky
interface BadgeFilter {
type: string;
color: string;
origin: string; // '' | 'SK' | 'none'
app: string; // '' | 'app' | 'none'
}
const EMPTY_BADGE_FILTER: BadgeFilter = { type: '', color: '', origin: '', app: '' };
// posledný filter – pri otvorení ďalšieho produktu ostane nastavený
let lastBadgeFilter: BadgeFilter = EMPTY_BADGE_FILTER;
export function filterBadges(badges: BadgeTemplate[], f: BadgeFilter): BadgeTemplate[] {
return badges
.filter((b) => {
const t = b.importTags || {};
if (f.type && t.type !== f.type) return false;
if (f.color && t.color !== f.color) return false;
if (f.origin === 'SK' && t.origin !== 'SK') return false;
if (f.origin === 'none' && t.origin === 'SK') return false;
if (f.app === 'app' && !t.app) return false;
if (f.app === 'none' && t.app) return false;
return true;
})
.sort((a, b) => a.name.localeCompare(b.name, 'sk'));
}
const BadgeFilterBar: React.FC<{ filter: BadgeFilter; onChange: (f: BadgeFilter) => void; shown: number; total: number }> = ({
filter,
onChange,
shown,
total
}) => {
const cls = (active: boolean) =>
`min-w-0 text-[10px] rounded-md border px-1 py-1 bg-white ${active ? 'border-red-400 text-red-700 font-bold' : 'border-gray-200 text-gray-600'}`;
const set = (k: keyof BadgeFilter) => (e: React.ChangeEvent<HTMLSelectElement>) => onChange({ ...filter, [k]: e.target.value });
const active = Object.values(filter).some(Boolean);
return (
<div className="flex items-center gap-1">
<div className="grid grid-cols-4 gap-1 flex-1 min-w-0">
<select value={filter.type} onChange={set('type')} className={cls(!!filter.type)} title="Typ cenovky">
<option value="">Typ: všetky</option>
{BADGE_IMPORT_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
<select value={filter.color} onChange={set('color')} className={cls(!!filter.color)} title="Farba cenovky">
<option value="">Farba: všetky</option>
<option value="cervena">Červená</option>
<option value="oranzova">Oranžová</option>
<option value="zelena">Zelená</option>
</select>
<select value={filter.origin} onChange={set('origin')} className={cls(!!filter.origin)} title="Pôvod">
<option value="">Pôvod: všetky</option>
<option value="SK">SK</option>
<option value="none">Bez pôvodu</option>
</select>
<select value={filter.app} onChange={set('app')} className={cls(!!filter.app)} title="Cenovka pre appkáčov">
<option value="">Appka: všetky</option>
<option value="app">Pre appkáčov</option>
<option value="none">Bežná</option>
</select>
</div>
{active && (
<button
type="button"
onClick={() => onChange(EMPTY_BADGE_FILTER)}
className="shrink-0 text-[10px] font-bold text-gray-500 hover:text-red-700 px-1"
title="Zrušiť filter"
>
{shown}/{total} ✕
</button>
)}
</div>
);
};
// Poradie fotiek ako vrstvy: navrchu zoznamu je fotka najviac vpredu
const PhotoOrderList: React.FC<{ product: CardProduct; onChange: (order: string[]) => void }> = ({ product, onChange }) => {
const stack = photoStack(product); // odzadu dopredu
@@ -247,6 +325,12 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
}) => {
// Po otvorení (aj klikom na produkt v náhľade) posunieme panel na tento produkt
const rootRef = useRef<HTMLDivElement>(null);
const [badgeFilter, setBadgeFilterState] = useState<BadgeFilter>(() => lastBadgeFilter);
const setBadgeFilter = (f: BadgeFilter) => {
lastBadgeFilter = f;
setBadgeFilterState(f);
};
const filteredBadges = filterBadges(availableBadges, badgeFilter);
useEffect(() => {
if (open) rootRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
}, [open]);
@@ -416,6 +500,7 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
{/* CENOVKA */}
<Block title="Cenovka">
<BadgeFilterBar filter={badgeFilter} onChange={setBadgeFilter} shown={filteredBadges.length} total={availableBadges.length} />
<div className="flex items-center gap-1.5">
<select
value={p.customBadgePngUrl ? 'png' : p.badgeCustomTemplate?.id || 'default'}
@@ -423,7 +508,14 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
className={`${inputCls} font-medium`}
>
<option value="default">{isLoyalty ? 'Predvolená (bez karty / s kartou)' : 'Predvolená COOP cenovka'}</option>
{availableBadges.map((b) => (
{/* aktuálna šablóna ostáva v zozname, aj keď ju filter skryje alebo už bola zmazaná */}
{p.badgeCustomTemplate && !p.customBadgePngUrl && !filteredBadges.some((b) => b.id === p.badgeCustomTemplate!.id) && (
<option value={p.badgeCustomTemplate.id}>
{p.badgeCustomTemplate.name}
{availableBadges.some((b) => b.id === p.badgeCustomTemplate!.id) ? '' : ' (zmazaná šablóna)'}
</option>
)}
{filteredBadges.map((b) => (
<option key={b.id} value={b.id}>
{b.name}
</option>