UNPKG

teamwork-cli

Version:

Command-line-interface for the Teamwork (https://www.teamwork.com/time-tracking) time entry system.

493 lines (408 loc) 14 kB
const dateFormat = require('dateformat'); const teamwork = require('./teamwork.js'); const userData = require('./user-data.js'); /************************************************************************************ * teamwork-cli functions ************************************************************************************/ const BREAKS = ['break', 'lunch']; const isToday = (date) => { return date && date.getDate() === (new Date()).getDate(); }; const getDurationString = (milliseconds) => { const minuteDiff = (milliseconds) / 1000 / 60; const hours = Math.floor(minuteDiff / 60); const minutes = Math.floor(minuteDiff) % 60; const hourStr = hours > 0 ? `${hours}h ` : ''; return `${hourStr}${minutes}m`; }; const getTimeWorkedString = (arrived, timers) => { if (isToday(arrived)) { let breaks = 0; if (timers) { Object.keys(timers) .filter(key => BREAKS.indexOf(key.toLowerCase()) >= 0) .forEach(key => { const timer = timers[key]; breaks = breaks + timer.duration; if (timer.running) { breaks = breaks + (new Date() - timer.started); } }); } return getDurationString((new Date() - arrived) - breaks); } else { return 'not set'; } }; const getTimerString = (id, timer) => { if (timer.running) { const duration = timer.duration + (new Date() - timer.started); return `${id}: ${getDurationString(duration)} - running`; } else { return `${id}: ${getDurationString(timer.duration)}`; } }; // calculates number of work days between two dates const workday_count = (start, end) => { let count = 0; let day = new Date(start); let month = day.getMonth(); while (day.getDate() <= end.getDate() && month === day.getMonth()) { if (day.getDay() !== 6 && day.getDay() !== 0) { ++count; } day.setDate(day.getDate() + 1); } return count; }; /** * Prints the time logged summary for the month */ const printTimeLogged = () => { let date = new Date(); date.setDate(1); const dateStr = dateFormat(date, "yyyymmdd"); const timeEntries = teamwork.getTimeEntries(dateStr); const today = new Date(); const requiredHours = 8 * (workday_count(date, today)); let lastDayOfMonth = new Date(today.getFullYear(), (today.getMonth() + 1) % 12, 0); const leftInMonth = 8 * (workday_count(today, lastDayOfMonth) - 1); let holiday = 0; let billable = 0; let nonbillable = 0; let todayHours = 0; timeEntries.map(entry => { const entryDate = new Date(entry.date); if (entryDate.getDate() <= today.getDate()) { const entryHours = parseFloat(entry.hours) + (parseFloat(entry.minutes) / 60.0); if (entry.isbillable == 0) { nonbillable += entryHours; } else { billable += entryHours; } if (entryDate.getDate() == today.getDate()) { todayHours += entryHours; } } }); const { arrived, timers } = userData.get(); Object.keys(timers) .filter(id => !isToday(timers[id].started)) .forEach(id => delete timers[id]); const total = billable + nonbillable + holiday; const nonPercent = Math.round(1000.0 * nonbillable / total) / 10.0; console.log(`\n Month Required Hours: ${requiredHours + leftInMonth}`); console.log(` Month Logged Hours: ${billable + nonbillable}\n`); console.log(` Month Billable Hours: ${billable}`); console.log(` Month NonBillable Hours: ${nonbillable} (${nonPercent}%)\n`); console.log(` Time worked: ${getTimeWorkedString(arrived, timers)}`); console.log(` Logged today: ${getDurationString(todayHours * 3600000)}\n`); Object.keys(timers).forEach(id => console.log(' ' + getTimerString(id, timers[id]))); if (total > requiredHours) { console.log(`\nYou are ${total - requiredHours} over for today.`); } else { console.log(`\nYou are ${requiredHours - total } short for today.`); } }; /** * Print tasks for the current year */ const printPreviousTasks = () => { let date = new Date(); date.setDate(1); date.setMonth(date.getMonth() - 1); const dateStr = dateFormat(date, "yyyymmdd"); const tasks = teamwork.getTimeEntries(dateStr); const descriptions = tasks .sort((a, b) => (a.date === b.date ? 0 : (a.date > b.date ? 1 : -1))) .map(t => `${t['todo-item-id']}: ${t['project-name']} : ${t['todo-item-name']}`); new Set(descriptions).forEach(t => console.log(t)); }; /** * Prints the entries for the given date */ const printDateEntries = (date) => { const timeEntries = teamwork.getTimeEntries(date, date); let total = 0; timeEntries.forEach(t => { const hours = (t.hours * 1.0 + t.minutes / 60.0); console.log('\n ' + t.description); console.log(` Project: ${t['project-name']}`); console.log(` TaskName: ${t['todo-item-name']}`); console.log(` TaskId: ${t['todo-item-id']}`); console.log(` Billable: ${t.isbillable == 1 ? "Yes" : "No"}`); console.log(` Hours: ${hours.toFixed(2)}`); total = total + hours; }); console.log(' \nTotal: ' + total.toFixed(2)); }; /** * @param data must be n x m array */ const logTable = (data) => { if (data.length === 0) { return; } const lengths = data[0].map(d => 0); data.forEach(row => { row.forEach((col, idx) => { if (lengths[idx] < col.length) { lengths[idx] = col.length; } }); }); data.forEach(row => { const rowStr = row.reduce((str, col, idx) => { const padding = ' '.repeat(lengths[idx] - col.length + 2); return str + col + padding; }, ''); console.log(rowStr); }); }; const getSinceDate = (date) => { if (!date || date.toLowerCase() === 'week') { const sunday = new Date(); sunday.setDate(sunday.getDate() - sunday.getDay()); return dateFormat(sunday, 'yyyymmdd'); } else if (date.toLowerCase() === 'month') { const theFirst = new Date(); theFirst.setDate(1); return dateFormat(theFirst, 'yyyymmdd'); } else { return date; } }; const printPercentages = (date) => { const sinceDate = getSinceDate(date); const timeEntries = teamwork.getTimeEntries( sinceDate, dateFormat(new Date(), 'yyyymmdd')); const projects = {}; timeEntries.forEach(entry => { const currentHours = projects[entry['project-id']]; let hours = currentHours ? currentHours : 0; hours += Number(entry.hours); hours += Number(entry.minutes) / 60.0; projects[entry['project-id']] = hours; }); const total = Object.keys(projects).reduce((t, key) => t + projects[key], 0); const twProjects = teamwork.getProjects(); const data = Object.keys(projects).map(proj => { const twp = twProjects.find(p => p.id === proj); return [twp ? twp.name : proj, projects[proj].toFixed(1) + 'h', (100 * projects[proj] / total).toFixed(1) + '%']; }); const month = sinceDate.substr(4, 2); const day = sinceDate.substr(6, 2); console.log(`\nProject totals since ${month}/${day}\n`); logTable([ ['Project', 'Total', 'Percent'], ...data, ['Total', total.toFixed(1) + 'h', '100.0%'] ]); }; const moveTimeEntry = (entry, taskId) => { console.log('tags: ', entry.tags); sendTimeEntry({ taskId, description: entry.description, date: dateFormat(new Date(entry.date), 'yyyymmdd'), hours: entry.hours, minutes: entry.minutes, isbillable: entry.isbillable, tags: entry.tags.map(t => t.name), }); teamwork.deleteTimeEntry(entry.id); }; /** * Search for tasks using given searchTerm * @param searchTerm text to search for * @param projectId (optional) limit search to project * @param taskListId (optional) limit search to task list */ const searchForTask = (searchTerm, projectId, taskListId) => { return teamwork.searchForTask(searchTerm, projectId, taskListId); }; /** * Tests if string is of format yyyymmdd */ const isDateString = (str) => { return /^\s*(20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01]))\s*$/.test(str); } let getPreviousMonday = () => { var date = new Date(); var day = date.getDay() || 7; return new Date().setDate(date.getDate() - day); } const DAYS_OF_WEEK = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; const getOffsetOfPrevious = (dayOfWeek) => { dayOfWeek = DAYS_OF_WEEK.indexOf(dayOfWeek.toLowerCase().substr(0, 3)); if (dayOfWeek <= 0) { console.warn("Day of week must start with " + DAYS_OF_WEEK); return 0; } let offset = dayOfWeek - (new Date()).getDay(); if (offset >= 0) { offset -= 7; } return offset; } const getDateString = (str) => { str = str.trim().toLowerCase(); let offset = 0; if (/^[-+]?\d+$/.test(str)) { offset = Number(str); } else if (str.startsWith("yest")) { // yesterday offset = -1; } else if (str.startsWith("tom")) { // tomorrow offset = 1; } else if (str.startsWith("tod")) { // today offset = 0; } else if (str.startsWith("next")) { // next <day of week> offset = (getOffsetOfPrevious(str.split(' ')[1]) + 7) || 7; } else if (str.startsWith("last ")) { // last <day of week> offset = getOffsetOfPrevious(str.split(' ')[1]); } else if (DAYS_OF_WEEK.indexOf(str.substr(0, 3) >= 0)) { // same as last day of week offset = getOffsetOfPrevious(str); } else { console.warn("Unrecognized date. Using today."); } // get the date const date = new Date(); if (offset > 1000 || offset < -1000) { console.warn("Variable date out of range. Using today."); } else { date.setDate(date.getDate() + offset); } return dateFormat(date, "yyyymmdd"); } /** * send time entry request - if taskID is not a number then it checks * if it is a favorite */ const sendTimeEntry = (entry) => { if (isNaN(entry.taskId)) { entry.taskId = userData.get().favorites[entry.taskId]; } if (!isDateString(entry.date)) { entry.date = getDateString(entry.date); } if (entry.tags) { entry.tags = entry.tags.join(','); } return teamwork.sendTimeEntry(entry); }; const startTimer = (id) => { const timers = userData.get().timers; const timer = timers[id]; if (!timer || !isToday(timer.started)) { timers[id] = { started: new Date(), running: true, duration: 0 } } else if (!timer.running) { timer.started = new Date(); timer.running = true; } userData.save(); }; const stopTimer = (id) => { const timers = userData.get().timers; const timer = timers[id]; if (timer && timer.running && isToday(timer.started)) { timer.duration += new Date() - timer.started; timer.running = false; userData.save(); } }; const modifyTimer = (id, hours, minutes) => { const timers = userData.get().timers; if (id && timers[id] && (hours !== 0 || minutes !== 0)) { const timer = timers[id]; timer.duration += hours * 3600000; timer.duration += minutes * 60000; userData.save(); return timer.duration; } else { console.log('Timer ' + id + ' not found'); return 0; } }; const listFavorites = (verbose) => { const { favorites } = userData.get(); if (!verbose) { console.log(Object.keys(favorites).join('\n')); } else { const longest = Object.keys(favorites) .reduce((a, b) => Math.max(a, b.length), 0); Object.keys(favorites).forEach(name => { const taskId = favorites[name]; const task = teamwork.getTask(taskId); const padding = name.length < longest ? ' '.repeat(longest - name.length) : ''; console.log(`${name}: ${padding + taskId} - ${task['project-name']} / ${task['todo-list-name']} / ${task.content}`); }); } }; const listTimers = () => { const { timers } = userData.get(); console.log(Object.keys(timers).join('\n')); }; const parseDateYYYYMMDD = (str) => { if (!str || str.length !== 8) { return null; } const year = Number(str.substr(0, 4)); const month = Number(str.substr(4, 2)); const day = Number(str.substr(6, 2)); return new Date(year, month - 1, day); }; const printItem = (str) => { if (!str) { str = 'time-worked'; } const { arrived, timers } = userData.get(); switch (str.toLowerCase()) { case 'time-worked': console.log(getTimeWorkedString(arrived, timers)); break; case 'timers': if (timers) { console.log( Object.keys(timers) .filter(key => timers[key].running) .map(key => { const timer = timers[key]; const duration = timer.duration + (new Date() - timer.started); return `${key}: ${getDurationString(duration)}`; }).join(', ') ); } break; } }; module.exports = { getDurationString, getSinceDate, listFavorites, listTimers, modifyTimer, moveTimeEntry, parseDateYYYYMMDD, printDateEntries, printItem, printPercentages, printPreviousTasks, printTimeLogged, searchForTask, sendTimeEntry, startTimer, stopTimer, };