crowdtime-cli 0.21.0__tar.gz → 0.21.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/PKG-INFO +1 -1
  2. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/pyproject.toml +1 -1
  3. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/__init__.py +1 -1
  4. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/clients_cmd.py +2 -2
  5. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/expense_cmd.py +38 -7
  6. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/invoice_cmd.py +5 -5
  7. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/org_cmd.py +1 -1
  8. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/projects_cmd.py +3 -3
  9. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/pto_cmd.py +1 -1
  10. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/report_cmd.py +1 -1
  11. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/tasks_cmd.py +1 -1
  12. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/team_cmd.py +5 -5
  13. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/timer_cmd.py +17 -3
  14. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/timesheet_cmd.py +4 -4
  15. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/formatters.py +4 -4
  16. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/main.py +2 -1
  17. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/SKILL.md +7 -6
  18. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/commands.md +10 -7
  19. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/pto.md +2 -2
  20. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +6 -4
  21. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/.gitignore +0 -0
  22. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/LICENSE +0 -0
  23. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/README.md +0 -0
  24. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/auth.py +0 -0
  25. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/client.py +0 -0
  26. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/__init__.py +0 -0
  27. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
  28. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
  29. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/billing_cmd.py +0 -0
  30. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/calendar_cmd.py +0 -0
  31. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/config_cmd.py +0 -0
  32. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
  33. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/insights_cmd.py +0 -0
  34. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/log_cmd.py +0 -0
  35. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
  36. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
  37. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/version_cmd.py +0 -0
  38. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/config.py +0 -0
  39. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/models.py +0 -0
  40. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/oauth.py +0 -0
  41. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/resolvers.py +0 -0
  42. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/utils.py +0 -0
  43. {crowdtime_cli-0.21.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crowdtime-cli
3
- Version: 0.21.0
3
+ Version: 0.21.1
4
4
  Summary: AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest
5
5
  Project-URL: Homepage, https://crowdtime.lat
6
6
  Project-URL: Documentation, https://crowdtime.lat/docs
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crowdtime-cli"
3
- version = "0.21.0"
3
+ version = "0.21.1"
4
4
  description = "AI-powered time tracking CLI — a modern, developer-friendly alternative to Harvest"
5
5
  readme = "README.md"
6
6
  license = {text = "Proprietary"}
@@ -1,3 +1,3 @@
1
1
  """CrowdTime CLI - AI-powered time tracking from the command line."""
2
2
 
3
- __version__ = "0.21.0"
3
+ __version__ = "0.21.1"
@@ -75,7 +75,7 @@ def list_clients(
75
75
  return
76
76
 
77
77
  table = Table(show_header=True, header_style="bold")
78
- table.add_column("ID", style="dim")
78
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
79
79
  table.add_column("Name")
80
80
  table.add_column("Contact")
81
81
  table.add_column("Projects", justify="right")
@@ -355,7 +355,7 @@ def list_contacts(
355
355
  return
356
356
 
357
357
  table = Table(show_header=True, header_style="bold")
358
- table.add_column("ID", style="dim")
358
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
359
359
  table.add_column("Name", width=20)
360
360
  table.add_column("Email", width=25)
361
361
  table.add_column("Phone", width=15)
@@ -21,16 +21,22 @@ console = Console()
21
21
  # ─── Helpers ──────────────────────────────────────────────────────
22
22
 
23
23
 
24
- def _print_expenses_table(expenses: list[dict]) -> None:
25
- """Print expenses in a Rich table."""
24
+ def _print_expenses_table(expenses: list[dict], me: str | None = None) -> None:
25
+ """Print expenses in a Rich table, with a User column whenever one isn't yours."""
26
26
  if not expenses:
27
27
  console.print("[dim]No expenses found.[/dim]")
28
28
  console.print("Run [bold]ct expense create[/bold] to log an expense.")
29
29
  return
30
30
 
31
31
  table = Table(show_header=True, header_style="bold")
32
- table.add_column("ID", style="dim")
32
+ # Full UUIDs: they are what show/edit/delete take, so never let Rich clip them.
33
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
33
34
  table.add_column("Date", width=12)
35
+ # The list holds every expense the caller can see — colleagues' too for
36
+ # managers and project leads — so mark whose each one is (CRO-93).
37
+ show_user = any(str(exp.get("user")) != me for exp in expenses)
38
+ if show_user:
39
+ table.add_column("User", width=16)
34
40
  table.add_column("Description", width=25)
35
41
  table.add_column("Category", width=14)
36
42
  table.add_column("Project", style="cyan", width=15)
@@ -51,9 +57,14 @@ def _print_expenses_table(expenses: list[dict]) -> None:
51
57
  is_billable = exp.get("is_billable", False)
52
58
  is_invoiced = exp.get("is_invoiced", False)
53
59
 
60
+ user_cell = (
61
+ ["You" if str(exp.get("user")) == me else exp.get("user_name", "") or ""]
62
+ if show_user else []
63
+ )
54
64
  table.add_row(
55
65
  str(exp.get("id", "")),
56
66
  exp.get("date", ""),
67
+ *user_cell,
57
68
  exp.get("description", ""),
58
69
  exp.get("category_name", exp.get("category", "")) or "",
59
70
  exp.get("project_name", exp.get("project", "")) or "",
@@ -65,6 +76,8 @@ def _print_expenses_table(expenses: list[dict]) -> None:
65
76
 
66
77
  console.print(table)
67
78
  console.print(f"\n[bold]Total:[/bold] {format_currency(total)} ({len(expenses)} expenses)")
79
+ if show_user:
80
+ console.print("[dim]Includes other people's expenses. Use --mine for only yours.[/dim]")
68
81
 
69
82
 
70
83
  # ─── Commands ─────────────────────────────────────────────────────
@@ -74,7 +87,12 @@ def _print_expenses_table(expenses: list[dict]) -> None:
74
87
  def expense_default(ctx: typer.Context) -> None:
75
88
  """Manage expenses."""
76
89
  if ctx.invoked_subcommand is None:
77
- list_expenses()
90
+ # Called directly, so every option needs a real value — Typer's
91
+ # defaults are OptionInfo objects outside the CLI parser.
92
+ list_expenses(
93
+ from_date=None, to_date=None, project=None, category=None,
94
+ billable=None, invoiced=None, mine=False, output_json=False,
95
+ )
78
96
 
79
97
 
80
98
  @app.command("list")
@@ -85,22 +103,35 @@ def list_expenses(
85
103
  category: Optional[str] = typer.Option(None, "--category", "-c", help="Filter by category."),
86
104
  billable: Optional[bool] = typer.Option(None, "--billable/--no-billable", help="Filter by billable status."),
87
105
  invoiced: Optional[bool] = typer.Option(None, "--invoiced/--uninvoiced", help="Filter by invoiced status."),
106
+ mine: bool = typer.Option(
107
+ False, "--mine",
108
+ help="Only your own expenses. Managers and project leads otherwise see colleagues' too.",
109
+ ),
88
110
  output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
89
111
  ) -> None:
90
112
  """List expenses with optional filters.
91
113
 
92
- By default, shows the current month's expenses.
114
+ By default, shows the current month's expenses — every expense you can
115
+ see. The table shows a User column whenever one isn't yours.
93
116
 
94
117
  Examples:
95
118
  ct expense list
96
119
  ct expense list --from 2026-03-01 --to 2026-03-31
97
120
  ct expense list --project myproject --billable
98
121
  ct expense list --category travel --uninvoiced
122
+ ct expense list --mine
99
123
  """
100
124
  client = CrowdTimeClient(require_auth=True, require_org=True)
101
125
 
102
126
  params: dict = {}
103
127
 
128
+ me = client.current_user_id()
129
+ if mine:
130
+ if not me:
131
+ format_error("Could not resolve your user id. Run ct auth login again.")
132
+ raise typer.Exit(1)
133
+ params["user"] = me
134
+
104
135
  # Default to current month if no date filters provided
105
136
  if not from_date and not to_date:
106
137
  today = date_type.today()
@@ -137,7 +168,7 @@ def list_expenses(
137
168
  if output_json:
138
169
  print_json(results)
139
170
  else:
140
- _print_expenses_table(results)
171
+ _print_expenses_table(results, me)
141
172
  except APIError as e:
142
173
  format_error(e.message)
143
174
  raise typer.Exit(1)
@@ -385,7 +416,7 @@ def list_categories(
385
416
  return
386
417
 
387
418
  table = Table(show_header=True, header_style="bold")
388
- table.add_column("ID", style="dim")
419
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
389
420
  table.add_column("Name", width=25)
390
421
 
391
422
  for cat in results:
@@ -1305,7 +1305,7 @@ def list_recurring(
1305
1305
  return
1306
1306
 
1307
1307
  table = Table(show_header=True, header_style="bold")
1308
- table.add_column("ID", style="dim")
1308
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
1309
1309
  table.add_column("Name", width=20)
1310
1310
  table.add_column("Client", width=18)
1311
1311
  table.add_column("Schedule", width=14)
@@ -1674,7 +1674,7 @@ def list_retainers(
1674
1674
  return
1675
1675
 
1676
1676
  table = Table(show_header=True, header_style="bold")
1677
- table.add_column("ID", style="dim")
1677
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
1678
1678
  table.add_column("Name", width=20)
1679
1679
  table.add_column("Client", width=18)
1680
1680
  table.add_column("Monthly", justify="right", width=12)
@@ -1878,7 +1878,7 @@ def list_withdrawals(
1878
1878
  return
1879
1879
 
1880
1880
  table = Table(show_header=True, header_style="bold")
1881
- table.add_column("ID", style="dim")
1881
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
1882
1882
  table.add_column("Invoice", width=14)
1883
1883
  table.add_column("Amount", justify="right", width=12)
1884
1884
  table.add_column("Date", width=12)
@@ -1981,7 +1981,7 @@ def tax_rates_list(
1981
1981
  return
1982
1982
 
1983
1983
  table = Table(title="Tax Rates")
1984
- table.add_column("ID", style="dim")
1984
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
1985
1985
  table.add_column("Name", style="bold")
1986
1986
  table.add_column("Rate %", justify="right")
1987
1987
  table.add_column("Default")
@@ -2140,7 +2140,7 @@ def uninvoiced_entries(
2140
2140
  table.add_column("Entries", justify="right")
2141
2141
  table.add_column("Hours", justify="right")
2142
2142
  table.add_column("Amount", justify="right", style="green")
2143
- table.add_column("Client ID", style="dim")
2143
+ table.add_column("Client ID", style="dim", no_wrap=True, min_width=36)
2144
2144
 
2145
2145
  total_entries = 0
2146
2146
  total_hours = Decimal("0")
@@ -383,7 +383,7 @@ def invitations(
383
383
  return
384
384
 
385
385
  table = Table(show_header=True, header_style="bold")
386
- table.add_column("ID", style="dim")
386
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
387
387
  table.add_column("Email", width=25)
388
388
  table.add_column("Role", width=18)
389
389
  table.add_column("Status", width=10)
@@ -618,7 +618,7 @@ def list_members(
618
618
  table.add_column("Project Role", width=16)
619
619
  table.add_column("Org Role", width=16)
620
620
  table.add_column("Rate", justify="right", width=10)
621
- table.add_column("User ID", style="dim")
621
+ table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
622
622
 
623
623
  for m in members_list:
624
624
  proj_role = m.get("project_role") or "[dim]-[/dim]"
@@ -677,7 +677,7 @@ def list_tasks(
677
677
  table.add_column("Task Rate", justify="right", width=12)
678
678
  table.add_column("Project Rate", justify="right", width=14)
679
679
  table.add_column("Active", width=8)
680
- table.add_column("Task ID", style="dim")
680
+ table.add_column("Task ID", style="dim", no_wrap=True, min_width=36)
681
681
 
682
682
  for t in tasks_list:
683
683
  billable = "[green]Yes[/green]" if t.get("task_is_billable") else "[dim]No[/dim]"
@@ -784,7 +784,7 @@ def assignable_users(
784
784
  return
785
785
 
786
786
  table = Table(title="Assignable users")
787
- table.add_column("ID", style="dim")
787
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
788
788
  table.add_column("Name")
789
789
  table.add_column("Email")
790
790
  table.add_column("Org role")
@@ -778,7 +778,7 @@ def allowance_list_cmd(
778
778
  from rich.table import Table
779
779
 
780
780
  table = Table(show_header=True, header_style="bold")
781
- table.add_column("User ID", style="dim")
781
+ table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
782
782
  table.add_column("Name")
783
783
  table.add_column("Days/year", justify="right")
784
784
  table.add_column("Counting from")
@@ -562,7 +562,7 @@ def saved_list(
562
562
  return
563
563
 
564
564
  table = Table(show_header=True, header_style="bold", title="Saved Reports")
565
- table.add_column("ID", width=36)
565
+ table.add_column("ID", no_wrap=True, min_width=36)
566
566
  table.add_column("Name", style="bold", width=25)
567
567
  table.add_column("Type", width=18)
568
568
  table.add_column("Shared", justify="center", width=8)
@@ -195,7 +195,7 @@ def list_tasks(
195
195
  return
196
196
 
197
197
  table = Table(show_header=True, header_style="bold")
198
- table.add_column("ID", style="dim")
198
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
199
199
  table.add_column("Name", width=25)
200
200
  table.add_column("Project", style="cyan", width=15)
201
201
  table.add_column("Billable", justify="center", width=8)
@@ -343,7 +343,7 @@ def list_assignments(
343
343
  return
344
344
 
345
345
  table = Table(show_header=True, header_style="bold")
346
- table.add_column("Assignment ID", style="dim")
346
+ table.add_column("Assignment ID", style="dim", no_wrap=True, min_width=36)
347
347
  table.add_column("Manager")
348
348
  table.add_column("Manager Email", style="dim")
349
349
  table.add_column("Direct Report")
@@ -402,8 +402,8 @@ def reports(
402
402
  table = Table(show_header=True, header_style="bold")
403
403
  table.add_column("Direct Report")
404
404
  table.add_column("Email", style="dim")
405
- table.add_column("User ID", style="dim")
406
- table.add_column("Assignment ID", style="dim")
405
+ table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
406
+ table.add_column("Assignment ID", style="dim", no_wrap=True, min_width=36)
407
407
 
408
408
  for row in rows:
409
409
  table.add_row(
@@ -496,7 +496,7 @@ def show(
496
496
  projects = profile.get("projects") or []
497
497
  if projects:
498
498
  table = Table(show_header=True, header_style="bold", title="Assigned projects")
499
- table.add_column("Project ID", style="dim")
499
+ table.add_column("Project ID", style="dim", no_wrap=True, min_width=36)
500
500
  table.add_column("Project")
501
501
  table.add_column("Client", style="dim")
502
502
  table.add_column("Role on project")
@@ -532,7 +532,7 @@ def show(
532
532
  by_project = overview.get("by_project") or []
533
533
  if by_project:
534
534
  table = Table(show_header=True, header_style="bold", title="By project")
535
- table.add_column("Project ID", style="dim")
535
+ table.add_column("Project ID", style="dim", no_wrap=True, min_width=36)
536
536
  table.add_column("Project")
537
537
  table.add_column("Client", style="dim")
538
538
  table.add_column("Hours", justify="right")
@@ -281,11 +281,25 @@ def continue_timer(
281
281
  """
282
282
  client = CrowdTimeClient(require_auth=True, require_org=True)
283
283
 
284
- # Fetch recent entries to find the last non-running one
284
+ # The time list holds everything the caller can see — colleagues' entries
285
+ # too for project leads and managers — so ask for your own (CRO-93).
286
+ me = client.current_user_id()
287
+ if not me:
288
+ format_error("Could not resolve your user id. Run ct auth login again.")
289
+ raise typer.Exit(1)
290
+
291
+ # Fetch recent entries to find the last non-running one. Server ordering is
292
+ # newest date first; future-dated entries and paid-leave rows aren't
293
+ # something you'd resume.
285
294
  try:
286
- data = client.get("/time/", params={"page_size": "1", "ordering": "-start_time"})
295
+ from datetime import date as date_type
296
+ data = client.get("/time/", params={
297
+ "user": me,
298
+ "date_to": date_type.today().isoformat(),
299
+ "page_size": "10",
300
+ })
287
301
  from ..formatters import extract_results
288
- entries = extract_results(data)
302
+ entries = [e for e in extract_results(data) if e.get("source") != "time_off"]
289
303
  if not entries:
290
304
  format_error("No recent entries to continue from.")
291
305
  raise typer.Exit(1)
@@ -41,7 +41,7 @@ def _print_timesheets_table(timesheets: list[dict], show_user: bool = False) ->
41
41
  return
42
42
 
43
43
  table = Table(show_header=True, header_style="bold")
44
- table.add_column("ID", style="dim")
44
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
45
45
  if show_user:
46
46
  table.add_column("User", width=20)
47
47
  table.add_column("Period", width=25)
@@ -684,7 +684,7 @@ def viewable_users(
684
684
  return
685
685
 
686
686
  table = Table(show_header=True, header_style="bold")
687
- table.add_column("ID", style="dim")
687
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
688
688
  table.add_column("Name", width=20)
689
689
  table.add_column("Email", width=30)
690
690
 
@@ -1121,7 +1121,7 @@ def compliance_report(
1121
1121
  title=f"[blue]Pending Approval ({len(pending)})[/blue]",
1122
1122
  )
1123
1123
  table.add_column("Member", width=22)
1124
- table.add_column("Timesheet ID", style="dim")
1124
+ table.add_column("Timesheet ID", style="dim", no_wrap=True, min_width=36)
1125
1125
  table.add_column("Hours", justify="right", width=8)
1126
1126
  table.add_column("Submitted", width=12)
1127
1127
  table.add_column("Days Pending", justify="right", width=13)
@@ -1147,7 +1147,7 @@ def compliance_report(
1147
1147
  title=f"[red]Rejected ({len(rejected_list)})[/red]",
1148
1148
  )
1149
1149
  table.add_column("Member", width=22)
1150
- table.add_column("Timesheet ID", style="dim")
1150
+ table.add_column("Timesheet ID", style="dim", no_wrap=True, min_width=36)
1151
1151
  table.add_column("Hours", justify="right", width=8)
1152
1152
  table.add_column("Reviewer Notes", width=35)
1153
1153
 
@@ -208,7 +208,7 @@ def format_entries_table(
208
208
  return
209
209
 
210
210
  table = Table(show_header=True, header_style="bold")
211
- table.add_column("ID", style="dim")
211
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
212
212
  if show_date:
213
213
  table.add_column("Date", width=12)
214
214
  if show_user:
@@ -352,7 +352,7 @@ def format_projects_table(projects: list[Project]) -> None:
352
352
  return
353
353
 
354
354
  table = Table(show_header=True, header_style="bold")
355
- table.add_column("ID", style="dim")
355
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
356
356
  table.add_column("Name", style="bold", width=20)
357
357
  table.add_column("Code", width=8)
358
358
  table.add_column("Client", width=15)
@@ -438,7 +438,7 @@ def format_favorites_table(favorites: list[FavoriteEntry]) -> None:
438
438
  return
439
439
 
440
440
  table = Table(show_header=True, header_style="bold")
441
- table.add_column("ID", style="dim")
441
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
442
442
  table.add_column("Project", style="cyan", width=15)
443
443
  table.add_column("Task", width=15)
444
444
  table.add_column("Description", width=30)
@@ -952,7 +952,7 @@ def format_timeoff_summary_panel(summary: TimeOffSummary) -> Panel:
952
952
  padding=(0, 2),
953
953
  )
954
954
  by_user_table.add_column("User", width=24)
955
- by_user_table.add_column("User ID", style="dim", width=36)
955
+ by_user_table.add_column("User ID", style="dim", no_wrap=True, min_width=36)
956
956
  by_user_table.add_column("Hours", justify="right", width=10)
957
957
  by_user_table.add_column("Days", justify="right", width=6)
958
958
  if summary.by_user:
@@ -428,13 +428,14 @@ def expense_list_alias(
428
428
  category: Optional[str] = typer.Option(None, "--category", "-c"),
429
429
  billable: Optional[bool] = typer.Option(None, "--billable/--no-billable"),
430
430
  invoiced: Optional[bool] = typer.Option(None, "--invoiced/--uninvoiced"),
431
+ mine: bool = typer.Option(False, "--mine"),
431
432
  output_json: bool = typer.Option(False, "--json"),
432
433
  ) -> None:
433
434
  """Alias for 'expense list'."""
434
435
  expense_cmd.list_expenses(
435
436
  from_date=from_date, to_date=to_date, project=project,
436
437
  category=category, billable=billable, invoiced=invoiced,
437
- output_json=output_json,
438
+ mine=mine, output_json=output_json,
438
439
  )
439
440
 
440
441
 
@@ -140,7 +140,8 @@ ct clients remove-contact <id> <contact-id>
140
140
 
141
141
  ### Expenses
142
142
  ```bash
143
- ct expense list # List expenses
143
+ ct expense list # List expenses you can see (colleagues' too if you're a manager or run a project)
144
+ ct expense list --mine # Only your own — use this for "my expenses" and totals
144
145
  ct expense list --from 2026-03-01 --to 2026-03-31 --billable
145
146
  ct expense create --amount 49.99 "Domain renewal" --project acme-website --category "Software" --vendor "Namecheap" --billable
146
147
  ct expense edit <id> --amount 59.99 --description "Domain + SSL renewal"
@@ -148,9 +149,8 @@ ct expense delete <id>
148
149
  ct expense categories # List expense categories
149
150
  ct expense add-category "Travel" # Create a new category
150
151
 
151
- # Short alias
152
- ct ex list # expense list
153
- ct ex create --amount 25 "Lunch with client" --category "Meals" --billable
152
+ # Short alias (list only — takes the same options as expense list)
153
+ ct ex --mine # expense list --mine
154
154
  ```
155
155
 
156
156
  **Who sees which expenses**: members and viewers see only their own; project managers also see expenses on projects they manage; account managers and above see all. Only the owner of an expense (or an account manager+) can edit or delete it, and not once it has been invoiced. `--project` must be a project the user is assigned to. The invoiced flag is set by invoicing and cannot be edited.
@@ -296,7 +296,8 @@ A favorite can only point at a project the user is assigned to (and a task on th
296
296
 
297
297
  ### Timesheets
298
298
  ```bash
299
- ct timesheet list # List your timesheets
299
+ ct timesheet list --mine # List your own timesheets
300
+ ct timesheet list # Everyone's you can see (a User column marks rows that aren't yours)
300
301
  ct timesheet list --status submitted # Filter by status
301
302
  ct timesheet submit --from 2026-03-10 --to 2026-03-16 # Submit full week
302
303
  ct timesheet submit --from 2026-03-30 --to 2026-03-31 # Partial week (end-of-month split)
@@ -437,7 +438,7 @@ ct pto add <user> <dates> # Add PTO on behalf of user, auto-approved
437
438
  ct pto list [--mine|--team] # List PTO entries with optional filters
438
439
  ct pto show <id> # Detail view of a single PTO entry (NEVER truncate the ID)
439
440
  ct pto edit <id> [fields] # Edit date range, kind, notes, hours/day (your own: pending only)
440
- ct pto cancel <id> [--force] # Cancel a PTO entry (own pending; or project manager+ any)
441
+ ct pto cancel <id> [--force] # Cancel a PTO entry (own pending; or project manager+ someone else's; your own decided leave needs another approver, admins excepted)
441
442
  ct pto approve <id> [--notes "..."] # Approve a pending entry (project manager+; never your own unless admin)
442
443
  ct pto reject <id> --notes "..." # Reject a pending entry — notes required (project manager+; never your own unless admin)
443
444
  ct pto calendar [--from --to --user --status] # PTO slice for a date window (approved only unless --status)
@@ -233,9 +233,9 @@ ct timer continue [--note/-n NOTE] [--billable/-b] [--no-billable/-B] [--json]
233
233
  | `--no-billable`, `-B` | flag | Override billable status (mark non-billable) |
234
234
  | `--json` | flag | Output as JSON |
235
235
 
236
- Starts a new timer with the same project, task, and description as the most recently stopped entry. Great for resuming after a break. Use `--billable`/`--no-billable` to override the original entry's billable status.
236
+ Starts a new timer with the same project, task, and description as your most recent entry (up to today; paid-leave rows are skipped). Great for resuming after a break. Use `--billable`/`--no-billable` to override the original entry's billable status.
237
237
 
238
- - **Endpoint:** `GET /time/` (fetch last entry) + `POST /time/start/`
238
+ - **Endpoint:** `GET /time/?user=<you>` (fetch your last entry) + `POST /time/start/`
239
239
  - **Alias:** `ct c` (e.g. `ct c -b` to continue as billable)
240
240
 
241
241
  ### ct timer discard
@@ -563,10 +563,12 @@ ct expense list [options]
563
563
  | `--category` | string | Filter by category |
564
564
  | `--billable` | flag | Only billable expenses |
565
565
  | `--invoiced` | flag | Only invoiced expenses |
566
+ | `--mine` | flag | Only your own expenses |
566
567
  | `--json` | flag | JSON output |
567
568
 
568
- Shows: ID, Date, Description, Amount, Category, Project, Billable, Invoiced.
569
- Scope: members and viewers see their own expenses; project managers also see those on projects they manage; account managers and above see all.
569
+ Shows: ID, Date, User (only when a row isn't yours), Description, Amount, Category, Project, Billable, Invoiced. The total adds up every row shown.
570
+ Scope: members and viewers see their own expenses; anyone running a project also sees those logged on it; account managers and above see all. Pass `--mine` whenever the question is about the user's own expenses — without it, a manager's or project lead's list and total include colleagues'. In `--json` output compare each row's `user` with yours.
571
+ Alias: `ct ex [options]` (list only).
570
572
  Endpoint: `GET /expenses/`
571
573
 
572
574
  ### ct expense show
@@ -602,8 +604,8 @@ ct expense create --amount AMOUNT DESCRIPTION [options]
602
604
  **Usage examples:**
603
605
  ```bash
604
606
  ct expense create --amount 49.99 "Domain renewal" --project acme-website --category "Software" --billable
605
- ct ex create --amount 125 "Client lunch" --category "Meals" --vendor "La Trattoria" --billable --notes "Q1 planning lunch"
606
- ct ex create --amount 29.99 "Adobe subscription" -p design-project --category "Software" -d yesterday -b
607
+ ct expense create --amount 125 "Client lunch" --category "Meals" --vendor "La Trattoria" --billable --notes "Q1 planning lunch"
608
+ ct expense create --amount 29.99 "Adobe subscription" -p design-project --category "Software" -d yesterday -b
607
609
  ```
608
610
 
609
611
  Endpoint: `POST /expenses/`
@@ -2860,7 +2862,7 @@ routing from the list payload.
2860
2862
  ct pto list [--mine] [--team] [--status/-s STATUS] [--kind/-k KIND] [--from DATE] [--to DATE] [--user/-u USER] [--json]
2861
2863
  ```
2862
2864
 
2863
- List PTO entries. Defaults to showing your own entries if `--mine` is passed, or all entries you can see if `--team` is passed. Without either flag, shows all visible entries.
2865
+ List PTO entries. Shows your own entries by default (same as `--mine`); `--team` shows every entry you can see.
2864
2866
 
2865
2867
  **Who can call it:**
2866
2868
  - Any member — sees own entries only (scoped by server regardless of flags).
@@ -2988,6 +2990,7 @@ Cancel a PTO entry. Transitions status to `canceled`.
2988
2990
  **Authorization:**
2989
2991
  - Own entry + `status=pending` → any member.
2990
2992
  - Manager+ → any non-canceled entry for users in their visibility scope (including approved entries).
2993
+ - Your own entry once it is approved or rejected → admins and owners only; a manager or project manager gets a `403` and must ask another approver (same rule as editing).
2991
2994
 
2992
2995
  `decided_by` / `decided_at` are NOT stamped by cancel (only by approve/reject).
2993
2996
 
@@ -45,7 +45,7 @@ If the user doesn't specify a kind, default to `vacation` but **confirm before s
45
45
  | Status | Meaning | Who can act next |
46
46
  |--------|---------|-----------------|
47
47
  | `pending` | Submitted, awaiting decision | Manager+ can approve or reject; owner of the entry can cancel or edit |
48
- | `approved` | Approved and active | Manager+ can cancel (removes from active capacity); manager+ can edit dates/hours (audit-logged) |
48
+ | `approved` | Approved and active | Manager+ can cancel (removes from active capacity) or edit dates/hours (audit-logged) — but not their own entry unless they are admin/owner |
49
49
  | `rejected` | Rejected by manager | Entry owner can submit a new request with corrected dates |
50
50
  | `canceled` | Canceled (no longer active) | Terminal state — no further transitions |
51
51
 
@@ -93,7 +93,7 @@ Project managers and account managers using `ct pto list --team` or `ct pto add
93
93
  | `ct pto add <user>` (on-behalf) | No | Within scope → approved (400 outside it) | Any user → approved |
94
94
  | `ct pto list / show` | Own only | Within scope | Any |
95
95
  | `ct pto edit` | Own + pending only | Within scope (any status); **their own** entry only while pending | Any |
96
- | `ct pto cancel` | Own + pending only | Within scope (any non-canceled) | Any |
96
+ | `ct pto cancel` | Own + pending only | Within scope (any non-canceled); **their own** entry only while pending | Any |
97
97
  | `ct pto approve` | No | Within scope + pending, **never their own** (403) | Any pending, including their own |
98
98
  | `ct pto reject` | No | Within scope + pending, **never their own** (403) | Any pending, including their own |
99
99
  | `ct pto calendar / summary` | Own | Within scope | Any |
@@ -577,8 +577,8 @@ ct timesheet submit --from 2026-03-09 --to 2026-03-15 --force
577
577
  ct timesheet submit --from monday --to wednesday --force # Close out March
578
578
  ct timesheet submit --from thursday --to friday --force # Start April
579
579
 
580
- # Check submission status
581
- ct timesheet list
580
+ # Check submission status (--mine: project leads and managers also see colleagues' timesheets)
581
+ ct timesheet list --mine
582
582
 
583
583
  # Made a mistake? Recall it back to draft (before approval) — confirms before executing
584
584
  ct timesheet recall <timesheet-id> --notes "Need to fix Wednesday entries"
@@ -714,7 +714,7 @@ ct expense create --amount 49.99 "Domain renewal" --project acme-website --categ
714
714
  ct expense create --amount 29.99 "Team Slack subscription" --category "Software"
715
715
 
716
716
  # Log a past expense
717
- ct ex create --amount 85 "Client dinner" --category "Meals" --vendor "La Trattoria" -d yesterday --billable -p acme-project
717
+ ct expense create --amount 85 "Client dinner" --category "Meals" --vendor "La Trattoria" -d yesterday --billable -p acme-project
718
718
  ```
719
719
 
720
720
  ### Setting Up Expense Categories
@@ -734,8 +734,10 @@ ct expense add-category "Hardware"
734
734
  ### Reviewing Expenses
735
735
 
736
736
  ```bash
737
- # List expenses for a period
737
+ # List expenses for a period. Managers and project leads also see colleagues'
738
+ # expenses here; add --mine when the user means their own
738
739
  ct expense list --from 2026-03-01 --to 2026-03-31
740
+ ct expense list --mine --from 2026-03-01 --to 2026-03-31
739
741
 
740
742
  # Filter by project and billable status
741
743
  ct expense list --project acme-website --billable
File without changes
File without changes