349 lines
14 KiB
JavaScript
349 lines
14 KiB
JavaScript
(() => {
|
||
'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 = root.querySelector('#quality-status')?.closest('label');
|
||
const grade = root.querySelector('#quality-grade')?.closest('label');
|
||
const pageSize = root.querySelector('#quality-page-size')?.closest('label');
|
||
|
||
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)) 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';
|
||
const text = target === 'blocked'
|
||
? `${blocked} blockierte Artikel öffnen`
|
||
: target === 'review'
|
||
? `${review} Artikel prüfen`
|
||
: target === 'ready'
|
||
? `${ready} bereite Artikel anzeigen`
|
||
: 'Alle Artikel anzeigen';
|
||
next.dataset.uxQualityNext = target;
|
||
if (next.textContent !== text) next.textContent = text;
|
||
}
|
||
}
|
||
|
||
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) {
|
||
const text = blockers
|
||
? `${blockers} Blocker zuerst`
|
||
: warnings
|
||
? `${warnings} Warnung${warnings === 1 ? '' : 'en'} prüfen`
|
||
: recommendations
|
||
? `${recommendations} Empfehlung${recommendations === 1 ? '' : 'en'}`
|
||
: 'Keine offenen Punkte';
|
||
const tone = blockers ? 'danger' : warnings ? 'warning' : recommendations ? 'info' : 'success';
|
||
if (summary.textContent !== text) summary.textContent = text;
|
||
if (summary.dataset.tone !== tone) summary.dataset.tone = tone;
|
||
}
|
||
|
||
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) 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'),
|
||
].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();
|
||
}
|
||
})(); |