done
This commit is contained in:
497
printer.js
497
printer.js
@@ -1380,16 +1380,25 @@ class PrinterManager {
|
||||
doc.moveDown(0.4);
|
||||
doc.fontSize(12).text('ITEMS:');
|
||||
doc.fontSize(11);
|
||||
const includePrices = (cfg.showPrices === 'true' || !cfg.showPrices);
|
||||
order.order.items.forEach(item => {
|
||||
const itemName = item.itemName || item.name || 'Unknown Item';
|
||||
const qty = item.qty || 1;
|
||||
const price = item.price || 0;
|
||||
const line = (cfg.showPrices === 'true' || !cfg.showPrices)
|
||||
const line = includePrices
|
||||
? `${qty}x ${itemName} - $${(price * qty).toFixed(2)}`
|
||||
: `${qty}x ${itemName}`;
|
||||
doc.text(line, { width: maxWidth });
|
||||
if (item.addons && Array.isArray(item.addons)) {
|
||||
item.addons.forEach(addon => doc.text(` + ${addon.name || addon}`));
|
||||
item.addons.forEach(addon => {
|
||||
const addonName = addon.name || addon;
|
||||
const addonPrice = Number(addon && addon.price);
|
||||
if (includePrices && isFinite(addonPrice) && addonPrice > 0) {
|
||||
doc.text(` + ${addonName} - $${addonPrice.toFixed(2)}`);
|
||||
} else {
|
||||
doc.text(` + ${addonName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.exclude && Array.isArray(item.exclude)) {
|
||||
item.exclude.forEach(ex => doc.text(` - NO ${ex.name || ex}`));
|
||||
@@ -1540,7 +1549,15 @@ class PrinterManager {
|
||||
: `${qty}x ${itemName}`;
|
||||
doc.text(line, { width: maxWidth });
|
||||
if (item.addons && Array.isArray(item.addons)) {
|
||||
item.addons.forEach(addon => doc.text(` + ${addon.name || addon}`));
|
||||
item.addons.forEach(addon => {
|
||||
const addonName = addon.name || addon;
|
||||
const addonPrice = Number(addon && addon.price);
|
||||
if (mergedShowPrices && isFinite(addonPrice) && addonPrice > 0) {
|
||||
doc.text(` + ${addonName} - $${addonPrice.toFixed(2)}`);
|
||||
} else {
|
||||
doc.text(` + ${addonName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.exclude && Array.isArray(item.exclude)) {
|
||||
item.exclude.forEach(ex => doc.text(` - NO ${ex.name || ex}`));
|
||||
@@ -1751,6 +1768,480 @@ class PrinterManager {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Abandoned Call Receipt =====
|
||||
|
||||
async renderAbandonedCallReceipt(instance, call, cfg) {
|
||||
const width = parseInt(cfg.paper_width, 10) || 48;
|
||||
const priority = call.callback_priority || 'low';
|
||||
const score = Number(call.callback_score) || 0;
|
||||
const isNarrow = width < 40;
|
||||
|
||||
this.safeCallOn(instance, 'clear');
|
||||
|
||||
// Logo
|
||||
const logoPath = cfg.logo_path || cfg.logoPath || null;
|
||||
if (logoPath && fs.existsSync(logoPath)) {
|
||||
try {
|
||||
const maxDots = this.getPrinterMaxDotsWidthFromConfig(cfg);
|
||||
await this.printLogoWithFitCustom(instance, logoPath, maxDots);
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Top border
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
|
||||
// Title: "ABANDONED CALL" in large centered text
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'setTextSize', 1, 1);
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println('ABANDONED CALL');
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'setTextNormal');
|
||||
|
||||
// Priority badge using inverted text for visual prominence
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
if (priority === 'critical') {
|
||||
this.safeCallOn(instance, 'setTextSize', 1, 1);
|
||||
this.safeCallOn(instance, 'invert', true);
|
||||
const pad = isNarrow ? ' ' : ' ';
|
||||
instance.println(`${pad}CRITICAL PRIORITY${pad}`);
|
||||
this.safeCallOn(instance, 'invert', false);
|
||||
this.safeCallOn(instance, 'setTextNormal');
|
||||
} else if (priority === 'high') {
|
||||
this.safeCallOn(instance, 'invert', true);
|
||||
const pad = isNarrow ? ' ' : ' ';
|
||||
instance.println(`${pad}HIGH PRIORITY${pad}`);
|
||||
this.safeCallOn(instance, 'invert', false);
|
||||
} else if (priority === 'medium') {
|
||||
instance.println('MEDIUM PRIORITY');
|
||||
} else {
|
||||
instance.println(`Priority: ${priority.toUpperCase()}`);
|
||||
}
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
|
||||
// Timestamp
|
||||
const callTime = call.call_started_at
|
||||
? new Date(Number(call.call_started_at) * 1000)
|
||||
: new Date();
|
||||
const timeStr = callTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const dateStr = callTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
instance.println(`${dateStr} ${timeStr}`);
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
|
||||
// Caller section
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println('CALLER');
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
this.safeCallOn(instance, 'alignLeft');
|
||||
|
||||
const phone = call.caller_phone_normalized || call.caller_phone || 'Unknown';
|
||||
const name = call.caller_name || '';
|
||||
|
||||
this.safeCallOn(instance, 'setTextSize', 1, 0);
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println(`Phone: ${phone}`);
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'setTextNormal');
|
||||
if (name) {
|
||||
instance.println(`Name: ${name}`);
|
||||
}
|
||||
|
||||
if (call.is_known_customer) {
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'invert', true);
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
const orderCount = Number(call.previous_order_count || 0);
|
||||
instance.println(` RETURNING CUSTOMER ${orderCount} previous order${orderCount !== 1 ? 's' : ''} `);
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'invert', false);
|
||||
}
|
||||
|
||||
instance.println(`Callback Score: ${score}/100`);
|
||||
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
|
||||
// What happened section
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println('WHAT HAPPENED');
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
this.safeCallOn(instance, 'alignLeft');
|
||||
|
||||
const stageMessages = {
|
||||
ring_only: 'Rang but hung up before AI could answer.',
|
||||
greeting_hangup: 'Heard the greeting then disconnected.',
|
||||
silent_post_greeting: `Stayed on for ${Math.round(Number(call.time_after_greeting_sec || 0))}s without speaking.`,
|
||||
minimal_speech: 'Spoke briefly then disconnected.',
|
||||
pre_intent: 'Began speaking but intent was unclear.',
|
||||
intent_identified: 'Expressed interest in ordering but disconnected.',
|
||||
partial_order: 'Started placing an order then disconnected.',
|
||||
partial_appointment: 'Was booking an appointment then disconnected.',
|
||||
pre_confirmation: 'Order was nearly complete before disconnecting.',
|
||||
system_failure: 'A system error caused the disconnection.'
|
||||
};
|
||||
|
||||
const stageMsg = stageMessages[call.abandonment_stage] || 'Call ended before completion.';
|
||||
instance.println(stageMsg);
|
||||
|
||||
const dur = Number(call.duration_seconds || 0);
|
||||
const durLabel = dur >= 60
|
||||
? `${Math.floor(dur / 60)}m ${dur % 60}s`
|
||||
: `${dur}s`;
|
||||
instance.println(`Call duration: ${durLabel}`);
|
||||
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
|
||||
// Partial order items
|
||||
const items = call.items || [];
|
||||
let parsedSnapshot = null;
|
||||
if (items.length === 0 && call.partial_order_snapshot) {
|
||||
try {
|
||||
parsedSnapshot = typeof call.partial_order_snapshot === 'string'
|
||||
? JSON.parse(call.partial_order_snapshot)
|
||||
: call.partial_order_snapshot;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const displayItems = items.length > 0
|
||||
? items
|
||||
: (parsedSnapshot && parsedSnapshot.orderData && parsedSnapshot.orderData.items) || [];
|
||||
|
||||
if (displayItems.length > 0) {
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println('ITEMS MENTIONED');
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
this.safeCallOn(instance, 'alignLeft');
|
||||
|
||||
const colWidths = this.getColumnWidths(cfg);
|
||||
for (const item of displayItems) {
|
||||
const itemName = item.item_name || item.itemName || item.name || 'Unknown';
|
||||
const qty = item.quantity || item.qty || 1;
|
||||
const price = item.unit_price || item.price || null;
|
||||
|
||||
if (price && colWidths.useTable) {
|
||||
try {
|
||||
instance.tableCustom([
|
||||
{ text: `${qty}x ${itemName}`, align: 'LEFT', width: colWidths.itemWidth },
|
||||
{ text: `$${Number(price).toFixed(2)}`, align: 'RIGHT', width: colWidths.priceWidth }
|
||||
]);
|
||||
} catch (_) {
|
||||
instance.println(` ${qty}x ${itemName} $${Number(price).toFixed(2)}`);
|
||||
}
|
||||
} else {
|
||||
instance.println(` ${qty}x ${itemName}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (call.partial_order_value || call.estimated_order_value) {
|
||||
const est = Number(call.partial_order_value || call.estimated_order_value);
|
||||
if (est > 0) {
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'alignRight');
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println(`Est. Value: $${est.toFixed(2)}`);
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'alignLeft');
|
||||
}
|
||||
}
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
}
|
||||
|
||||
// LLM callback script
|
||||
if (call.llm_callback_script) {
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println('SUGGESTED CALLBACK SCRIPT');
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
this.safeCallOn(instance, 'alignLeft');
|
||||
|
||||
const script = String(call.llm_callback_script);
|
||||
const maxLineLen = width || 48;
|
||||
const words = script.split(' ');
|
||||
let line = '';
|
||||
for (const word of words) {
|
||||
if ((line + ' ' + word).trim().length > maxLineLen) {
|
||||
instance.println(line.trim());
|
||||
line = word;
|
||||
} else {
|
||||
line += (line ? ' ' : '') + word;
|
||||
}
|
||||
}
|
||||
if (line.trim()) instance.println(line.trim());
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
}
|
||||
|
||||
// Action section
|
||||
if (score >= 60) {
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
this.safeCallOn(instance, 'setTextSize', 1, 1);
|
||||
this.safeCallOn(instance, 'invert', true);
|
||||
this.safeCallOn(instance, 'bold', true);
|
||||
instance.println(' CALLBACK RECOMMENDED ');
|
||||
this.safeCallOn(instance, 'bold', false);
|
||||
this.safeCallOn(instance, 'invert', false);
|
||||
this.safeCallOn(instance, 'setTextNormal');
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'alignLeft');
|
||||
instance.println('[ ] Called back');
|
||||
instance.println('[ ] No answer');
|
||||
instance.println('[ ] Skipped');
|
||||
}
|
||||
|
||||
// Footer
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
this.safeCallOn(instance, 'alignCenter');
|
||||
instance.println(`Ref #${call.id || ''}`);
|
||||
instance.println('Powered by Think Link AI');
|
||||
this.safeCallOn(instance, 'drawLine');
|
||||
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'newLine');
|
||||
this.safeCallOn(instance, 'partialCut');
|
||||
}
|
||||
|
||||
async printAbandonedCallReceipt(call, printerConfigs) {
|
||||
if (!printerConfigs || printerConfigs.length === 0) {
|
||||
console.log('[PrinterManager] No printers configured for abandoned call receipts');
|
||||
return { success: false, successCount: 0 };
|
||||
}
|
||||
|
||||
const callId = call && call.id ? call.id : 'unknown';
|
||||
console.log(`[PrinterManager] Printing abandoned call #${callId} to ${printerConfigs.length} printer(s)`);
|
||||
|
||||
const jobs = printerConfigs.map(async (cfg, idx) => {
|
||||
console.log(`[PrinterManager] Abandoned call print ${idx + 1}/${printerConfigs.length}: ${cfg.name}`);
|
||||
|
||||
if (cfg.type === 'system') {
|
||||
const filePath = path.join(os.tmpdir(), `kitchen-agent-abandoned-${callId}-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`);
|
||||
try {
|
||||
await this.writeAbandonedCallPdfToPath(filePath, call, cfg);
|
||||
await pdfPrinter.print(filePath, { printer: cfg.interface });
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
} finally {
|
||||
try { fs.unlinkSync(filePath); } catch (_) {}
|
||||
}
|
||||
} else {
|
||||
const instance = this.createPrinterInstanceFromConfig(cfg);
|
||||
await this.renderAbandonedCallReceipt(instance, call, cfg);
|
||||
|
||||
let normalizedInterface = cfg.interface;
|
||||
if (cfg.type === 'network') {
|
||||
if (!normalizedInterface.startsWith('tcp://')) {
|
||||
const hasPort = normalizedInterface.includes(':');
|
||||
normalizedInterface = hasPort ? `tcp://${normalizedInterface}` : `tcp://${normalizedInterface}:9100`;
|
||||
}
|
||||
} else if (cfg.type === 'com' || cfg.type === 'serial') {
|
||||
if (/^COM\d+$/i.test(normalizedInterface)) {
|
||||
normalizedInterface = `\\\\.\\${normalizedInterface.toUpperCase()}`;
|
||||
}
|
||||
}
|
||||
|
||||
await this.executeForInstance(instance, normalizedInterface);
|
||||
}
|
||||
console.log(`[PrinterManager] Abandoned call printed to: ${cfg.name}`);
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(jobs);
|
||||
const successCount = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failCount = results.length - successCount;
|
||||
console.log(`[PrinterManager] Abandoned call #${callId}: ${successCount} succeeded, ${failCount} failed`);
|
||||
|
||||
return { success: successCount > 0, successCount };
|
||||
}
|
||||
|
||||
async writeAbandonedCallPdfToPath(filePath, call, cfg) {
|
||||
const doc = new PDFDocument({ size: [226, 800], margin: 10 });
|
||||
const pageWidth = 206; // 226 - 2*10 margin
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = fs.createWriteStream(filePath);
|
||||
doc.pipe(stream);
|
||||
|
||||
const priority = call.callback_priority || 'low';
|
||||
const score = Number(call.callback_score) || 0;
|
||||
|
||||
// Top rule
|
||||
doc.moveTo(10, doc.y).lineTo(216, doc.y).stroke();
|
||||
doc.moveDown(0.4);
|
||||
|
||||
// Title
|
||||
doc.fontSize(16).font('Helvetica-Bold').text('ABANDONED CALL', { align: 'center' });
|
||||
doc.moveDown(0.2);
|
||||
|
||||
// Priority badge - draw a filled rectangle behind text for critical/high
|
||||
if (priority === 'critical' || priority === 'high') {
|
||||
const badgeText = priority === 'critical' ? 'CRITICAL PRIORITY' : 'HIGH PRIORITY';
|
||||
const badgeWidth = doc.widthOfString(badgeText, { fontSize: 11 }) + 16;
|
||||
const badgeX = (226 - badgeWidth) / 2;
|
||||
const badgeY = doc.y;
|
||||
doc.save();
|
||||
doc.rect(badgeX, badgeY, badgeWidth, 16).fill('#000');
|
||||
doc.fontSize(11).font('Helvetica-Bold').fillColor('#fff')
|
||||
.text(badgeText, 10, badgeY + 3, { align: 'center', width: pageWidth });
|
||||
doc.fillColor('#000').restore();
|
||||
doc.y = badgeY + 20;
|
||||
} else {
|
||||
const label = priority === 'medium' ? 'MEDIUM PRIORITY' : `Priority: ${priority.toUpperCase()}`;
|
||||
doc.fontSize(10).font('Helvetica-Bold').text(label, { align: 'center' });
|
||||
}
|
||||
doc.moveDown(0.3);
|
||||
|
||||
// Divider
|
||||
doc.moveTo(10, doc.y).lineTo(216, doc.y).stroke();
|
||||
doc.moveDown(0.3);
|
||||
|
||||
// Timestamp
|
||||
const callTime = call.call_started_at ? new Date(Number(call.call_started_at) * 1000) : new Date();
|
||||
const timeStr = callTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
const dateStr = callTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
doc.fontSize(9).font('Helvetica').text(`${dateStr} ${timeStr}`, { align: 'center' });
|
||||
doc.moveDown(0.3);
|
||||
|
||||
// Caller section header
|
||||
doc.fontSize(10).font('Helvetica-Bold').text('CALLER', { align: 'center' });
|
||||
doc.moveTo(10, doc.y + 2).lineTo(216, doc.y + 2).stroke();
|
||||
doc.moveDown(0.3);
|
||||
|
||||
// Phone number prominent
|
||||
doc.fontSize(12).font('Helvetica-Bold').text(`Phone: ${call.caller_phone_normalized || call.caller_phone || 'Unknown'}`, { align: 'left' });
|
||||
doc.font('Helvetica');
|
||||
if (call.caller_name) {
|
||||
doc.fontSize(10).text(`Name: ${call.caller_name}`);
|
||||
}
|
||||
|
||||
if (call.is_known_customer) {
|
||||
doc.moveDown(0.15);
|
||||
const custText = `RETURNING CUSTOMER ${call.previous_order_count || 0} previous orders`;
|
||||
const custWidth = doc.widthOfString(custText, { fontSize: 8 }) + 10;
|
||||
const custX = 10;
|
||||
const custY = doc.y;
|
||||
doc.save();
|
||||
doc.rect(custX, custY, Math.min(custWidth, pageWidth), 13).fill('#000');
|
||||
doc.fontSize(8).font('Helvetica-Bold').fillColor('#fff')
|
||||
.text(custText, custX + 4, custY + 3, { width: pageWidth - 8 });
|
||||
doc.fillColor('#000').restore();
|
||||
doc.y = custY + 16;
|
||||
}
|
||||
|
||||
doc.fontSize(9).font('Helvetica').text(`Callback Score: ${score}/100`);
|
||||
doc.moveDown(0.3);
|
||||
|
||||
// What happened section
|
||||
doc.fontSize(10).font('Helvetica-Bold').text('WHAT HAPPENED', { align: 'center' });
|
||||
doc.moveTo(10, doc.y + 2).lineTo(216, doc.y + 2).stroke();
|
||||
doc.moveDown(0.3);
|
||||
|
||||
const stageMessages = {
|
||||
ring_only: 'Rang but hung up before AI could answer.',
|
||||
greeting_hangup: 'Heard the greeting then disconnected.',
|
||||
silent_post_greeting: 'Stayed on without speaking.',
|
||||
minimal_speech: 'Spoke briefly then disconnected.',
|
||||
pre_intent: 'Began speaking but intent was unclear.',
|
||||
intent_identified: 'Expressed interest in ordering but disconnected.',
|
||||
partial_order: 'Started placing an order then disconnected.',
|
||||
partial_appointment: 'Was booking an appointment then disconnected.',
|
||||
pre_confirmation: 'Order was nearly complete before disconnecting.',
|
||||
system_failure: 'A system error caused the disconnection.'
|
||||
};
|
||||
|
||||
doc.fontSize(9).font('Helvetica').text(stageMessages[call.abandonment_stage] || 'Call ended before completion.');
|
||||
const dur = Number(call.duration_seconds || 0);
|
||||
const durLabel = dur >= 60 ? `${Math.floor(dur / 60)}m ${dur % 60}s` : `${dur}s`;
|
||||
doc.text(`Call duration: ${durLabel}`);
|
||||
doc.moveDown(0.3);
|
||||
|
||||
// Items mentioned
|
||||
const items = call.items || [];
|
||||
let parsedSnapshot = null;
|
||||
if (items.length === 0 && call.partial_order_snapshot) {
|
||||
try {
|
||||
parsedSnapshot = typeof call.partial_order_snapshot === 'string'
|
||||
? JSON.parse(call.partial_order_snapshot)
|
||||
: call.partial_order_snapshot;
|
||||
} catch (_) {}
|
||||
}
|
||||
const displayItems = items.length > 0
|
||||
? items
|
||||
: (parsedSnapshot && parsedSnapshot.orderData && parsedSnapshot.orderData.items) || [];
|
||||
|
||||
if (displayItems.length > 0) {
|
||||
doc.fontSize(10).font('Helvetica-Bold').text('ITEMS MENTIONED', { align: 'center' });
|
||||
doc.moveTo(10, doc.y + 2).lineTo(216, doc.y + 2).stroke();
|
||||
doc.moveDown(0.3);
|
||||
doc.fontSize(9).font('Helvetica');
|
||||
for (const item of displayItems) {
|
||||
const itemName = item.item_name || item.itemName || item.name || 'Unknown';
|
||||
const qty = item.quantity || item.qty || 1;
|
||||
const price = item.unit_price || item.price || null;
|
||||
const line = price ? `${qty}x ${itemName} $${Number(price).toFixed(2)}` : `${qty}x ${itemName}`;
|
||||
doc.text(line);
|
||||
}
|
||||
if (call.partial_order_value || call.estimated_order_value) {
|
||||
const est = Number(call.partial_order_value || call.estimated_order_value);
|
||||
if (est > 0) {
|
||||
doc.moveDown(0.15);
|
||||
doc.font('Helvetica-Bold').text(`Est. Value: $${est.toFixed(2)}`, { align: 'right' });
|
||||
doc.font('Helvetica');
|
||||
}
|
||||
}
|
||||
doc.moveDown(0.3);
|
||||
}
|
||||
|
||||
// Callback script
|
||||
if (call.llm_callback_script) {
|
||||
doc.fontSize(10).font('Helvetica-Bold').text('SUGGESTED CALLBACK SCRIPT', { align: 'center' });
|
||||
doc.moveTo(10, doc.y + 2).lineTo(216, doc.y + 2).stroke();
|
||||
doc.moveDown(0.3);
|
||||
doc.fontSize(9).font('Helvetica').text(call.llm_callback_script, { width: pageWidth });
|
||||
doc.moveDown(0.3);
|
||||
}
|
||||
|
||||
// Action section
|
||||
if (score >= 60) {
|
||||
doc.moveTo(10, doc.y).lineTo(216, doc.y).stroke();
|
||||
doc.moveDown(0.3);
|
||||
const actionText = 'CALLBACK RECOMMENDED';
|
||||
const actionWidth = doc.widthOfString(actionText, { fontSize: 12 }) + 20;
|
||||
const actionX = (226 - actionWidth) / 2;
|
||||
const actionY = doc.y;
|
||||
doc.save();
|
||||
doc.rect(actionX, actionY, actionWidth, 18).fill('#000');
|
||||
doc.fontSize(12).font('Helvetica-Bold').fillColor('#fff')
|
||||
.text(actionText, 10, actionY + 4, { align: 'center', width: pageWidth });
|
||||
doc.fillColor('#000').restore();
|
||||
doc.y = actionY + 22;
|
||||
doc.moveDown(0.2);
|
||||
doc.fontSize(9).font('Helvetica').fillColor('#000');
|
||||
doc.text('[ ] Called back [ ] No answer [ ] Skipped');
|
||||
}
|
||||
|
||||
// Footer
|
||||
doc.moveDown(0.3);
|
||||
doc.moveTo(10, doc.y).lineTo(216, doc.y).stroke();
|
||||
doc.moveDown(0.2);
|
||||
doc.fontSize(8).font('Helvetica').text(`Ref #${call.id || ''}`, { align: 'center' });
|
||||
doc.text('Powered by Think Link AI', { align: 'center' });
|
||||
|
||||
doc.end();
|
||||
stream.on('finish', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PrinterManager();
|
||||
|
||||
Reference in New Issue
Block a user