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

349 lines
14 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 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();
}
})();