feat: predvolena cenovka je sablona Super vyhodne | Cervena (neda sa zmazat)

- volba Predvolena COOP cenovka v produkte zrusena; produkty bez sablony
  dostanu predvolenu sablonu automaticky
- sablony z prehliadaca (localStorage) sa pridavaju len ak este nie su
  ulozene v databaze, zmazane sablony sa uz nevracaju

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:49:27 +02:00
1 parent 184e55ad5b
commit b747a3ba74
5 files changed
+56 -10

No files matched your search

+30 -1
View File
@@ -71,7 +71,8 @@ export default function Home() {
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
const existingIds = new Set(loaded.map((b) => b.id)); const existingIds = new Set(loaded.map((b) => b.id));
for (const b of parsed) { for (const b of parsed) {
if (!existingIds.has(b.id)) { // len šablóny neuložené do databázy – zmazané šablóny sa z prehliadača nesmú vrátiť
if (!existingIds.has(b.id) && String(b.id).startsWith('badge-')) {
loaded.push(b); loaded.push(b);
existingIds.add(b.id); existingIds.add(b.id);
} }
@@ -92,6 +93,34 @@ export default function Home() {
fetchBadges(); fetchBadges();
}, [isBadgeEditorOpen]); }, [isBadgeEditorOpen]);
// Predvolená cenovka: produkty bez šablóny (nové, importované bez zhody, staršie s pôvodnou COOP cenovkou)
// dostanú predvolenú šablónu z databázy
const defaultBadge = availableBadges.find((b) => b.isDefault);
useEffect(() => {
if (!defaultBadge) return;
// jednorazové doplnenie chýbajúcej cenovky do dát (zmení stav len keď nejaký produkt šablónu nemá)
// eslint-disable-next-line react-hooks/set-state-in-effect
setCampaign((prev) => {
let changed = false;
const modules = prev.modules.map((m) => {
if (m.type !== 'products_grid') return m;
const products = m.data.products || [];
if (!products.some((p: ProductItem) => !p.badgeCustomTemplate && !p.customBadgePngUrl)) return m;
changed = true;
return {
...m,
data: {
...m.data,
products: products.map((p: ProductItem) =>
p.badgeCustomTemplate || p.customBadgePngUrl ? p : { ...p, badgeCustomTemplate: defaultBadge, badgeId: defaultBadge.id }
)
}
};
});
return changed ? { ...prev, modules } : prev;
});
}, [campaign.modules, defaultBadge]);
// Pomocné funkcie pre fixné ukotvenie hlavičky, spodných letákov, receptov a pätičky // Pomocné funkcie pre fixné ukotvenie hlavičky, spodných letákov, receptov a pätičky
const isTopModule = (m: NewsletterModule) => m.type === 'header' || m.type === 'announcement'; const isTopModule = (m: NewsletterModule) => m.type === 'header' || m.type === 'announcement';
+18 -6
View File
@@ -49,7 +49,8 @@ function toBadgeDbPayload(badge: BadgeTemplate) {
align: badge.align || 'center', align: badge.align || 'center',
paddingTop: badge.paddingTop ?? 4, paddingTop: badge.paddingTop ?? 4,
paddingBottom: badge.paddingBottom ?? 4, paddingBottom: badge.paddingBottom ?? 4,
importTags: badge.importTags importTags: badge.importTags,
isDefault: badge.isDefault || undefined
} }
}; };
} }
@@ -363,7 +364,8 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const local = JSON.parse(localStorage.getItem(LOCAL_KEY) || '[]'); const local = JSON.parse(localStorage.getItem(LOCAL_KEY) || '[]');
if (Array.isArray(local)) { if (Array.isArray(local)) {
const ids = new Set(loaded.map((b) => b.id)); const ids = new Set(loaded.map((b) => b.id));
for (const b of local as BadgeTemplate[]) if (!ids.has(b.id)) loaded.push(b); // z prehliadača len šablóny, ktoré ešte neboli uložené do databázy (zmazané sa nesmú vrátiť)
for (const b of local as BadgeTemplate[]) if (!ids.has(b.id) && b.id.startsWith('badge-')) loaded.push(b);
} }
} catch { } catch {
// localStorage nemusí byť dostupný // localStorage nemusí byť dostupný
@@ -553,7 +555,7 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
const duplicateTemplate = async (b: BadgeTemplate) => { const duplicateTemplate = async (b: BadgeTemplate) => {
if (!confirmDiscard()) return; if (!confirmDiscard()) return;
const copy: BadgeTemplate = { ...JSON.parse(JSON.stringify(b)), id: newLocalBadgeId(), name: `${b.name} (kópia)` }; const copy: BadgeTemplate = { ...JSON.parse(JSON.stringify(b)), id: newLocalBadgeId(), name: `${b.name} (kópia)`, isDefault: undefined };
let saved = copy; let saved = copy;
try { try {
const { data, error } = await supabase.from('badges').insert([toBadgeDbPayload(copy)]).select(); const { data, error } = await supabase.from('badges').insert([toBadgeDbPayload(copy)]).select();
@@ -569,6 +571,10 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
}; };
const deleteTemplate = async (b: BadgeTemplate) => { const deleteTemplate = async (b: BadgeTemplate) => {
if (b.isDefault) {
alert('Predvolenú cenovku nie je možné zmazať.');
return;
}
if (!confirm(`Zmazať šablónu „${b.name}“? Produkty, ktoré ju už používajú, si ju ponechajú.`)) return; if (!confirm(`Zmazať šablónu „${b.name}“? Produkty, ktoré ju už používajú, si ju ponechajú.`)) return;
if (!b.id.startsWith('badge-')) { if (!b.id.startsWith('badge-')) {
try { try {
@@ -743,9 +749,15 @@ export const BadgeEditorModal: React.FC<BadgeEditorModalProps> = ({
<button type="button" onClick={(e) => { e.stopPropagation(); duplicateTemplate(b); }} className="p-0.5 text-gray-300 hover:text-blue-600 opacity-0 group-hover:opacity-100" title="Duplikovať šablónu"> <button type="button" onClick={(e) => { e.stopPropagation(); duplicateTemplate(b); }} className="p-0.5 text-gray-300 hover:text-blue-600 opacity-0 group-hover:opacity-100" title="Duplikovať šablónu">
<Copy className="w-3.5 h-3.5" /> <Copy className="w-3.5 h-3.5" />
</button> </button>
<button type="button" onClick={(e) => { e.stopPropagation(); deleteTemplate(b); }} className="p-0.5 text-gray-300 hover:text-red-600 opacity-0 group-hover:opacity-100" title="Zmazať šablónu"> {b.isDefault ? (
<Trash2 className="w-3.5 h-3.5" /> <span className="text-[9px] font-bold text-gray-500 bg-gray-100 rounded px-1" title="Predvolená cenovka produktov – nedá sa zmazať">
</button> predvolená
</span>
) : (
<button type="button" onClick={(e) => { e.stopPropagation(); deleteTemplate(b); }} className="p-0.5 text-gray-300 hover:text-red-600 opacity-0 group-hover:opacity-100" title="Zmazať šablónu">
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div> </div>
</div> </div>
))} ))}
+4 -2
View File
@@ -503,11 +503,12 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
<BadgeFilterBar filter={badgeFilter} onChange={setBadgeFilter} shown={filteredBadges.length} total={availableBadges.length} /> <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 || (isLoyalty ? 'default' : availableBadges.find((b) => b.isDefault)?.id || '')}
onChange={(e) => selectBadge(e.target.value)} onChange={(e) => selectBadge(e.target.value)}
className={`${inputCls} font-medium`} className={`${inputCls} font-medium`}
> >
<option value="default">{isLoyalty ? 'Predvolená (bez karty / s kartou)' : 'Predvolená COOP cenovka'}</option> {/* bežné produkty: predvolená je šablóna označená ako predvolená (nastaví sa automaticky) */}
{isLoyalty && <option value="default">Predvolená (bez karty / s kartou)</option>}
{/* aktuálna šablóna ostáva v zozname, aj keď ju filter skryje alebo už bola zmazaná */} {/* 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) && ( {p.badgeCustomTemplate && !p.customBadgePngUrl && !filteredBadges.some((b) => b.id === p.badgeCustomTemplate!.id) && (
<option value={p.badgeCustomTemplate.id}> <option value={p.badgeCustomTemplate.id}>
@@ -518,6 +519,7 @@ export const ProductEditor: React.FC<ProductEditorProps> = ({
{filteredBadges.map((b) => ( {filteredBadges.map((b) => (
<option key={b.id} value={b.id}> <option key={b.id} value={b.id}>
{b.name} {b.name}
{b.isDefault ? ' (predvolená)' : ''}
</option> </option>
))} ))}
{p.customBadgePngUrl && <option value="png">Vlastná PNG cenovka</option>} {p.customBadgePngUrl && <option value="png">Vlastná PNG cenovka</option>}
+3 -1
View File
@@ -20,6 +20,7 @@ export interface BadgeRow {
paddingTop?: number; paddingTop?: number;
paddingBottom?: number; paddingBottom?: number;
importTags?: BadgeImportTags; importTags?: BadgeImportTags;
isDefault?: boolean;
} }
| null; | null;
} }
@@ -36,7 +37,8 @@ export function badgeFromRow(d: BadgeRow): BadgeTemplate {
align: cfg?.align || 'center', align: cfg?.align || 'center',
paddingTop: cfg?.paddingTop ?? 4, paddingTop: cfg?.paddingTop ?? 4,
paddingBottom: cfg?.paddingBottom ?? 4, paddingBottom: cfg?.paddingBottom ?? 4,
importTags: cfg?.importTags importTags: cfg?.importTags,
isDefault: cfg?.isDefault || undefined
}; };
} }
+1
View File
@@ -245,6 +245,7 @@ export interface BadgeTemplate {
paddingTop?: number; paddingTop?: number;
paddingBottom?: number; paddingBottom?: number;
importTags?: BadgeImportTags; // podľa čoho sa šablóna vyberie pri importe podkladov (ZIP) importTags?: BadgeImportTags; // podľa čoho sa šablóna vyberie pri importe podkladov (ZIP)
isDefault?: boolean; // predvolená cenovka produktov – nedá sa zmazať
} }
// Druhy cenoviek v podkladoch: „Zľava grafická“, a keď je prázdna, základná cenovka s výkričníkom alebo bez // Druhy cenoviek v podkladoch: „Zľava grafická“, a keď je prázdna, základná cenovka s výkričníkom alebo bez