diff --git a/cli/trakcli/config/models.py b/cli/trakcli/config/models.py index 0619e36..e5379ed 100644 --- a/cli/trakcli/config/models.py +++ b/cli/trakcli/config/models.py @@ -8,4 +8,4 @@ class Project(NamedTuple): categories: list[str] = [] tags: list[str] = [] customer: str = "" - fare: int = 1 + rate: int = 1 diff --git a/cli/trakcli/create/commands/project.py b/cli/trakcli/create/commands/project.py index afff2c6..cbd619d 100644 --- a/cli/trakcli/create/commands/project.py +++ b/cli/trakcli/create/commands/project.py @@ -50,7 +50,7 @@ def create_project( else [], tags=[t.strip() for t in tags.split(",")] if tags != "" else [], customer=customer, - fare=hour_rate, + rate=hour_rate, ) with open(details_path, "w") as details_file: diff --git a/cli/trakcli/create/commands/session.py b/cli/trakcli/create/commands/session.py index 7d22ea5..afe35a8 100644 --- a/cli/trakcli/create/commands/session.py +++ b/cli/trakcli/create/commands/session.py @@ -1,11 +1,11 @@ from datetime import datetime, timedelta from typing import Annotated, Optional +from trakcli.projects.utils.print_missing_project import print_missing_project import typer from rich import print as rprint from rich.panel import Panel -from trakcli.config.main import get_config from trakcli.database.database import add_session from trakcli.database.models import Record from trakcli.projects.database import get_projects_from_config @@ -79,10 +79,8 @@ def create_session( ), ] = False, ): - CONFIG = get_config() - # Check if the project exists - projects_in_config = get_projects_from_config(CONFIG) + projects_in_config = get_projects_from_config() if len(projects_in_config): if project_id in projects_in_config: # Check if today or when is passed @@ -146,21 +144,7 @@ def create_session( ) ) else: - renderable_projects_list = "\n • ".join(projects_in_config) - rprint( - Panel( - title="[red]Missing project[/red]", - renderable=print_with_padding( - "This project doesn't exists.\n\n" - f"Awailable projects: \n • {renderable_projects_list}\n\n\n\n" - "Try to run the `trak create project ` command if you want to create a new project." - ), - ) - ) + print_missing_project(projects_in_config) return else: rprint(projects_in_config) - - # Check if hours or minutes is passed - # Get category - # Get tag diff --git a/cli/trakcli/create/commands/work.py b/cli/trakcli/create/commands/work.py index 1b97e48..818b895 100644 --- a/cli/trakcli/create/commands/work.py +++ b/cli/trakcli/create/commands/work.py @@ -1,5 +1,147 @@ +from datetime import datetime +from typing import Annotated + +import typer from rich import print as rprint +from rich.panel import Panel + +from trakcli.projects.database import get_project_from_config, get_projects_from_config +from trakcli.projects.utils.print_missing_project import print_missing_project +from trakcli.utils.print_with_padding import print_with_padding +from trakcli.works.database import ( + get_project_works_from_config, + set_project_works_in_config, +) +from trakcli.works.models import Work + + +def create_work( + id: Annotated[ + str, + typer.Argument(help="The id for the new work."), + ], + project_id: Annotated[ + str, + typer.Option( + "--project-id", + "-p", + help="The id of the project where the new work will be placed.", + ), + ], + name: Annotated[ + str, + typer.Option( + "--name", + "-n", + help="A readable name for the new work.", + ), + ], + time: Annotated[ + int, + typer.Option( + "--time", + "-t", + help="", + ), + ], + from_date: Annotated[ + datetime, + typer.Option( + "--from", + help="", + formats=["%Y-%m-%d"], + ), + ], + to_date: Annotated[ + datetime, + typer.Option( + "--to", + help="", + formats=["%Y-%m-%d"], + ), + ], + description: Annotated[ + str, + typer.Option( + "--description", + "-d", + help="", + ), + ] = "", + rate: Annotated[ + int, + typer.Option( + "--rate", + "-r", + help="", + ), + ] = 1, +): + projects_in_config = get_projects_from_config() + + if project_id in projects_in_config: + details = get_project_from_config(project_id) + + # Check if project esists + if details: + works = get_project_works_from_config(project_id) + + # Check if id already exists + if works is not None: + work_ids = [w["id"] for w in works] + if id in work_ids: + rprint("") + rprint( + Panel.fit( + title="[yellow]This work id already exists[/yellow]", + renderable=print_with_padding( + "You should change the id parameter or you can just use the work already in the configuration." + ), + ) + ) + + return + + new_work = Work( + id=id, + name=name, + time=time, + rate=rate, + from_date=from_date.strftime("%Y-%d-%m"), + to_date=to_date.strftime("%Y-%d-%m"), + description=description, + done=False, + ) + + if works is not None: + works.append(new_work._asdict()) + else: + works = [new_work._asdict()] + + set_project_works_in_config(project_id, works) + + rprint("") + rprint( + Panel.fit( + title="[green]Work created[/green]", + renderable=print_with_padding(f"Work {id} created."), + ) + ) + + return + else: + rprint("") + rprint( + Panel.fit( + title="[red]Error in config[/red]", + renderable=print_with_padding( + "Error in/with details file in project's configuration." + ), + ) + ) + return + else: + print_missing_project(projects_in_config) -def create_work(): - rprint("Create a work") + return diff --git a/cli/trakcli/main.py b/cli/trakcli/main.py index bc0e519..6afb49a 100644 --- a/cli/trakcli/main.py +++ b/cli/trakcli/main.py @@ -16,6 +16,7 @@ ) from trakcli.config.commands import app as config_app from trakcli.config.main import get_config +from trakcli.create import app as create_app from trakcli.database.database import ( add_session, get_current_session, @@ -27,9 +28,10 @@ from trakcli.initialize import initialize_trak from trakcli.projects.commands import app as projects_app from trakcli.projects.database import get_projects_from_config +from trakcli.projects.utils.print_missing_project import print_missing_project from trakcli.report.commands.main import report from trakcli.utils.print_with_padding import print_with_padding -from trakcli.create import app as create_app +from trakcli.works import app as works_app console = Console() @@ -46,6 +48,7 @@ app.add_typer(config_app, name="config", help="Interact with your configuration.") app.add_typer(projects_app, name="projects", help="Interact with your projects.") app.add_typer(create_app, name="create", help="Create something in trak.") +app.add_typer(works_app, name="works", help="Interact with your works.") @app.callback() @@ -151,25 +154,16 @@ def start_tracker( Panel.fit( title="▶️ Start", renderable=print_with_padding( - f"""[bold green]{project}[/bold green] started. - - Have a good session!""" + ( + f"[bold green]{project}[/bold green] started.\n\n" + "Have a good session!" + ) ), ) ) else: - renderable_projects_list = "\n • ".join(projects_in_config) - rprint("") - rprint( - Panel( - title="[red]Missing project[/red]", - renderable=print_with_padding( - "This project doesn't exists.\n\n" - f"Awailable projects: \n • {renderable_projects_list}\n\n\n\n" - "Try to run the `trak create project ` command if you want to create a new project." - ), - ) - ) + print_missing_project(projects_in_config) + return else: formatted_start_time = datetime.fromisoformat(record["start"]).strftime( @@ -186,6 +180,8 @@ def start_tracker( ) ) + return + @app.command("stop", help="Stop the current trak session.") def stop_tracker(): @@ -197,9 +193,10 @@ def stop_tracker(): if record: stop_trak_session() message = print_with_padding( - f"""The [bold green]{record['project']}[/bold green] session is over. - -Good job!""" + ( + f"The [bold green]{record['project']}[/bold green] session is over.\n\n" + "Good job!" + ) ) rprint(Panel.fit(title="⏹️ Stop", renderable=message)) @@ -272,9 +269,12 @@ def status( Panel( title="💬 No active session", renderable=print_with_padding( - """Ther aren't active sessions. - -Use the command: trak start to start a new session of work.""" + ( + "Ther aren't active sessions.\n\n" + "Use the command: trak start to start a new session of work." + ) ), ) ) + + return diff --git a/cli/trakcli/projects/database.py b/cli/trakcli/projects/database.py index 101b330..8026f90 100644 --- a/cli/trakcli/projects/database.py +++ b/cli/trakcli/projects/database.py @@ -31,3 +31,17 @@ def get_projects_from_config(): projects.append(details.get("id", "ERROR: No id!")) return projects + + +def get_project_from_config(project_id: str): + """Get a project in the config by id.""" + + project_path = pathlib.Path(TRAK_FOLDER / "projects" / project_id) + + if project_path.exists() and project_path.is_dir(): + details_path = project_path / "details.json" + with open(details_path, "r") as f: + details = json.load(f) + return details + else: + return None diff --git a/cli/trakcli/projects/utils/__init__.py b/cli/trakcli/projects/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/trakcli/projects/utils/print_missing_project.py b/cli/trakcli/projects/utils/print_missing_project.py new file mode 100644 index 0000000..8a87680 --- /dev/null +++ b/cli/trakcli/projects/utils/print_missing_project.py @@ -0,0 +1,20 @@ +from rich import print as rprint +from rich.panel import Panel +from trakcli.utils.print_with_padding import print_with_padding + + +def print_missing_project(projects_in_config): + renderable_projects_list = "\n • ".join(projects_in_config) + rprint("") + rprint( + Panel.fit( + title="[red]Missing project[/red]", + renderable=print_with_padding( + "This project doesn't exists.\n\n" + f"Awailable projects: \n\n • {renderable_projects_list}\n\n\n\n" + "Try to run the `trak create project ` command if you want to create a new project." + ), + ) + ) + + return diff --git a/cli/trakcli/works/__init__.py b/cli/trakcli/works/__init__.py new file mode 100644 index 0000000..2c63bf3 --- /dev/null +++ b/cli/trakcli/works/__init__.py @@ -0,0 +1,12 @@ +import typer + +from trakcli.works.commands.delete import delete_work +from trakcli.works.commands.done import done_work +from trakcli.works.commands.list import list_works + +app = typer.Typer() + + +app.command("list")(list_works) +app.command("delete")(delete_work) +app.command("done")(done_work) diff --git a/cli/trakcli/works/commands/__init__.py b/cli/trakcli/works/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/trakcli/works/commands/delete.py b/cli/trakcli/works/commands/delete.py new file mode 100644 index 0000000..cbca720 --- /dev/null +++ b/cli/trakcli/works/commands/delete.py @@ -0,0 +1,54 @@ +from typing import Annotated +from rich.panel import Panel +from trakcli.projects.utils.print_missing_project import print_missing_project +from rich.prompt import Confirm + + +import typer +from rich import print as rprint + +from trakcli.projects.database import get_projects_from_config +from trakcli.works.database import ( + get_project_works_from_config, + set_project_works_in_config, +) + + +def delete_work( + work_id: Annotated[str, typer.Argument()], + project_id: Annotated[ + str, + typer.Option( + "--in", "--of", "-p", help="The project's id in which the work is located." + ), + ], +): + """Delete a work from a project.""" + + projects = get_projects_from_config() + + if project_id in projects: + delete = Confirm.ask( + f"Are you sure you want to delete the [green]{work_id}[/green] work from [green]{project_id}[/green] project?", + default=False, + ) + if not delete: + rprint("") + rprint("[yellow]Not deleting.[/yellow]") + raise typer.Abort() + + works = get_project_works_from_config(project_id) + if works is not None: + filtered_works = [w for w in works if w["id"] != work_id] + + set_project_works_in_config(project_id, filtered_works) + + rprint("") + rprint( + Panel.fit( + title="[green]Success[/green]", + renderable=f"Work {work_id} successfully deleted from {project_id} project.", + ) + ) + else: + print_missing_project(projects) diff --git a/cli/trakcli/works/commands/done.py b/cli/trakcli/works/commands/done.py new file mode 100644 index 0000000..18548d6 --- /dev/null +++ b/cli/trakcli/works/commands/done.py @@ -0,0 +1,73 @@ +from typing import Annotated + +import typer +from rich import print as rprint +from rich.panel import Panel +from rich.prompt import Confirm + +from trakcli.projects.database import get_projects_from_config +from trakcli.projects.utils.print_missing_project import print_missing_project +from trakcli.utils.print_with_padding import print_with_padding +from trakcli.works.database import ( + get_project_works_from_config, + set_project_works_in_config, +) + + +def done_work( + work_id: Annotated[str, typer.Argument()], + project_id: Annotated[ + str, + typer.Option( + "--in", "--of", "-p", help="The project's id in which the work is located." + ), + ], +): + """Mark as done a work of a project.""" + + projects = get_projects_from_config() + + if project_id in projects: + confirm_done = Confirm.ask( + f"Are you sure you want to mark the [green]{work_id}[/green] work from [green]{project_id}[/green] project as done?", + default=False, + ) + if not confirm_done: + rprint("") + rprint("[yellow]Not marked as done.[/yellow]") + raise typer.Abort() + + works = get_project_works_from_config(project_id) + if works is not None: + works_ids = [w["id"] for w in works] + if work_id in works_ids: + filtered_works = [ + {**w, "done": True} if w["id"] == work_id else w for w in works + ] + + set_project_works_in_config(project_id, filtered_works) + + rprint("") + rprint( + Panel.fit( + title="[green]Success[/green]", + renderable=f"Work {work_id} successfully from {project_id} project marked as done.", + ) + ) + else: + rprint("") + rprint( + Panel.fit( + title="[red]The work doesn't exist[/red]", + renderable=print_with_padding( + ( + "You can create a new work with the command:\n" + "trak create work -p -n -t --from 2024-01-01 --to 2024-02-01" + ) + ), + ) + ) + + return + else: + print_missing_project(projects) diff --git a/cli/trakcli/works/commands/list.py b/cli/trakcli/works/commands/list.py new file mode 100644 index 0000000..708eef7 --- /dev/null +++ b/cli/trakcli/works/commands/list.py @@ -0,0 +1,112 @@ +from datetime import datetime +from typing import Annotated +from rich.panel import Panel +from trakcli.projects.utils.print_missing_project import print_with_padding + +import typer +from rich import print as rprint +from rich.table import Table + +from trakcli.projects.database import get_project_from_config, get_projects_from_config +from trakcli.works.database import get_project_works_from_config + +ALL_PROJECTS = "all" + + +def print_project_works(works, project_id): + """Print a table of works by project.""" + works_table = Table(title=f"Works for project {project_id}") + + works_table.add_column("Id", no_wrap=True) + works_table.add_column("Name", no_wrap=True) + works_table.add_column("Description") + works_table.add_column("Time") + works_table.add_column("Rate") + works_table.add_column("From") + works_table.add_column("To") + + if works is not None and len(works): + for w in works: + time = w.get("time", "Missin time!") + rate = w.get("rate", "0") + + from_date = w.get("from_date", None) + if from_date is not None: + try: + from_date = datetime.fromisoformat(from_date).strftime("%Y-%m-%d") + except ValueError: + rprint( + f"[red]Error in {w['id']}'s from_date of {project_id} project.[/red]" + ) + + to_date = w.get("to_date", None) + if to_date is not None: + try: + to_date = datetime.fromisoformat(to_date).strftime("%Y-%m-%d") + except ValueError: + rprint( + f"[red]Error in {w['id']}'s to_date of {project_id} project.[/red]" + ) + + works_table.add_row( + w.get("id", "Missing id!"), + w.get("name", "Missing name!"), + w.get("description", ""), + f"{time}", + f"{rate}", + from_date, + to_date, + ) + + rprint("") + rprint(works_table) + + return + else: + rprint("") + rprint( + Panel.fit( + title="[red]No works[/red]", + renderable=print_with_padding( + ( + "You do not have any active work currently for this project.\n\n" + "You can create one with the command:\n" + "trak create work -p -n -t --from 2024-01-01 --to 2024-02-01" + ) + ), + ) + ) + + +def list_works( + project_id: Annotated[str, typer.Argument()] = ALL_PROJECTS, + done: Annotated[ + bool, typer.Option("--done", "-d", help="Show also done works.") + ] = False, +): + """List the works in a project or all of them.""" + + if project_id != ALL_PROJECTS: + details = get_project_from_config(project_id) + + # Check if project esists + if details: + works = get_project_works_from_config(project_id) + + if works is not None and done is False: + works = [w for w in works if w.get("done", False) is False] + + print_project_works(works, project_id) + + return + else: + # Show all current projects + projects = get_projects_from_config() + + for project in projects: + works = get_project_works_from_config(project) + + if works is not None: + print_project_works(works, project) + + return diff --git a/cli/trakcli/works/database.py b/cli/trakcli/works/database.py new file mode 100644 index 0000000..1307a5f --- /dev/null +++ b/cli/trakcli/works/database.py @@ -0,0 +1,36 @@ +import json +import pathlib + +from trakcli.config.main import TRAK_FOLDER + + +def get_project_works_from_config(project_id: str): + """Get the project works in the config by id.""" + + project_path = pathlib.Path(TRAK_FOLDER / "projects" / project_id) + + if project_path.exists() and project_path.is_dir(): + works_path = project_path / "works.json" + if works_path.exists() and works_path.is_file(): + with open(works_path, "r") as f: + try: + works = json.load(f) + return works + except Exception: + return None + else: + return None + + +def set_project_works_in_config(project_id: str, works: list[dict]): + """Get the project works in the config by id.""" + + project_path = pathlib.Path(TRAK_FOLDER / "projects" / project_id) + + if project_path.exists() and project_path.is_dir(): + works_path = project_path / "works.json" + if works_path.exists() and works_path.is_file(): + with open(works_path, "w") as works_file: + json.dump(works, works_file, indent=2, separators=(",", ": ")) + else: + return None diff --git a/cli/trakcli/works/models.py b/cli/trakcli/works/models.py new file mode 100644 index 0000000..b2d9a32 --- /dev/null +++ b/cli/trakcli/works/models.py @@ -0,0 +1,12 @@ +from typing import NamedTuple + + +class Work(NamedTuple): + id: str + name: str + time: int + rate: int + from_date: str + to_date: str + description: str = "" + done: bool = False