const config = require('../config'); const database = require('../database'); const apiClient = require('../api-client'); const printer = require('../printer'); const path = require('path'); const fs = require('fs'); const { pipeline } = require('stream/promises'); // Middleware to check authentication via signed cookie async function requireAuth(req, reply) { const raw = req.cookies && req.cookies.kitchen_session; if (!raw) { reply.redirect('/login'); return; } const { valid, value } = req.unsignCookie(raw || ''); if (!valid) { reply.redirect('/login'); return; } const token = config.get('authToken'); const expiry = config.get('tokenExpiry'); const apiClient = require('../api-client'); if (!token || apiClient.isTokenExpired(expiry) || value !== token) { reply.redirect('/login'); return; } } async function settingsRoutes(fastify, options) { // Settings page fastify.get('/settings', { preHandler: requireAuth }, async (req, reply) => { const appConfig = config.getAll(); // Fetch available bots let bots = []; if (appConfig.authToken) { const botsResult = await apiClient.getBots(appConfig.authToken); if (!botsResult.error && botsResult.bots) { bots = botsResult.bots; } } return reply.view('settings', { config: appConfig, bots: bots, message: req.query.message || null, error: req.query.error || null }); }); // Save settings fastify.post('/settings/save', { preHandler: requireAuth }, async (req, reply) => { try { const { selectedBotId, pollingInterval, dashboardRefreshInterval, showOrderStats, soundNotificationsEnabled, soundVolume, printerType, printerInterface, printerPath, printerWidth, fontSize, lineStyle, qrCodeEnabled, qrCodeSize, qrCodeCorrection, qrCodeContentTemplate, headerText, footerText, businessName, businessAddress, businessPhone, businessWebsite, businessEmail, businessContactSize, showCustomerInfo, showOrderItems, showPrices, showTimestamps, selectedPrintersJson } = req.body; // Validate and save configuration const configToSave = {}; if (selectedBotId) configToSave.selectedBotId = selectedBotId; if (pollingInterval) configToSave.pollingInterval = pollingInterval; if (dashboardRefreshInterval) configToSave.dashboardRefreshInterval = dashboardRefreshInterval; configToSave.showOrderStats = showOrderStats === 'on' ? 'true' : 'false'; // Sound notification settings configToSave.soundNotificationsEnabled = soundNotificationsEnabled === 'on' ? 'true' : 'false'; if (soundVolume !== undefined) configToSave.soundVolume = soundVolume; if (printerType) configToSave.printerType = printerType; if (printerInterface) configToSave.printerInterface = printerInterface; if (printerPath) configToSave.printerPath = printerPath; if (printerWidth) configToSave.printerWidth = printerWidth; if (fontSize) configToSave.fontSize = fontSize; if (lineStyle) configToSave.lineStyle = lineStyle; configToSave.qrCodeEnabled = qrCodeEnabled === 'on' ? 'true' : 'false'; if (qrCodeSize) configToSave.qrCodeSize = parseInt(qrCodeSize, 10); if (qrCodeCorrection) configToSave.qrCodeCorrection = qrCodeCorrection; if (qrCodeContentTemplate !== undefined) configToSave.qrCodeContentTemplate = qrCodeContentTemplate; if (headerText !== undefined) configToSave.headerText = headerText; if (footerText !== undefined) configToSave.footerText = footerText; if (businessName !== undefined) configToSave.businessName = businessName; if (businessAddress !== undefined) configToSave.businessAddress = businessAddress; if (businessPhone !== undefined) configToSave.businessPhone = businessPhone; if (businessWebsite !== undefined) configToSave.businessWebsite = businessWebsite; if (businessEmail !== undefined) configToSave.businessEmail = businessEmail; if (businessContactSize) configToSave.businessContactSize = businessContactSize; configToSave.showCustomerInfo = showCustomerInfo === 'on' ? 'true' : 'false'; configToSave.showOrderItems = showOrderItems === 'on' ? 'true' : 'false'; configToSave.showPrices = showPrices === 'on' ? 'true' : 'false'; configToSave.showTimestamps = showTimestamps === 'on' ? 'true' : 'false'; // Multi-printer selection (stored raw JSON string) // Smart sync: If multi-printer list is empty and main printer is configured, // auto-populate multi-printer with main printer to avoid confusion if (selectedPrintersJson !== undefined) { try { let parsed = JSON.parse(selectedPrintersJson || '[]'); if (!Array.isArray(parsed)) parsed = []; // If multi-printer list is empty but main printer is configured, // auto-add the main printer to the multi-printer list if (parsed.length === 0 && printerInterface && printerPath) { const mainType = printerInterface === 'serial' ? 'com' : printerInterface; parsed.push({ type: mainType, interface: printerPath }); console.log('[Settings] Auto-populated multi-printer list with main printer:', mainType, printerPath); } configToSave.selectedPrintersJson = JSON.stringify(parsed); } catch (e) { console.warn('[Settings] Failed to process selectedPrintersJson:', e.message); // ignore malformed input; keep previous config } } // Save to database config.setMultiple(configToSave); // Reinitialize printer with new config const updatedConfig = config.getAll(); printer.initializePrinter(updatedConfig); return reply.redirect('/settings?message=Settings saved successfully'); } catch (error) { console.error('Failed to save settings:', error.message); return reply.redirect('/settings?error=Failed to save settings'); } }); // Test printer fastify.post('/settings/test-printer', { preHandler: requireAuth }, async (req, reply) => { try { const appConfig = config.getAll(); // Allow ad-hoc test using current unsaved selections from UI if (req.body && typeof req.body.selectedPrintersJson !== 'undefined') { try { const parsed = JSON.parse(req.body.selectedPrintersJson || '[]'); if (Array.isArray(parsed)) { appConfig.selectedPrintersJson = JSON.stringify(parsed); } } catch (_) { // Ignore bad input for test; fall back to saved config } } // Always initialize to ensure latest selections are used printer.initializePrinter(appConfig); const result = await printer.testPrint(); if (result.success) { return { error: false, message: 'Test print successful' }; } else { return { error: true, message: result.error || 'Test print failed' }; } } catch (error) { console.error('Test print error:', error.message); return { error: true, message: error.message }; } }); // Detect available printers and COM ports (hardware detection) fastify.get('/api/printers/detect', { preHandler: requireAuth }, async (req, reply) => { try { const list = await printer.getAvailablePrinters(); return { error: false, printers: list }; } catch (error) { console.error('List printers error:', error.message); return { error: true, message: error.message, printers: [] }; } }); // Get all configured printers from database fastify.get('/api/printers/list', { preHandler: requireAuth }, async (req, reply) => { try { const printers = database.getAllPrinters(); return { error: false, printers }; } catch (error) { console.error('Get printers error:', error.message); return { error: true, message: error.message, printers: [] }; } }); // Get single printer configuration fastify.get('/api/printers/:id', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = parseInt(req.params.id, 10); const printerConfig = database.getPrinter(printerId); if (!printerConfig) { return { error: true, message: 'Printer not found' }; } return { error: false, printer: printerConfig }; } catch (error) { console.error('Get printer error:', error.message); return { error: true, message: error.message }; } }); // Create new printer configuration fastify.post('/api/printers/create', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = database.addPrinter(req.body); const newPrinter = database.getPrinter(printerId); return { error: false, message: 'Printer created successfully', printer: newPrinter }; } catch (error) { console.error('Create printer error:', error.message); return { error: true, message: error.message }; } }); // Update printer configuration fastify.put('/api/printers/:id', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = parseInt(req.params.id, 10); database.updatePrinter(printerId, req.body); const updatedPrinter = database.getPrinter(printerId); return { error: false, message: 'Printer updated successfully', printer: updatedPrinter }; } catch (error) { console.error('Update printer error:', error.message); return { error: true, message: error.message }; } }); // Delete printer configuration fastify.delete('/api/printers/:id', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = parseInt(req.params.id, 10); database.deletePrinter(printerId); return { error: false, message: 'Printer deleted successfully' }; } catch (error) { console.error('Delete printer error:', error.message); return { error: true, message: error.message }; } }); // Set printer as default fastify.post('/api/printers/:id/set-default', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = parseInt(req.params.id, 10); database.setDefaultPrinter(printerId); return { error: false, message: 'Default printer updated' }; } catch (error) { console.error('Set default printer error:', error.message); return { error: true, message: error.message }; } }); // Toggle printer enabled/disabled fastify.post('/api/printers/:id/toggle-enabled', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = parseInt(req.params.id, 10); const result = database.togglePrinterEnabled(printerId); return { error: false, message: 'Printer status updated', is_enabled: result.is_enabled }; } catch (error) { console.error('Toggle printer error:', error.message); return { error: true, message: error.message }; } }); // Test specific printer fastify.post('/api/printers/:id/test', { preHandler: requireAuth }, async (req, reply) => { try { const printerId = parseInt(req.params.id, 10); const printerConfig = database.getPrinter(printerId); if (!printerConfig) { return { error: true, message: 'Printer not found' }; } const result = await printer.testPrintWithConfig(printerConfig); if (result.success) { return { error: false, message: result.message || 'Test print successful' }; } else { return { error: true, message: result.error || 'Test print failed' }; } } catch (error) { console.error('Test printer error:', error.message); return { error: true, message: error.message }; } }); // Upload logo - requires @fastify/multipart (now supports per-printer logos) fastify.post('/settings/upload-logo', { preHandler: requireAuth }, async (req, reply) => { try { // Get multipart data const data = await req.file(); if (!data) { return { error: true, message: 'No file uploaded' }; } // Ensure uploads directory exists const uploadsDir = path.join(__dirname, '..', 'public', 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Save file const filename = `logo-${Date.now()}${path.extname(data.filename)}`; const filepath = path.join(uploadsDir, filename); // Write file using stream await pipeline(data.file, fs.createWriteStream(filepath)); // If printer_id is provided in fields, update that printer's logo // Otherwise update global config (backward compatibility) const fields = data.fields; const printerId = fields && fields.printer_id ? parseInt(fields.printer_id.value, 10) : null; if (printerId) { // Update specific printer's logo const printerConfig = database.getPrinter(printerId); if (printerConfig) { database.updatePrinter(printerId, { ...printerConfig, logo_path: filepath }); } } else { // Update global config (backward compatibility) config.set('logoPath', filepath); try { const updated = config.getAll(); printer.initializePrinter(updated); } catch (e) { // proceed even if reinit fails } } return { error: false, message: 'Logo uploaded successfully', path: `/public/uploads/${filename}`, filepath }; } catch (error) { console.error('Logo upload error:', error.message); return { error: true, message: error.message }; } }); // Get available bots fastify.get('/api/bots', { preHandler: requireAuth }, async (req, reply) => { const appConfig = config.getAll(); if (!appConfig.authToken) { return { error: true, message: 'Not authenticated' }; } const result = await apiClient.getBots(appConfig.authToken); if (result.error) { return { error: true, message: result.message }; } return { error: false, bots: result.bots || [] }; }); // Get notification settings (for dashboard) fastify.get('/api/notification-settings', async (req, reply) => { try { const appConfig = config.getAll(); return { error: false, soundNotificationsEnabled: appConfig.soundNotificationsEnabled || 'true', soundVolume: appConfig.soundVolume || '80', newOrderSoundPath: appConfig.newOrderSoundPath || '/public/sounds/new-order-notification.mp3', canceledOrderSoundPath: appConfig.canceledOrderSoundPath || '/public/sounds/canceled-order-notification.mp3' }; } catch (error) { console.error('Failed to get notification settings:', error.message); return { error: true, message: error.message }; } }); // Upload sound file for notifications fastify.post('/settings/upload-sound', { preHandler: requireAuth }, async (req, reply) => { try { // Get multipart data const data = await req.file(); if (!data) { return { error: true, message: 'No file uploaded' }; } // Ensure sounds directory exists const soundsDir = path.join(__dirname, '..', 'public', 'sounds'); if (!fs.existsSync(soundsDir)) { fs.mkdirSync(soundsDir, { recursive: true }); } // Validate file type const validExtensions = ['.mp3', '.wav', '.ogg']; const fileExt = path.extname(data.filename).toLowerCase(); if (!validExtensions.includes(fileExt)) { return { error: true, message: 'Invalid file type. Please upload MP3, WAV, or OGG file.' }; } // Save file const filename = `notification-${Date.now()}${fileExt}`; const filepath = path.join(soundsDir, filename); // Write file using stream await pipeline(data.file, fs.createWriteStream(filepath)); // Get the sound type from fields (newOrder or canceled) const fields = data.fields; const soundType = fields && fields.soundType ? fields.soundType.value : null; const publicPath = `/public/sounds/${filename}`; // Update config based on sound type if (soundType === 'newOrder') { config.set('newOrderSoundPath', publicPath); } else if (soundType === 'canceled') { config.set('canceledOrderSoundPath', publicPath); } return { error: false, message: 'Sound uploaded successfully', path: publicPath, soundType: soundType }; } catch (error) { console.error('Sound upload error:', error.message); return { error: true, message: error.message }; } }); } module.exports = settingsRoutes;