diff --git a/public/publish-center-ux.js b/public/publish-center-ux.js new file mode 100644 index 0000000..7afb576 --- /dev/null +++ b/public/publish-center-ux.js @@ -0,0 +1,365 @@ +(() => { + 'use strict'; + + const STATUS_DRAWER_KEY = 'vendoo_publish_status_open_v1'; + const OPERATIONAL_VIEWS = new Set(['queue', 'errors', 'scheduled']); + const VIEW_META = { + smart: { + title: 'Manuell veröffentlichen', + help: 'Daten und Fotos kontrolliert kopieren', + stage: 2, + context: 'Artikel auswählen und Smart Copy öffnen.', + }, + direct: { + title: 'Direkt veröffentlichen', + help: 'Über verbundene Marktplatzkonten', + stage: 2, + context: 'Artikel prüfen, speichern und sicher einreihen.', + }, + queue: { + title: 'Aufträge', + help: 'Publishing-Queue überwachen', + stage: 3, + context: 'Laufende und abgeschlossene Publishing-Aufträge verwalten.', + }, + scheduled: { + title: 'Geplant', + help: 'Zeitgesteuerte Veröffentlichungen', + stage: 3, + context: 'Geplante Veröffentlichungen kontrollieren oder entfernen.', + }, + errors: { + title: 'Fehler & Verlauf', + help: 'Probleme und Ereignisse prüfen', + stage: 3, + context: 'Fehler nachvollziehen und betroffene Aufträge öffnen.', + }, + }; + + let refreshQueued = false; + let publishObserver = null; + + function publishSection() { + return document.getElementById('publish'); + } + + function createElement(tag, className, text) { + const element = document.createElement(tag); + if (className) element.className = className; + if (text !== undefined) element.textContent = text; + return element; + } + + function setText(node, value) { + if (node && node.textContent !== String(value)) node.textContent = String(value); + } + + function activeView() { + return publishSection()?.querySelector('[data-publish-view].active')?.dataset.publishView || 'smart'; + } + + function decorateViewButton(button) { + if (!button || button.dataset.uxPublishDecorated === 'true') return; + const view = button.dataset.publishView; + const meta = VIEW_META[view]; + if (!meta) return; + const badge = button.querySelector('.publish-tab-count, .publish-tab-badge'); + const title = createElement('strong', 'ux-publish-tab-title', meta.title); + const help = createElement('small', 'ux-publish-tab-help', meta.help); + const copy = createElement('span', 'ux-publish-tab-copy'); + copy.append(title, help); + button.replaceChildren(copy); + if (badge) button.append(badge); + button.setAttribute('role', 'tab'); + button.setAttribute('aria-label', `${meta.title}: ${meta.help}`); + button.dataset.uxPublishDecorated = 'true'; + } + + function createViewGroup(label, views) { + const group = createElement('section', 'ux-publish-view-group'); + group.dataset.publishViewGroup = label.toLowerCase(); + const heading = createElement('span', 'ux-publish-view-group-label', label); + const items = createElement('div', 'ux-publish-view-group-items'); + for (const view of views) { + const button = publishSection()?.querySelector(`[data-publish-view="${view}"]`); + if (!button) continue; + decorateViewButton(button); + items.append(button); + } + group.append(heading, items); + return group; + } + + function reorganizeViewNavigation() { + const nav = publishSection()?.querySelector('.publish-view-tabs'); + if (!nav || nav.querySelector('.ux-publish-view-groups')) return; + nav.setAttribute('role', 'tablist'); + const shell = createElement('div', 'ux-publish-view-groups'); + shell.append( + createViewGroup('Vorbereiten', ['smart', 'direct']), + createViewGroup('Betrieb', ['queue', 'scheduled', 'errors']), + ); + nav.replaceChildren(shell); + nav.addEventListener('keydown', event => { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + const buttons = [...nav.querySelectorAll('[data-publish-view]')]; + if (!buttons.length) return; + const current = Math.max(0, buttons.indexOf(document.activeElement)); + let target = current; + if (event.key === 'ArrowRight') target = (current + 1) % buttons.length; + if (event.key === 'ArrowLeft') target = (current - 1 + buttons.length) % buttons.length; + if (event.key === 'Home') target = 0; + if (event.key === 'End') target = buttons.length - 1; + event.preventDefault(); + buttons[target].focus(); + buttons[target].click(); + }); + } + + function createGuidance() { + const section = publishSection(); + const nav = section?.querySelector('.publish-view-tabs'); + if (!section || !nav || section.querySelector('.ux-publish-guide')) return; + + const guide = createElement('section', 'ux-publish-guide'); + guide.setAttribute('aria-label', 'Publishing-Ablauf'); + const copy = createElement('div', 'ux-publish-guide-copy'); + const eyebrow = createElement('span', 'ux-publish-guide-eyebrow', 'Sicher veröffentlichen'); + const title = createElement('h3', '', 'Vom fertigen Artikel zum kontrollierten Auftrag'); + const context = createElement('p', '', VIEW_META.smart.context); + context.id = 'ux-publish-context'; + copy.append(eyebrow, title, context); + + const stages = createElement('div', 'ux-publish-stages'); + const stageData = [ + ['1', 'Artikel wählen', 'Suche und Filter nutzen'], + ['2', 'Methode wählen', 'Smart Copy oder Direkt'], + ['3', 'Auftrag überwachen', 'Queue, Planung und Fehler'], + ]; + for (const [number, label, help] of stageData) { + const stage = createElement('div', 'ux-publish-stage'); + stage.dataset.publishStage = number; + const marker = createElement('span', 'ux-publish-stage-number', number); + const stageCopy = createElement('span', 'ux-publish-stage-copy'); + stageCopy.append(createElement('strong', '', label), createElement('small', '', help)); + stage.append(marker, stageCopy); + stages.append(stage); + } + guide.append(copy, stages); + nav.before(guide); + } + + function moveStatusRail() { + const section = publishSection(); + const grid = section?.querySelector('.publish-center-grid'); + const rail = grid?.querySelector('.publish-status-rail'); + if (!section || !grid || !rail || section.querySelector('.ux-publish-status-drawer')) return; + + const details = createElement('details', 'ux-publish-status-drawer'); + details.open = localStorage.getItem(STATUS_DRAWER_KEY) === 'true'; + const summary = createElement('summary', 'ux-publish-status-summary'); + const summaryCopy = createElement('span', 'ux-publish-status-summary-copy'); + summaryCopy.append( + createElement('strong', '', 'Status, Schnellaktionen und Aktivität'), + createElement('small', '', 'Nur öffnen, wenn du den Publishing-Betrieb prüfen möchtest.'), + ); + const health = createElement('span', 'ux-publish-health', 'Keine offenen Probleme'); + health.id = 'ux-publish-health'; + summary.append(summaryCopy, health); + const content = createElement('div', 'ux-publish-status-content'); + content.append(rail); + details.append(summary, content); + details.addEventListener('toggle', () => localStorage.setItem(STATUS_DRAWER_KEY, String(details.open))); + grid.before(details); + } + + function enhanceBatchActions() { + const bar = publishSection()?.querySelector('.publish-batch-bar'); + if (!bar || bar.querySelector('.ux-publish-batch-primary')) return; + const primary = createElement('div', 'ux-publish-batch-primary'); + const more = createElement('details', 'ux-publish-batch-more'); + const summary = createElement('summary', '', 'Weitere Aktionen'); + const secondary = createElement('div', 'ux-publish-batch-secondary'); + for (const id of ['publish-batch-copy', 'publish-batch-ebay', 'publish-batch-schedule']) { + const button = document.getElementById(id); + if (button) primary.append(button); + } + for (const id of ['publish-download-photos', 'publish-batch-etsy']) { + const button = document.getElementById(id); + if (button) secondary.append(button); + } + more.append(summary, secondary); + bar.append(primary, more); + } + + function createActiveFilters() { + const filters = publishSection()?.querySelector('.publish-list-filters'); + if (!filters || document.getElementById('ux-publish-active-filters')) return; + const active = createElement('div', 'ux-publish-active-filters'); + active.id = 'ux-publish-active-filters'; + active.setAttribute('aria-live', 'polite'); + filters.after(active); + } + + function dispatchControl(control, value) { + if (!control) return; + control.value = value; + control.dispatchEvent(new Event(control.tagName === 'INPUT' ? 'input' : 'change', { bubbles: true })); + queueRefresh(); + } + + function createFilterChip(label, control, value = '') { + const chip = createElement('span', 'ux-publish-filter-chip'); + chip.append(createElement('span', '', label)); + const clear = createElement('button', 'ux-publish-filter-clear', '×'); + clear.type = 'button'; + clear.setAttribute('aria-label', `${label} entfernen`); + clear.addEventListener('click', () => dispatchControl(control, value)); + chip.append(clear); + return chip; + } + + function selectedOptionText(select) { + if (!select?.value) return ''; + return select.options[select.selectedIndex]?.textContent?.trim() || select.value; + } + + function updateActiveFilters() { + const container = document.getElementById('ux-publish-active-filters'); + if (!container) return; + const search = document.getElementById('publish-search'); + const platform = document.getElementById('publish-filter-platform'); + const status = document.getElementById('publish-filter-status'); + const chips = []; + if (search?.value.trim()) chips.push(createFilterChip(`Suche: ${search.value.trim()}`, search)); + if (platform?.value) chips.push(createFilterChip(`Plattform: ${selectedOptionText(platform)}`, platform)); + if (status?.value) chips.push(createFilterChip(`Status: ${selectedOptionText(status)}`, status)); + container.replaceChildren(); + container.classList.toggle('is-empty', chips.length === 0); + if (!chips.length) return; + container.append(createElement('strong', '', 'Aktive Filter'), ...chips); + } + + function annotateListingCards() { + document.querySelectorAll('#publish-list .publish-list-card').forEach(card => { + card.tabIndex = 0; + const title = card.querySelector('.publish-card-copy > strong')?.textContent?.trim() || 'Artikel'; + card.setAttribute('aria-label', `${title} für die Veröffentlichung auswählen`); + const status = card.querySelector('.publish-card-status'); + if (status) card.dataset.publishStatus = [...status.classList].find(name => name.startsWith('status-'))?.slice(7) || 'prepared'; + if (card.dataset.uxPublishKeyboard === 'true') return; + card.dataset.uxPublishKeyboard = 'true'; + card.addEventListener('keydown', event => { + if ((event.key === 'Enter' || event.key === ' ') && event.target === card) { + event.preventDefault(); + card.click(); + } + }); + }); + } + + function selectionCount() { + const text = document.getElementById('publish-selected-count')?.textContent || '0'; + return Number.parseInt(text, 10) || 0; + } + + function updateSelectionState() { + const section = publishSection(); + const count = selectionCount(); + if (!section) return; + section.toggleAttribute('data-has-publish-selection', count > 0); + const more = section.querySelector('.ux-publish-batch-more'); + if (!count && more) more.open = false; + const headingAction = document.getElementById('publish-add-selected-ebay'); + if (headingAction) headingAction.hidden = count === 0; + } + + function updateHealth() { + const failed = Number(document.getElementById('publish-status-failed')?.textContent || 0); + const pending = Number(document.getElementById('publish-status-pending')?.textContent || 0); + const processing = Number(document.getElementById('publish-status-processing')?.textContent || 0); + const health = document.getElementById('ux-publish-health'); + if (!health) return; + health.className = `ux-publish-health${failed ? ' is-danger' : pending || processing ? ' is-active' : ''}`; + setText(health, failed ? `${failed} Fehler prüfen` : processing ? `${processing} in Bearbeitung` : pending ? `${pending} wartend` : 'Keine offenen Probleme'); + } + + function updateViewState() { + const section = publishSection(); + if (!section) return; + const view = activeView(); + const meta = VIEW_META[view] || VIEW_META.smart; + section.dataset.publishMode = view; + section.toggleAttribute('data-publish-operational', OPERATIONAL_VIEWS.has(view)); + section.querySelectorAll('[data-publish-view]').forEach(button => { + const active = button.dataset.publishView === view; + button.setAttribute('aria-selected', String(active)); + button.tabIndex = active ? 0 : -1; + }); + section.querySelectorAll('[data-publish-stage]').forEach(stage => { + const number = Number(stage.dataset.publishStage); + stage.classList.toggle('is-active', number === meta.stage); + stage.classList.toggle('is-complete', number < meta.stage); + }); + setText(document.getElementById('ux-publish-context'), meta.context); + } + + function refreshPresentation() { + refreshQueued = false; + updateViewState(); + updateSelectionState(); + updateActiveFilters(); + updateHealth(); + annotateListingCards(); + } + + function queueRefresh() { + if (refreshQueued) return; + refreshQueued = true; + window.requestAnimationFrame(refreshPresentation); + } + + function bindControls() { + for (const id of ['publish-search', 'publish-filter-platform', 'publish-filter-status']) { + const control = document.getElementById(id); + if (!control || control.dataset.uxPublishBound === 'true') continue; + control.dataset.uxPublishBound = 'true'; + control.addEventListener('input', queueRefresh); + control.addEventListener('change', queueRefresh); + } + } + + function observePublishCenter() { + const section = publishSection(); + if (!section) return; + publishObserver = new MutationObserver(queueRefresh); + publishObserver.observe(section, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ['class', 'disabled', 'hidden'], + }); + window.addEventListener('beforeunload', () => publishObserver?.disconnect(), { once: true }); + } + + function initializePublishCenterUx() { + const section = publishSection(); + if (!section || section.classList.contains('ux-publish-ready')) return; + reorganizeViewNavigation(); + createGuidance(); + moveStatusRail(); + enhanceBatchActions(); + createActiveFilters(); + bindControls(); + observePublishCenter(); + refreshPresentation(); + section.classList.add('ux-publish-ready'); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePublishCenterUx, { once: true }); + } else { + initializePublishCenterUx(); + } +})(); \ No newline at end of file