Files
vda-to-edifact-converter/js/app.js
zed 67bd64b688 Separaten Output-Ordner für INVRPT → VDA 4913 Konvertierungen hinzufügen
Der eingehende Watcher nutzt denselben Input-Ordner für alle Dateitypen,
schreibt INVRPT-Konvertierungen nun aber in einen konfigurierbaren
separaten Output-Ordner. Bleibt das Feld leer, wird der Standard-
Output-Ordner als Fallback verwendet (abwärtskompatibel).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 12:22:45 +01:00

1490 lines
60 KiB
JavaScript
Raw 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.
/**
* UI Orchestration logic with Config support, 4 converter modes,
* page navigation, and Watcher integration.
*/
window.EDIBridge = window.EDIBridge || {};
class App {
constructor() {
this.mode = 'outbound-bosch';
this.currentPage = 'converter';
this.currentData = null;
this.rawContent = null;
this.masterData = {};
this.config = null;
this.configFileName = 'config.txt';
this.watcher = null;
this.werksWatcher = null;
this.initElements();
this.bindEvents();
this.loadConfigFromStorage();
this.initWatcher();
this.initOutboundWatcher();
this.initWerksWatcher();
// Initialize Sub-modules
if (window.EDIBridge.Viewer) {
this.viewer = new window.EDIBridge.Viewer(this);
}
if (window.EDIBridge.Editor) {
this.editor = new window.EDIBridge.Editor(this);
}
if (window.EDIBridge.HistoryManager) {
this.historyManager = new window.EDIBridge.HistoryManager(this);
}
}
// ─── Elements ────────────────────────────────────────────────────
initElements() {
this.dropZone = document.getElementById('dropZone');
this.fileInput = document.getElementById('fileInput');
this.configInput = document.getElementById('configInput');
this.enrichmentSection = document.getElementById('enrichmentSection');
this.resultSection = document.getElementById('resultSection');
this.previewArea = document.getElementById('previewArea');
this.dropText = document.getElementById('dropText');
this.configStatus = document.getElementById('configStatus');
this.statsGrid = document.getElementById('stats-grid');
this.ownCustomerNumber = document.getElementById('ownCustomerNumber');
}
// ─── Events ──────────────────────────────────────────────────────
bindEvents() {
if (this.dropZone) {
this.dropZone.onclick = (e) => { e.stopPropagation(); if (this.fileInput) this.fileInput.click(); };
this.dropZone.ondragover = (e) => { e.preventDefault(); this.dropZone.classList.add('dragover'); };
this.dropZone.ondragleave = () => this.dropZone.classList.remove('dragover');
this.dropZone.ondrop = (e) => {
e.preventDefault();
this.dropZone.classList.remove('dragover');
this.handleFile(e.dataTransfer.files[0]);
};
}
if (this.fileInput) {
this.fileInput.onchange = (e) => this.handleFile(e.target.files[0]);
}
if (this.configInput) {
this.configInput.onchange = (e) => this.loadConfig(e.target.files[0]);
}
}
// ─── Watcher Init ────────────────────────────────────────────────
initWatcher() {
const WatcherBridge = window.EDIBridge.WatcherBridge;
if (!WatcherBridge) return;
this.watcher = new WatcherBridge();
this.watcher.onStatusChange = (status) => this.updateWatcherUI(status);
this.watcher.onLogEntry = (entry) => this.appendLogEntry(entry);
// Check if running in Electron
if (!this.watcher.isElectron()) {
const notice = document.getElementById('electronNotice');
if (notice) notice.style.display = 'flex';
// Disable folder buttons
const btnIn = document.getElementById('btnSelectInput');
const btnOut = document.getElementById('btnSelectOutput');
const btnInvrptOut = document.getElementById('btnSelectInvrptOutput');
const btnStart = document.getElementById('btnStartWatcher');
if (btnIn) btnIn.disabled = true;
if (btnOut) btnOut.disabled = true;
if (btnInvrptOut) btnInvrptOut.disabled = true;
if (btnStart) btnStart.disabled = true;
} else {
// Load saved settings
this.watcher.loadSettings().then(settings => {
if (settings.inputDir) {
document.getElementById('inputDirPath').value = settings.inputDir;
}
if (settings.outputDir) {
document.getElementById('outputDirPath').value = settings.outputDir;
}
if (settings.invrptOutputDir) {
document.getElementById('invrptOutputPath').value = settings.invrptOutputDir;
}
});
}
}
initOutboundWatcher() {
const OutboundWatcherBridge = window.EDIBridge.OutboundWatcherBridge;
if (!OutboundWatcherBridge) return;
this.outboundWatcher = new OutboundWatcherBridge();
this.outboundWatcher.onStatusChange = (status) => this.updateOutboundWatcherUI(status);
this.outboundWatcher.onLogEntry = (entry) => this.appendLogEntry(entry);
if (!this.outboundWatcher.isElectron()) return;
// Load saved settings
window.electronAPI.loadSettings().then(settings => {
if (settings.outboundInputDir) {
document.getElementById('outboundInputPath').value = settings.outboundInputDir;
}
if (settings.outboundOutputDir) {
document.getElementById('outboundOutputPath').value = settings.outboundOutputDir;
}
});
}
initWerksWatcher() {
const WerksWatcherBridge = window.EDIBridge.WerksWatcherBridge;
if (!WerksWatcherBridge) return;
this.werksWatcher = new WerksWatcherBridge();
this.werksWatcher.onStatusChange = (status) => this.updateWerksWatcherUI(status);
this.werksWatcher.onLogEntry = (entry) => this.appendLogEntry(entry);
if (!this.werksWatcher.isElectron()) return;
// Load saved settings
window.electronAPI.loadSettings().then(settings => {
if (settings.werksInputDir) {
document.getElementById('werksInputPath').value = settings.werksInputDir;
}
if (settings.werksOutputDir) {
document.getElementById('werksOutputPath').value = settings.werksOutputDir;
}
});
}
showPage(page) {
this.currentPage = page;
document.querySelectorAll('.page-nav-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.page === page);
});
document.getElementById('pageConverter').style.display = page === 'converter' ? '' : 'none';
document.getElementById('pageSettings').style.display = page === 'settings' ? '' : 'none';
const pageViewer = document.getElementById('pageViewer');
if (pageViewer) pageViewer.style.display = page === 'viewer' ? '' : 'none';
const pageEditor = document.getElementById('pageEditor');
if (pageEditor) pageEditor.style.display = page === 'editor' ? '' : 'none';
const pageHistory = document.getElementById('pageHistory');
if (pageHistory) pageHistory.style.display = page === 'history' ? '' : 'none';
// Activate history manager when page is shown
if (page === 'history' && this.historyManager) {
this.historyManager.activate();
}
if (window.lucide) try { lucide.createIcons(); } catch (e) { }
}
// ─── Config ──────────────────────────────────────────────────────
async loadConfigFromStorage() {
let stored = null;
// Try getting config from Database first
if (window.electronAPI && window.electronAPI.dbGetConfig) {
try {
stored = await window.electronAPI.dbGetConfig();
if (stored) console.log("Config loaded from Database");
} catch (e) {
console.error("Failed to load config from DB:", e);
}
}
// Fallback to localStorage if DB doesn't have it
if (!stored) {
stored = localStorage.getItem('ediConfig');
if (stored) console.log("Config loaded from LocalStorage fallback");
}
if (stored) {
const ConfigParser = window.EDIBridge.ConfigParser;
this.config = ConfigParser.parse(stored);
if (this.watcher) this.watcher.config = this.config;
this.updateConfigUI('Gespeichert (DB/Local)');
}
}
loadConfig(file) {
if (!file) return;
this.configFileName = file.name;
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
const ConfigParser = window.EDIBridge.ConfigParser;
this.config = ConfigParser.parse(text);
if (this.watcher) this.watcher.config = this.config;
localStorage.setItem('ediConfig', text);
this.updateConfigUI(file.name);
if (this.currentData) {
this.autoPopulateNADs();
this.renderEnrichmentTable();
}
};
reader.readAsText(file);
}
updateConfigUI(name) {
this.configStatus.innerHTML = '<i data-lucide="check-circle"></i><span>Konfig: ' + name + '</span>';
this.configStatus.classList.add('loaded');
const badge = document.getElementById('nadSourceBadge');
if (badge) {
badge.textContent = 'Aus Konfig';
badge.className = 'badge badge-config';
}
document.getElementById('btnSaveConfig').style.display = 'inline-flex';
this.renderMappingTable();
this.renderIdMapping711Table();
// Populate Own Customer Number
if (this.ownCustomerNumber && window.EDIBridge.ConfigParser) {
this.ownCustomerNumber.value = window.EDIBridge.ConfigParser.lookupGeneral(this.config, 'OWN_CUSTOMER_NUMBER', '');
}
if (window.lucide) lucide.createIcons();
}
async saveConfig() {
if (!this.config) return;
this.mergeUIToConfig();
const ConfigParser = window.EDIBridge.ConfigParser;
const text = ConfigParser.serialize(this.config);
// Save to LocalStorage array as fallback
localStorage.setItem('ediConfig', text);
// Save to Database (the main requirement)
if (window.electronAPI && window.electronAPI.dbInsertConfig) {
try {
await window.electronAPI.dbInsertConfig(text);
console.log("Config successfully saved to Database");
} catch (e) {
console.error("Failed to save config to DB:", e);
}
}
// Sync with watcher
if (this.watcher) this.watcher.config = this.config;
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = this.configFileName || 'config.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Revoke after a short delay to ensure browser handled the click
setTimeout(() => URL.revokeObjectURL(url), 100);
}
saveToStorage() {
if (!this.config) {
// If no config object yet, create an empty one
const ConfigParser = window.EDIBridge.ConfigParser;
this.config = { __sectionMap__: {} };
this.updateConfigUI('Gespeichert (Local)');
}
this.mergeUIToConfig();
const ConfigParser = window.EDIBridge.ConfigParser;
const text = ConfigParser.serialize(this.config);
localStorage.setItem('ediConfig', text);
console.log("Config auto-saved to LocalStorage");
if (this.watcher) this.watcher.config = this.config;
}
mergeUIToConfig() {
const ConfigParser = window.EDIBridge.ConfigParser;
const updateSection = (sectionName, selector, dataKey) => {
const section = ConfigParser.getSection(this.config, sectionName) || ConfigParser.ensureSection(this.config, sectionName);
document.querySelectorAll(selector).forEach(input => {
const val = input.value.trim();
const key = input.dataset[dataKey];
if (val) {
section[key] = val;
} else {
delete section[key];
}
});
};
// Sync General Settings
if (this.ownCustomerNumber) {
const genSection = ConfigParser.ensureSection(this.config, 'GENERAL');
const ownCust = this.ownCustomerNumber.value.trim();
if (ownCust) genSection['OWN_CUSTOMER_NUMBER'] = ownCust;
else delete genSection['OWN_CUSTOMER_NUMBER'];
}
updateSection('PAC_WEIGHT', '.pac-weight-input', 'pacmat');
updateSection('PAC_LENGTH', '.pac-ln-input', 'pacmat');
updateSection('PAC_WIDTH', '.pac-wd-input', 'pacmat');
updateSection('PAC_HEIGHT', '.pac-ht-input', 'pacmat');
updateSection('LIN_WEIGHT', '.lin-weight-input', 'suppmat');
// Sync NAD fields back to config
if (this.activeSourceId || this.activeTargetId) {
const quals = ['by', 'se', 'st', 'sf'];
const idMap = { 'by': this.activeTargetId, 'st': this.activeTargetId, 'se': this.activeSourceId, 'sf': this.activeSourceId };
const fields = {
'id': 'PARTY',
'qual': 'QUAL',
'name': 'NAME',
'name2': 'NAME2',
'street': 'STREET',
'city': 'CITY',
'zip': 'POSTCODE',
'country': 'COUNTRY'
};
quals.forEach(q => {
const plantId = idMap[q];
if (!plantId) return;
Object.entries(fields).forEach(([f, sectionSuffix]) => {
const el = document.getElementById(`nad-${q}-${f}`);
if (el) {
const val = el.value.trim();
const sectionName = `NAD_${q.toUpperCase()}_${sectionSuffix}`;
const section = ConfigParser.ensureSection(this.config, sectionName);
if (val) section[plantId] = val;
else delete section[plantId];
}
});
});
}
}
autoPopulateNADs() {
if (!this.config || !this.currentData) return;
const ConfigParser = window.EDIBridge.ConfigParser;
const h = this.currentData.interchanges[0]?.header;
if (!h) return;
this.activeTargetId = h.targetId;
this.activeSourceId = h.sourceId;
const targetId = h.targetId;
const sourceId = h.sourceId;
const by = ConfigParser.lookupNAD(this.config, 'BY', targetId);
if (by.name) this.fillNAD('by', by);
const se = ConfigParser.lookupNAD(this.config, 'SE', sourceId);
if (se.name) this.fillNAD('se', se);
const st = ConfigParser.lookupNAD(this.config, 'ST', targetId);
if (st.name) this.fillNAD('st', st);
const sf = ConfigParser.lookupNAD(this.config, 'SF', sourceId);
if (sf.name) this.fillNAD('sf', sf);
}
fillNAD(prefix, data) {
const set = (field, val) => {
const el = document.getElementById('nad-' + prefix + '-' + field);
if (el) el.value = val || '';
};
set('id', data.id);
set('qual', data.qual);
set('name', data.name);
set('name2', data.name2);
set('street', data.street);
set('city', data.city);
set('zip', data.zip);
set('country', data.country);
}
// ─── Mode Toggle ─────────────────────────────────────────────────
setMode(mode) {
this.mode = mode;
const modes = ['outbound-bosch', 'outbound-zf', 'outbound-ifm', 'inbound-bosch', 'inbound-ifm', 'inbound-invrpt', 'validate-edifact'];
const btnIds = ['btnOutboundBosch', 'btnOutboundZf', 'btnOutboundIfm', 'btnInboundBosch', 'btnInboundIfm', 'btnInboundInvrpt', 'btnValidateEdifact'];
modes.forEach((m, i) => {
const btn = document.getElementById(btnIds[i]);
if (btn) btn.classList.toggle('active', m === mode);
});
// Update drop zone text
const labelMap = {
'outbound-bosch': 'VDA 4913 Datei auswählen → Bosch DESADV',
'outbound-zf': 'VDA 4913 Datei auswählen → ZF DESADV',
'outbound-ifm': 'VDA 4913 Datei auswählen → IFM DELVRY03',
'inbound-bosch': 'EDIFACT DELFOR Datei auswählen → VDA 4905',
'inbound-ifm': 'IFM DELFOR D04A Datei auswählen → VDA 4905',
'inbound-invrpt': 'INVRPT Datei auswählen → VDA 4913 EDL36',
'validate-edifact': 'EDIFACT Datei auswählen zur reinen Validierung/Prüfung',
};
if (this.dropText) this.dropText.textContent = labelMap[mode] || '';
// Hide enrichment/result when switching modes
if (this.enrichmentSection) this.enrichmentSection.style.display = 'none';
if (this.resultSection) this.resultSection.style.display = 'none';
}
// ─── File Handling ───────────────────────────────────────────────
handleFile(file) {
if (!file) return;
this.currentFilename = file.name;
const reader = new FileReader();
reader.onload = (e) => {
this.rawContent = e.target.result;
this._processContent(this.rawContent);
};
reader.readAsText(file);
}
_processContent(content) {
const trimmed = content.trim();
// Check if we should auto-detect based on customer ID
const VDAParser = window.EDIBridge.VDAParser;
let detectedMode = null;
if (VDAParser) {
let customerId = VDAParser.getCustomerId(trimmed);
// If not VDA, try EDIFACT/DELFOR
if (!customerId && window.EDIBridge.DelforParser) {
customerId = window.EDIBridge.DelforParser.getCustomerId(trimmed);
}
console.log("Detected Customer ID:", customerId);
if (customerId) {
const ConfigParser = window.EDIBridge.ConfigParser;
detectedMode = ConfigParser.lookupCustomerMode(this.config, customerId);
console.log("Mapped mode for ID", customerId, ":", detectedMode);
if (detectedMode) {
this.setMode(detectedMode);
}
}
}
switch (this.mode) {
case 'outbound-bosch':
this.processOutboundBosch(trimmed);
break;
case 'outbound-zf':
this.processOutboundZf(trimmed);
break;
case 'outbound-ifm':
this.processOutboundIfm(trimmed);
break;
case 'inbound-bosch':
this.processInboundBosch(trimmed);
break;
case 'inbound-ifm':
this.processInboundIfm(trimmed);
break;
case 'inbound-invrpt':
this.processInboundInvrpt(trimmed);
break;
case 'validate-edifact':
this.processValidateEdifact(trimmed);
break;
default:
// Auto-detect format fallback
const upper = trimmed.toUpperCase();
if (upper.includes('INVRPT')) {
this.setMode('inbound-invrpt');
this.processInboundInvrpt(trimmed);
} else if (upper.startsWith('UNA') || upper.startsWith('UNB')) {
this.setMode('inbound-bosch');
this.processInboundBosch(trimmed);
} else {
this.setMode('outbound-bosch');
this.processOutboundBosch(trimmed);
}
}
}
// ─── Inbound: Bosch DELFOR → VDA 4905 ────────────────────────────
processInboundBosch(content) {
this.statsGrid.innerHTML = '';
this.enrichmentSection.style.display = 'none';
this.currentData = null;
try {
const parser = window.EDIBridge.DelforParser;
const generator = window.EDIBridge.VDA4905Generator;
if (!parser || !generator) throw new Error('Inbound-Module (DelforParser/VDA4905Generator) nicht geladen.');
const parsed = parser.parse(content);
const vda4905 = generator.generate(parsed);
this.previewArea.innerText = vda4905;
this.resultSection.style.display = 'block';
document.getElementById('resultTitle').innerText = 'VDA 4905 Ergebnis (DELFOR → VDA 4905)';
const statsHtml = `
<div class="stat-card">
<div class="stat-value">${parsed.segments.length}</div>
<div class="stat-label">EDIFACT Segments</div>
</div>
<div class="stat-card">
<div class="stat-value">${vda4905.split('\n').filter(l => l.trim()).length}</div>
<div class="stat-label">VDA 4905 Records</div>
</div>
`;
this.statsGrid.innerHTML = statsHtml;
this.resultSection.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
alert('Inbound Error (Bosch): ' + e.message);
console.error(e);
}
}
// ─── Inbound: IFM DELFOR D04A → VDA 4905 ────────────────────────
processInboundIfm(content) {
this.statsGrid.innerHTML = '';
this.enrichmentSection.style.display = 'none';
this.currentData = null;
try {
const Converter = window.EDIBridge.DelforToVDA4905Converter;
if (!Converter) throw new Error('IFM DELFOR Converter (DelforToVDA4905Converter) nicht geladen.');
const result = Converter.convertWithValidation(content);
if (result.errors && result.errors.length > 0) {
console.warn('IFM DELFOR Validation Warnings:', result.errors);
}
const vda4905 = result.output || result;
const outputText = typeof vda4905 === 'string' ? vda4905 : JSON.stringify(vda4905, null, 2);
this.previewArea.innerText = outputText;
this.resultSection.style.display = 'block';
document.getElementById('resultTitle').innerText = 'VDA 4905 Ergebnis (IFM DELFOR D04A → VDA 4905)';
const lines = outputText.split('\n').filter(l => l.trim()).length;
const statsHtml = `
<div class="stat-card">
<div class="stat-value">${lines}</div>
<div class="stat-label">VDA 4905 Records</div>
</div>
`;
this.statsGrid.innerHTML = statsHtml;
if (result.warnings && result.warnings.length > 0) {
this.statsGrid.innerHTML += `
<div class="stat-card stat-warning">
<div class="stat-value">${result.warnings.length}</div>
<div class="stat-label">Warnungen</div>
</div>
`;
}
this.resultSection.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
alert('Inbound Error (IFM): ' + e.message);
console.error(e);
}
}
// ─── Inbound: INVRPT D13A → VDA 4913 ────────────────────────
processInboundInvrpt(content) {
this.statsGrid.innerHTML = '';
this.enrichmentSection.style.display = 'none';
this.currentData = null;
try {
const Converter = window.EDIBridge.InvrptToVDA4913;
if (!Converter) throw new Error('INVRPT-Module (InvrptToVDA4913) nicht geladen.');
const vda4913 = Converter.convert(content, this.config);
this.previewArea.innerText = vda4913;
this.resultSection.style.display = 'block';
document.getElementById('resultTitle').innerText = 'VDA 4913 Ergebnis (INVRPT → VDA 4913)';
const statsHtml = `
<div class="stat-card">
<div class="stat-value">${vda4913.split('\\n').filter(l => l.trim()).length}</div>
<div class="stat-label">VDA 4913 Records</div>
</div>
`;
this.statsGrid.innerHTML = statsHtml;
this.resultSection.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
alert('Inbound Error (INVRPT): ' + e.message);
console.error(e);
}
}
// ─── Utility: Validate EDIFACT (ts-edifact) ─────────────────────
async processValidateEdifact(content) {
this.statsGrid.innerHTML = '';
this.enrichmentSection.style.display = 'none';
this.currentData = null;
if (!window.electronAPI || !window.electronAPI.validateEdifact) {
alert('Validierung (ts-edifact) ist nur in der Electron-App verfügbar.');
return;
}
try {
const result = await window.electronAPI.validateEdifact(content);
const parsed = await window.electronAPI.parseEdifactLib(content);
document.getElementById('resultTitle').innerText = 'EDIFACT Validierungsprozess';
let html = `<div style="font-family: inherit;">
<h3 style="margin-top:0; color: ${result.valid ? '#10b981' : '#ef4444'}">
${result.valid ? '<i data-lucide="check-circle" style="vertical-align:middle"></i> Gültiges EDIFACT' : '<i data-lucide="x-circle" style="vertical-align:middle"></i> Ungültiges EDIFACT'}
</h3>`;
if (result.stats.messageType) {
html += `<p><strong>Nachrichtentyp:</strong> ${result.stats.messageType} (Version: ${result.stats.messageVersion})</p>`;
}
if (result.stats.sender || result.stats.receiver) {
html += `<p><strong>Sender:</strong> ${result.stats.sender || '-'} &nbsp;&nbsp; <strong>Empfänger:</strong> ${result.stats.receiver || '-'}</p>`;
}
if (result.errors && result.errors.length > 0) {
html += `<div style="background:#fee2e2; color:#b91c1c; padding: 15px; border-radius: 6px; margin-top: 15px;">
<h4 style="margin-top:0"><i data-lucide="alert-circle" style="vertical-align:middle; width:16px;"></i> Fehler</h4>
<ul style="margin-bottom:0">${result.errors.map(e => `<li>${e}</li>`).join('')}</ul>
</div>`;
}
if (result.warnings && result.warnings.length > 0) {
html += `<div style="background:#fef3c7; color:#b45309; padding: 15px; border-radius: 6px; margin-top: 15px;">
<h4 style="margin-top:0"><i data-lucide="alert-triangle" style="vertical-align:middle; width:16px;"></i> Warnungen</h4>
<ul style="margin-bottom:0">${result.warnings.map(e => `<li>${e}</li>`).join('')}</ul>
</div>`;
}
html += `<h4 style="margin-top: 25px;">Extrahierte Segmente (Rohdaten)</h4>
<div style="background:#1e1e1e; color:#d4d4d4; padding:15px; border-radius:6px; overflow-x:auto; font-family:'Fira Code', monospace; font-size:13px; max-height: 400px; overflow-y:auto;">
<pre style="margin:0;">${JSON.stringify(parsed.segments, null, 2)}</pre>
</div>
</div>`;
// Since previewArea usually escapes HTML via innerText, we set innerHTML and handle styling:
this.previewArea.innerHTML = html;
this.previewArea.style.whiteSpace = 'normal';
this.previewArea.style.backgroundColor = 'transparent';
this.previewArea.style.padding = '0';
this.resultSection.style.display = 'block';
const statsHtml = `
<div class="stat-card" style="border-left: 4px solid ${result.valid ? '#10b981' : '#ef4444'}">
<div class="stat-value">${result.valid ? 'OK' : 'ERR'}</div>
<div class="stat-label">Syntax-Status</div>
</div>
<div class="stat-card">
<div class="stat-value">${result.stats.segmentCount || 0}</div>
<div class="stat-label">Segmente</div>
</div>
`;
this.statsGrid.innerHTML = statsHtml;
this.resultSection.scrollIntoView({ behavior: 'smooth' });
if (window.lucide) lucide.createIcons();
} catch (e) {
alert('Validierungsfehler: ' + e.message);
console.error(e);
}
}
// ─── Outbound: VDA 4913 → Bosch DESADV ──────────────────────────
processOutboundBosch(content) {
const VDAParser = window.EDIBridge.VDAParser;
const vda = VDAParser.parse(content);
if (vda.interchanges.length === 0) {
alert('Keine gültigen VDA 4913 Daten gefunden.');
return;
}
this.currentData = vda;
this.updateStats(vda);
this.autoPopulateNADs();
this.renderEnrichmentTable();
}
// ─── Outbound: VDA 4913 → ZF DESADV ──────────────────────────
processOutboundZf(content) {
const VDAParser = window.EDIBridge.VDAParser;
const vda = VDAParser.parse(content);
if (vda.interchanges.length === 0) {
alert('Keine gültigen VDA 4913 Daten gefunden.');
return;
}
this.currentData = vda;
this.updateStats(vda);
this.autoPopulateNADs();
this.renderEnrichmentTable();
}
// ─── Outbound: VDA 4913 → IFM DELVRY03 ──────────────────────────
processOutboundIfm(content) {
this.statsGrid.innerHTML = '';
this.enrichmentSection.style.display = 'none';
try {
const VDAParser = window.EDIBridge.VDAParser;
const Converter = window.EDIBridge.VDA4913ToDELVRY03;
if (!VDAParser) throw new Error('VDA Parser nicht geladen.');
if (!Converter) throw new Error('VDA4913ToDELVRY03 Converter nicht geladen.');
const parsed = VDAParser.parse(content);
if (!parsed.interchanges || parsed.interchanges.length === 0) {
alert('Keine gültigen VDA 4913 Daten gefunden.');
return;
}
const idocs = Converter.convert(parsed, this.config || {});
const xml = Converter.toXML(idocs);
this.previewArea.innerText = xml;
this.resultSection.style.display = 'block';
document.getElementById('resultTitle').innerText = 'IFM DELVRY03 Ergebnis (VDA 4913 → DELVRY03)';
const statsHtml = `
<div class="stat-card">
<div class="stat-value">${parsed.interchanges.length}</div>
<div class="stat-label">Interchanges</div>
</div>
<div class="stat-card">
<div class="stat-value">${idocs.length}</div>
<div class="stat-label">IDocs erzeugt</div>
</div>
`;
this.statsGrid.innerHTML = statsHtml;
this.resultSection.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
alert('Outbound Error (IFM): ' + e.message);
console.error(e);
}
}
// ─── Stats ───────────────────────────────────────────────────────
updateStats(vda) {
if (!vda || !vda.interchanges) return;
const count = vda.interchanges.length;
const dnCount = vda.interchanges.reduce((acc, i) =>
acc + i.transports.reduce((tAcc, t) => tAcc + t.deliveryNotes.length, 0), 0);
this.statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-value">${count}</div>
<div class="stat-label">Interchanges</div>
</div>
<div class="stat-card">
<div class="stat-value">${dnCount}</div>
<div class="stat-label">Delivery Notes</div>
</div>
`;
}
// ─── Enrichment Table ────────────────────────────────────────────
renderEnrichmentTable() {
const tbody = document.querySelector('#enrichmentTable tbody');
if (!tbody || !this.currentData) return;
tbody.innerHTML = '';
const ConfigParser = window.EDIBridge.ConfigParser;
const positions = [];
this.currentData.interchanges.forEach(i => {
i.transports.forEach(t => {
t.deliveryNotes.forEach(dn => {
dn.positions.forEach(pos => positions.push(pos));
});
});
});
positions.forEach(pos => {
const row = document.createElement('tr');
const suppMat = pos.data.suppMat;
let weight = 0;
let status = '<span class="badge badge-vda">Aus VDA</span>';
if (this.config) {
weight = ConfigParser.lookupLinWeight(this.config, suppMat);
if (weight > 0) {
status = '<span class="badge badge-config">Aus Konfig</span>';
}
}
row.innerHTML = `
<td>${pos.data.custMat}</td>
<td>${suppMat}</td>
<td>${pos.data.qty}</td>
<td>${pos.data.unit}</td>
<td><input type="number" step="0.001" class="nad-input lin-weight-input" data-suppmat="${suppMat}" value="${weight || ''}" placeholder="0.000" onchange="app.saveToStorage()"></td>
<td>${status}</td>
`;
tbody.appendChild(row);
});
this.enrichmentSection.style.display = 'block';
this.renderPackagingTable();
}
renderPackagingTable() {
const tbody = document.querySelector('#packagingTable tbody');
if (!tbody || !this.currentData) return;
tbody.innerHTML = '';
const ConfigParser = window.EDIBridge.ConfigParser;
const seen = new Set();
const packages = [];
this.currentData.interchanges.forEach(i => {
i.transports.forEach(t => {
t.deliveryNotes.forEach(dn => {
dn.positions.forEach(pos => {
pos.packaging.forEach(p => {
const key = p.packMatSupp;
if (!seen.has(key)) {
seen.add(key);
packages.push(p);
}
});
});
});
});
});
packages.forEach(p => {
const row = document.createElement('tr');
const packMatSupp = p.packMatSupp;
let weight = null, ln = null, wd = null, ht = null;
let status = '<span class="badge badge-vda">Default</span>';
if (this.config) {
const w = ConfigParser.lookupPacWeight(this.config, packMatSupp);
if (w > 0) weight = w;
const l = ConfigParser.lookupPacDimension(this.config, packMatSupp, 'LENGTH');
if (l > 0) ln = l;
const w_d = ConfigParser.lookupPacDimension(this.config, packMatSupp, 'WIDTH');
if (w_d > 0) wd = w_d;
const h = ConfigParser.lookupPacDimension(this.config, packMatSupp, 'HEIGHT');
if (h > 0) ht = h;
if (weight || ln || wd || ht) {
status = '<span class="badge badge-config">Aus Konfig</span>';
}
}
row.innerHTML = `
<td>${p.packMatCust}</td>
<td>${packMatSupp}</td>
<td>${p.qty}</td>
<td><input type="number" step="0.1" class="nad-input pac-weight-input" data-pacmat="${packMatSupp}" value="${weight || ''}" placeholder="1.0" onchange="app.saveToStorage()"></td>
<td><input type="number" class="nad-input pac-ln-input" data-pacmat="${packMatSupp}" value="${ln || ''}" placeholder="40" onchange="app.saveToStorage()"></td>
<td><input type="number" class="nad-input pac-wd-input" data-pacmat="${packMatSupp}" value="${wd || ''}" placeholder="30" onchange="app.saveToStorage()"></td>
<td><input type="number" class="nad-input pac-ht-input" data-pacmat="${packMatSupp}" value="${ht || ''}" placeholder="15" onchange="app.saveToStorage()"></td>
<td>${status}</td>
`;
tbody.appendChild(row);
});
}
// ─── Customer Mapping Editor ─────────────────────────────────────
renderMappingTable() {
if (!this.config) return;
const ConfigParser = window.EDIBridge.ConfigParser;
const section = ConfigParser.getSection(this.config, 'CUSTOMER_MAPPING') || {};
const tbody = document.querySelector('#mappingTable tbody');
if (!tbody) return;
tbody.innerHTML = '';
const formatMap = {
'outbound-bosch': 'VDA 4913 → Bosch DESADV',
'outbound-zf': 'VDA 4913 → ZF DESADV',
'outbound-ifm': 'VDA 4913 → IFM DELVRY03',
'inbound-bosch': 'DELFOR → VDA 4905',
'inbound-ifm': 'IFM DELFOR → VDA 4905',
'inbound-invrpt': 'INVRPT → VDA 4913',
'validate-edifact': 'EDIFACT Validieren'
};
for (const [id, mode] of Object.entries(section)) {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${id}</strong></td>
<td>${formatMap[mode] || mode}</td>
<td>
<button class="btn-secondary" onclick="app.removeMapping('${id}')" title="Löschen">
<i data-lucide="trash-2" style="width: 16px; height: 16px; color: #ef4444;"></i>
</button>
</td>
`;
tbody.appendChild(row);
}
if (window.lucide) lucide.createIcons();
}
addMapping() {
if (!this.config) {
alert('Bitte zuerst eine Konfiguration laden!');
return;
}
const idInput = document.getElementById('newMappingId');
const modeInput = document.getElementById('newMappingMode');
const id = idInput.value.trim();
const mode = modeInput.value;
if (!id) {
alert('Bitte eine Kundennummer eingeben.');
return;
}
const ConfigParser = window.EDIBridge.ConfigParser;
const section = ConfigParser.ensureSection(this.config, 'CUSTOMER_MAPPING');
section[id] = mode;
idInput.value = '';
this.renderMappingTable();
this.saveToStorage();
// Update UI status to show it's active but maybe needs file export
if (this.configStatus.classList.contains('loaded')) {
// Keep "loaded" but maybe change icon temporarily or just keep it
} else {
this.updateConfigUI('Gespeichert (Local)');
}
if (window.lucide) lucide.createIcons();
}
removeMapping(id) {
if (!this.config) return;
const ConfigParser = window.EDIBridge.ConfigParser;
const section = ConfigParser.getSection(this.config, 'CUSTOMER_MAPPING');
if (section && section[id]) {
delete section[id];
this.renderMappingTable();
this.saveToStorage();
if (window.lucide) lucide.createIcons();
}
}
// ─── VDA 711 ID Mapping Editor ───────────────────────────────────
renderIdMapping711Table() {
if (!this.config) return;
const ConfigParser = window.EDIBridge.ConfigParser;
const section = ConfigParser.getSection(this.config, 'VDA711_ID_MAPPING') || {};
const tbody = document.querySelector('#idMapping711Table tbody');
if (!tbody) return;
tbody.innerHTML = '';
for (const [oldId, newId] of Object.entries(section)) {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${oldId}</strong></td>
<td><strong>${newId}</strong></td>
<td>
<button class="btn-secondary" onclick="app.removeIdMapping711('${oldId}')" title="Löschen">
<i data-lucide="trash-2" style="width: 16px; height: 16px; color: #ef4444;"></i>
</button>
</td>
`;
tbody.appendChild(row);
}
if (window.lucide) lucide.createIcons();
}
addIdMapping711() {
if (!this.config) {
alert('Bitte zuerst eine Konfiguration laden!');
return;
}
const oldInput = document.getElementById('newIdMapOriginal');
const newInput = document.getElementById('newIdMapTarget');
const oldId = oldInput.value.trim();
const newId = newInput.value.trim();
if (!oldId || !newId) {
alert('Bitte sowohl die alte als auch die neue ID eingeben.');
return;
}
const ConfigParser = window.EDIBridge.ConfigParser;
const section = ConfigParser.ensureSection(this.config, 'VDA711_ID_MAPPING');
section[oldId] = newId;
oldInput.value = '';
newInput.value = '';
this.renderIdMapping711Table();
this.saveToStorage();
}
removeIdMapping711(id) {
if (!this.config) return;
const ConfigParser = window.EDIBridge.ConfigParser;
const section = ConfigParser.getSection(this.config, 'VDA711_ID_MAPPING');
if (section && section[id]) {
delete section[id];
this.renderIdMapping711Table();
this.saveToStorage();
}
}
// ─── Collect Data ────────────────────────────────────────────────
collectNADData() {
const readNAD = (prefix) => ({
id: document.getElementById(`nad-${prefix}-id`).value.trim(),
qual: document.getElementById(`nad-${prefix}-qual`).value.trim(),
name: document.getElementById(`nad-${prefix}-name`).value.trim(),
name2: document.getElementById(`nad-${prefix}-name2`).value.trim(),
street: document.getElementById(`nad-${prefix}-street`).value.trim(),
city: document.getElementById(`nad-${prefix}-city`).value.trim(),
zip: document.getElementById(`nad-${prefix}-zip`).value.trim(),
country: document.getElementById(`nad-${prefix}-country`).value.trim()
});
return { BY: readNAD('by'), SE: readNAD('se'), ST: readNAD('st'), SF: readNAD('sf') };
}
collectWeights() {
const linWeights = {};
document.querySelectorAll('.lin-weight-input').forEach(input => {
const val = parseFloat(input.value);
if (val) linWeights[input.dataset.suppmat] = val;
});
const pacWeights = {};
document.querySelectorAll('.pac-weight-input').forEach(input => {
const val = parseFloat(input.value);
if (val) pacWeights[input.dataset.pacmat] = val;
});
const pacDims = {};
document.querySelectorAll('.pac-ln-input').forEach(input => {
const key = input.dataset.pacmat;
const dims = pacDims[key] || { ln: 0, wd: 0, ht: 0 };
dims.ln = parseFloat(input.value) || dims.ln; pacDims[key] = dims;
});
document.querySelectorAll('.pac-wd-input').forEach(input => {
const key = input.dataset.pacmat;
const dims = pacDims[key] || { ln: 0, wd: 0, ht: 0 };
dims.wd = parseFloat(input.value) || dims.wd; pacDims[key] = dims;
});
document.querySelectorAll('.pac-ht-input').forEach(input => {
const key = input.dataset.pacmat;
const dims = pacDims[key] || { ln: 0, wd: 0, ht: 0 };
dims.ht = parseFloat(input.value) || dims.ht; pacDims[key] = dims;
});
return { linWeights, pacWeights, pacDims };
}
// ─── Generate & Download ─────────────────────────────────────────
generateOutput() {
if (this.mode === 'outbound-bosch') {
const VDA4913ToDESADV = window.EDIBridge.VDA4913ToDESADV;
if (!VDA4913ToDESADV) return;
const nadData = this.collectNADData();
const weights = this.collectWeights();
const edifact = VDA4913ToDESADV.convert(this.rawContent, nadData, weights, this.config);
this.previewArea.innerText = edifact;
this.resultSection.style.display = 'block';
document.getElementById('resultTitle').innerText = 'Bosch DESADV Ergebnis';
this.resultSection.scrollIntoView({ behavior: 'smooth' });
} else if (this.mode === 'outbound-zf') {
const VDA4913ToDESADVZF = window.EDIBridge.VDA4913ToDESADVZF;
if (!VDA4913ToDESADVZF) {
console.error("VDA4913ToDESADVZF is not loaded");
return;
}
const nadData = this.collectNADData();
const weights = this.collectWeights();
const edifact = VDA4913ToDESADVZF.convert(this.rawContent, nadData, weights, this.config);
this.previewArea.innerText = edifact;
this.resultSection.style.display = 'block';
document.getElementById('resultTitle').innerText = 'ZF DESADV Ergebnis';
this.resultSection.scrollIntoView({ behavior: 'smooth' });
} else if (this.mode === 'outbound-ifm') {
// Already processed in processOutboundIfm
this.processOutboundIfm(this.rawContent);
}
}
downloadResult() {
const text = this.previewArea.innerText;
if (!text) return;
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
let base = this.currentFilename || 'output';
const lastDot = base.lastIndexOf('.');
if (lastDot !== -1) base = base.substring(0, lastDot);
const extMap = {
'outbound-bosch': '.edi',
'outbound-zf': '.edi',
'outbound-ifm': '.delvry',
'inbound-bosch': '.vda',
'inbound-ifm': '.vda',
'inbound-invrpt': '.vda',
};
a.download = base + (extMap[this.mode] || '.edi');
a.click();
URL.revokeObjectURL(url);
}
viewResult() {
const text = this.previewArea.innerText;
if (!text) return;
this.showPage('viewer');
if (this.viewer) {
this.viewer.processContent(text);
}
}
editResult() {
const text = this.previewArea.innerText;
if (!text) return;
this.showPage('editor');
if (this.editor) {
this.editor.fileName = this.currentFilename || 'output.edi';
this.editor.processContent(text);
}
}
/**
* Log a conversion to the SQLite history database.
* @param {Object} opts
*/
async _logConversion(opts) {
if (!window.electronAPI || !window.electronAPI.dbInsert) return;
try {
await window.electronAPI.dbInsert({
dateiname: opts.dateiname || this.currentFilename || 'unbekannt',
ausgangsdateiname: opts.ausgangsdateiname || null,
quellformat: opts.quellformat || null,
zielformat: opts.zielformat || null,
konvertierungsmodus: opts.konvertierungsmodus || this.mode,
kunden_id: opts.kunden_id || null,
eingang_daten: opts.eingang_daten || null,
ausgang_daten: opts.ausgang_daten || null,
status: opts.status || 'ERFOLGREICH',
fehlermeldung: opts.fehlermeldung || null,
quelle: opts.quelle || 'MANUELL'
});
} catch (e) {
console.warn('[DB] Log error:', e);
}
}
// ─── Settings: Folder Selection ──────────────────────────────────
async selectInputFolder() {
if (!this.watcher) return;
const folder = await this.watcher.selectFolder();
if (folder) {
document.getElementById('inputDirPath').value = folder;
this.watcher.saveSettings({
inputDir: folder,
outputDir: document.getElementById('outputDirPath').value
});
}
}
async selectOutputFolder() {
if (!this.watcher) return;
const folder = await this.watcher.selectFolder();
if (folder) {
document.getElementById('outputDirPath').value = folder;
this.watcher.saveSettings({
inputDir: document.getElementById('inputDirPath').value,
outputDir: folder
});
}
}
async selectInvrptOutputFolder() {
if (!this.watcher) return;
const folder = await this.watcher.selectFolder();
if (folder) {
document.getElementById('invrptOutputPath').value = folder;
this.watcher.saveSettings({ invrptOutputDir: folder });
}
}
// ─── Settings: Watcher Controls ──────────────────────────────────
async startWatcher() {
if (!this.watcher) return;
const inputDir = document.getElementById('inputDirPath').value;
const outputDir = document.getElementById('outputDirPath').value;
const invrptOutputDir = document.getElementById('invrptOutputPath').value;
if (!inputDir || !outputDir) {
alert('Bitte beide Ordner (Input & Output) angeben.');
return;
}
const result = await this.watcher.start(inputDir, outputDir, invrptOutputDir);
if (result.error) {
alert('Watcher-Fehler: ' + result.error);
}
}
async stopWatcher() {
if (this.watcher) await this.watcher.stop();
}
async pauseWatcher() {
if (this.watcher) await this.watcher.pause();
}
async resumeWatcher() {
if (this.watcher) await this.watcher.resume();
}
// ─── Settings: Outbound Watcher Controls ──────────────────────────
async selectOutboundInput() {
if (!this.outboundWatcher) return;
const folder = await window.electronAPI.selectFolder();
if (folder) {
document.getElementById('outboundInputPath').value = folder;
window.electronAPI.saveSettings({
outboundInputDir: folder
});
}
}
async selectOutboundOutput() {
if (!this.outboundWatcher) return;
const folder = await window.electronAPI.selectFolder();
if (folder) {
document.getElementById('outboundOutputPath').value = folder;
window.electronAPI.saveSettings({
outboundOutputDir: folder
});
}
}
async startOutboundWatcher() {
if (!this.outboundWatcher) return;
const inPath = document.getElementById('outboundInputPath').value;
const outPath = document.getElementById('outboundOutputPath').value;
if (!inPath || !outPath) {
alert('Bitte beide Pfade für ausgehende Dateien angeben.');
return;
}
await this.outboundWatcher.start(inPath, outPath);
}
async stopOutboundWatcher() {
if (this.outboundWatcher) await this.outboundWatcher.stop();
}
async pauseOutboundWatcher() {
if (this.outboundWatcher) await this.outboundWatcher.pause();
}
async resumeOutboundWatcher() {
if (this.outboundWatcher) await this.outboundWatcher.resume();
}
updateOutboundWatcherUI(status) {
const dot = document.getElementById('outboundWatcherDot');
const text = document.getElementById('outboundWatcherStatusText');
const btnStart = document.getElementById('btnStartOutboundWatcher');
const btnPause = document.getElementById('btnPauseOutboundWatcher');
const btnResume = document.getElementById('btnResumeOutboundWatcher');
const btnStop = document.getElementById('btnStopOutboundWatcher');
dot.className = 'watcher-status-dot';
switch (status) {
case 'running':
dot.classList.add('running');
text.textContent = 'Aktiv Läuft';
btnStart.disabled = true;
btnPause.disabled = false;
btnPause.style.display = '';
btnResume.style.display = 'none';
btnStop.disabled = false;
break;
case 'paused':
dot.classList.add('paused');
text.textContent = 'Pausiert';
btnStart.disabled = true;
btnPause.style.display = 'none';
btnResume.style.display = '';
btnStop.disabled = false;
break;
case 'stopped':
default:
dot.classList.add('stopped');
text.textContent = 'Gestoppt';
btnStart.disabled = false;
btnPause.disabled = true;
btnPause.style.display = '';
btnResume.style.display = 'none';
btnStop.disabled = true;
break;
}
if (window.lucide) try { lucide.createIcons(); } catch (e) { }
}
// ─── Settings: Werksnummer Watcher Controls ──────────────────────
async selectWerksInput() {
if (!this.werksWatcher) return;
const folder = await window.electronAPI.selectFolder();
if (folder) {
document.getElementById('werksInputPath').value = folder;
window.electronAPI.saveSettings({
werksInputDir: folder
});
}
}
async selectWerksOutput() {
if (!this.werksWatcher) return;
const folder = await window.electronAPI.selectFolder();
if (folder) {
document.getElementById('werksOutputPath').value = folder;
window.electronAPI.saveSettings({
werksOutputDir: folder
});
}
}
async startWerksWatcher() {
if (!this.werksWatcher) return;
const inPath = document.getElementById('werksInputPath').value;
const outPath = document.getElementById('werksOutputPath').value;
if (!inPath || !outPath) {
alert('Bitte beide Werks-Pfade angeben.');
return;
}
await this.werksWatcher.start(inPath, outPath);
}
async stopWerksWatcher() {
if (this.werksWatcher) await this.werksWatcher.stop();
}
async pauseWerksWatcher() {
if (this.werksWatcher) await this.werksWatcher.pause();
}
async resumeWerksWatcher() {
if (this.werksWatcher) await this.werksWatcher.resume();
}
updateWerksWatcherUI(status) {
const dot = document.getElementById('werksWatcherDot');
const text = document.getElementById('werksWatcherStatusText');
const btnStart = document.getElementById('btnStartWerksWatcher');
const btnPause = document.getElementById('btnPauseWerksWatcher');
const btnResume = document.getElementById('btnResumeWerksWatcher');
const btnStop = document.getElementById('btnStopWerksWatcher');
dot.className = 'watcher-status-dot';
switch (status) {
case 'running':
dot.classList.add('running');
text.textContent = 'Aktiv Läuft';
btnStart.disabled = true;
btnPause.disabled = false;
btnPause.style.display = '';
btnResume.style.display = 'none';
btnStop.disabled = false;
break;
case 'paused':
dot.classList.add('paused');
text.textContent = 'Pausiert';
btnStart.disabled = true;
btnPause.style.display = 'none';
btnResume.style.display = '';
btnStop.disabled = false;
break;
case 'stopped':
default:
dot.classList.add('stopped');
text.textContent = 'Gestoppt';
btnStart.disabled = false;
btnPause.disabled = true;
btnPause.style.display = '';
btnResume.style.display = 'none';
btnStop.disabled = true;
break;
}
if (window.lucide) try { lucide.createIcons(); } catch (e) { }
}
updateWatcherUI(status) {
const dot = document.getElementById('watcherDot');
const text = document.getElementById('watcherStatusText');
const btnStart = document.getElementById('btnStartWatcher');
const btnPause = document.getElementById('btnPauseWatcher');
const btnResume = document.getElementById('btnResumeWatcher');
const btnStop = document.getElementById('btnStopWatcher');
// Reset
dot.className = 'watcher-status-dot';
switch (status) {
case 'running':
dot.classList.add('running');
text.textContent = 'Aktiv Überwachung läuft';
btnStart.disabled = true;
btnPause.disabled = false;
btnPause.style.display = '';
btnResume.style.display = 'none';
btnStop.disabled = false;
break;
case 'paused':
dot.classList.add('paused');
text.textContent = 'Pausiert';
btnStart.disabled = true;
btnPause.style.display = 'none';
btnResume.style.display = '';
btnStop.disabled = false;
break;
case 'stopped':
default:
dot.classList.add('stopped');
text.textContent = 'Gestoppt';
btnStart.disabled = false;
btnPause.disabled = true;
btnPause.style.display = '';
btnResume.style.display = 'none';
btnStop.disabled = true;
break;
}
if (window.lucide) try { lucide.createIcons(); } catch (e) { }
}
// ─── Log ─────────────────────────────────────────────────────────
appendLogEntry(entry) {
const container = document.getElementById('logContainer');
if (!container) return;
// Remove empty message if present
const empty = container.querySelector('.log-empty');
if (empty) empty.remove();
const el = document.createElement('div');
el.className = 'log-entry log-' + entry.level;
el.innerHTML = `
<span class="log-time">${entry.time}</span>
<span class="log-level">${entry.level.toUpperCase()}</span>
${entry.fileName ? `<span class="log-file">${entry.fileName}</span>` : ''}
<span class="log-msg">${entry.message}</span>
`;
container.prepend(el);
}
clearLog() {
const container = document.getElementById('logContainer');
if (container) {
container.innerHTML = '<div class="log-empty">Noch keine Aktivitäten.</div>';
}
if (this.watcher) this.watcher.log = [];
}
}
window.EDIBridge.App = App;