crowdtime-cli 0.20.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.20.0 → crowdtime_cli-0.21.1}/PKG-INFO +1 -1
  2. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/pyproject.toml +1 -1
  3. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/__init__.py +1 -1
  4. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/clients_cmd.py +2 -2
  5. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/expense_cmd.py +38 -7
  6. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/invoice_cmd.py +5 -5
  7. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/org_cmd.py +1 -1
  8. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/projects_cmd.py +3 -3
  9. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/pto_cmd.py +4 -4
  10. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/report_cmd.py +1 -1
  11. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/tasks_cmd.py +1 -1
  12. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/team_cmd.py +5 -5
  13. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/timer_cmd.py +17 -3
  14. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/timesheet_cmd.py +78 -14
  15. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/formatters.py +4 -4
  16. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/main.py +2 -1
  17. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/SKILL.md +10 -7
  18. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/commands.md +31 -10
  19. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/pto.md +3 -2
  20. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +12 -5
  21. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/.gitignore +0 -0
  22. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/LICENSE +0 -0
  23. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/README.md +0 -0
  24. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/auth.py +0 -0
  25. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/client.py +0 -0
  26. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/__init__.py +0 -0
  27. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
  28. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
  29. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/billing_cmd.py +0 -0
  30. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/calendar_cmd.py +0 -0
  31. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/config_cmd.py +0 -0
  32. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
  33. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/insights_cmd.py +0 -0
  34. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/log_cmd.py +0 -0
  35. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
  36. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
  37. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/commands/version_cmd.py +0 -0
  38. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/config.py +0 -0
  39. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/models.py +0 -0
  40. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/oauth.py +0 -0
  41. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/resolvers.py +0 -0
  42. {crowdtime_cli-0.20.0 → crowdtime_cli-0.21.1}/src/crowdtime_cli/utils.py +0 -0
  43. {crowdtime_cli-0.20.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.20.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.20.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.20.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")
@@ -55,7 +55,7 @@ console = Console()
55
55
 
56
56
 
57
57
  # Valid enum values mirrored from the contract.
58
- _VALID_KINDS = {"vacation", "sick", "holiday", "personal", "other"}
58
+ _VALID_KINDS = {"vacation", "sick", "holiday", "personal", "birthday", "other"}
59
59
  _VALID_STATUSES = {"pending", "approved", "rejected", "canceled"}
60
60
 
61
61
 
@@ -211,7 +211,7 @@ def request_cmd(
211
211
  ...,
212
212
  help="Date or range: '2026-06-01', '2026-06-01:2026-06-05', 'today', 'next-monday'.",
213
213
  ),
214
- kind: str = typer.Option("vacation", "--kind", "-k", help="vacation|sick|holiday|personal|other."),
214
+ kind: str = typer.Option("vacation", "--kind", "-k", help="vacation|sick|holiday|personal|birthday|other. Birthday: one day, once per calendar year."),
215
215
  hours_per_day: Optional[str] = typer.Option(
216
216
  None, "--hours-per-day",
217
217
  help="Decimal hours. Empty/omitted = full day from membership capacity.",
@@ -256,7 +256,7 @@ def add_cmd(
256
256
  ...,
257
257
  help="Date or range: '2026-06-01', '2026-06-01:2026-06-05', 'today', 'next-monday'.",
258
258
  ),
259
- kind: str = typer.Option("vacation", "--kind", "-k", help="vacation|sick|holiday|personal|other."),
259
+ kind: str = typer.Option("vacation", "--kind", "-k", help="vacation|sick|holiday|personal|birthday|other. Birthday: one day, once per calendar year."),
260
260
  hours_per_day: Optional[str] = typer.Option(
261
261
  None, "--hours-per-day",
262
262
  help="Decimal hours per day. Omit for full-day from membership.",
@@ -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)
@@ -1,4 +1,4 @@
1
- """Timesheet commands: list, submit, approve, reject, recall, reopen, team overview."""
1
+ """Timesheet commands: list, submit, remind, approve, reject, recall, reopen, team overview."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -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)
@@ -246,17 +246,27 @@ def submit_timesheet(
246
246
  period_end: str = typer.Option(
247
247
  ..., "--to", help="Period end date (YYYY-MM-DD or keyword)."
248
248
  ),
249
+ user: Optional[str] = typer.Option(
250
+ None, "--user", "-u",
251
+ help="Submit on someone's behalf (email or UUID). Reviewers only.",
252
+ ),
249
253
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt."),
250
254
  output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
251
255
  ) -> None:
252
256
  """Submit a timesheet for approval.
253
257
 
254
- Computes total hours from your time entries in the given period.
258
+ Computes total hours from the time entries in the given period.
255
259
  Shows a preview of hours before submitting (use --force to skip).
256
260
 
261
+ --user submits a week someone logged but never sent. It needs the same
262
+ authority as approving their whole timesheet (admin, their manager, or
263
+ running every project they logged on); the submission is then reviewed
264
+ as usual.
265
+
257
266
  Examples:
258
267
  ct timesheet submit --from 2026-03-09 --to 2026-03-15
259
268
  ct timesheet submit --from monday --to friday --force
269
+ ct timesheet submit --from 2026-03-09 --to 2026-03-15 --user ana@acme.com
260
270
  """
261
271
  try:
262
272
  start = format_date(parse_date(period_start))
@@ -267,13 +277,23 @@ def submit_timesheet(
267
277
 
268
278
  client = CrowdTimeClient(require_auth=True, require_org=True)
269
279
 
280
+ user_id = None
281
+ if user:
282
+ from .pto_cmd import _resolve_user_to_uuid
283
+
284
+ try:
285
+ user_id = _resolve_user_to_uuid(client, user)
286
+ except APIError as e:
287
+ format_error(e.message)
288
+ raise typer.Exit(1)
289
+
270
290
  # Preview: fetch entries for the period and show summary before confirming
271
291
  if not force and not output_json:
272
292
  try:
273
- entries_data = client.get("/time-entries/", params={
274
- "date_from": start,
275
- "date_to": end,
276
- })
293
+ entry_params = {"date_from": start, "date_to": end}
294
+ if user_id:
295
+ entry_params["user"] = user_id
296
+ entries_data = client.get("/time-entries/", params=entry_params)
277
297
  entries = extract_results(entries_data)
278
298
  total_h = sum(Decimal(str(e.get("hours", 0))) for e in entries)
279
299
  billable_h = sum(
@@ -296,10 +316,10 @@ def submit_timesheet(
296
316
  pass # If preview fails, proceed to submit (server will validate)
297
317
 
298
318
  try:
299
- result = client.post("/timesheets/submit/", data={
300
- "period_start": start,
301
- "period_end": end,
302
- })
319
+ payload = {"period_start": start, "period_end": end}
320
+ if user_id:
321
+ payload["user_id"] = user_id
322
+ result = client.post("/timesheets/submit/", data=payload)
303
323
 
304
324
  if output_json:
305
325
  print_json(result)
@@ -316,6 +336,50 @@ def submit_timesheet(
316
336
  raise typer.Exit(1)
317
337
 
318
338
 
339
+ @app.command("remind")
340
+ def remind_timesheet(
341
+ user: str = typer.Option(..., "--user", "-u", help="Who to remind (email or UUID)."),
342
+ period_start: str = typer.Option(
343
+ ..., "--from", help="Period start date (YYYY-MM-DD or keyword)."
344
+ ),
345
+ period_end: str = typer.Option(
346
+ ..., "--to", help="Period end date (YYYY-MM-DD or keyword)."
347
+ ),
348
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
349
+ ) -> None:
350
+ """Email someone a reminder to submit their timesheet (project_manager+).
351
+
352
+ For a week they haven't submitted. One reminder per person and period an hour.
353
+
354
+ Examples:
355
+ ct timesheet remind --user ana@acme.com --from 2026-03-09 --to 2026-03-15
356
+ """
357
+ from .pto_cmd import _resolve_user_to_uuid
358
+
359
+ try:
360
+ start = format_date(parse_date(period_start))
361
+ end = format_date(parse_date(period_end))
362
+ except ValueError as e:
363
+ format_error(str(e))
364
+ raise typer.Exit(1)
365
+
366
+ client = CrowdTimeClient(require_auth=True, require_org=True)
367
+ try:
368
+ user_id = _resolve_user_to_uuid(client, user)
369
+ result = client.post("/timesheets/remind/", data={
370
+ "user_id": user_id,
371
+ "period_start": start,
372
+ "period_end": end,
373
+ })
374
+ if output_json:
375
+ print_json(result)
376
+ else:
377
+ format_success(f"Reminder sent to {user} for {start} → {end}.")
378
+ except APIError as e:
379
+ format_error(e.message)
380
+ raise typer.Exit(1)
381
+
382
+
319
383
  @app.command("approve")
320
384
  def approve_timesheet(
321
385
  timesheet_id: str = typer.Argument(..., help="Timesheet ID to approve."),
@@ -620,7 +684,7 @@ def viewable_users(
620
684
  return
621
685
 
622
686
  table = Table(show_header=True, header_style="bold")
623
- table.add_column("ID", style="dim")
687
+ table.add_column("ID", style="dim", no_wrap=True, min_width=36)
624
688
  table.add_column("Name", width=20)
625
689
  table.add_column("Email", width=30)
626
690
 
@@ -1057,7 +1121,7 @@ def compliance_report(
1057
1121
  title=f"[blue]Pending Approval ({len(pending)})[/blue]",
1058
1122
  )
1059
1123
  table.add_column("Member", width=22)
1060
- table.add_column("Timesheet ID", style="dim")
1124
+ table.add_column("Timesheet ID", style="dim", no_wrap=True, min_width=36)
1061
1125
  table.add_column("Hours", justify="right", width=8)
1062
1126
  table.add_column("Submitted", width=12)
1063
1127
  table.add_column("Days Pending", justify="right", width=13)
@@ -1083,7 +1147,7 @@ def compliance_report(
1083
1147
  title=f"[red]Rejected ({len(rejected_list)})[/red]",
1084
1148
  )
1085
1149
  table.add_column("Member", width=22)
1086
- table.add_column("Timesheet ID", style="dim")
1150
+ table.add_column("Timesheet ID", style="dim", no_wrap=True, min_width=36)
1087
1151
  table.add_column("Hours", justify="right", width=8)
1088
1152
  table.add_column("Reviewer Notes", width=35)
1089
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,10 +296,13 @@ 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)
304
+ ct timesheet submit --from 2026-03-10 --to 2026-03-16 --user ana@acme.com # Submit a week someone logged but never sent (same authority as whole approve)
305
+ ct timesheet remind --user ana@acme.com --from 2026-03-10 --to 2026-03-16 # Email them a reminder to submit (project_manager+)
303
306
  ct timesheet recall <id> # Recall submitted timesheet back to draft (--force to skip confirm)
304
307
  ct timesheet approve <id> # Approve the whole timesheet (admin, direct manager, or PM of every project in it)
305
308
  ct timesheet approve <id> --project <project-id> # Approve one project's portion (PM for that project)
@@ -424,7 +427,7 @@ ct log 0.25 --project argo --task BE --notes "Daily standup"
424
427
 
425
428
  ### Time Off (PTO)
426
429
 
427
- Per-user time-off entries with a kind (vacation, sick, holiday, personal, other) and a status lifecycle (pending → approved/rejected; pending → canceled). Members submit requests that await approval, and their approvers are emailed automatically; project managers and above can also add PTO directly on behalf of someone in their scope (auto-approved).
430
+ Per-user time-off entries with a kind (vacation, sick, holiday, personal, birthday, other) and a status lifecycle (pending → approved/rejected; pending → canceled). Members submit requests that await approval, and their approvers are emailed automatically; project managers and above can also add PTO directly on behalf of someone in their scope (auto-approved).
428
431
 
429
432
  > **IMPORTANT**: Never use magic routing (`ct "..."`) or `ct ai parse` for PTO operations. Always construct explicit `ct pto ...` commands.
430
433
  > Full reference: `references/pto.md` and `references/commands.md` § PTO.
@@ -435,7 +438,7 @@ ct pto add <user> <dates> # Add PTO on behalf of user, auto-approved
435
438
  ct pto list [--mine|--team] # List PTO entries with optional filters
436
439
  ct pto show <id> # Detail view of a single PTO entry (NEVER truncate the ID)
437
440
  ct pto edit <id> [fields] # Edit date range, kind, notes, hours/day (your own: pending only)
438
- 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)
439
442
  ct pto approve <id> [--notes "..."] # Approve a pending entry (project manager+; never your own unless admin)
440
443
  ct pto reject <id> --notes "..." # Reject a pending entry — notes required (project manager+; never your own unless admin)
441
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/`
@@ -1735,13 +1737,14 @@ Lists your timesheets, plus — for managers and project managers — the timesh
1735
1737
  ### ct timesheet submit
1736
1738
 
1737
1739
  ```
1738
- ct timesheet submit --from DATE --to DATE [--force/-f] [--json]
1740
+ ct timesheet submit --from DATE --to DATE [--user/-u USER] [--force/-f] [--json]
1739
1741
  ```
1740
1742
 
1741
1743
  | Option | Type | Description |
1742
1744
  |--------|------|-------------|
1743
1745
  | `--from` | string (required) | Period start date |
1744
1746
  | `--to` | string (required) | Period end date |
1747
+ | `--user`, `-u` | string | Submit on someone's behalf (email or UUID) |
1745
1748
  | `--force`, `-f` | flag | Skip confirmation prompt |
1746
1749
  | `--json` | flag | JSON output |
1747
1750
 
@@ -1749,6 +1752,23 @@ Submits a timesheet for the given period (any date range — not limited to full
1749
1752
 
1750
1753
  A zero-hour period is rejected **unless every working day in it is covered by approved time off** — a full vacation week has no time entries by design and is submitted as-is. If someone was away and the submit is rejected, the fix is an approved PTO entry (`ct pto request`), not a fake time entry. Endpoint: `POST /timesheets/submit/`
1751
1754
 
1755
+ `--user` submits a week someone logged but never sent (body `user_id`). It needs the same authority as approving their whole timesheet — admin/owner, their direct manager, or running every project they logged on that period — otherwise `403`. The submission is recorded as theirs (the audit event names who sent it) and is then approved or rejected as usual. To nudge them to do it themselves, use `ct timesheet remind`.
1756
+
1757
+ ### ct timesheet remind
1758
+
1759
+ ```
1760
+ ct timesheet remind --user USER --from DATE --to DATE [--json]
1761
+ ```
1762
+
1763
+ | Option | Type | Description |
1764
+ |--------|------|-------------|
1765
+ | `--user`, `-u` | string (required) | Who to remind (email or UUID) |
1766
+ | `--from` | string (required) | Period start date |
1767
+ | `--to` | string (required) | Period end date |
1768
+ | `--json` | flag | JSON output |
1769
+
1770
+ Emails the person a reminder to submit their timesheet for the period, naming who asked (project_manager+, anyone on your roster — `ct timesheet viewable-users`). Refused with `400` when a timesheet for the period is already submitted or approved, and with `429` when that person was reminded about the same period less than an hour ago. Endpoint: `POST /timesheets/remind/` → `202`.
1771
+
1752
1772
  ### ct timesheet approve
1753
1773
 
1754
1774
  ```
@@ -2740,7 +2760,7 @@ Submit a PTO request for yourself. Created with `status=pending` — must be app
2740
2760
  | Argument/Option | Type | Required | Description |
2741
2761
  |-----------------|------|----------|-------------|
2742
2762
  | `<dates>` | string | yes | `YYYY-MM-DD` or `YYYY-MM-DD:YYYY-MM-DD` |
2743
- | `--kind`, `-k` | string | no | `vacation` (default), `sick`, `holiday`, `personal`, `other` |
2763
+ | `--kind`, `-k` | string | no | `vacation` (default), `sick`, `holiday`, `personal`, `birthday` (one day, once per calendar year), `other` |
2744
2764
  | `--hours-per-day`, `-h` | decimal | no | Hours/day. Empty = capacity-based default (see `references/pto.md`) |
2745
2765
  | `--notes`, `-n` | string | no | Reason or notes |
2746
2766
  | `--json` | flag | no | JSON output |
@@ -2786,7 +2806,7 @@ Add a PTO entry on behalf of another user (project manager and above). The entry
2786
2806
  |-----------------|------|----------|-------------|
2787
2807
  | `<user>` | string | yes | User email or UUID |
2788
2808
  | `<dates>` | string | yes | `YYYY-MM-DD` or `YYYY-MM-DD:YYYY-MM-DD` |
2789
- | `--kind`, `-k` | string | no | `vacation` (default), `sick`, `holiday`, `personal`, `other` |
2809
+ | `--kind`, `-k` | string | no | `vacation` (default), `sick`, `holiday`, `personal`, `birthday` (one day, once per calendar year), `other` |
2790
2810
  | `--hours-per-day`, `-h` | decimal | no | Hours/day. Empty = capacity-based default |
2791
2811
  | `--notes`, `-n` | string | no | Reason or context notes |
2792
2812
  | `--json` | flag | no | JSON output |
@@ -2842,7 +2862,7 @@ routing from the list payload.
2842
2862
  ct pto list [--mine] [--team] [--status/-s STATUS] [--kind/-k KIND] [--from DATE] [--to DATE] [--user/-u USER] [--json]
2843
2863
  ```
2844
2864
 
2845
- 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.
2846
2866
 
2847
2867
  **Who can call it:**
2848
2868
  - Any member — sees own entries only (scoped by server regardless of flags).
@@ -2970,6 +2990,7 @@ Cancel a PTO entry. Transitions status to `canceled`.
2970
2990
  **Authorization:**
2971
2991
  - Own entry + `status=pending` → any member.
2972
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).
2973
2994
 
2974
2995
  `decided_by` / `decided_at` are NOT stamped by cancel (only by approve/reject).
2975
2996
 
@@ -25,6 +25,7 @@ A `TimeOffEntry` represents a block of time off for a single user within an orga
25
25
  | `sick` | Sick Leave | Illness, medical appointment, recovery. |
26
26
  | `holiday` | Public Holiday | National or regional public holiday observed by the org. |
27
27
  | `personal` | Personal Day | Personal errands, family obligations, other non-vacation personal time. |
28
+ | `birthday` | Birthday | The person's birthday day off. **One day, once per calendar year** — a second one in the same year, or a multi-day range, is rejected. |
28
29
  | `other` | Other | Anything that doesn't fit the above categories. |
29
30
 
30
31
  If the user doesn't specify a kind, default to `vacation` but **confirm before submitting**.
@@ -44,7 +45,7 @@ If the user doesn't specify a kind, default to `vacation` but **confirm before s
44
45
  | Status | Meaning | Who can act next |
45
46
  |--------|---------|-----------------|
46
47
  | `pending` | Submitted, awaiting decision | Manager+ can approve or reject; owner of the entry can cancel or edit |
47
- | `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 |
48
49
  | `rejected` | Rejected by manager | Entry owner can submit a new request with corrected dates |
49
50
  | `canceled` | Canceled (no longer active) | Terminal state — no further transitions |
50
51
 
@@ -92,7 +93,7 @@ Project managers and account managers using `ct pto list --team` or `ct pto add
92
93
  | `ct pto add <user>` (on-behalf) | No | Within scope → approved (400 outside it) | Any user → approved |
93
94
  | `ct pto list / show` | Own only | Within scope | Any |
94
95
  | `ct pto edit` | Own + pending only | Within scope (any status); **their own** entry only while pending | Any |
95
- | `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 |
96
97
  | `ct pto approve` | No | Within scope + pending, **never their own** (403) | Any pending, including their own |
97
98
  | `ct pto reject` | No | Within scope + pending, **never their own** (403) | Any pending, including their own |
98
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"
@@ -634,6 +634,11 @@ ct timesheet reject <timesheet-id> --notes "Missing entries for Wednesday — pl
634
634
  # Reject multiple timesheets at once
635
635
  ct timesheet bulk-reject <id1> <id2> --notes "Incomplete entries for the week"
636
636
 
637
+ # Someone logged the week but never submitted ("No timesheet" in `ct timesheet team`)?
638
+ # There's nothing to reopen — nudge them, or submit it for them and then approve it
639
+ ct timesheet remind --user ana@acme.com --from 2026-03-09 --to 2026-03-15
640
+ ct timesheet submit --from 2026-03-09 --to 2026-03-15 --user ana@acme.com --force
641
+
637
642
  # Approved it too early? Reopen — back to draft, entries unlocked, user fixes & resubmits
638
643
  ct timesheet reopen <timesheet-id> --notes "Approved before Friday's entries were added"
639
644
  ct timesheet reopen <timesheet-id> --notes "Wrong project on Tuesday" --force
@@ -709,7 +714,7 @@ ct expense create --amount 49.99 "Domain renewal" --project acme-website --categ
709
714
  ct expense create --amount 29.99 "Team Slack subscription" --category "Software"
710
715
 
711
716
  # Log a past expense
712
- 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
713
718
  ```
714
719
 
715
720
  ### Setting Up Expense Categories
@@ -729,8 +734,10 @@ ct expense add-category "Hardware"
729
734
  ### Reviewing Expenses
730
735
 
731
736
  ```bash
732
- # 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
733
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
734
741
 
735
742
  # Filter by project and billable status
736
743
  ct expense list --project acme-website --billable
@@ -1477,7 +1484,7 @@ Use this when a member wants to request time off. The request goes into `pending
1477
1484
 
1478
1485
  **Before running any command, ask the user:**
1479
1486
  1. What dates? (single day or range) — if they say "next week" or "a few days in June", ASK for the exact dates. Never assume.
1480
- 2. What kind? (vacation / sick / holiday / personal / other) — if not specified, default to `vacation` and confirm.
1487
+ 2. What kind? (vacation / sick / holiday / personal / birthday / other) — if not specified, default to `vacation` and confirm. Birthday is a single day, once per calendar year.
1481
1488
  3. Hours per day? — leave empty unless they want partial days (e.g. half-day sick leave).
1482
1489
  4. Notes? — optional but useful for the manager.
1483
1490
 
File without changes
File without changes