diff --git a/cli/trakcli/report/commands/main.py b/cli/trakcli/report/commands/main.py index 2d760cd..371a758 100644 --- a/cli/trakcli/report/commands/main.py +++ b/cli/trakcli/report/commands/main.py @@ -1,14 +1,17 @@ -from datetime import datetime, timedelta -from typing import Annotated -from rich.panel import Panel +from datetime import datetime +from typing import Annotated, Optional import typer from rich import print as rprint from rich.table import Table from trakcli.database.basic import get_db_content -from trakcli.utils.print_with_padding import print_with_padding -from trakcli.utils.same_week import same_week +from trakcli.report.commands.main_functions import ( + create_details_table, + filter_records, + get_grouped_records, + get_table_title, +) ALL_PROJECTS = "all" @@ -23,6 +26,14 @@ def report( help="Consider only the billable records.", ), ] = False, + details: Annotated[ + bool, + typer.Option( + "--details", + "-d", + help="Show all sessions that occurred in the chosen period in detail.", + ), + ] = False, today: Annotated[ bool, typer.Option( @@ -59,150 +70,85 @@ def report( ), ] = False, start: Annotated[ - str, + Optional[datetime], typer.Option( "--start", help="Start date (e.g. 2023-10-08) for the time range. If --end is not provided, trak will report the data for the provided date.", + formats=["%Y-%m-%d"], ), - ] = "", + ] = None, end: Annotated[ - str, + Optional[datetime], typer.Option( "--end", help="End date (e.g. 2023-11-24) for the time range. Won't work without the start flag.", + formats=["%Y-%m-%d"], ), - ] = "", + ] = None, ): """Get reports for your projects.""" - parsed_json = get_db_content() - - table_title = "Report" - if today: - table_title += " for today" - elif yesterday: - table_title += " for yestarday" - elif week: - table_title += " for this week" - elif month: - table_title += " for this month" - elif year: - table_title += " for this year" - elif start and end == "": - table_title += f" for the day {start}" - elif start and end: - table_title += f" for the period from {start} to {end}" - - table = Table(title=table_title) - - table.add_column("🏷️ Project", style="cyan", no_wrap=True) - table.add_column("🧮 Time spent", style="magenta") - - actual_month = datetime.today().month - actual_year = datetime.today().year - - start_date = datetime.today() - end_date = datetime.today() - try: - if start: - start_date = datetime.fromisoformat(start).date() - if end: - end_date = datetime.fromisoformat(end).date() - except ValueError: - rprint( - Panel( - title="🔴 Invalid date", - renderable=print_with_padding( - ( - "The provided date it's invalid." - "\n\n" - f"Try with a date like {datetime.now().date()}." - ) - ), - ) - ) + db_content = get_db_content() + + report_table_title = get_table_title( + today, yesterday, week, month, year, start, end + ) - return + main_table = Table(title=report_table_title) - grouped = {} + main_table.add_column("🏷️ Project", style="cyan", no_wrap=True) + main_table.add_column("🧮 Time spent", style="magenta") - for record in parsed_json: - record_project = record.get("project", False) - if record_project: - if record_project == project or project == ALL_PROJECTS: - if isinstance(grouped.get(record_project, False), list): - grouped[record_project].append(record) - else: - grouped[record_project] = [record] + grouped = get_grouped_records(project, db_content, ALL_PROJECTS) + records = [] + details_tables = [] + total_acc_seconds = 0 for g in grouped: - records = grouped[g] - - if billable: - records = [ - record for record in grouped[g] if record["billable"] == billable - ] - - if yesterday: - records = [ - record - for record in records - if datetime.fromisoformat(record["end"]).date() - == datetime.today().date() - timedelta(1) - ] - elif today: - records = [ - record - for record in records - if datetime.fromisoformat(record["end"]).date() - == datetime.today().date() - ] - elif week: - records = [ - record - for record in records - if same_week( - datetime.fromisoformat(record["end"]).date().strftime("%Y%m%d"), - ) - ] - elif month: - records = [ - record - for record in records - if datetime.fromisoformat(record["end"]).month == actual_month - and datetime.fromisoformat(record["end"]).year == actual_year - ] - elif start and end == "": - records = [ - record - for record in records - if datetime.fromisoformat(record["end"]).date() == start_date - ] - elif start and end: - records = [ - record - for record in records - if datetime.fromisoformat(record["end"]).date() >= start_date - and datetime.fromisoformat(record["end"]).date() <= end_date - ] + records = filter_records( + grouped[g], billable, yesterday, today, week, month, start, end + ) acc_seconds = 0 for record in records: - start_datetime = datetime.fromisoformat(record["start"]) - end_datetime = datetime.fromisoformat(record["end"]) + record_start = record.get("start", "") + record_end = record.get("end", "") - diff = end_datetime - start_datetime + if record_start != "" and record_end != "": + start_datetime = datetime.fromisoformat(record_start) + end_datetime = datetime.fromisoformat(record_end) - acc_seconds = acc_seconds + diff.seconds + diff = end_datetime - start_datetime - m, _ = divmod(diff.seconds, 60) - h, m = divmod(m, 60) + acc_seconds = acc_seconds + diff.seconds + m, _ = divmod(diff.seconds, 60) + h, m = divmod(m, 60) + + total_acc_seconds += acc_seconds m, _ = divmod(acc_seconds, 60) h, m = divmod(m, 60) - table.add_row(g, f"[bold]{h}h {m}m[/bold]") + main_table.add_row(g, f"[bold]{h}h {m}m[/bold]") + + if details and len(records): + details_tables.append(create_details_table(g, records)) rprint("") - rprint(table) + + # Add Total if all projects + if project == ALL_PROJECTS: + m, _ = divmod(total_acc_seconds, 60) + h, m = divmod(m, 60) + + main_table.add_section() + main_table.add_row("Total", f"[bold]{h}h {m}m[/bold]") + + # Print summary report table + rprint(main_table) + + # Print details + for details_table in details_tables: + rprint("") + rprint(details_table) diff --git a/cli/trakcli/report/commands/main_functions.py b/cli/trakcli/report/commands/main_functions.py new file mode 100644 index 0000000..8b0a83a --- /dev/null +++ b/cli/trakcli/report/commands/main_functions.py @@ -0,0 +1,139 @@ +from datetime import datetime, timedelta + +from rich.table import Table + +from trakcli.utils.format_date import format_date +from trakcli.utils.same_week import same_week + + +def get_table_title(today, yesterday, week, month, year, start, end): + table_title = "Report" + + if today: + table_title += " for today" + elif yesterday: + table_title += " for yestarday" + elif week: + table_title += " for this week" + elif month: + table_title += " for this month" + elif year: + table_title += " for this year" + elif start and end == "": + table_title += f" for the day {start}" + elif start and end: + table_title += f" for the period from {start} to {end}" + + return table_title + + +def create_details_table(project, records): + details_table = Table(title=f"Sessions for {project}") + + details_table.add_column("Start", style="green", no_wrap=True) + details_table.add_column("End", style="orange3", no_wrap=True) + details_table.add_column("Category", style="steel_blue1") + details_table.add_column("Tag", style="steel_blue3") + details_table.add_column("Hours", style="yellow", no_wrap=True) + details_table.add_column("Billable") + + for record in records: + record_start = record.get("start", "") + record_end = record.get("end", "") or datetime.now().isoformat() + + h, m = 0, 0 + + if record_start != "": + start_datetime = datetime.fromisoformat(record_start) + end_datetime = datetime.fromisoformat(record_end) + + diff = end_datetime - start_datetime + + m, _ = divmod(diff.seconds, 60) + h, m = divmod(m, 60) + + details_table.add_row( + format_date(record["start"]), + format_date(record["end"]) if record["end"] != "" else "🏃 Ongoing", + record["category"] or "---", + record["tag"] or "---", + f"{h}h {m}m" if record_start != "" else "", + "✅" if record["billable"] else "", + ) + + return details_table + + +def get_grouped_records(project, records, all): + grouped = {} + for record in records: + record_project = record.get("project", False) + + if record_project: + if record_project == project or project == all: + if isinstance(grouped.get(record_project, False), list): + grouped[record_project].append(record) + else: + grouped[record_project] = [record] + + return grouped + + +def filter_records(records, billable, yesterday, today, week, month, start, end): + actual_month = datetime.today().month + actual_year = datetime.today().year + + if billable: + records = [record for record in records if record["billable"] == billable] + + # Only one time filer type is allowed + if yesterday: + records = [ + record + for record in records + if record["end"] + and datetime.fromisoformat(record["end"]).date() + == datetime.today().date() - timedelta(1) + ] + elif today: + records = [ + record + for record in records + if record["end"] + and datetime.fromisoformat(record["end"]).date() == datetime.today().date() + ] + elif week: + records = [ + record + for record in records + if record["end"] + and same_week( + datetime.fromisoformat(record["end"]).date().strftime("%Y%m%d"), + ) + ] + elif month: + records = [ + record + for record in records + if record["start"] + and record["end"] + and datetime.fromisoformat(record["end"]).month == actual_month + and datetime.fromisoformat(record["end"]).year == actual_year + ] + elif start is not None and end is None: + records = [ + record + for record in records + if record.get("end", "") != "" + and datetime.fromisoformat(record["end"]).date() == start.date() + ] + elif start is not None and end is not None: + records = [ + record + for record in records + if record.get("end", "") != "" + and datetime.fromisoformat(record["end"]).date() >= start.date() + and datetime.fromisoformat(record["end"]).date() <= end.date() + ] + + return records