WIP: Vendoo 1.44.0 – UX Foundation & Guided Workspaces #29
@@ -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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user