/** * Electron Main Process * - Window management * - Folder watcher via chokidar * - IPC handlers for renderer communication * - Archive functionality: moves processed files to inputDir/archiv */ const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron'); const path = require('path'); const fs = require('fs'); const chokidar = require('chokidar'); const ConversionDB = require('./js/db'); const edifactValidator = require('./js/edifact-validator'); let mainWindow = null; let watcher = null; let watcherPaused = false; let watcherConfig = { inputDir: '', outputDir: '', invrptOutputDir: '', mode: 'auto' }; let outboundWatcher = null; let outboundWatcherPaused = false; let outboundWatcherConfig = { inputDir: '', outputDir: '', mode: 'auto' }; let werksWatcher = null; let werksWatcherPaused = false; let werksWatcherConfig = { inputDir: '', outputDir: '' }; // ─── Database & Storage ───────────────────────────────────────────── const dataDir = path.join(__dirname, 'db'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } let conversionDB = null; // ─── Window ────────────────────────────────────────────────────────── function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 900, minHeight: 600, title: 'ERP EDI Bridge', icon: path.join(__dirname, 'src', 'assets', 'icon.png'), webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, } }); mainWindow.loadFile('index.html'); // Create Application Menu to handle shortcuts (including Zoom Fix) const template = [ { label: 'Datei', submenu: [ { label: 'Beenden', role: 'quit' } ] }, { label: 'Ansicht', submenu: [ { label: 'Neu laden', role: 'reload' }, { label: 'Entwicklertools', role: 'toggleDevTools' }, { type: 'separator' }, { label: 'Vergrößern', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, { label: 'Vergrößern (Alternativ)', accelerator: 'CmdOrCtrl+=', role: 'zoomIn', visible: false }, { label: 'Verkleinern', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, { label: 'Zoom zurücksetzen', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }, { type: 'separator' }, { label: 'Vollbild', role: 'togglefullscreen' } ] } ]; const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); } app.whenReady().then(async () => { // Initialize conversion history database using async sqlite3 (local directory) const dbPath = path.join(dataDir, 'conversions.sqlite3'); conversionDB = new ConversionDB(dbPath); try { await conversionDB.init(); console.log(`[DB] Initialized at: ${dbPath}`); } catch (e) { console.error('[DB] Initialization error:', e); } createWindow(); }); app.on('window-all-closed', () => { if (conversionDB) conversionDB.close(); app.quit(); }); // ─── IPC: Folder dialog ───────────────────────────────────────────── ipcMain.handle('select-folder', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] }); if (result.canceled || result.filePaths.length === 0) return null; return result.filePaths[0]; }); // ─── IPC: Settings persistence ────────────────────────────────────── const settingsPath = path.join(dataDir, 'watcher-settings.json'); function loadSettings() { try { if (fs.existsSync(settingsPath)) { return JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } } catch (e) { console.error('Settings load error:', e); } return { inputDir: '', outputDir: '', invrptOutputDir: '', mode: 'auto', outboundInputDir: '', outboundOutputDir: '', werksInputDir: '', werksOutputDir: '' }; } function saveSettings(settings) { try { fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); } catch (e) { console.error('Settings save error:', e); } } ipcMain.handle('load-settings', () => loadSettings()); ipcMain.handle('save-settings', (_, settings) => { // Preserve existing settings and merge new ones const current = loadSettings(); const updated = { ...current, ...settings }; // Update active configs if they are loaded watcherConfig = { ...watcherConfig, ...updated }; outboundWatcherConfig = { inputDir: updated.outboundInputDir || outboundWatcherConfig.inputDir, outputDir: updated.outboundOutputDir || outboundWatcherConfig.outputDir }; werksWatcherConfig = { inputDir: updated.werksInputDir || werksWatcherConfig.inputDir, outputDir: updated.werksOutputDir || werksWatcherConfig.werksOutputDir }; saveSettings(updated); return true; }); // ─── IPC: Read file content ───────────────────────────────────────── ipcMain.handle('read-file', async (_, filePath) => { try { return fs.readFileSync(filePath, 'utf8'); } catch (e) { return { error: e.message }; } }); // ─── IPC: Write file content ──────────────────────────────────────── ipcMain.handle('write-file', async (_, filePath, content) => { try { // Ensure output directory exists const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content, 'utf8'); return { success: true }; } catch (e) { return { error: e.message }; } }); // ─── IPC: Move file to archive ────────────────────────────────────── ipcMain.handle('move-to-archive', async (_, filePath) => { try { const dir = path.dirname(filePath); const archiveDir = path.join(dir, 'archiv'); const fileName = path.basename(filePath); // Create archiv subfolder if needed if (!fs.existsSync(archiveDir)) { fs.mkdirSync(archiveDir, { recursive: true }); } // If a file with the same name exists in archive, add timestamp let destPath = path.join(archiveDir, fileName); if (fs.existsSync(destPath)) { const baseName = fileName.replace(/(\.[^.]+)$/, ''); const ext = path.extname(fileName); const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); destPath = path.join(archiveDir, `${baseName}_${ts}${ext}`); } fs.renameSync(filePath, destPath); return { success: true, archivePath: destPath }; } catch (e) { return { error: e.message }; } }); // ─── Folder Watcher ───────────────────────────────────────────────── function sendToRenderer(channel, data) { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(channel, data); } } ipcMain.handle('start-watcher', (_, config) => { if (watcher) { watcher.close(); watcher = null; } watcherConfig = { ...watcherConfig, ...config }; saveSettings(watcherConfig); const inputDir = watcherConfig.inputDir; const outputDir = watcherConfig.outputDir; const invrptOutputDir = watcherConfig.invrptOutputDir || ''; if (!inputDir || !outputDir) { return { error: 'Input- und Output-Ordner müssen angegeben werden.' }; } if (!fs.existsSync(inputDir)) { return { error: `Input-Ordner existiert nicht: ${inputDir}` }; } if (!fs.existsSync(outputDir)) { try { fs.mkdirSync(outputDir, { recursive: true }); } catch (e) { return { error: `Output-Ordner konnte nicht erstellt werden: ${e.message}` }; } } watcherPaused = false; // Watch only the input directory itself, not subdirectories (depth: 0) // This avoids triggering on files moved to the archiv subfolder watcher = chokidar.watch(inputDir, { ignored: [ /(^|[\/\\])\../, // dotfiles path.join(inputDir, 'archiv', '**'), // ignore archiv subfolder path.join(inputDir, 'archiv'), ], persistent: true, ignoreInitial: true, depth: 0, usePolling: true, interval: 1000, // slightly more frequent polling binaryInterval: 1000, awaitWriteFinish: { stabilityThreshold: 1000, // wait 1s after last write before firing pollInterval: 100 } }); // Diagnostic logging for all events watcher.on('all', (event, path) => { console.log(`[Watcher Debug] Event: ${event} on ${path}`); }); watcher.on('add', (filePath) => { if (watcherPaused) { console.log(`[Watcher] Paused. Ignoring: ${filePath}`); return; } const ext = path.extname(filePath).toLowerCase(); const fileName = path.basename(filePath); // Skip files in archiv directory (double safety) if (filePath.includes(path.sep + 'archiv' + path.sep)) { console.log(`[Watcher] Ignoring file in archive: ${fileName}`); return; } // Expanded EDI file extensions, case-insensitive check const allowedExts = ['.txt', '.vda', '.edi', '.dat', '.seq', '.tmp', '.idoc', '']; if (!allowedExts.includes(ext)) { console.log(`[Watcher] Ignoring file with extension "${ext}": ${fileName}`); return; } console.log(`[Watcher] File detected and accepted: ${filePath}`); // Small delay to ensure file is fully written and closed setTimeout(() => { try { const content = fs.readFileSync(filePath, 'utf8'); sendToRenderer('watcher-file-detected', { filePath, fileName, content, outputDir, invrptOutputDir }); console.log(`[Watcher] Sent to renderer: ${fileName} (${content.length} bytes)`); } catch (e) { console.error(`[Watcher] Read error: ${e.message}`); sendToRenderer('watcher-error', { filePath: fileName, error: `Datei konnte nicht gelesen werden: ${e.message}` }); } }, 500); }); watcher.on('error', (err) => { console.error(`[Watcher] Error: ${err.message}`); sendToRenderer('watcher-error', { error: `Watcher-Fehler: ${err.message}` }); }); watcher.on('ready', () => { console.log(`[Watcher] Ready. Watching: ${inputDir}`); sendToRenderer('watcher-ready', { inputDir }); }); return { success: true, status: 'running' }; }); ipcMain.handle('stop-watcher', () => { if (watcher) { watcher.close(); watcher = null; console.log('[Watcher] Stopped.'); } watcherPaused = false; return { success: true, status: 'stopped' }; }); ipcMain.handle('pause-watcher', () => { watcherPaused = true; console.log('[Watcher] Paused.'); return { success: true, status: 'paused' }; }); ipcMain.handle('resume-watcher', () => { if (!watcher) { return { error: 'Kein Watcher aktiv. Bitte zuerst starten.' }; } watcherPaused = false; console.log('[Watcher] Resumed.'); return { success: true, status: 'running' }; }); ipcMain.handle('get-watcher-status', () => { if (!watcher) return { status: 'stopped' }; if (watcherPaused) return { status: 'paused' }; return { status: 'running' }; }); // ─── Outbound Folder Watcher ──────────────────────────────────────── ipcMain.handle('start-outbound-watcher', (_, config) => { if (outboundWatcher) { outboundWatcher.close(); outboundWatcher = null; } outboundWatcherConfig = { ...outboundWatcherConfig, ...config }; // Save to global settings const current = loadSettings(); saveSettings({ ...current, outboundInputDir: outboundWatcherConfig.inputDir, outboundOutputDir: outboundWatcherConfig.outputDir }); const inputDir = outboundWatcherConfig.inputDir; const outputDir = outboundWatcherConfig.outputDir; if (!inputDir || !outputDir) { return { error: 'Input- und Output-Ordner müssen angegeben werden.' }; } if (!fs.existsSync(inputDir)) { return { error: `Input-Ordner existiert nicht: ${inputDir}` }; } if (!fs.existsSync(outputDir)) { try { fs.mkdirSync(outputDir, { recursive: true }); } catch (e) { return { error: `Output-Ordner konnte nicht erstellt werden: ${e.message}` }; } } outboundWatcherPaused = false; outboundWatcher = chokidar.watch(inputDir, { ignored: [ /(^|[\/\\])\../, path.join(inputDir, 'archiv', '**'), path.join(inputDir, 'archiv'), ], persistent: true, ignoreInitial: true, depth: 0, usePolling: true, interval: 1000, binaryInterval: 1000, awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 100 } }); outboundWatcher.on('all', (event, path) => { console.log(`[OutboundWatcher Debug] Event: ${event} on ${path}`); }); outboundWatcher.on('add', (filePath) => { if (outboundWatcherPaused) { console.log(`[OutboundWatcher] Paused. Ignoring: ${filePath}`); return; } const ext = path.extname(filePath).toLowerCase(); const fileName = path.basename(filePath); if (filePath.includes(path.sep + 'archiv' + path.sep)) { console.log(`[OutboundWatcher] Ignoring file in archive: ${fileName}`); return; } const allowedExts = ['.txt', '.vda', '.edi', '.dat', '.seq', '.tmp', '.idoc', '']; if (!allowedExts.includes(ext)) { console.log(`[OutboundWatcher] Ignoring file with extension "${ext}": ${fileName}`); return; } console.log(`[OutboundWatcher] File detected and accepted: ${filePath}`); setTimeout(() => { try { const content = fs.readFileSync(filePath, 'utf8'); sendToRenderer('outbound-watcher-file-detected', { filePath, fileName, content, outputDir }); console.log(`[OutboundWatcher] Sent to renderer: ${fileName} (${content.length} bytes)`); } catch (e) { console.error(`[OutboundWatcher] Read error: ${e.message}`); sendToRenderer('outbound-watcher-error', { filePath: fileName, error: `Datei konnte nicht gelesen werden: ${e.message}` }); } }, 500); }); outboundWatcher.on('error', (err) => { console.error(`[OutboundWatcher] Error: ${err.message}`); sendToRenderer('outbound-watcher-error', { error: `Watcher-Fehler: ${err.message}` }); }); outboundWatcher.on('ready', () => { console.log(`[OutboundWatcher] Ready. Watching: ${inputDir}`); sendToRenderer('outbound-watcher-ready', { inputDir }); }); return { success: true, status: 'running' }; }); ipcMain.handle('stop-outbound-watcher', () => { if (outboundWatcher) { outboundWatcher.close(); outboundWatcher = null; console.log('[OutboundWatcher] Stopped.'); } outboundWatcherPaused = false; return { success: true, status: 'stopped' }; }); ipcMain.handle('pause-outbound-watcher', () => { outboundWatcherPaused = true; console.log('[OutboundWatcher] Paused.'); return { success: true, status: 'paused' }; }); ipcMain.handle('resume-outbound-watcher', () => { if (!outboundWatcher) { return { error: 'Kein Watcher aktiv. Bitte zuerst starten.' }; } outboundWatcherPaused = false; console.log('[OutboundWatcher] Resumed.'); return { success: true, status: 'running' }; }); ipcMain.handle('get-outbound-watcher-status', () => { if (!outboundWatcher) return { status: 'stopped' }; if (outboundWatcherPaused) return { status: 'paused' }; return { status: 'running' }; }); // ─── Werksnummer Watcher ─────────────────────────────────────────── ipcMain.handle('start-werks-watcher', (_, config) => { if (werksWatcher) { werksWatcher.close(); werksWatcher = null; } werksWatcherConfig = { ...werksWatcherConfig, ...config }; // Save to global settings const current = loadSettings(); saveSettings({ ...current, werksInputDir: werksWatcherConfig.inputDir, werksOutputDir: werksWatcherConfig.outputDir }); const { inputDir, outputDir } = werksWatcherConfig; if (!inputDir || !outputDir) { return { error: 'Input- und Output-Ordner müssen angegeben werden.' }; } if (!fs.existsSync(inputDir)) { return { error: `Input-Ordner existiert nicht: ${inputDir}` }; } if (!fs.existsSync(outputDir)) { try { fs.mkdirSync(outputDir, { recursive: true }); } catch (e) { return { error: `Output-Ordner konnte nicht erstellt werden: ${e.message}` }; } } werksWatcherPaused = false; werksWatcher = chokidar.watch(inputDir, { ignored: [/(^|[\/\\])\../, path.join(inputDir, 'archiv', '**')], persistent: true, ignoreInitial: true, depth: 0, usePolling: true, interval: 1000, awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 100 } }); werksWatcher.on('add', (filePath) => { if (werksWatcherPaused) return; const fileName = path.basename(filePath); if (!fileName.startsWith('RA_VDA')) return; console.log(`[WerksWatcher] Testing file: ${fileName}`); setTimeout(() => { try { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split(/\r?\n/); let plant = null; for (const line of lines) { if (line.startsWith('512') && line.length >= 8) { const extracted = line.substring(5, 8).trim(); if (extracted === '100' || extracted === '280') { plant = extracted; break; } } } if (!plant) { console.log(`[WerksWatcher] No relevant plant (100/280) found in ${fileName}`); return; } const prefix = plant === '100' ? 'RA_LEIFERS.' : 'RA_PITESTI.'; const newName = fileName.replace('RA_VDA', prefix); const outPath = path.join(outputDir, newName); fs.copyFileSync(filePath, outPath); console.log(`[WerksWatcher] Processed: ${fileName} -> ${newName}`); sendToRenderer('werks-watcher-success', { fileName, newName, plant: plant === '100' ? 'Leifers (100)' : 'Pitesti (280)' }); // Delete original file fs.unlinkSync(filePath); } catch (e) { console.error(`[WerksWatcher] Error processing ${fileName}:`, e); sendToRenderer('werks-watcher-error', { fileName, error: e.message }); } }, 500); }); werksWatcher.on('ready', () => { sendToRenderer('werks-watcher-ready', { inputDir }); }); return { success: true, status: 'running' }; }); ipcMain.handle('stop-werks-watcher', () => { if (werksWatcher) { werksWatcher.close(); werksWatcher = null; } werksWatcherPaused = false; return { success: true, status: 'stopped' }; }); ipcMain.handle('pause-werks-watcher', () => { werksWatcherPaused = true; return { success: true, status: 'paused' }; }); ipcMain.handle('resume-werks-watcher', () => { if (!werksWatcher) return { error: 'Nicht aktiv.' }; werksWatcherPaused = false; return { success: true, status: 'running' }; }); ipcMain.handle('get-werks-watcher-status', () => { if (!werksWatcher) return { status: 'stopped' }; if (werksWatcherPaused) return { status: 'paused' }; return { status: 'running' }; }); // ─── IPC: Conversion History DB ───────────────────────────────────── ipcMain.handle('db-insert', async (_, record) => { try { return await conversionDB.insert(record); } catch (e) { console.error('[DB] Insert error:', e); return { error: e.message }; } }); ipcMain.handle('db-get-all', async (_, params) => { try { const { limit, offset, statusFilter, search, dateFilter } = params || {}; console.log(`[DB] db-get-all called with params:`, { limit, offset, statusFilter, search, dateFilter }); const result = await conversionDB.getAll(limit || 50, offset || 0, statusFilter || 'ALL', search || '', dateFilter || 'ALL'); console.log(`[DB] db-get-all returned: ${result.rows.length} rows, Total: ${result.total}`); return result; } catch (e) { console.error('[DB] db-get-all error:', e); return { rows: [], total: 0, error: e.message }; } }); ipcMain.handle('db-get-by-id', async (_, id) => { try { return await conversionDB.getById(id); } catch (e) { console.error('[DB] GetById error:', e); return null; } }); ipcMain.handle('db-delete', async (_, id) => { try { return { success: await conversionDB.deleteById(id) }; } catch (e) { console.error('[DB] Delete error:', e); return { error: e.message }; } }); ipcMain.handle('db-get-stats', async () => { try { return await conversionDB.getStats(); } catch (e) { console.error('[DB] Stats error:', e); return { total: 0, success: 0, error: 0, today: 0 }; } }); // ─── IPC: Config DB ───────────────────────────────────────────────── ipcMain.handle('db-insert-config', async (_, configData) => { try { return await conversionDB.insertConfig(configData); } catch (e) { console.error('[DB] Insert config error:', e); return { error: e.message }; } }); ipcMain.handle('db-get-config', async () => { try { return await conversionDB.getConfig(); } catch (e) { console.error('[DB] Get config error:', e); return null; } }); // ─── IPC: EDIFACT Validation (ts-edifact library) ──────────────────── ipcMain.handle('validate-edifact', (_, content) => { try { return edifactValidator.validateEdifact(content); } catch (e) { return { valid: false, errors: [e.message], warnings: [], stats: {} }; } }); ipcMain.handle('parse-edifact-lib', (_, content) => { try { return edifactValidator.parseWithLib(content); } catch (e) { return { segments: [], segmentCount: 0, error: e.message }; } });