Files
kitchen-agent/routes/settings.js

460 lines
17 KiB
JavaScript
Raw Normal View History

2025-10-23 19:02:56 -04:00
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;