mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
CmdTimesheet: Rewrote the command
- Updated the 'timesheet' command with a more compact report that accepts a filter, and has a default filter showing the last four weeks of completed and started tasks.
This commit is contained in:
parent
4c20ff04c2
commit
440cfb009e
10 changed files with 182 additions and 190 deletions
|
@ -26,6 +26,7 @@
|
|||
|
||||
#include <cmake.h>
|
||||
#include <CmdTimesheet.h>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <stdlib.h>
|
||||
#include <Context.h>
|
||||
|
@ -33,6 +34,7 @@
|
|||
#include <Table.h>
|
||||
#include <Datetime.h>
|
||||
#include <main.h>
|
||||
#include <util.h>
|
||||
#include <i18n.h>
|
||||
#include <format.h>
|
||||
|
||||
|
@ -42,16 +44,16 @@ extern Context context;
|
|||
CmdTimesheet::CmdTimesheet ()
|
||||
{
|
||||
_keyword = "timesheet";
|
||||
_usage = "task timesheet [weeks]";
|
||||
_usage = "task [filter] timesheet";
|
||||
_description = STRING_CMD_TIMESHEET_USAGE;
|
||||
_read_only = true;
|
||||
_displays_id = false;
|
||||
_needs_gc = true;
|
||||
_uses_context = false;
|
||||
_accepts_filter = false;
|
||||
_accepts_filter = true;
|
||||
_accepts_modifications = false;
|
||||
_accepts_miscellaneous = true;
|
||||
_category = Command::Category::graphs;
|
||||
_accepts_miscellaneous = false;
|
||||
_category = Command::Category::report;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -59,165 +61,145 @@ int CmdTimesheet::execute (std::string& output)
|
|||
{
|
||||
int rc = 0;
|
||||
|
||||
// Scan the pending tasks.
|
||||
handleRecurrence ();
|
||||
std::vector <Task> all = context.tdb2.all_tasks ();
|
||||
|
||||
// What day of the week does the user consider the first?
|
||||
int weekStart = Datetime::dayOfWeek (context.config.get ("weekstart"));
|
||||
if (weekStart != 0 && weekStart != 1)
|
||||
throw std::string (STRING_DATE_BAD_WEEKSTART);
|
||||
|
||||
// Determine the date of the first day of the most recent report.
|
||||
Datetime today;
|
||||
Datetime start;
|
||||
start -= (((today.dayOfWeek () - weekStart) + 7) % 7) * 86400;
|
||||
|
||||
// Roll back to midnight.
|
||||
start = Datetime (start.year (), start.month (), start.day ());
|
||||
Datetime end = start + (7 * 86400);
|
||||
|
||||
// Determine how many reports to run.
|
||||
int quantity = 1;
|
||||
std::vector <std::string> words = context.cli2.getWords ();
|
||||
if (words.size () == 1)
|
||||
quantity = strtol (words[0].c_str (), NULL, 10);;
|
||||
|
||||
std::stringstream out;
|
||||
for (int week = 0; week < quantity; ++week)
|
||||
// Detect a filter.
|
||||
bool hasFilter {false};
|
||||
for (auto& a : context.cli2._args)
|
||||
{
|
||||
Datetime endString (end);
|
||||
endString -= 86400;
|
||||
if (a.hasTag ("FILTER"))
|
||||
{
|
||||
hasFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string title = start.toString (context.config.get ("dateformat"))
|
||||
+ " - "
|
||||
+ endString.toString (context.config.get ("dateformat"));
|
||||
if (! hasFilter)
|
||||
{
|
||||
auto defaultFilter = context.config.get ("report.timesheet.filter");
|
||||
if (defaultFilter == "")
|
||||
defaultFilter = "(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)";
|
||||
context.cli2.addFilter (defaultFilter);
|
||||
}
|
||||
|
||||
Color bold;
|
||||
if (context.color ())
|
||||
bold = Color ("bold");
|
||||
// Apply filter to get a set of tasks.
|
||||
handleRecurrence ();
|
||||
Filter filter;
|
||||
std::vector <Task> filtered;
|
||||
filter.subset (filtered);
|
||||
|
||||
out << '\n'
|
||||
<< bold.colorize (title)
|
||||
// Subset the tasks to only those that are either completed or started.
|
||||
// The _key attribute is represents either the 'start' or 'end' date.
|
||||
int num_completed = 0;
|
||||
int num_started = 0;
|
||||
std::vector <Task> shown;
|
||||
for (auto& task : filtered)
|
||||
{
|
||||
if (task.getStatus () == Task::completed)
|
||||
{
|
||||
task.set ("_key", task.get ("end"));
|
||||
++num_completed;
|
||||
}
|
||||
|
||||
if (task.getStatus () == Task::pending && task.has ("start"))
|
||||
{
|
||||
task.set ("_key", task.get ("start"));
|
||||
++num_started;
|
||||
}
|
||||
|
||||
shown.push_back (task);
|
||||
}
|
||||
|
||||
// Sort tasks by _key.
|
||||
std::sort (shown.begin (),
|
||||
shown.end (),
|
||||
[](const Task& a, const Task& b) { return a.get ("_key") < b.get ("_key"); });
|
||||
|
||||
// Render the completed table.
|
||||
Table table;
|
||||
table.width (context.getWidth ());
|
||||
if (context.config.getBoolean ("obfuscate"))
|
||||
table.obfuscate ();
|
||||
table.add ("Wk");
|
||||
table.add ("Date");
|
||||
table.add ("Day");
|
||||
table.add ("Action");
|
||||
table.add ("Project");
|
||||
table.add ("Due");
|
||||
table.add ("Task");
|
||||
|
||||
if (context.color ())
|
||||
{
|
||||
table.forceColor ();
|
||||
table.colorHeader (Color ("underline " + context.config.get ("color.label")));
|
||||
table.colorOdd (Color (context.config.get ("color.alternate")));
|
||||
}
|
||||
else
|
||||
table.underlineHeaders ();
|
||||
|
||||
auto dateformat = context.config.get ("dateformat");
|
||||
|
||||
int previous_week = -1;
|
||||
std::string previous_date = "";
|
||||
std::string previous_day = "";
|
||||
int weekCounter = 0;
|
||||
Color week_color;
|
||||
for (auto& task : shown)
|
||||
{
|
||||
Datetime key (task.get_date ("_key"));
|
||||
|
||||
std::string label = task.has ("end") ? "Completed"
|
||||
: task.has ("start") ? "Started"
|
||||
: "";
|
||||
|
||||
auto week = key.week ();
|
||||
auto date = key.toString (dateformat);
|
||||
auto due = task.has ("due") ? Datetime (task.get ("due")).toString (dateformat) : "";
|
||||
auto day = Datetime::dayNameShort (key.dayOfWeek ());
|
||||
|
||||
Color task_color;
|
||||
autoColorize (task, task_color);
|
||||
|
||||
// Add a blank line between weeks.
|
||||
if (week != previous_week && previous_week != -1)
|
||||
{
|
||||
auto row = table.addRowEven ();
|
||||
table.set (row, 0, " ");
|
||||
}
|
||||
|
||||
// Keep track of unique week numbers.
|
||||
if (week != previous_week)
|
||||
++weekCounter;
|
||||
|
||||
// User-defined oddness.
|
||||
int row;
|
||||
if (weekCounter % 2)
|
||||
row = table.addRowOdd ();
|
||||
else
|
||||
row = table.addRowEven ();
|
||||
|
||||
// If the data doesn't change, it doesn't get shown.
|
||||
table.set (row, 0, (week != previous_week ? format ("W{1}", week) : ""));
|
||||
table.set (row, 1, (date != previous_date ? date : ""));
|
||||
table.set (row, 2, (day != previous_day ? day : ""));
|
||||
table.set (row, 3, label);
|
||||
table.set (row, 4, task.get ("project"));
|
||||
table.set (row, 5, due);
|
||||
table.set (row, 6, task.get ("description"), task_color);
|
||||
|
||||
previous_week = week;
|
||||
previous_date = date;
|
||||
previous_day = day;
|
||||
}
|
||||
|
||||
// Render the table.
|
||||
std::stringstream out;
|
||||
if (table.rows ())
|
||||
out << optionalBlankLine ()
|
||||
<< table.render ()
|
||||
<< '\n';
|
||||
|
||||
// Render the completed table.
|
||||
Table completed;
|
||||
completed.width (context.getWidth ());
|
||||
completed.add (" ");
|
||||
completed.add (STRING_COLUMN_LABEL_PROJECT);
|
||||
completed.add (STRING_COLUMN_LABEL_DUE, false);
|
||||
completed.add (STRING_COLUMN_LABEL_DESC);
|
||||
|
||||
Color label;
|
||||
if (context.color ())
|
||||
{
|
||||
label = Color (context.config.get ("color.label"));
|
||||
completed.colorHeader (label);
|
||||
}
|
||||
|
||||
for (auto& task : all)
|
||||
{
|
||||
// If task completed within range.
|
||||
if (task.getStatus () == Task::completed)
|
||||
{
|
||||
Datetime compDate (task.get_date ("end"));
|
||||
if (compDate >= start && compDate < end)
|
||||
{
|
||||
Color c;
|
||||
autoColorize (task, c);
|
||||
|
||||
int row = completed.addRow ();
|
||||
std::string format = context.config.get ("dateformat.report");
|
||||
if (format == "")
|
||||
format = context.config.get ("dateformat");
|
||||
completed.set (row, 1, task.get ("project"), c);
|
||||
|
||||
if(task.has ("due"))
|
||||
{
|
||||
Datetime dt (task.get_date ("due"));
|
||||
completed.set (row, 2, dt.toString (format));
|
||||
}
|
||||
|
||||
std::string description = task.get ("description");
|
||||
int indent = context.config.getInteger ("indent.annotation");
|
||||
|
||||
for (auto& ann : task.getAnnotations ())
|
||||
description += '\n'
|
||||
+ std::string (indent, ' ')
|
||||
+ Datetime (ann.first.substr (11)).toString (context.config.get ("dateformat"))
|
||||
+ ' '
|
||||
+ ann.second;
|
||||
|
||||
completed.set (row, 3, description, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out << " " << format (STRING_CMD_TIMESHEET_DONE, completed.rows ()) << '\n';
|
||||
|
||||
if (completed.rows ())
|
||||
out << completed.render ()
|
||||
<< '\n';
|
||||
|
||||
// Now render the started table.
|
||||
Table started;
|
||||
started.width (context.getWidth ());
|
||||
started.add (" ");
|
||||
started.add (STRING_COLUMN_LABEL_PROJECT);
|
||||
started.add (STRING_COLUMN_LABEL_DUE, false);
|
||||
started.add (STRING_COLUMN_LABEL_DESC);
|
||||
started.colorHeader (label);
|
||||
|
||||
for (auto& task : all)
|
||||
{
|
||||
// If task started within range, but not completed withing range.
|
||||
if (task.getStatus () == Task::pending &&
|
||||
task.has ("start"))
|
||||
{
|
||||
Datetime startDate (task.get_date ("start"));
|
||||
if (startDate >= start && startDate < end)
|
||||
{
|
||||
Color c;
|
||||
autoColorize (task, c);
|
||||
|
||||
int row = started.addRow ();
|
||||
std::string format = context.config.get ("dateformat.report");
|
||||
if (format == "")
|
||||
format = context.config.get ("dateformat");
|
||||
started.set (row, 1, task.get ("project"), c);
|
||||
|
||||
if (task.has ("due"))
|
||||
{
|
||||
Datetime dt (task.get_date ("due"));
|
||||
started.set (row, 2, dt.toString (format));
|
||||
}
|
||||
|
||||
std::string description = task.get ("description");
|
||||
int indent = context.config.getInteger ("indent.annotation");
|
||||
|
||||
for (auto& ann : task.getAnnotations ())
|
||||
description += '\n'
|
||||
+ std::string (indent, ' ')
|
||||
+ Datetime (ann.first.substr (11)).toString (context.config.get ("dateformat"))
|
||||
+ ' '
|
||||
+ ann.second;
|
||||
|
||||
started.set (row, 3, description, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out << " " << format (STRING_CMD_TIMESHEET_STARTED, started.rows ()) << '\n';
|
||||
|
||||
if (started.rows ())
|
||||
out << started.render ()
|
||||
<< "\n\n";
|
||||
|
||||
// Prior week.
|
||||
start -= 7 * 86400;
|
||||
end -= 7 * 86400;
|
||||
}
|
||||
if (context.verbose ("affected"))
|
||||
out << format ("{1} completed, {2} started.", num_completed, num_started)
|
||||
<< '\n';
|
||||
|
||||
output = out.str ();
|
||||
return rc;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue