367 lines
16 KiB
JavaScript
367 lines
16 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
const ROOT_ID = 'image-factory';
|
|
const JOB_FILTER_KEY = 'vendoo_image_factory_job_filter';
|
|
const JOB_FILTERS = ['all', 'active', 'completed', 'issues'];
|
|
|
|
const createNode = (tag, className = '', text = '') => {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text) node.textContent = text;
|
|
return node;
|
|
};
|
|
|
|
const scrollToNode = node => {
|
|
if (!node) return;
|
|
node.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
window.requestAnimationFrame(() => {
|
|
const focusTarget = node.matches('input,button,select,textarea') ? node : node.querySelector('input,button,select,textarea');
|
|
focusTarget?.focus({ preventScroll: true });
|
|
});
|
|
};
|
|
|
|
function parseSelectionSummary(root) {
|
|
const summary = root.querySelector('#image-factory-selection-count')?.textContent || '';
|
|
const match = summary.match(/(\d+)\s+Bilder?.*?(\d+)\s+Profile?/i);
|
|
if (match) return { sources: Number(match[1]), profiles: Number(match[2]) };
|
|
|
|
const sourceGrid = root.querySelector('#image-factory-source-grid');
|
|
const profileGrid = root.querySelector('#image-factory-profile-grid');
|
|
const countSelected = container => {
|
|
if (!container) return 0;
|
|
const checked = container.querySelectorAll('input[type="checkbox"]:checked, input[type="radio"]:checked').length;
|
|
if (checked) return checked;
|
|
return container.querySelectorAll('.is-selected, .selected, [aria-pressed="true"]').length;
|
|
};
|
|
return { sources: countSelected(sourceGrid), profiles: countSelected(profileGrid) };
|
|
}
|
|
|
|
function productionCounts(root) {
|
|
const read = status => Number(root.querySelector(`[data-batch-summary="${status}"]`)?.textContent || 0);
|
|
return {
|
|
queued: read('queued'),
|
|
running: read('running'),
|
|
paused: read('paused'),
|
|
completed: read('completed'),
|
|
partial: read('partial'),
|
|
failed: read('failed'),
|
|
};
|
|
}
|
|
|
|
function createStep(number, id, title, copy) {
|
|
const button = createNode('button', 'ux-image-factory-step');
|
|
button.type = 'button';
|
|
button.dataset.uxFactoryStep = id;
|
|
button.setAttribute('aria-pressed', 'false');
|
|
|
|
const index = createNode('span', 'ux-image-factory-step-index', String(number));
|
|
const body = createNode('span', 'ux-image-factory-step-copy');
|
|
body.append(createNode('strong', '', title), createNode('small', '', copy));
|
|
const state = createNode('span', 'ux-image-factory-step-state', 'Offen');
|
|
state.dataset.uxFactoryStepState = id;
|
|
button.append(index, body, state);
|
|
return button;
|
|
}
|
|
|
|
function createWorkflow(root) {
|
|
if (root.querySelector('.ux-image-factory-workflow')) return;
|
|
const layout = root.querySelector('.image-factory-layout');
|
|
if (!layout) return;
|
|
|
|
const workflow = createNode('section', 'ux-image-factory-workflow');
|
|
workflow.setAttribute('aria-label', 'Ablauf der Bildproduktion');
|
|
|
|
const heading = createNode('div', 'ux-image-factory-workflow-head');
|
|
const headingCopy = createNode('div');
|
|
headingCopy.append(
|
|
createNode('span', 'ux-image-factory-kicker', 'Geführte Bildproduktion'),
|
|
createNode('h3', '', 'Vom Quellbild zur fertigen Ausgabe'),
|
|
createNode('p', '', 'Vendoo zeigt nur den jeweils relevanten Arbeitsschritt. Bestehende Profile, Jobs und Dateien bleiben unverändert.')
|
|
);
|
|
const next = createNode('button', 'vd-button primary ux-image-factory-next', 'Quellbilder auswählen');
|
|
next.type = 'button';
|
|
next.dataset.uxFactoryNext = 'sources';
|
|
heading.append(headingCopy, next);
|
|
|
|
const steps = createNode('div', 'ux-image-factory-steps');
|
|
steps.append(
|
|
createStep(1, 'sources', 'Quellbilder', 'Bilder für den Auftrag wählen'),
|
|
createStep(2, 'profiles', 'Ausgabeprofile', 'Formate und Qualitätsziele festlegen'),
|
|
createStep(3, 'review', 'Prüfen & starten', 'Name, Optionen und Umfang kontrollieren'),
|
|
createStep(4, 'jobs', 'Aufträge & Ergebnisse', 'Fortschritt und Fehler überwachen')
|
|
);
|
|
|
|
workflow.append(heading, steps);
|
|
layout.before(workflow);
|
|
}
|
|
|
|
function createStage(id, number, title, copy) {
|
|
const stage = createNode('section', `ux-image-factory-stage ux-image-factory-stage-${id}`);
|
|
stage.dataset.uxFactoryStage = id;
|
|
stage.id = `ux-image-factory-stage-${id}`;
|
|
|
|
const head = createNode('header', 'ux-image-factory-stage-head');
|
|
const index = createNode('span', 'ux-image-factory-stage-index', String(number).padStart(2, '0'));
|
|
const text = createNode('div');
|
|
text.append(createNode('h3', '', title), createNode('p', '', copy));
|
|
head.append(index, text);
|
|
stage.append(head);
|
|
return stage;
|
|
}
|
|
|
|
function restructureConfiguration(root) {
|
|
const config = root.querySelector('.image-factory-config');
|
|
if (!config || config.dataset.uxFactoryStructured === 'true') return;
|
|
|
|
const sourceHead = config.querySelector('.image-factory-source-head');
|
|
const sourceGrid = config.querySelector('#image-factory-source-grid');
|
|
const profileHead = config.querySelector('.image-factory-profile-head');
|
|
const profileGrid = config.querySelector('#image-factory-profile-grid');
|
|
const nameField = config.querySelector(':scope > label.field');
|
|
const options = config.querySelector('.image-factory-options');
|
|
const submit = config.querySelector('.image-factory-submit');
|
|
if (!sourceHead || !sourceGrid || !profileHead || !profileGrid || !submit) return;
|
|
|
|
const sourceStage = createStage('sources', 1, 'Quellbilder auswählen', 'Suche oder filtere die Medienbibliothek und markiere die Bilder, die verarbeitet werden sollen.');
|
|
const sourceTools = sourceHead.querySelector('.image-factory-source-tools');
|
|
const sourceSearch = sourceTools?.querySelector('#image-factory-source-search');
|
|
const sourceControls = createNode('details', 'ux-image-factory-source-controls');
|
|
const sourceControlsSummary = createNode('summary', '', 'Auswahlwerkzeuge');
|
|
const sourceControlsBody = createNode('div', 'ux-image-factory-source-controls-body');
|
|
if (sourceTools) {
|
|
if (sourceSearch) sourceStage.append(sourceSearch);
|
|
[...sourceTools.children].forEach(child => sourceControlsBody.append(child));
|
|
sourceControls.append(sourceControlsSummary, sourceControlsBody);
|
|
}
|
|
sourceStage.append(sourceHead, sourceControls, sourceGrid);
|
|
|
|
const profileStage = createStage('profiles', 2, 'Ausgabeprofile festlegen', 'Wähle ein oder mehrere Zielformate. Jedes Profil erzeugt pro Quellbild eine eigenständige Ausgabe.');
|
|
profileStage.append(profileHead, profileGrid);
|
|
|
|
const reviewStage = createStage('review', 3, 'Auftrag prüfen und starten', 'Vergib einen Namen, kontrolliere den Ausgabeumfang und aktiviere nur bei Bedarf zusätzliche Verarbeitung.');
|
|
if (nameField) reviewStage.append(nameField);
|
|
if (options) {
|
|
options.open = false;
|
|
options.classList.add('ux-image-factory-advanced');
|
|
reviewStage.append(options);
|
|
}
|
|
reviewStage.append(submit);
|
|
|
|
const originalTitle = config.querySelector(':scope > .section-title-row');
|
|
originalTitle?.remove();
|
|
config.append(sourceStage, profileStage, reviewStage);
|
|
config.dataset.uxFactoryStructured = 'true';
|
|
}
|
|
|
|
function createMonitor(root) {
|
|
const status = root.querySelector('.image-factory-status');
|
|
const layout = root.querySelector('.image-factory-layout');
|
|
if (!status || !layout || root.querySelector('.ux-image-factory-monitor')) return;
|
|
|
|
const monitor = createNode('details', 'ux-image-factory-monitor');
|
|
const summary = createNode('summary');
|
|
const summaryCopy = createNode('span');
|
|
summaryCopy.append(createNode('strong', '', 'Produktionsmonitor'), createNode('small', '', 'Wartende, laufende und fehlerhafte Aufträge'));
|
|
const badge = createNode('span', 'ux-image-factory-monitor-badge', 'Keine aktiven Aufträge');
|
|
badge.dataset.uxFactoryMonitorBadge = 'true';
|
|
summary.append(summaryCopy, badge);
|
|
monitor.append(summary, status);
|
|
layout.append(monitor);
|
|
}
|
|
|
|
function createJobControls(root) {
|
|
const jobs = root.querySelector('.image-factory-jobs');
|
|
const list = root.querySelector('#image-factory-jobs');
|
|
if (!jobs || !list || jobs.querySelector('.ux-image-factory-job-filter')) return;
|
|
|
|
jobs.id = 'ux-image-factory-stage-jobs';
|
|
const filter = createNode('div', 'ux-image-factory-job-filter');
|
|
filter.setAttribute('role', 'group');
|
|
filter.setAttribute('aria-label', 'Produktionsaufträge filtern');
|
|
const definitions = [
|
|
['all', 'Alle'],
|
|
['active', 'Aktiv'],
|
|
['completed', 'Fertig'],
|
|
['issues', 'Probleme'],
|
|
];
|
|
definitions.forEach(([value, label]) => {
|
|
const button = createNode('button', 'vd-button compact', label);
|
|
button.type = 'button';
|
|
button.dataset.uxFactoryJobFilter = value;
|
|
button.setAttribute('aria-pressed', 'false');
|
|
filter.append(button);
|
|
});
|
|
|
|
const titleRow = jobs.querySelector(':scope > .section-title-row');
|
|
titleRow?.after(filter);
|
|
let stored = 'all';
|
|
try { stored = localStorage.getItem(JOB_FILTER_KEY) || 'all'; } catch {}
|
|
root.dataset.uxFactoryJobFilter = JOB_FILTERS.includes(stored) ? stored : 'all';
|
|
}
|
|
|
|
function classifyJob(node) {
|
|
if (node.classList.contains('empty-state')) return 'empty';
|
|
const text = (node.textContent || '').toLowerCase();
|
|
if (/fehler|fehlgeschlagen|teilweise|failed|partial|abgebrochen|cancelled/.test(text)) return 'issues';
|
|
if (/wartend|läuft|laufend|pausiert|queued|running|paused|in bearbeitung/.test(text)) return 'active';
|
|
if (/fertig|abgeschlossen|completed|erfolgreich|success/.test(text)) return 'completed';
|
|
return 'other';
|
|
}
|
|
|
|
function applyJobFilter(root) {
|
|
const selected = JOB_FILTERS.includes(root.dataset.uxFactoryJobFilter) ? root.dataset.uxFactoryJobFilter : 'all';
|
|
root.querySelectorAll('[data-ux-factory-job-filter]').forEach(button => {
|
|
const active = button.dataset.uxFactoryJobFilter === selected;
|
|
button.classList.toggle('active', active);
|
|
button.setAttribute('aria-pressed', String(active));
|
|
});
|
|
|
|
const list = root.querySelector('#image-factory-jobs');
|
|
if (!list) return;
|
|
[...list.children].forEach(job => {
|
|
const category = classifyJob(job);
|
|
job.hidden = selected !== 'all' && category !== 'empty' && category !== selected;
|
|
});
|
|
}
|
|
|
|
function updateWorkflow(root) {
|
|
const selection = parseSelectionSummary(root);
|
|
const counts = productionCounts(root);
|
|
const hasSources = selection.sources > 0;
|
|
const hasProfiles = selection.profiles > 0;
|
|
const ready = hasSources && hasProfiles;
|
|
const activeJobs = counts.queued + counts.running + counts.paused;
|
|
const issues = counts.failed + counts.partial;
|
|
const hasJobs = activeJobs + counts.completed + issues > 0;
|
|
|
|
root.dataset.uxFactoryHasSources = String(hasSources);
|
|
root.dataset.uxFactoryHasProfiles = String(hasProfiles);
|
|
root.dataset.uxFactoryReady = String(ready);
|
|
root.dataset.uxFactoryHasJobs = String(hasJobs);
|
|
|
|
const current = !hasSources ? 'sources' : !hasProfiles ? 'profiles' : activeJobs || issues ? 'jobs' : 'review';
|
|
root.dataset.uxFactoryCurrentStep = current;
|
|
|
|
const completed = {
|
|
sources: hasSources,
|
|
profiles: hasProfiles,
|
|
review: ready && hasJobs,
|
|
jobs: hasJobs && activeJobs === 0 && issues === 0,
|
|
};
|
|
|
|
root.querySelectorAll('[data-ux-factory-step]').forEach(step => {
|
|
const id = step.dataset.uxFactoryStep;
|
|
const isCurrent = id === current;
|
|
step.classList.toggle('is-current', isCurrent);
|
|
step.classList.toggle('is-complete', Boolean(completed[id]));
|
|
step.setAttribute('aria-pressed', String(isCurrent));
|
|
const state = step.querySelector('[data-ux-factory-step-state]');
|
|
if (state) state.textContent = completed[id] ? 'Erledigt' : isCurrent ? 'Jetzt' : 'Offen';
|
|
});
|
|
|
|
const next = root.querySelector('[data-ux-factory-next]');
|
|
if (next) {
|
|
if (!hasSources) {
|
|
next.dataset.uxFactoryNext = 'sources';
|
|
next.textContent = 'Quellbilder auswählen';
|
|
} else if (!hasProfiles) {
|
|
next.dataset.uxFactoryNext = 'profiles';
|
|
next.textContent = 'Ausgabeprofile wählen';
|
|
} else if (activeJobs || issues) {
|
|
next.dataset.uxFactoryNext = 'jobs';
|
|
next.textContent = issues ? 'Probleme prüfen' : 'Produktion überwachen';
|
|
} else {
|
|
next.dataset.uxFactoryNext = 'submit';
|
|
next.textContent = 'Auftrag prüfen & starten';
|
|
}
|
|
}
|
|
|
|
const badge = root.querySelector('[data-ux-factory-monitor-badge]');
|
|
if (badge) {
|
|
badge.textContent = issues
|
|
? `${issues} Problem${issues === 1 ? '' : 'e'}`
|
|
: activeJobs
|
|
? `${activeJobs} aktiv`
|
|
: counts.completed
|
|
? `${counts.completed} fertig`
|
|
: 'Keine aktiven Aufträge';
|
|
badge.dataset.tone = issues ? 'danger' : activeJobs ? 'working' : counts.completed ? 'success' : 'neutral';
|
|
}
|
|
|
|
const monitor = root.querySelector('.ux-image-factory-monitor');
|
|
if (monitor && !monitor.dataset.uxFactoryInitialized) {
|
|
monitor.open = activeJobs > 0 || issues > 0;
|
|
monitor.dataset.uxFactoryInitialized = 'true';
|
|
}
|
|
applyJobFilter(root);
|
|
}
|
|
|
|
function stageTarget(root, id) {
|
|
if (id === 'jobs') return root.querySelector('#ux-image-factory-stage-jobs');
|
|
return root.querySelector(`#ux-image-factory-stage-${id}`);
|
|
}
|
|
|
|
function bindActions(root) {
|
|
root.addEventListener('click', event => {
|
|
const step = event.target.closest('[data-ux-factory-step]');
|
|
if (step) {
|
|
scrollToNode(stageTarget(root, step.dataset.uxFactoryStep));
|
|
return;
|
|
}
|
|
|
|
const next = event.target.closest('[data-ux-factory-next]');
|
|
if (next) {
|
|
const target = next.dataset.uxFactoryNext;
|
|
if (target === 'submit') {
|
|
scrollToNode(stageTarget(root, 'review'));
|
|
root.querySelector('#image-factory-submit')?.focus({ preventScroll: true });
|
|
} else {
|
|
scrollToNode(stageTarget(root, target));
|
|
}
|
|
return;
|
|
}
|
|
|
|
const filter = event.target.closest('[data-ux-factory-job-filter]');
|
|
if (filter) {
|
|
root.dataset.uxFactoryJobFilter = filter.dataset.uxFactoryJobFilter;
|
|
try { localStorage.setItem(JOB_FILTER_KEY, root.dataset.uxFactoryJobFilter); } catch {}
|
|
applyJobFilter(root);
|
|
}
|
|
});
|
|
}
|
|
|
|
function observeFactory(root) {
|
|
const observer = new MutationObserver(() => window.requestAnimationFrame(() => updateWorkflow(root)));
|
|
const targets = [
|
|
root.querySelector('#image-factory-selection-count'),
|
|
root.querySelector('#image-factory-source-grid'),
|
|
root.querySelector('#image-factory-profile-grid'),
|
|
root.querySelector('#image-factory-summary'),
|
|
root.querySelector('#image-factory-jobs'),
|
|
].filter(Boolean);
|
|
targets.forEach(target => observer.observe(target, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class', 'checked', 'aria-pressed'] }));
|
|
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
|
|
}
|
|
|
|
function initializeImageFactoryUx() {
|
|
const root = document.getElementById(ROOT_ID);
|
|
if (!root || root.dataset.uxImageFactoryReady === 'true') return;
|
|
createWorkflow(root);
|
|
restructureConfiguration(root);
|
|
createMonitor(root);
|
|
createJobControls(root);
|
|
bindActions(root);
|
|
observeFactory(root);
|
|
root.dataset.uxImageFactoryReady = 'true';
|
|
root.classList.add('ux-image-factory-ready');
|
|
updateWorkflow(root);
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initializeImageFactoryUx, { once: true });
|
|
} else {
|
|
initializeImageFactoryUx();
|
|
}
|
|
})();
|