diff --git a/cli/trakcli/config/models.py b/cli/trakcli/config/models.py index 6dad9f9..0619e36 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 = "" - hour_rate: int = 1 + fare: int = 1 diff --git a/cli/trakcli/create/__init__.py b/cli/trakcli/create/__init__.py new file mode 100644 index 0000000..6ae17b3 --- /dev/null +++ b/cli/trakcli/create/__init__.py @@ -0,0 +1,13 @@ +import typer + +from trakcli.create.commands.session import create_session +from trakcli.create.commands.work import create_work +from trakcli.create.commands.project import create_project + + +app = typer.Typer() + + +app.command("session")(create_session) +app.command("work")(create_work) +app.command("project")(create_project) diff --git a/cli/trakcli/create/commands/__init__.py b/cli/trakcli/create/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/trakcli/create/commands/project.py b/cli/trakcli/create/commands/project.py new file mode 100644 index 0000000..afff2c6 --- /dev/null +++ b/cli/trakcli/create/commands/project.py @@ -0,0 +1,82 @@ +import json +import pathlib + +import typer +from rich import print as rprint +from rich.panel import Panel + +from trakcli.config.main import TRAK_FOLDER +from trakcli.config.models import Project +from trakcli.utils.print_with_padding import print_with_padding + + +def create_project( + project_id: str, +): + rprint("") + path = pathlib.Path(TRAK_FOLDER / "projects" / project_id) + files = ["details.json", "works.json", "archived_works.json"] + + path.mkdir(parents=True, exist_ok=True) + details_path = path / "details.json" + details_path_exists = details_path.exists() + + # Create files if not exists + for f in files: + try: + with open(path / f, "x") as file: + file.write("") + except FileExistsError: + rprint(f"The file {path / f} already exists, so it won't be created.") + + if not details_path_exists: + name = typer.prompt(text="Readable name", default="") + description = typer.prompt("Description", default="") + categories = typer.prompt( + "Categories (CSV format)", + default="", + ) + tags = typer.prompt("Tags (CSV format)", default="") + customer = typer.prompt("Customer", default="") + hour_rate = typer.prompt("Hour rate", default=1, show_default=True) + + if project_id: + new_project = Project( + id=project_id, + name=name, + description=description, + categories=[c.strip() for c in categories.split(",")] + if categories != "" + else [], + tags=[t.strip() for t in tags.split(",")] if tags != "" else [], + customer=customer, + fare=hour_rate, + ) + + with open(details_path, "w") as details_file: + json.dump( + new_project._asdict(), + details_file, + indent=2, + separators=(",", ": "), + ) + + rprint("") + rprint( + Panel.fit( + title="[green]Success[/green]", + renderable=print_with_padding(f"Project {project_id} created."), + ) + ) + + return + else: + rprint("") + rprint( + Panel.fit( + title="[yellow]Already exists[/yellow]", + renderable=print_with_padding( + f"Project {project_id} already has a configuration." + ), + ) + ) diff --git a/cli/trakcli/create/commands/session.py b/cli/trakcli/create/commands/session.py new file mode 100644 index 0000000..7d22ea5 --- /dev/null +++ b/cli/trakcli/create/commands/session.py @@ -0,0 +1,166 @@ +from datetime import datetime, timedelta +from typing import Annotated, Optional + +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 +from trakcli.utils.print_with_padding import print_with_padding + + +def create_session( + project_id: str, + today: Annotated[ + Optional[datetime], + typer.Option( + "--today", + help="For a task happend today, just enter a the time.", + formats=["%H:%M"], + ), + ] = None, + when: Annotated[ + Optional[datetime], + typer.Option( + "--when", + "-w", + help="Last name of person to greet.", + formats=["%Y-%m-%dT%H:%M"], + ), + ] = None, + hours: Annotated[ + Optional[int], + typer.Option( + "--hours", + "-h", + help="Hours spent in sessions.", + ), + ] = None, + minutes: Annotated[ + Optional[int], + typer.Option( + "--minutes", + "-m", + help="Minutes spent in the session.", + ), + ] = None, + category: Annotated[ + str, + typer.Option( + "--category", + "-c", + help="Add a category to the tracked time. Useful in the reporting phase.", + ), + ] = "", + tag: Annotated[ + str, + typer.Option( + "--tag", + "-t", + help="Add a tag to the tracked time. Useful in the reporting phase.", + ), + ] = "", + billable: Annotated[ + bool, + typer.Option( + "--billable", + "-b", + help="The project is billable.", + ), + ] = False, + dryrun: Annotated[ + bool, + typer.Option( + "--dry-run", + help="Check the session you are about to create, without save it.", + ), + ] = False, +): + CONFIG = get_config() + + # Check if the project exists + projects_in_config = get_projects_from_config(CONFIG) + if len(projects_in_config): + if project_id in projects_in_config: + # Check if today or when is passed + start_timedate = datetime.today() + if today or when: + # Create the start date for the session + if today: + now = datetime.today() + today_time = today.time() + start_timedate = now.replace( + hour=today_time.hour, minute=today_time.minute + ) + if when: + start_timedate = when + + end_timedate = start_timedate + if hours or minutes: + if hours: + end_timedate = end_timedate + timedelta(hours=hours) + if minutes: + end_timedate = end_timedate + timedelta(minutes=minutes) + + new_session = Record( + project=project_id, + start=start_timedate.isoformat(), + end=end_timedate.isoformat(), + billable=billable, + category=category, + tag=tag, + ) + + if not dryrun: + add_session(new_session) + rprint("") + rprint("✅ Session created.") + + rprint("") + rprint( + Panel.fit( + title=project_id, + renderable=print_with_padding( + ( + f"start: {new_session.start}\n" + f"end: {new_session.end}\n" + f"billable: {new_session.billable}\n" + f"category: {new_session.category}\n" + f"tag: {new_session.tag}" + ) + ), + ) + ) + + return + else: + rprint( + Panel( + title="[red]Missing start time[/red]", + renderable=print_with_padding( + "Use the `--today` or `--when` flag." + ), + ) + ) + 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." + ), + ) + ) + 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 new file mode 100644 index 0000000..1b97e48 --- /dev/null +++ b/cli/trakcli/create/commands/work.py @@ -0,0 +1,5 @@ +from rich import print as rprint + + +def create_work(): + rprint("Create a work") diff --git a/cli/trakcli/main.py b/cli/trakcli/main.py index 7631924..bc0e519 100644 --- a/cli/trakcli/main.py +++ b/cli/trakcli/main.py @@ -26,8 +26,10 @@ from trakcli.dev.commands import app as dev_app 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.report.commands.main import report from trakcli.utils.print_with_padding import print_with_padding +from trakcli.create import app as create_app console = Console() @@ -43,6 +45,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.callback() @@ -131,27 +134,43 @@ def start_tracker( project = typer.prompt("Which project do you want to track?") record = tracking_already_started() + projects_in_config = get_projects_from_config() if not record: - add_session( - Record( - project=project, - start=datetime.now().isoformat(), - billable=billable, - category=category, - tag=tag, + if project in projects_in_config: + add_session( + Record( + project=project, + start=datetime.now().isoformat(), + billable=billable, + category=category, + tag=tag, + ) ) - ) - rprint( - Panel.fit( - title="▶️ Start", - renderable=print_with_padding( - f"""[bold green]{project}[/bold green] started. + rprint( + Panel.fit( + title="▶️ Start", + renderable=print_with_padding( + f"""[bold green]{project}[/bold green] started. -Have a good session!""" - ), + 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." + ), + ) + ) + return else: formatted_start_time = datetime.fromisoformat(record["start"]).strftime( "%m/%d/%Y, %H:%M" diff --git a/cli/trakcli/projects/commands.py b/cli/trakcli/projects/commands.py index f3568b6..1d4fca8 100644 --- a/cli/trakcli/projects/commands.py +++ b/cli/trakcli/projects/commands.py @@ -1,16 +1,20 @@ -import json -from rich.panel import Panel -from rich.table import Table +import pathlib +import shutil + import typer from rich import print as rprint -from trakcli.config.main import CONFIG_FILE_PATH, get_config, get_db_file_path -from trakcli.config.models import Project -from trakcli.utils.print_with_padding import print_with_padding +from rich.panel import Panel +from rich.table import Table +from trakcli.config.main import ( + TRAK_FOLDER, + get_db_file_path, +) from trakcli.projects.database import ( get_projects_from_config, get_projects_from_db, ) +from trakcli.utils.print_with_padding import print_with_padding app = typer.Typer() @@ -19,12 +23,11 @@ def list(): """List the projects.""" - CONFIG = get_config() db_path = get_db_file_path() - projcts_in_db = get_projects_from_db(db_path) - projects_in_config = get_projects_from_config(CONFIG) - combined = {*projcts_in_db, *projects_in_config} + projects_in_db = get_projects_from_db(db_path) + projects_in_config = get_projects_from_config() + combined = {*projects_in_db, *projects_in_config} number_of_projects = len(combined) @@ -33,111 +36,62 @@ def list(): ) table.add_column("id", style="green", no_wrap=True) + table.add_column("from", style="cyan", no_wrap=True) + + for project in projects_in_config: + table.add_row(project, "config") - for c in combined: - table.add_row(c) + projects_id_db_only = False + for project in projects_in_db: + if project not in projects_in_config: + projects_id_db_only = True + table.add_row(project, "database") rprint("") rprint(table) - - -@app.command(help="Create a project.") -def create(): - """Create a project.""" - - rprint( - Panel( - title="Tips", - renderable=print_with_padding( - text="Try to use the exact same name for customers. Grouping will be easier." - ), - ) - ) - - id = typer.prompt( - "Id", - ) - name = typer.prompt(text="Readable name", default="") - description = typer.prompt("Description", default="") - categories = typer.prompt( - "Categories (CSV format)", - default="", - ) - tags = typer.prompt("Tags (CSV format)", default="") - customer = typer.prompt("Customer", default="") - hour_rate = typer.prompt("Hour rate", default=1, show_default=True) - - if id: - new_project = Project( - id=id, - name=name, - description=description, - categories=[c.strip() for c in categories.split(",")] - if categories != "" - else [], - tags=[t.strip() for t in tags.split(",")] if tags != "" else [], - customer=customer, - hour_rate=hour_rate, - ) - - config = get_config() - - projects = config.get("projects", []) - - # Check if id is unique - if new_project.id not in [p.get("id", "") for p in projects]: - projects.append(new_project._asdict()) - config["projects"] = projects - - with open(CONFIG_FILE_PATH, "w") as open_config: - json.dump(config, open_config, indent=2, separators=(",", ": ")) - - rprint("") - rprint( - Panel( - title="Success", - renderable=print_with_padding( - f"[green]Project {id} created.[/green]" - ), - ) - ) - else: - rprint("") - rprint( - Panel( - title="Error", - renderable=print_with_padding( - "[red]This project already exists.[/red]" - ), - ) + rprint("") + if projects_id_db_only: + rprint( + Panel.fit( + title="Tip", + renderable=print_with_padding( + ( + "You have projects that don't exist in configuration.\n" + "Plase, run the `trak create project ` command to configure your project." + ) + ), ) + ) @app.command(help="Delete a project.") -def delete(id: str): +def delete(project_id: str): """Delete a project.""" - config = get_config() + project_path = pathlib.Path(TRAK_FOLDER / "projects" / project_id) - projects = config.get("projects", []) + rprint("") + if project_path.exists(): + delete = typer.confirm( + f"Are you sure you want to delete the {project_id} project?" + ) + if not delete: + raise typer.Abort() - if id in [p.get("id", "") for p in projects]: - config["projects"] = [p for p in projects if p.get("id", "") != id] + shutil.rmtree(project_path) - with open(CONFIG_FILE_PATH, "w") as open_config: - json.dump(config, open_config, indent=2, separators=(",", ": ")) - rprint( - Panel( - title="Success", - renderable=print_with_padding( - f"[green]Project {id} deleted.[/green]" - ), - ) + rprint( + Panel.fit( + title="[green]Deleted[/green]", + renderable=print_with_padding( + f"The {project_id} has been delete correctly." + ), ) + ) else: rprint( - Panel( - title="Error", + Panel.fit( + title="[red]Error[/red]", renderable=print_with_padding( "[red]This project doesn't exists.[/red]" ), diff --git a/cli/trakcli/projects/database.py b/cli/trakcli/projects/database.py index ba198a7..101b330 100644 --- a/cli/trakcli/projects/database.py +++ b/cli/trakcli/projects/database.py @@ -1,5 +1,8 @@ import json from pathlib import Path +import pathlib + +from trakcli.config.main import TRAK_FOLDER def get_projects_from_db(db_path: Path): @@ -13,9 +16,18 @@ def get_projects_from_db(db_path: Path): return {record.get("project", "") for record in parsed_json} -def get_projects_from_config(config): +def get_projects_from_config(): """Get the projects in the config.""" - projects = config.get("projects", []) + projects_path = pathlib.Path(TRAK_FOLDER / "projects") + + projects = [] + + for x in projects_path.iterdir(): + if x.is_dir(): + details_path = x / "details.json" + with open(details_path, "r") as f: + details = json.load(f) + projects.append(details.get("id", "ERROR: No id!")) - return [p.get("id", "ERROR: No id!") for p in projects] + return projects