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:
1 parent
2a0b1f37ba
commit
c8543b4d9b
1 file changed
+95
-3
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
// Editor jedného produktu (bežné aj vernostné produkty) – zbalený po produktoch, len to, čo treba:
|
// 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.
|
// texty, fotka (zoom/posun), cenovka (šablóna, veľkosť, ceny), dostupnosť a ďalšie možnosti.
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/newsletter';
|
import { BADGE_IMPORT_TYPES, BadgeTemplate, CardProduct, ProductLabel, ProductPhoto } from '@/types/newsletter';
|
||||||
import { ChevronDown, ChevronUp, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Trash2, Pencil, Globe, Layers } from 'lucide-react';
|
||||||
import { ImageUploadInput } from './ImageUploadInput';
|
import { ImageUploadInput } from './ImageUploadInput';
|
||||||
import { pickAlt } from '@/lib/altText';
|
import { pickAlt } from '@/lib/altText';
|
||||||
@@ -99,6 +99,84 @@ const ResetLink: React.FC<{ onClick: () => void }> = ({ onClick }) => (
|
|||||||
</button>
|
</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
|
// Poradie fotiek ako vrstvy: navrchu zoznamu je fotka najviac vpredu
|
||||||
const PhotoOrderList: React.FC<{ product: CardProduct; onChange: (order: string[]) => void }> = ({ product, onChange }) => {
|
const PhotoOrderList: React.FC<{ product: CardProduct; onChange: (order: string[]) => void }> = ({ product, onChange }) => {
|
||||||
const stack = photoStack(product); // odzadu dopredu
|
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
|
// Po otvorení (aj klikom na produkt v náhľade) posunieme panel na tento produkt
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (open) rootRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
if (open) rootRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||||
}, [open]);
|
}, [open]);
|
||||||
@@ -416,6 +500,7 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
|||||||
|
|
||||||
{/* CENOVKA */}
|
{/* CENOVKA */}
|
||||||
<Block title="Cenovka">
|
<Block title="Cenovka">
|
||||||
|
<BadgeFilterBar filter={badgeFilter} onChange={setBadgeFilter} shown={filteredBadges.length} total={availableBadges.length} />
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<select
|
<select
|
||||||
value={p.customBadgePngUrl ? 'png' : p.badgeCustomTemplate?.id || 'default'}
|
value={p.customBadgePngUrl ? 'png' : p.badgeCustomTemplate?.id || 'default'}
|
||||||
@@ -423,7 +508,14 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
|
|||||||
className={`${inputCls} font-medium`}
|
className={`${inputCls} font-medium`}
|
||||||
>
|
>
|
||||||
<option value="default">{isLoyalty ? 'Predvolená (bez karty / s kartou)' : 'Predvolená COOP cenovka'}</option>
|
<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}>
|
<option key={b.id} value={b.id}>
|
||||||
{b.name}
|
{b.name}
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
Reference in new issue
Block a user