WIP: Vendoo 1.44.0 – UX Foundation & Guided Workspaces #29

Draft
Masterluke77 wants to merge 284 commits from release/1.44.0-ux-foundation-production-update-v1 into main
Showing only changes of commit b2f8af16d8 - Show all commits
+347
View File
@@ -0,0 +1,347 @@
(() => {
'use strict';
const ROOT_ID = 'quality-center';
const FILTER_OPEN_KEY = 'vendoo_quality_filters_open';
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const safeNumber = value => {
const match = String(value ?? '').replace(/\./g, '').match(/-?\d+/);
return match ? Number(match[0]) : 0;
};
const focusNode = node => {
if (!node) return;
node.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.requestAnimationFrame(() => {
const target = node.matches('button,input,select,summary') ? node : node.querySelector('button,input,select,summary');
target?.focus({ preventScroll: true });
});
};
function createPriorityButton(status, label, copy) {
const button = createNode('button', 'ux-quality-priority');
button.type = 'button';
button.dataset.uxQualityStatus = status;
button.setAttribute('aria-pressed', 'false');
const count = createNode('strong', 'ux-quality-priority-count', '0');
count.dataset.uxQualityCount = status;
const body = createNode('span', 'ux-quality-priority-copy');
body.append(createNode('b', '', label), createNode('small', '', copy));
button.append(count, body);
return button;
}
function createPriorityBoard(root) {
if (root.querySelector('.ux-quality-priority-board')) return;
const summary = root.querySelector('.quality-summary-grid');
if (!summary) return;
const board = createNode('section', 'ux-quality-priority-board');
board.setAttribute('aria-label', 'Qualitätsprioritäten');
const heading = createNode('div', 'ux-quality-priority-head');
const copy = createNode('div');
copy.append(
createNode('span', 'ux-quality-kicker', 'Prioritäten statt Zahlenflut'),
createNode('h3', '', 'Was braucht jetzt Aufmerksamkeit?'),
createNode('p', '', 'Blocker zuerst beheben, danach Warnungen prüfen. Veröffentlichungsbereite Artikel bleiben erreichbar, stehen aber nicht im Weg.')
);
const next = createNode('button', 'vd-button primary ux-quality-next', 'Blockierte Artikel öffnen');
next.type = 'button';
next.dataset.uxQualityNext = 'blocked';
heading.append(copy, next);
const priorities = createNode('div', 'ux-quality-priorities');
priorities.setAttribute('role', 'group');
priorities.setAttribute('aria-label', 'Qualitätsstatus filtern');
priorities.append(
createPriorityButton('blocked', 'Blockiert', 'Harte Fehler verhindern die Veröffentlichung'),
createPriorityButton('review', 'Prüfen', 'Warnungen und Verbesserungen kontrollieren'),
createPriorityButton('ready', 'Bereit', 'Ohne Blocker veröffentlichbar'),
createPriorityButton('all', 'Alle Artikel', 'Gesamten Bestand anzeigen')
);
board.append(heading, priorities);
summary.before(board);
summary.classList.add('ux-quality-summary-secondary');
}
function restructureToolbar(root) {
const toolbar = root.querySelector('.quality-toolbar');
if (!toolbar || toolbar.dataset.uxQualityStructured === 'true') return;
const search = toolbar.querySelector('.quality-search');
const status = toolbar.querySelector('label:has(#quality-status)');
const grade = toolbar.querySelector('label:has(#quality-grade)');
const pageSize = toolbar.querySelector('label:has(#quality-page-size)');
const shell = createNode('div', 'ux-quality-toolbar-shell');
const primary = createNode('div', 'ux-quality-toolbar-primary');
if (search) primary.append(search);
const filters = createNode('details', 'ux-quality-filter-panel');
const summary = createNode('summary', '', 'Filter & Anzeige');
const body = createNode('div', 'ux-quality-filter-body');
[status, grade, pageSize].filter(Boolean).forEach(node => body.append(node));
filters.append(summary, body);
try { filters.open = localStorage.getItem(FILTER_OPEN_KEY) === 'true'; } catch {}
filters.addEventListener('toggle', () => {
try { localStorage.setItem(FILTER_OPEN_KEY, String(filters.open)); } catch {}
});
const chips = createNode('div', 'ux-quality-filter-chips');
chips.setAttribute('aria-live', 'polite');
shell.append(primary, filters, chips);
toolbar.replaceChildren(shell);
toolbar.dataset.uxQualityStructured = 'true';
}
function createChip(label, onRemove) {
const button = createNode('button', 'ux-quality-filter-chip', `${label} ×`);
button.type = 'button';
button.addEventListener('click', onRemove);
return button;
}
function updateFilterChips(root) {
const chips = root.querySelector('.ux-quality-filter-chips');
if (!chips) return;
chips.replaceChildren();
const search = root.querySelector('#quality-search');
const status = root.querySelector('#quality-status');
const grade = root.querySelector('#quality-grade');
if (search?.value.trim()) {
chips.append(createChip(`Suche: ${search.value.trim()}`, () => {
search.value = '';
search.dispatchEvent(new Event('input', { bubbles: true }));
search.focus();
}));
}
if (status && status.value !== 'all') {
chips.append(createChip(`Status: ${status.options[status.selectedIndex]?.textContent || status.value}`, () => {
status.value = 'all';
status.dispatchEvent(new Event('change', { bubbles: true }));
}));
}
if (grade && grade.value !== 'all') {
chips.append(createChip(`Note: ${grade.value}`, () => {
grade.value = 'all';
grade.dispatchEvent(new Event('change', { bubbles: true }));
}));
}
chips.classList.toggle('is-empty', chips.childElementCount === 0);
}
function setStatusFilter(root, value, { focus = true } = {}) {
const select = root.querySelector('#quality-status');
if (!select) return;
select.value = value;
select.dispatchEvent(new Event('change', { bubbles: true }));
if (focus) focusNode(root.querySelector('#quality-list'));
updateFilterChips(root);
updatePriorities(root);
}
function updatePriorities(root) {
const blocked = safeNumber(root.querySelector('#quality-summary-blocked')?.textContent);
const ready = safeNumber(root.querySelector('#quality-summary-ready')?.textContent);
const total = safeNumber(root.querySelector('#quality-summary-total')?.textContent);
const review = Math.max(0, total - blocked - ready);
const counts = { blocked, review, ready, all: total };
Object.entries(counts).forEach(([status, count]) => {
const node = root.querySelector(`[data-ux-quality-count="${status}"]`);
if (node) node.textContent = String(count);
});
const selected = root.querySelector('#quality-status')?.value || 'all';
root.querySelectorAll('[data-ux-quality-status]').forEach(button => {
const active = button.dataset.uxQualityStatus === selected;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
const next = root.querySelector('[data-ux-quality-next]');
if (next) {
const target = blocked > 0 ? 'blocked' : review > 0 ? 'review' : ready > 0 ? 'ready' : 'all';
next.dataset.uxQualityNext = target;
next.textContent = target === 'blocked'
? `${blocked} blockierte Artikel öffnen`
: target === 'review'
? `${review} Artikel prüfen`
: target === 'ready'
? `${ready} bereite Artikel anzeigen`
: 'Alle Artikel anzeigen';
}
}
function groupDetailSections(root) {
const detail = root.querySelector('#quality-detail-content');
if (!detail || detail.dataset.uxQualityStructured === 'true') return;
const financials = detail.querySelector('#quality-financials');
const duplicates = detail.querySelector('.quality-duplicates');
const images = detail.querySelector('.quality-images');
const history = detail.querySelector('.quality-history');
const issueTabs = detail.querySelector('.quality-issue-tabs');
const issues = detail.querySelector('#quality-issues');
const actions = detail.querySelector('.quality-detail-actions');
if (actions) {
const main = createNode('div', 'ux-quality-main-actions');
const edit = actions.querySelector('#quality-open-article');
const ai = actions.querySelector('#quality-open-ai');
const analyze = actions.querySelector('#quality-run-analysis');
[edit, ai].filter(Boolean).forEach(button => main.append(button));
const more = createNode('details', 'ux-quality-more-actions');
const summary = createNode('summary', '', 'Weitere Aktionen');
const body = createNode('div', 'ux-quality-more-actions-body');
if (analyze) body.append(analyze);
more.append(summary, body);
actions.append(main, more);
}
if (issueTabs && issues) {
const section = createNode('section', 'ux-quality-issue-section');
const head = createNode('div', 'ux-quality-section-head');
head.append(createNode('div', '', 'Priorisierte Probleme'), createNode('span', 'ux-quality-issue-summary', 'Wird ausgewertet …'));
section.append(head, issueTabs, issues);
actions?.after(section);
}
const secondary = createNode('details', 'ux-quality-secondary');
const secondarySummary = createNode('summary');
const summaryCopy = createNode('span');
summaryCopy.append(createNode('strong', '', 'Weitere Prüfungen'), createNode('small', '', 'Erlös, Duplikate, Bilder und Score-Verlauf'));
secondarySummary.append(summaryCopy, createNode('span', 'ux-quality-secondary-badge', 'Details'));
const secondaryBody = createNode('div', 'ux-quality-secondary-body');
[financials, duplicates, images, history].filter(Boolean).forEach(node => secondaryBody.append(node));
secondary.append(secondarySummary, secondaryBody);
detail.append(secondary);
detail.dataset.uxQualityStructured = 'true';
}
function parseReadiness(root) {
const text = root.querySelector('#quality-readiness')?.textContent || '';
const blockers = safeNumber(text.match(/(\d+)\s+Blocker/i)?.[1]);
const warnings = safeNumber(text.match(/(\d+)\s+Warnungen/i)?.[1]);
const recommendations = safeNumber(text.match(/(\d+)\s+Empfehlungen/i)?.[1]);
return { blockers, warnings, recommendations };
}
function updateDetailGuidance(root) {
const detail = root.querySelector('#quality-detail-content');
if (!detail || detail.classList.contains('hidden')) return;
const { blockers, warnings, recommendations } = parseReadiness(root);
const summary = root.querySelector('.ux-quality-issue-summary');
if (summary) {
summary.textContent = blockers
? `${blockers} Blocker zuerst`
: warnings
? `${warnings} Warnung${warnings === 1 ? '' : 'en'} prüfen`
: recommendations
? `${recommendations} Empfehlung${recommendations === 1 ? '' : 'en'}`
: 'Keine offenen Punkte';
summary.dataset.tone = blockers ? 'danger' : warnings ? 'warning' : recommendations ? 'info' : 'success';
}
const blockerTab = root.querySelector('[data-quality-issue-filter="error"]');
const allTab = root.querySelector('[data-quality-issue-filter="all"]');
if (blockerTab) blockerTab.classList.toggle('ux-quality-has-priority', blockers > 0);
if (allTab) allTab.classList.toggle('ux-quality-all-clear', blockers === 0 && warnings === 0 && recommendations === 0);
const secondary = root.querySelector('.ux-quality-secondary');
if (secondary && blockers > 0) secondary.open = false;
}
function bindInteractions(root) {
root.querySelectorAll('[data-ux-quality-status]').forEach(button => {
button.addEventListener('click', () => setStatusFilter(root, button.dataset.uxQualityStatus));
button.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const buttons = [...root.querySelectorAll('[data-ux-quality-status]')];
const index = buttons.indexOf(button);
const nextIndex = event.key === 'Home' ? 0
: event.key === 'End' ? buttons.length - 1
: event.key === 'ArrowRight' ? (index + 1) % buttons.length
: (index - 1 + buttons.length) % buttons.length;
buttons[nextIndex]?.focus();
});
});
root.querySelector('[data-ux-quality-next]')?.addEventListener('click', event => {
setStatusFilter(root, event.currentTarget.dataset.uxQualityNext);
});
['quality-search', 'quality-status', 'quality-grade', 'quality-page-size'].forEach(id => {
const field = root.querySelector(`#${id}`);
field?.addEventListener('input', () => updateFilterChips(root));
field?.addEventListener('change', () => {
updateFilterChips(root);
updatePriorities(root);
});
});
}
function observeState(root) {
let scheduled = false;
const schedule = () => {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(() => {
scheduled = false;
updatePriorities(root);
updateFilterChips(root);
updateDetailGuidance(root);
});
};
const observer = new MutationObserver(schedule);
[
root.querySelector('.quality-summary-grid'),
root.querySelector('#quality-readiness'),
root.querySelector('#quality-issues'),
root.querySelector('#quality-detail-content'),
].filter(Boolean).forEach(node => observer.observe(node, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: ['class'],
}));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function initializeQualityCenterUx() {
const root = document.getElementById(ROOT_ID);
if (!root || root.classList.contains('ux-quality-center-ready')) return;
createPriorityBoard(root);
restructureToolbar(root);
groupDetailSections(root);
bindInteractions(root);
updatePriorities(root);
updateFilterChips(root);
updateDetailGuidance(root);
observeState(root);
root.classList.add('ux-quality-center-ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeQualityCenterUx, { once: true });
} else {
initializeQualityCenterUx();
}
})();