Files
vda-to-edifact-converter/main.js
2026-03-13 09:53:40 +01:00

719 lines
24 KiB
JavaScript

/**
* 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: '', 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: '', mode: 'auto',
outboundInputDir: '', outboundOutputDir: '', mode: 'auto',
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;
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
});
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 };
}
});