370 lines
15 KiB
JavaScript
370 lines
15 KiB
JavaScript
(() => {
|
||
'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 signature = JSON.stringify([search?.value.trim() || '', platform?.value || '', status?.value || '']);
|
||
if (container.dataset.filterSignature === signature) return;
|
||
container.dataset.filterSignature = signature;
|
||
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');
|
||
const shouldHide = count === 0;
|
||
if (headingAction && headingAction.hidden !== shouldHide) headingAction.hidden = shouldHide;
|
||
}
|
||
|
||
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;
|
||
const className = `ux-publish-health${failed ? ' is-danger' : pending || processing ? ' is-active' : ''}`;
|
||
if (health.className !== className) health.className = className;
|
||
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();
|
||
}
|
||
})(); |