feat(ux): simplify publish center workflow

This commit is contained in:
Masterluke77
2026-07-11 08:53:20 +02:00
parent 00cee8d2d6
commit 3e516c5799
+365
View File
@@ -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();
}
})();