Files
vendoo/public/publish-center-ux.js

370 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
'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();
}
})();