From 7c3f28ca84749d7420789ca1ae6ce3dbbae68f8f Mon Sep 17 00:00:00 2001 From: Hoid Date: Mon, 9 Feb 2026 01:33:12 +0000 Subject: [PATCH] FreeScout: use Customer and Website custom fields instead of customers table --- bin/freescout | 82 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/bin/freescout b/bin/freescout index 684efc4..ebbac08 100755 --- a/bin/freescout +++ b/bin/freescout @@ -19,11 +19,44 @@ async function getConn() { }); } +// Load custom field dropdown options (id -> {optionId: label}) +async function loadCustomFieldOptions(conn) { + const [fields] = await conn.query('SELECT id, name, options FROM custom_fields'); + const map = {}; + fields.forEach(f => { + map[f.name.toLowerCase()] = { id: f.id, options: f.options ? JSON.parse(f.options) : {} }; + }); + return map; +} + function formatHours(seconds) { const h = seconds / 3600; return Math.round(h * 100) / 100; } +// Get custom field values for conversations, resolved to labels +async function getConversationCustomFields(conn, conversationIds, cfMap) { + if (!conversationIds.length) return {}; + const [rows] = await conn.query( + 'SELECT conversation_id, custom_field_id, value FROM conversation_custom_field WHERE conversation_id IN (?)', + [conversationIds] + ); + // Build: { convId: { fieldName: resolvedValue } } + const result = {}; + const idToField = {}; + Object.entries(cfMap).forEach(([name, f]) => { idToField[f.id] = { name, options: f.options }; }); + + rows.forEach(r => { + if (!result[r.conversation_id]) result[r.conversation_id] = {}; + const field = idToField[r.custom_field_id]; + if (field) { + const resolved = field.options[r.value] || r.value || '-'; + result[r.conversation_id][field.name] = resolved; + } + }); + return result; +} + // --- Commands --- async function cmdMailboxes() { @@ -54,9 +87,10 @@ async function cmdReport(args) { } const conn = await getConn(); + const cfMap = await loadCustomFieldOptions(conn); + let query = ` SELECT - t.id as timelog_id, t.time_spent, t.created_at, c.id as conversation_id, @@ -87,13 +121,17 @@ async function cmdReport(args) { return; } - console.log('DATE\tCONV#\tSUBJECT\tCUSTOMER\tUSER\tHOURS\tMAILBOX'); + const convIds = [...new Set(rows.map(r => r.conversation_id))]; + const cfValues = await getConversationCustomFields(conn, convIds, cfMap); + + console.log('DATE\tCONV#\tSUBJECT\tCUSTOMER\tWEBSITE\tUSER\tHOURS'); let totalSeconds = 0; rows.forEach(r => { const date = new Date(r.created_at).toISOString().slice(0, 10); const hours = formatHours(r.time_spent); totalSeconds += r.time_spent; - console.log(`${date}\t#${r.conversation_number}\t${r.subject || '(no subject)'}\t${r.customer_email || '-'}\t${r.first_name} ${r.last_name}\t${hours}\t${r.mailbox_name}`); + const cf = cfValues[r.conversation_id] || {}; + console.log(`${date}\t#${r.conversation_number}\t${r.subject || '(no subject)'}\t${cf.customer || '-'}\t${cf.website || '-'}\t${r.first_name} ${r.last_name}\t${hours}`); }); console.log(`\nTotal: ${formatHours(totalSeconds)} hours (${rows.length} entries)`); @@ -113,24 +151,20 @@ async function cmdExcel(args) { } const conn = await getConn(); + const cfMap = await loadCustomFieldOptions(conn); + let query = ` SELECT t.time_spent, t.created_at, + c.id as conversation_id, c.number as conversation_number, c.subject, - c.customer_email, - m.name as mailbox_name, - u.first_name, - u.last_name, - cu.first_name as customer_first, - cu.last_name as customer_last, - cu.company as customer_company + c.customer_email FROM timelogs t JOIN conversations c ON t.conversation_id = c.id JOIN mailboxes m ON c.mailbox_id = m.id JOIN users u ON t.user_id = u.id - LEFT JOIN customers cu ON c.customer_id = cu.id WHERE t.created_at >= ? AND t.created_at < DATE_ADD(?, INTERVAL 1 DAY) `; const params = [from, to]; @@ -141,47 +175,46 @@ async function cmdExcel(args) { query += ' ORDER BY t.created_at'; const [rows] = await conn.query(query, params); - await conn.end(); if (rows.length === 0) { console.log('No timelogs found for this period.'); + await conn.end(); return; } + const convIds = [...new Set(rows.map(r => r.conversation_id))]; + const cfValues = await getConversationCustomFields(conn, convIds, cfMap); + await conn.end(); + // Group by conversation const byConv = {}; rows.forEach(r => { const key = r.conversation_number; if (!byConv[key]) { + const cf = cfValues[r.conversation_id] || {}; byConv[key] = { number: r.conversation_number, subject: r.subject || '(no subject)', - customer_email: r.customer_email || '-', - customer_name: [r.customer_first, r.customer_last].filter(Boolean).join(' ') || '-', - customer_company: r.customer_company || '-', - mailbox: r.mailbox_name, + customer: cf.customer || '-', + website: cf.website || '-', totalSeconds: 0, - entries: [] }; } byConv[key].totalSeconds += r.time_spent; - byConv[key].entries.push(r); }); // Build Excel const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet('Time Report'); - // Header ws.columns = [ { header: 'Conv #', key: 'number', width: 10 }, { header: 'Subject', key: 'subject', width: 40 }, { header: 'Customer', key: 'customer', width: 25 }, - { header: 'Company', key: 'company', width: 25 }, + { header: 'Website', key: 'website', width: 25 }, { header: 'Hours', key: 'hours', width: 10 }, ]; - // Style header ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; @@ -191,14 +224,13 @@ async function cmdExcel(args) { ws.addRow({ number: `#${conv.number}`, subject: conv.subject, - customer: conv.customer_name !== '-' ? conv.customer_name : conv.customer_email, - company: conv.customer_company, + customer: conv.customer, + website: conv.website, hours: formatHours(conv.totalSeconds), }); }); - // Total row - const totalRow = ws.addRow({ number: '', subject: '', customer: '', company: 'TOTAL', hours: formatHours(totalSeconds) }); + const totalRow = ws.addRow({ number: '', subject: '', customer: '', website: 'TOTAL', hours: formatHours(totalSeconds) }); totalRow.font = { bold: true }; await wb.xlsx.writeFile(output);