mite-cli
Version:
command line tool for time tracking service mite.de
283 lines (256 loc) • 10.1 kB
JavaScript
#!/usr/bin/env node
'use strict';
const program = require('commander');
const colors = require('ansi-colors');
const miteApi = require('mite-api');
const pkg = require('./../package.json');
const config = require('./config');
const formater = require('./lib/formater');
const DataOutput = require('./lib/data-output');
const listCommand = require('./lib/commands/list');
const columnOptions = require('./lib/options/columns');
const commandOptions = require('./lib/options');
const { handleError } = require('./lib/errors');
const { guessRequestParamsFromPeriod } = require('./lib/period');
const { FORMAT } = require('./lib/data-output');
// no other options or comamnd line arguments used, default "period" to "today"
if (process.argv.length === 2) {
process.argv.splice(2, 0, 'today');
}
program
.version(pkg.version)
.arguments('[period]')
.description('list time entries', {
period:
`name of the period for which the time entries should be shown. \
Can be single dates, duraions and weekday names: \n\
- "today" shows todays time entries (default)\n\
- "last_week" or "last-week" shows entries from the whole last week\n\
- "7days", "2days", "3m" shows time entries since 7 days, 2days or 3 months\n\
- "cw2" for the calendar week 2, MO-SO\n\
- "2021 cw3" for 2021 calendar week 3, Mo-So\n\
- "2019-10-12" shows all entries from that exact date\n\
- "2021-01" entries for january 2021, or even shorter "211"\n\
- "2020" all entries from 2020\n\
- "friday", "fr" or other weekday names or abbreviations show all entries since \
this last weekday`
})
.option.apply(program, commandOptions.toArgs(commandOptions.billable),
'show entries which are billable or not billable'
)
.option(
'--columns <columns>',
columnOptions.description(listCommand.columns.options),
columnOptions.parse
)
.option(
'--customer-id <id>',
'customer id, can be either a single ID, or multiple comma-separated IDs.'
)
.option.apply(program, commandOptions.toArgs(commandOptions.json))
.option.apply(program, commandOptions.toArgs(commandOptions.plain))
.option.apply(program, commandOptions.toArgs(commandOptions.pretty))
.option(
'--from <period|YYYY-MM-DD>',
'in combination with "to" used for selecting a specific time frame of ' +
'time entries to return, same as "period" argument or a specific date in ' +
'the format "YYYY-MM-DD". The period argument is ignored.'
)
.option(
'--group-by <column>',
'optional grouping parameter which should be used. Valid values are ' +
listCommand.groupBy.options.join(', ')
)
.option(
'-l, --limit <limit>',
'optional number of items to show, default is infinite',
undefined,
((val) => parseInt(val, 10))
)
.option(
'--locked <true|false>',
'filter entries by their locked status',
(val => ['true', 'yes', 'ja', 'ok', '1'].indexOf(val.toLowerCase()) > -1),
null
)
.option(
'--min-duration <minDuration>',
'filter out entries which have a duration below the given value (client side)',
)
.option(
'--max-duration <maxDuration>',
'filter out entries which have a duration above the given value (client side)',
)
.option(
'--project-id <id>',
'project id, can be either a single ID, or multiple comma-separated IDs.'
)
.option(
'--to <period|YYYY-MM-DD>',
'in combination with "from" used for selecting a specific time frame of ' +
'time entries to return, same as "period" argument or a specific date in ' +
'the format "YYYY-MM-DD". The period argument is ignored.'
)
.option.apply(program, commandOptions.toArgs(commandOptions.tracking))
.option(
'--reversed',
'reverse the sorting direction',
)
.option(
'-s --search <query>',
'search within the notes, to filter by multiple criteria connected with OR use comma-separated query values',
((val) => {
if (typeof val === 'string') {
return val.split(/\s*,\s*/);
}
return val;
})
)
.option(
'--service-id <id>',
'service id, can be either a single ID, or multiple comma-separated IDs.'
)
.option.apply(program, commandOptions.toArgs(
commandOptions.sort,
commandOptions.sort.description(listCommand.sort.options),
listCommand.sort.default
))
.option(
'--user-id <id>',
'optional single user id who’s time entries should be returned or ' +
'multiple values comma-separated. Note that the current user may not ' +
'have the permission ot read other user’s time entries which will result ' +
'in an empty response',
((val) => {
if (typeof val === 'string') {
return val.split(/\s*,\s*/);
}
return val;
})
)
.addHelpText('after', `
Examples:
list all entries from the current month:
mite list this_month
list all entries which note contains the given search query:
mite list this_year --search JIRA-123
show all entries from two services 123 and 38171
mite list last_month --service-id 123,38171
show all time entries from a specific date
mite list 2019-10-21
show time entries from last thursday
mite list thursday
show all users who tracked billable entries ordered by the amount of time
they have tracked:
mite list this_year --billable true --columns user,duration --group-by user --sort duration
export all time-entries from the current month as csv:
mite list last_week --json --columns user,id | jq -rM '.[] | @csv'
create a report of all customer and their generated profits:
mite list this_year --group-by customer --sort revenue
The output of mite list can be forwarded to other commands using xargs. The
following example will delete all matchin entries:
mite list this_month --search="query" --columns id --plain | xargs -n1 mite delete
Get the notes for one specific service, project for the last month to put
them on a bill or similar
mite list last_month --project-id 2681601 --service-id 325329 --columns note --plain | sort -u
Show a seperate report for all users showing the revenues and times per
service for all users matching a query:
mite users --search marc --columns id --plain | xargs mite list last_month --group-by service --user-id
Create PDF with time entries from a specific project for the last month
NO_COLOR=1 mite list last-month --project-id 1234 --columns date,service,note,duration --json | jq -rM '.[] | @csv' | npx csv2md
`)
.parse();
/**
* Returns the request options for requesting the time entries or grouped
* time entries for the given time entries and filtering options.
*
* @param {string} period
* @param {object} opts
* @return {Object<String, any>} time entries or grouped time entries
*/
function getRequestOptions(period, opts) {
return {
...guessRequestParamsFromPeriod(period),
...(typeof opts.billable === 'boolean' && { billable: opts.billable }),
...(opts.customerId && { customer_id: opts.customerId }),
...(opts.from && { from: opts.from }),
...(opts.groupBy && { group_by: opts.groupBy }),
...(opts.limit && { limit: opts.limit }),
...(typeof opts.locked === 'boolean' && { locked: opts.locked }),
...(opts.projectId && { project_id: opts.projectId }),
...(opts.reversed && { direction: 'asc' }),
...(opts.search && { note: opts.search }),
...(opts.serviceId && { service_id: opts.serviceId }),
...(opts.sort && { sort: opts.sort }),
...(typeof opts.tracking === 'boolean' && { tracking: opts.tracking }),
...(opts.to && { to: opts.to }),
...(opts.userId && { user_id: opts.userId}),
};
}
/**
* Callback function to filter time entries which are shown depending on the
* command line options
*
* @param {object} opts
* @param {MiteTimeEntry} entry
* @return {boolean}
*/
function filterData(opts, row) {
let valid = true;
if (opts.minDuration) {
const minDuration = formater.durationToMinutes(opts.minDuration, 10);
valid &= row.minutes >= minDuration;
}
if (opts.maxDuration) {
const maxDuration = formater.durationToMinutes(opts.maxDuration, 10);
valid &= row.minutes <= maxDuration;
}
return valid;
}
/**
* @param {array<object>} items
* @param {import('./lib/data-output').ColumnDefinition[]} columns
* @param {import('./lib/data-output').FORMAT} format
* @param {boolean} [includeSums=true]
* @returns {array<object>}
*/
function getReport(items, columns, format, includeSums = true) {
const tableData = DataOutput.compileTableData(items, columns, format);
// Table footer
// add table footer if any of the table columns has a reducer
if (includeSums && columnOptions.hasReducer(columns)) {
let footerColumns = DataOutput.getTableFooterColumns(items, columns);
footerColumns = footerColumns.map(v => v ? colors.bold(v) : v); // make footer bold
tableData.push(footerColumns);
}
return DataOutput.formatData(tableData, format, columns);
}
function main(period) {
const mite = miteApi(config.get());
const opts = program.opts();
const requestOpts = getRequestOptions(period, opts);
// "columns" default option is different
if (!opts.columns) {
// when groupBy is used, make sure that revenue and duration are used
if (opts.groupBy) {
opts.columns = opts.groupBy + ',revenue,duration';
} else {
opts.columns = config.get().listColumns;
}
}
const columns = columnOptions.resolve(opts.columns, listCommand.columns.options);
mite.getTimeEntries(requestOpts, (err, results) => {
if (err) return handleError(err);
// decide wheter to output grouped report or single entry report
let items = results
.map(data => data[opts.groupBy ? 'time_entry_group' : 'time_entry'])
.filter(v => v)
.filter(filterData.bind(this, opts))
;
const format = DataOutput.getFormatFromOptions(opts, config);
const report = getReport(items, columns, format, format === FORMAT.TABLE);
process.stdout.write(`${report}\n`);
});
} // main
const period = program.args[0];
main(period);