#!/usr/bin/env node // FreeScout time tracking CLI — query timelogs and generate invoice Excel const mysql = require('mysql2/promise'); const ExcelJS = require('exceljs'); const fs = require('fs'); const path = require('path'); const credFile = process.env.CRED_FILE || path.join(__dirname, '..', '.credentials', 'services.env'); const env = {}; fs.readFileSync(credFile, 'utf8').split('\n').forEach(l => { const m = l.match(/^(\w+)=(.+)$/); if (m) env[m[1]] = m[2]; }); async function getConn() { return mysql.createConnection({ host: env.CLOONAR_DB_HOST, port: parseInt(env.CLOONAR_DB_PORT || '3306'), user: env.CLOONAR_DB_USER, password: env.CLOONAR_DB_PASS, database: env.FREESCOUT_DB_NAME, }); } function formatHours(seconds) { const h = seconds / 3600; return Math.round(h * 100) / 100; } // --- Commands --- async function cmdMailboxes() { const conn = await getConn(); const [rows] = await conn.query('SELECT id, name FROM mailboxes ORDER BY name'); console.log('ID\tNAME'); rows.forEach(r => console.log(`${r.id}\t${r.name}`)); await conn.end(); } async function cmdUsers() { const conn = await getConn(); const [rows] = await conn.query('SELECT id, first_name, last_name, email FROM users ORDER BY first_name'); console.log('ID\tNAME\tEMAIL'); rows.forEach(r => console.log(`${r.id}\t${r.first_name} ${r.last_name}\t${r.email}`)); await conn.end(); } async function cmdReport(args) { const mailboxId = getFlag(args, '--mailbox'); const from = getFlag(args, '--from'); const to = getFlag(args, '--to'); const userId = getFlag(args, '--user'); if (!from || !to) { console.error('Usage: freescout report --from YYYY-MM-DD --to YYYY-MM-DD [--mailbox ID] [--user ID]'); process.exit(1); } const conn = await getConn(); let query = ` SELECT t.id as timelog_id, 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 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 WHERE t.created_at >= ? AND t.created_at < DATE_ADD(?, INTERVAL 1 DAY) `; const params = [from, to]; if (mailboxId) { query += ' AND c.mailbox_id = ?'; params.push(mailboxId); } if (userId) { query += ' AND t.user_id = ?'; params.push(userId); } query += ' ORDER BY t.created_at'; const [rows] = await conn.query(query, params); if (rows.length === 0) { console.log('No timelogs found for this period.'); await conn.end(); return; } console.log('DATE\tCONV#\tSUBJECT\tCUSTOMER\tUSER\tHOURS\tMAILBOX'); 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}`); }); console.log(`\nTotal: ${formatHours(totalSeconds)} hours (${rows.length} entries)`); await conn.end(); } async function cmdExcel(args) { const mailboxId = getFlag(args, '--mailbox'); const from = getFlag(args, '--from'); const to = getFlag(args, '--to'); const userId = getFlag(args, '--user'); const output = getFlag(args, '--output') || `invoice-${from}-${to}.xlsx`; if (!from || !to) { console.error('Usage: freescout excel --from YYYY-MM-DD --to YYYY-MM-DD [--mailbox ID] [--user ID] [--output file.xlsx]'); process.exit(1); } const conn = await getConn(); let query = ` SELECT t.time_spent, t.created_at, 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 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]; if (mailboxId) { query += ' AND c.mailbox_id = ?'; params.push(mailboxId); } if (userId) { query += ' AND t.user_id = ?'; params.push(userId); } 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.'); return; } // Group by conversation const byConv = {}; rows.forEach(r => { const key = r.conversation_number; if (!byConv[key]) { 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, 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: 'Hours', key: 'hours', width: 10 }, ]; // Style header ws.getRow(1).font = { bold: true }; ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }; let totalSeconds = 0; Object.values(byConv).forEach(conv => { totalSeconds += conv.totalSeconds; ws.addRow({ number: `#${conv.number}`, subject: conv.subject, customer: conv.customer_name !== '-' ? conv.customer_name : conv.customer_email, company: conv.customer_company, hours: formatHours(conv.totalSeconds), }); }); // Total row const totalRow = ws.addRow({ number: '', subject: '', customer: '', company: 'TOTAL', hours: formatHours(totalSeconds) }); totalRow.font = { bold: true }; await wb.xlsx.writeFile(output); console.log(`Written: ${output}`); console.log(`${Object.keys(byConv).length} conversations, ${formatHours(totalSeconds)} hours total`); } // --- Helpers --- function getFlag(args, flag) { const idx = args.indexOf(flag); if (idx === -1) return null; return args[idx + 1] || null; } // --- Main --- const [cmd, ...args] = process.argv.slice(2); const commands_map = { mailboxes: cmdMailboxes, users: cmdUsers, report: cmdReport, excel: cmdExcel }; if (!cmd || !commands_map[cmd]) { console.log(`Usage: freescout [args] Commands: mailboxes List mailboxes users List users report --from DATE --to DATE Show time report (tab-separated) [--mailbox ID] [--user ID] excel --from DATE --to DATE Generate invoice Excel [--mailbox ID] [--user ID] [--output file.xlsx]`); process.exit(0); } commands_map[cmd](args).catch(e => { console.error(e.message); process.exit(1); });