Initial commit
This commit is contained in:
117
routes/auth.js
Normal file
117
routes/auth.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const config = require('../config');
|
||||
const apiClient = require('../api-client');
|
||||
|
||||
async function authRoutes(fastify, options) {
|
||||
|
||||
// Root route - redirect based on signed session cookie validity
|
||||
fastify.get('/', async (req, reply) => {
|
||||
try {
|
||||
const raw = req.cookies && req.cookies.kitchen_session;
|
||||
if (raw) {
|
||||
const { valid, value } = req.unsignCookie(raw || '');
|
||||
if (valid) {
|
||||
const token = config.get('authToken');
|
||||
const expiry = config.get('tokenExpiry');
|
||||
if (token && !apiClient.isTokenExpired(expiry) && value === token) {
|
||||
return reply.redirect('/dashboard');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return reply.redirect('/login');
|
||||
});
|
||||
|
||||
// Login page
|
||||
fastify.get('/login', async (req, reply) => {
|
||||
try {
|
||||
const raw = req.cookies && req.cookies.kitchen_session;
|
||||
if (raw) {
|
||||
const { valid, value } = req.unsignCookie(raw || '');
|
||||
if (valid) {
|
||||
const token = config.get('authToken');
|
||||
const expiry = config.get('tokenExpiry');
|
||||
if (token && !apiClient.isTokenExpired(expiry) && value === token) {
|
||||
return reply.redirect('/dashboard');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return reply.view('login', {
|
||||
error: req.query.error || null,
|
||||
recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY || ''
|
||||
});
|
||||
});
|
||||
|
||||
// Login handler
|
||||
fastify.post('/auth/login', async (req, reply) => {
|
||||
const { login, password, 'g-recaptcha-response': recaptchaToken } = req.body;
|
||||
|
||||
// Validate inputs
|
||||
if (!login || !password) {
|
||||
return reply.view('login', {
|
||||
error: 'Please provide email and password',
|
||||
recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY || ''
|
||||
});
|
||||
}
|
||||
|
||||
if (!recaptchaToken) {
|
||||
return reply.view('login', {
|
||||
error: 'reCAPTCHA verification required',
|
||||
recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY || ''
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Call API to authenticate
|
||||
const result = await apiClient.login(login, password, recaptchaToken);
|
||||
|
||||
if (result.error) {
|
||||
return reply.view('login', {
|
||||
error: result.message || 'Login failed',
|
||||
recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY || ''
|
||||
});
|
||||
}
|
||||
|
||||
// Store auth token and expiration
|
||||
config.set('authToken', result.token);
|
||||
config.set('tokenExpiry', result.expirationDate);
|
||||
config.set('userEmail', login);
|
||||
|
||||
// Set signed cookie for session; secure only on HTTPS
|
||||
const isHttps = (req.protocol === 'https') || ((req.headers['x-forwarded-proto'] || '').toString().toLowerCase() === 'https');
|
||||
reply.setCookie('kitchen_session', result.token, {
|
||||
signed: true,
|
||||
httpOnly: true,
|
||||
secure: isHttps,
|
||||
sameSite: 'strict',
|
||||
maxAge: 30 * 24 * 60 * 60,
|
||||
path: '/'
|
||||
});
|
||||
|
||||
return reply.redirect('/dashboard');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error.message);
|
||||
return reply.view('login', {
|
||||
error: 'An error occurred during login. Please try again.',
|
||||
recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY || ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Logout handler
|
||||
fastify.post('/auth/logout', async (req, reply) => {
|
||||
// Allow multiple sessions: only clear the browser cookie
|
||||
reply.clearCookie('kitchen_session');
|
||||
return reply.redirect('/login');
|
||||
});
|
||||
|
||||
fastify.get('/auth/logout', async (req, reply) => {
|
||||
// Allow multiple sessions: only clear the browser cookie
|
||||
reply.clearCookie('kitchen_session');
|
||||
return reply.redirect('/login');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = authRoutes;
|
||||
|
||||
35
routes/dashboard.js
Normal file
35
routes/dashboard.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const config = require('../config');
|
||||
const database = require('../database');
|
||||
|
||||
// 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 dashboardRoutes(fastify, options) {
|
||||
|
||||
// Dashboard page
|
||||
fastify.get('/dashboard', { preHandler: requireAuth }, async (req, reply) => {
|
||||
const appConfig = config.getAll();
|
||||
const stats = database.getOrderStats();
|
||||
|
||||
return reply.view('dashboard', {
|
||||
config: appConfig,
|
||||
stats: stats,
|
||||
showStats: appConfig.showOrderStats !== 'false'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = dashboardRoutes;
|
||||
|
||||
343
routes/orders.js
Normal file
343
routes/orders.js
Normal file
@@ -0,0 +1,343 @@
|
||||
const config = require('../config');
|
||||
const database = require('../database');
|
||||
const apiClient = require('../api-client');
|
||||
const printer = require('../printer');
|
||||
|
||||
// Middleware to check authentication via signed cookie (JSON response)
|
||||
async function requireAuth(req, reply) {
|
||||
const raw = req.cookies && req.cookies.kitchen_session;
|
||||
if (!raw) { return reply.code(401).send({ error: true, message: 'Not authenticated' }); }
|
||||
const { valid, value } = req.unsignCookie(raw || '');
|
||||
if (!valid) { return reply.code(401).send({ error: true, message: 'Not authenticated' }); }
|
||||
const token = config.get('authToken');
|
||||
const expiry = config.get('tokenExpiry');
|
||||
const apiClient = require('../api-client');
|
||||
if (!token || apiClient.isTokenExpired(expiry) || value !== token) {
|
||||
return reply.code(401).send({ error: true, message: 'Not authenticated' });
|
||||
}
|
||||
}
|
||||
|
||||
async function ordersRoutes(fastify, options) {
|
||||
|
||||
// Get orders with filters
|
||||
fastify.get('/api/orders', { preHandler: requireAuth }, async (req, reply) => {
|
||||
const filters = {
|
||||
status: req.query.status,
|
||||
limit: parseInt(req.query.limit, 10) || 50
|
||||
};
|
||||
|
||||
// Get today's date for stats
|
||||
const today = new Date();
|
||||
filters.date = today;
|
||||
|
||||
const orders = database.getOrders(filters);
|
||||
const stats = database.getOrderStats();
|
||||
|
||||
return { error: false, orders, stats };
|
||||
});
|
||||
|
||||
// Update order status
|
||||
fastify.post('/api/orders/:id/status', { preHandler: requireAuth }, async (req, reply) => {
|
||||
const orderId = parseInt(req.params.id, 10);
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status) {
|
||||
return { error: true, message: 'Status is required' };
|
||||
}
|
||||
|
||||
// Valid local statuses
|
||||
const validStatuses = ['new', 'preparing', 'ready', 'completed'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return { error: true, message: 'Invalid status' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Update local database
|
||||
database.updateOrderStatus(orderId, status);
|
||||
|
||||
// Sync to backend if status is completed (maps to finished)
|
||||
if (status === 'completed') {
|
||||
const appConfig = config.getAll();
|
||||
const order = database.getOrderById(orderId);
|
||||
|
||||
if (order && appConfig.authToken && appConfig.selectedBotId) {
|
||||
// Determine the action based on order type
|
||||
let action = 'finished';
|
||||
if (order.order.type === 'delivery') {
|
||||
action = 'delivered';
|
||||
} else if (order.order.type === 'pickup') {
|
||||
action = 'picked_up';
|
||||
}
|
||||
|
||||
const result = await apiClient.modifyOrder(
|
||||
appConfig.authToken,
|
||||
appConfig.selectedBotId,
|
||||
orderId,
|
||||
action
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
console.error('Failed to sync order status to backend:', result.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { error: false };
|
||||
} catch (error) {
|
||||
console.error('Failed to update order status:', error.message);
|
||||
return { error: true, message: 'Failed to update order status' };
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel order
|
||||
fastify.post('/api/orders/:id/cancel', { preHandler: requireAuth }, async (req, reply) => {
|
||||
const orderId = parseInt(req.params.id, 10);
|
||||
const { reason } = req.body;
|
||||
|
||||
try {
|
||||
// Check if cancellation already printed to prevent duplicates
|
||||
if (database.hasPrintedCancellation(orderId)) {
|
||||
console.log(`[API] Cancellation already printed for order #${orderId}, skipping duplicate`);
|
||||
database.updateOrderStatus(orderId, 'canceled');
|
||||
return { error: false, message: 'Order already canceled' };
|
||||
}
|
||||
|
||||
// Update local database
|
||||
database.updateOrderStatus(orderId, 'canceled');
|
||||
|
||||
// Sync to backend
|
||||
const appConfig = config.getAll();
|
||||
if (appConfig.authToken && appConfig.selectedBotId) {
|
||||
const result = await apiClient.modifyOrder(
|
||||
appConfig.authToken,
|
||||
appConfig.selectedBotId,
|
||||
orderId,
|
||||
'cancel',
|
||||
reason || 'Canceled by kitchen'
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
console.error('Failed to sync cancellation to backend:', result.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Print cancellation receipt
|
||||
const order = database.getOrderById(orderId);
|
||||
if (order) {
|
||||
try {
|
||||
// Add to print queue with deduplication check
|
||||
const jobId = database.addToPrintQueue(orderId, 'canceled');
|
||||
if (!jobId) {
|
||||
console.log(`[API] Cancellation print job not created (duplicate prevention) for order #${orderId}`);
|
||||
return { error: false, message: 'Cancellation recorded' };
|
||||
}
|
||||
|
||||
database.markPrintJobProcessing(jobId);
|
||||
|
||||
// Get enabled printers from database
|
||||
const printerConfigs = database.getEnabledPrinters();
|
||||
let result;
|
||||
|
||||
if (printerConfigs && printerConfigs.length > 0) {
|
||||
// Use new per-printer config system
|
||||
result = await printer.printOrderReceiptWithPrinterConfigs(
|
||||
order,
|
||||
printerConfigs,
|
||||
'canceled',
|
||||
{ reason: reason || 'Canceled by kitchen' }
|
||||
);
|
||||
} else {
|
||||
// Fallback to legacy system if no printer configs
|
||||
if (!printer.printer) {
|
||||
printer.initializePrinter(appConfig);
|
||||
}
|
||||
result = await printer.printOrderReceipt(order, 'canceled', { reason: reason || 'Canceled by kitchen' });
|
||||
}
|
||||
|
||||
if (result && result.success) {
|
||||
database.markOrderPrinted(orderId);
|
||||
database.markPrintJobCompleted(jobId);
|
||||
// Cleanup any other pending jobs for this order+type
|
||||
database.cleanupDuplicateJobs(jobId, orderId, 'canceled');
|
||||
} else {
|
||||
// Mark as pending for worker retry
|
||||
database.markPrintJobPending(jobId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to print cancellation receipt:', error.message);
|
||||
// Let the worker retry - find the job and mark it pending
|
||||
try {
|
||||
const lastJobIdRow = database.db.prepare("SELECT id FROM print_queue WHERE order_id = ? AND print_type = 'canceled' ORDER BY id DESC LIMIT 1").get(orderId);
|
||||
if (lastJobIdRow && lastJobIdRow.id) { database.markPrintJobPending(lastJobIdRow.id); }
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
return { error: false };
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel order:', error.message);
|
||||
return { error: true, message: 'Failed to cancel order' };
|
||||
}
|
||||
});
|
||||
|
||||
// Reprint order
|
||||
fastify.post('/api/orders/:id/reprint', { preHandler: requireAuth }, async (req, reply) => {
|
||||
const orderId = parseInt(req.params.id, 10);
|
||||
|
||||
try {
|
||||
const order = database.getOrderById(orderId);
|
||||
|
||||
if (!order) {
|
||||
return { error: true, message: 'Order not found' };
|
||||
}
|
||||
|
||||
const printType = order.localStatus === 'canceled' ? 'canceled' : 'new';
|
||||
|
||||
// Check for recent ACTIVE job to prevent double-enqueue while in-flight
|
||||
const activeCheck = database.hasActiveOrRecentJob(orderId, 'reprint', 10);
|
||||
if (activeCheck.hasActive && (activeCheck.status === 'pending' || activeCheck.status === 'processing')) {
|
||||
console.log(`[API] Reprint request for order #${orderId} blocked - active job ${activeCheck.jobId} (${activeCheck.status}) exists`);
|
||||
return { error: false, message: 'Print already in progress' };
|
||||
}
|
||||
|
||||
// Add to print queue with deduplication
|
||||
const jobId = database.addToPrintQueue(orderId, 'reprint');
|
||||
if (!jobId) {
|
||||
console.log(`[API] Reprint job not created (duplicate prevention) for order #${orderId}`);
|
||||
return { error: false, message: 'Print recently completed, skipping duplicate' };
|
||||
}
|
||||
|
||||
database.markPrintJobProcessing(jobId);
|
||||
|
||||
// Print receipt using per-printer configs
|
||||
const printerConfigs = database.getEnabledPrinters();
|
||||
|
||||
let result;
|
||||
if (printerConfigs && printerConfigs.length > 0) {
|
||||
// Use new per-printer config system
|
||||
result = await printer.printOrderReceiptWithPrinterConfigs(order, printerConfigs, printType, { cooldownMs: 2000 });
|
||||
} else {
|
||||
// Fallback to legacy system
|
||||
const appConfig = config.getAll();
|
||||
printer.initializePrinter(appConfig);
|
||||
result = await printer.printOrderReceipt(order, printType, { cooldownMs: 2000 });
|
||||
}
|
||||
|
||||
if (result && result.success) {
|
||||
database.markOrderPrinted(orderId);
|
||||
database.markPrintJobCompleted(jobId);
|
||||
// Cleanup any other pending jobs for this order+type
|
||||
database.cleanupDuplicateJobs(jobId, orderId, 'reprint');
|
||||
return { error: false, message: (result && result.message) ? result.message : 'Receipt sent to printer' };
|
||||
} else {
|
||||
// Mark as pending so the worker can retry when printer is online
|
||||
database.markPrintJobPending(jobId);
|
||||
return { error: true, message: result.error || 'Print failed' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reprint order:', error.message);
|
||||
try { database.resetStuckProcessingJobs(60); } catch (_) {}
|
||||
return { error: true, message: 'Failed to reprint order' };
|
||||
}
|
||||
});
|
||||
|
||||
// Get single order details
|
||||
fastify.get('/api/orders/:id', { preHandler: requireAuth }, async (req, reply) => {
|
||||
const orderId = parseInt(req.params.id, 10);
|
||||
const order = database.getOrderById(orderId);
|
||||
|
||||
if (!order) {
|
||||
return { error: true, message: 'Order not found' };
|
||||
}
|
||||
|
||||
return { error: false, order };
|
||||
});
|
||||
|
||||
// Manual sync trigger (for testing/debugging)
|
||||
fastify.post('/api/sync-now', { preHandler: requireAuth }, async (req, reply) => {
|
||||
try {
|
||||
// Trigger the poller manually
|
||||
if (fastify.orderPoller) {
|
||||
fastify.orderPoller.poll();
|
||||
return { error: false, message: 'Manual sync triggered' };
|
||||
} else {
|
||||
return { error: true, message: 'Order poller not available' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Manual sync error:', error.message);
|
||||
return { error: true, message: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Health check for external API server
|
||||
fastify.get('/api/health/external', { preHandler: requireAuth }, async (req, reply) => {
|
||||
try {
|
||||
const appConfig = config.getAll();
|
||||
const token = appConfig.authToken;
|
||||
const botId = appConfig.selectedBotId;
|
||||
|
||||
if (!token || !botId) {
|
||||
return {
|
||||
error: false,
|
||||
status: 'unconfigured',
|
||||
message: 'API not configured',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if (apiClient.isTokenExpired(appConfig.tokenExpiry)) {
|
||||
return {
|
||||
error: false,
|
||||
status: 'offline',
|
||||
message: 'Token expired',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Try to fetch bots list as a lightweight health check
|
||||
const startTime = Date.now();
|
||||
const result = await apiClient.getBots(token);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
if (result.error) {
|
||||
return {
|
||||
error: false,
|
||||
status: 'offline',
|
||||
message: result.message || 'API server unreachable',
|
||||
responseTime: responseTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: false,
|
||||
status: 'online',
|
||||
message: 'API server connected',
|
||||
responseTime: responseTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('External API health check error:', error.message);
|
||||
return {
|
||||
error: false,
|
||||
status: 'offline',
|
||||
message: error.message || 'Health check failed',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Health check for local dashboard server
|
||||
fastify.get('/api/health/local', { preHandler: requireAuth }, async (req, reply) => {
|
||||
return {
|
||||
error: false,
|
||||
status: 'online',
|
||||
message: 'Local server connected',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = ordersRoutes;
|
||||
|
||||
459
routes/settings.js
Normal file
459
routes/settings.js
Normal file
@@ -0,0 +1,459 @@
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user