230 lines
8.1 KiB
JavaScript
230 lines
8.1 KiB
JavaScript
const config = require('../config');
|
|
const database = require('../database');
|
|
const apiClient = require('../api-client');
|
|
const printer = require('../printer');
|
|
|
|
// Page-level auth (redirects to login)
|
|
async function requireAuthPage(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');
|
|
if (!token || apiClient.isTokenExpired(expiry)) {
|
|
reply.redirect('/login');
|
|
return;
|
|
}
|
|
if (value === token) return;
|
|
const previousToken = config.get('previousAuthToken');
|
|
if (previousToken && value === previousToken) {
|
|
const isHttps = (req.protocol === 'https') || ((req.headers['x-forwarded-proto'] || '').toString().toLowerCase() === 'https');
|
|
reply.setCookie('kitchen_session', token, {
|
|
signed: true, httpOnly: true, secure: isHttps,
|
|
sameSite: 'strict', maxAge: 30 * 24 * 60 * 60, path: '/'
|
|
});
|
|
return;
|
|
}
|
|
reply.redirect('/login');
|
|
}
|
|
|
|
// API-level auth (returns 401 JSON)
|
|
async function requireAuthApi(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');
|
|
if (!token || apiClient.isTokenExpired(expiry)) {
|
|
return reply.code(401).send({ error: true, message: 'Not authenticated' });
|
|
}
|
|
if (value === token) return;
|
|
const previousToken = config.get('previousAuthToken');
|
|
if (previousToken && value === previousToken) {
|
|
const isHttps = (req.protocol === 'https') || ((req.headers['x-forwarded-proto'] || '').toString().toLowerCase() === 'https');
|
|
reply.setCookie('kitchen_session', token, {
|
|
signed: true, httpOnly: true, secure: isHttps,
|
|
sameSite: 'strict', maxAge: 30 * 24 * 60 * 60, path: '/'
|
|
});
|
|
return;
|
|
}
|
|
return reply.code(401).send({ error: true, message: 'Not authenticated' });
|
|
}
|
|
|
|
async function abandonedCallRoutes(fastify, options) {
|
|
|
|
// Page route
|
|
fastify.get('/abandoned-calls', { preHandler: requireAuthPage }, async (req, reply) => {
|
|
const appConfig = config.getAll();
|
|
const stats = database.getAbandonedCallStats();
|
|
return reply.view('abandoned-calls', {
|
|
config: appConfig,
|
|
stats
|
|
});
|
|
});
|
|
|
|
// API: list abandoned calls (proxy to backend or serve from cache)
|
|
fastify.get('/api/abandoned-calls', { preHandler: requireAuthApi }, async (req, reply) => {
|
|
const appConfig = config.getAll();
|
|
const token = appConfig.authToken;
|
|
const botId = appConfig.selectedBotId;
|
|
|
|
if (!token || !botId) {
|
|
return { error: true, message: 'Not configured' };
|
|
}
|
|
|
|
const options = {
|
|
limit: parseInt(req.query.limit, 10) || 50,
|
|
offset: parseInt(req.query.offset, 10) || 0
|
|
};
|
|
if (req.query.stage) options.stage = req.query.stage;
|
|
if (req.query.priority) options.priority = req.query.priority;
|
|
|
|
const result = await apiClient.getAbandonedCalls(token, botId, options);
|
|
|
|
if (result.error) {
|
|
// Fall back to local cache
|
|
const cached = database.getCachedAbandonedCalls({
|
|
status: req.query.priority ? undefined : undefined,
|
|
priority: req.query.priority || undefined,
|
|
limit: options.limit
|
|
});
|
|
return { error: false, calls: cached, cached: true };
|
|
}
|
|
|
|
// Update local cache
|
|
if (result.calls) {
|
|
for (const call of result.calls) {
|
|
database.cacheAbandonedCall(call.id, call);
|
|
}
|
|
}
|
|
|
|
return { error: false, calls: result.calls || [] };
|
|
});
|
|
|
|
// API: callback queue
|
|
fastify.get('/api/abandoned-calls/callback-queue', { preHandler: requireAuthApi }, async (req, reply) => {
|
|
const appConfig = config.getAll();
|
|
const token = appConfig.authToken;
|
|
const botId = appConfig.selectedBotId;
|
|
|
|
if (!token || !botId) {
|
|
return { error: true, message: 'Not configured' };
|
|
}
|
|
|
|
const limit = parseInt(req.query.limit, 10) || 20;
|
|
const offset = parseInt(req.query.offset, 10) || 0;
|
|
const result = await apiClient.getAbandonedCallbackQueue(token, botId, limit, offset);
|
|
|
|
if (result.error) {
|
|
const cached = database.getCachedAbandonedCalls({ status: 'queued', limit });
|
|
return { error: false, queue: cached, cached: true };
|
|
}
|
|
|
|
if (result.queue) {
|
|
for (const call of result.queue) {
|
|
database.cacheAbandonedCall(call.id, call);
|
|
}
|
|
}
|
|
|
|
return { error: false, queue: result.queue || [] };
|
|
});
|
|
|
|
// API: update callback action
|
|
fastify.post('/api/abandoned-calls/:id/action', { preHandler: requireAuthApi }, async (req, reply) => {
|
|
const abandonedCallId = parseInt(req.params.id, 10);
|
|
const { action, notes } = req.body || {};
|
|
|
|
if (!action) {
|
|
return { error: true, message: 'Action is required' };
|
|
}
|
|
|
|
const validActions = ['call_back', 'reached', 'no_answer', 'converted', 'dismissed', 'deferred'];
|
|
if (!validActions.includes(action)) {
|
|
return { error: true, message: 'Invalid action' };
|
|
}
|
|
|
|
const appConfig = config.getAll();
|
|
const token = appConfig.authToken;
|
|
|
|
if (!token) {
|
|
return { error: true, message: 'Not configured' };
|
|
}
|
|
|
|
const result = await apiClient.updateAbandonedCallback(token, abandonedCallId, action, notes || '');
|
|
|
|
if (!result.error) {
|
|
const statusMap = {
|
|
call_back: 'attempted', reached: 'reached', no_answer: 'no_answer',
|
|
converted: 'converted', dismissed: 'dismissed', deferred: 'deferred'
|
|
};
|
|
database.updateCachedCallbackStatus(abandonedCallId, statusMap[action] || action);
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
// API: metrics
|
|
fastify.get('/api/abandoned-calls/metrics', { preHandler: requireAuthApi }, async (req, reply) => {
|
|
const appConfig = config.getAll();
|
|
const token = appConfig.authToken;
|
|
const botId = appConfig.selectedBotId;
|
|
|
|
if (!token || !botId) {
|
|
return { error: true, message: 'Not configured' };
|
|
}
|
|
|
|
const startDate = parseInt(req.query.startDate, 10) || 0;
|
|
const endDate = parseInt(req.query.endDate, 10) || 0;
|
|
|
|
return await apiClient.getAbandonedCallMetrics(token, botId, startDate, endDate);
|
|
});
|
|
|
|
// API: reprint abandoned call receipt
|
|
fastify.post('/api/abandoned-calls/:id/reprint', { preHandler: requireAuthApi }, async (req, reply) => {
|
|
const abandonedCallId = parseInt(req.params.id, 10);
|
|
|
|
// Get call data from cache
|
|
const cached = database.getCachedAbandonedCalls({ limit: 200 });
|
|
const call = cached.find(c => c.id === abandonedCallId);
|
|
|
|
if (!call) {
|
|
// Try fetching from API
|
|
const appConfig = config.getAll();
|
|
const result = await apiClient.getAbandonedCalls(appConfig.authToken, appConfig.selectedBotId, { limit: 1 });
|
|
const fromApi = (result.calls || []).find(c => c.id === abandonedCallId);
|
|
if (!fromApi) {
|
|
return { error: true, message: 'Abandoned call not found' };
|
|
}
|
|
return await doPrint(fromApi);
|
|
}
|
|
|
|
return await doPrint(call);
|
|
|
|
async function doPrint(callData) {
|
|
try {
|
|
const printerConfigs = database.getAbandonedCallPrinters();
|
|
if (!printerConfigs || printerConfigs.length === 0) {
|
|
return { error: true, message: 'No printers configured for abandoned call receipts' };
|
|
}
|
|
const result = await printer.printAbandonedCallReceipt(callData, printerConfigs);
|
|
if (result && result.success) {
|
|
return { error: false, message: `Printed on ${result.successCount} printer(s)` };
|
|
}
|
|
return { error: true, message: 'Print failed' };
|
|
} catch (err) {
|
|
return { error: true, message: err.message };
|
|
}
|
|
}
|
|
});
|
|
|
|
// API: pending count (lightweight, for dashboard badge)
|
|
fastify.get('/api/abandoned-calls/pending-count', { preHandler: requireAuthApi }, async (req, reply) => {
|
|
const count = database.getPendingAbandonedCallCount();
|
|
return { error: false, count };
|
|
});
|
|
}
|
|
|
|
module.exports = abandonedCallRoutes;
|