crowdtime-cli 0.15.0__tar.gz → 0.17.0__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.
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/PKG-INFO +1 -1
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/pyproject.toml +1 -1
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/__init__.py +1 -1
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/ai_cmd.py +17 -1
- crowdtime_cli-0.17.0/src/crowdtime_cli/commands/calendar_cmd.py +136 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/log_cmd.py +4 -2
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/projects_cmd.py +46 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/pto_cmd.py +23 -11
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/formatters.py +9 -1
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/main.py +26 -12
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/models.py +20 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/skills/crowdtime/SKILL.md +68 -8
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/skills/crowdtime/references/commands.md +99 -9
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/skills/crowdtime/references/pto.md +42 -4
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +2 -2
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/.gitignore +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/LICENSE +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/README.md +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/auth.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/client.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/__init__.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/billing_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/config_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/insights_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/invoice_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/org_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/report_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/team_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/timesheet_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/commands/version_cmd.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/config.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/oauth.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/resolvers.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/utils.py +0 -0
- {crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/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.
|
|
3
|
+
Version: 0.17.0
|
|
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
|
|
@@ -37,6 +37,7 @@ def parse(
|
|
|
37
37
|
ct ai parse "2 hours on project alpha doing code review"
|
|
38
38
|
ct ai parse "spent yesterday afternoon on bug fixes for client X"
|
|
39
39
|
ct ai parse "30min standup" --force
|
|
40
|
+
ct ai parse "daily 15 min standup on argo all week" # creates 5 entries
|
|
40
41
|
"""
|
|
41
42
|
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
42
43
|
|
|
@@ -54,8 +55,13 @@ def parse(
|
|
|
54
55
|
console.print("[dim]Dry run - not saving.[/dim]")
|
|
55
56
|
return
|
|
56
57
|
|
|
58
|
+
occurrence_count = len(result.occurrence_dates)
|
|
57
59
|
if not force:
|
|
58
|
-
|
|
60
|
+
prompt = (
|
|
61
|
+
f"Create {occurrence_count} entries?" if occurrence_count > 1
|
|
62
|
+
else "Create this entry?"
|
|
63
|
+
)
|
|
64
|
+
if not typer.confirm(prompt):
|
|
59
65
|
console.print("[dim]Cancelled.[/dim]")
|
|
60
66
|
return
|
|
61
67
|
|
|
@@ -65,6 +71,16 @@ def parse(
|
|
|
65
71
|
**result.parsed_fields,
|
|
66
72
|
})
|
|
67
73
|
|
|
74
|
+
# A recurring parse comes back as {count, entries}; a single date keeps
|
|
75
|
+
# the plain TimeEntry shape.
|
|
76
|
+
if "entries" in confirm_data:
|
|
77
|
+
entries = [TimeEntry(**e) for e in confirm_data["entries"]]
|
|
78
|
+
format_success(f"{confirm_data['count']} entries created from AI parse")
|
|
79
|
+
from ..formatters import format_entry_summary
|
|
80
|
+
for entry in entries:
|
|
81
|
+
format_entry_summary(entry)
|
|
82
|
+
return
|
|
83
|
+
|
|
68
84
|
# Response may be a full TimeEntry or a minimal {detail, time_entry_id}
|
|
69
85
|
if "id" in confirm_data and "project_name" in confirm_data:
|
|
70
86
|
entry = TimeEntry(**confirm_data)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Calendar commands: status, events, disconnect (CRO-74).
|
|
2
|
+
|
|
3
|
+
Connecting requires a browser for the Google consent screen, so that step
|
|
4
|
+
lives in the web app (Settings → Integrations). Once connected, the CLI can
|
|
5
|
+
read the day's meetings and turn one into a time entry.
|
|
6
|
+
|
|
7
|
+
Events are fetched live from Google on every call and are never stored by
|
|
8
|
+
CrowdTime — only the entry you create from one is.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from ..client import APIError, CrowdTimeClient
|
|
20
|
+
from ..formatters import format_error, format_success, print_json
|
|
21
|
+
from ..utils import format_date, parse_date
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(help="Google Calendar integration.")
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("status")
|
|
28
|
+
def status(
|
|
29
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Show whether a calendar is connected.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
ct calendar status
|
|
35
|
+
"""
|
|
36
|
+
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
data = client.get("/calendar/")
|
|
40
|
+
except APIError as e:
|
|
41
|
+
format_error(e.message)
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
if output_json:
|
|
45
|
+
print_json(data)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if data.get("available") is False:
|
|
49
|
+
console.print("[dim]Google Calendar is not configured for this deployment.[/dim]")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if not data.get("connected"):
|
|
53
|
+
console.print("[dim]No calendar connected.[/dim]")
|
|
54
|
+
console.print(
|
|
55
|
+
"[dim]Connect one from the web app: Settings → Integrations.[/dim]"
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
account = data.get("account_email") or "(unknown account)"
|
|
60
|
+
console.print(f"Connected: [green]{account}[/green]")
|
|
61
|
+
if data.get("last_used_at"):
|
|
62
|
+
console.print(f" Last read: {data['last_used_at']}")
|
|
63
|
+
if data.get("last_error"):
|
|
64
|
+
console.print(f" [yellow]Last error:[/yellow] {data['last_error']}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command("events")
|
|
68
|
+
def events(
|
|
69
|
+
date: Optional[str] = typer.Option(None, "--date", "-d", help="Day to read (YYYY-MM-DD). Defaults to today."),
|
|
70
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""List your calendar events for a day.
|
|
73
|
+
|
|
74
|
+
Fetched live from Google. Use the hours and title to create an entry:
|
|
75
|
+
|
|
76
|
+
ct calendar events
|
|
77
|
+
ct log 0.25 --project argo --task BE --notes "Daily standup"
|
|
78
|
+
"""
|
|
79
|
+
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
80
|
+
|
|
81
|
+
params = {}
|
|
82
|
+
if date:
|
|
83
|
+
params["date"] = format_date(parse_date(date))
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
data = client.get("/calendar/events/", params=params)
|
|
87
|
+
except APIError as e:
|
|
88
|
+
format_error(e.message)
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
if output_json:
|
|
92
|
+
print_json(data)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
items = data.get("events", [])
|
|
96
|
+
if not items:
|
|
97
|
+
console.print(f"[dim]No events on {data.get('date', 'that day')}.[/dim]")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
table = Table(title=f"Calendar — {data.get('date', '')}")
|
|
101
|
+
table.add_column("Title")
|
|
102
|
+
table.add_column("Hours", justify="right")
|
|
103
|
+
table.add_column("Start", style="dim")
|
|
104
|
+
for event in items:
|
|
105
|
+
title = event.get("title", "")
|
|
106
|
+
if event.get("declined"):
|
|
107
|
+
title = f"[dim]{title} (declined)[/dim]"
|
|
108
|
+
table.add_row(
|
|
109
|
+
title,
|
|
110
|
+
"—" if event.get("all_day") else f"{event.get('hours', 0)}",
|
|
111
|
+
"all day" if event.get("all_day") else (event.get("start", "") or "")[11:16],
|
|
112
|
+
)
|
|
113
|
+
console.print(table)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command("disconnect")
|
|
117
|
+
def disconnect(
|
|
118
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation."),
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Disconnect your calendar and revoke CrowdTime's access.
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
ct calendar disconnect
|
|
124
|
+
"""
|
|
125
|
+
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
126
|
+
|
|
127
|
+
if not force and not typer.confirm("Disconnect your Google Calendar?"):
|
|
128
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
client.delete("/calendar/")
|
|
133
|
+
format_success("Calendar disconnected.")
|
|
134
|
+
except APIError as e:
|
|
135
|
+
format_error(e.message)
|
|
136
|
+
raise typer.Exit(1)
|
|
@@ -43,7 +43,8 @@ def create(
|
|
|
43
43
|
task: Optional[str] = typer.Option(None, "--task", "-t", help="Task name or ID."),
|
|
44
44
|
date: Optional[str] = typer.Option(None, "--date", "-d", help="Date in YYYY-MM-DD (default: today)."),
|
|
45
45
|
billable: Optional[bool] = typer.Option(None, "--billable/--no-billable", "-b/-B",
|
|
46
|
-
help="
|
|
46
|
+
help="Override billable status. Account managers only — "
|
|
47
|
+
"otherwise the project/task setting decides."),
|
|
47
48
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
48
49
|
) -> None:
|
|
49
50
|
"""Log a completed time entry.
|
|
@@ -137,7 +138,8 @@ def edit(
|
|
|
137
138
|
task: Optional[str] = typer.Option(None, "--task", "-t", help="New task."),
|
|
138
139
|
date: Optional[str] = typer.Option(None, "--date", "-d", help="New date (YYYY-MM-DD)."),
|
|
139
140
|
billable: Optional[bool] = typer.Option(None, "--billable/--no-billable", "-b/-B",
|
|
140
|
-
help="
|
|
141
|
+
help="Override billable status. Account managers only — "
|
|
142
|
+
"otherwise the project/task setting decides."),
|
|
141
143
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
142
144
|
) -> None:
|
|
143
145
|
"""Edit an existing time entry."""
|
|
@@ -755,6 +755,52 @@ def update_member(
|
|
|
755
755
|
raise typer.Exit(1)
|
|
756
756
|
|
|
757
757
|
|
|
758
|
+
@app.command("assignable-users")
|
|
759
|
+
def assignable_users(
|
|
760
|
+
project_id: str = typer.Argument(..., help="Project ID."),
|
|
761
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
762
|
+
) -> None:
|
|
763
|
+
"""List org members who can still be added to a project.
|
|
764
|
+
|
|
765
|
+
Everyone active in the organization who is not already on the project.
|
|
766
|
+
Use this to find user IDs for `ct projects add-members`.
|
|
767
|
+
|
|
768
|
+
Examples:
|
|
769
|
+
ct projects assignable-users <project-id>
|
|
770
|
+
"""
|
|
771
|
+
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
772
|
+
|
|
773
|
+
try:
|
|
774
|
+
project_uuid = resolve_project(client, project_id)
|
|
775
|
+
data = client.get(f"/projects/{project_uuid}/assignable-users/")
|
|
776
|
+
users = extract_results(data) or data
|
|
777
|
+
|
|
778
|
+
if output_json:
|
|
779
|
+
print_json(users)
|
|
780
|
+
return
|
|
781
|
+
|
|
782
|
+
if not users:
|
|
783
|
+
console.print("[dim]Everyone in the organization is already on this project.[/dim]")
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
table = Table(title="Assignable users")
|
|
787
|
+
table.add_column("ID", style="dim")
|
|
788
|
+
table.add_column("Name")
|
|
789
|
+
table.add_column("Email")
|
|
790
|
+
table.add_column("Org role")
|
|
791
|
+
for u in users:
|
|
792
|
+
table.add_row(
|
|
793
|
+
u.get("id", ""),
|
|
794
|
+
u.get("full_name", ""),
|
|
795
|
+
u.get("email", ""),
|
|
796
|
+
u.get("org_role", ""),
|
|
797
|
+
)
|
|
798
|
+
console.print(table)
|
|
799
|
+
except APIError as e:
|
|
800
|
+
format_error(e.message)
|
|
801
|
+
raise typer.Exit(1)
|
|
802
|
+
|
|
803
|
+
|
|
758
804
|
@app.command("add-members")
|
|
759
805
|
def add_members_bulk(
|
|
760
806
|
project_id: str = typer.Argument(..., help="Project ID."),
|
|
@@ -5,13 +5,13 @@ Mirrors the PTO API at `/organizations/<slug>/time-off/` — see
|
|
|
5
5
|
|
|
6
6
|
Subcommands:
|
|
7
7
|
request – ask for time off (status=pending)
|
|
8
|
-
add –
|
|
8
|
+
add – on-behalf creation by project manager+ (auto-approved)
|
|
9
9
|
list – list / filter entries (own or team)
|
|
10
10
|
show – detail panel
|
|
11
11
|
edit – patch fields
|
|
12
|
-
cancel – self/manager+ cancel
|
|
13
|
-
approve – manager+ approve
|
|
14
|
-
reject – manager+ reject (notes required)
|
|
12
|
+
cancel – self/project manager+ cancel
|
|
13
|
+
approve – project manager+ approve
|
|
14
|
+
reject – project manager+ reject (notes required)
|
|
15
15
|
calendar – render-ready month grid
|
|
16
16
|
summary – aggregate hours/days for a window
|
|
17
17
|
"""
|
|
@@ -215,7 +215,7 @@ def request_cmd(
|
|
|
215
215
|
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Optional free-text notes."),
|
|
216
216
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
217
217
|
) -> None:
|
|
218
|
-
"""Request time off for yourself (status=pending until
|
|
218
|
+
"""Request time off for yourself (status=pending until an approver decides)."""
|
|
219
219
|
_validate_kind(kind)
|
|
220
220
|
start, end = _parse_range_or_exit(dates)
|
|
221
221
|
hpd = _hours_payload(hours_per_day)
|
|
@@ -258,9 +258,15 @@ def add_cmd(
|
|
|
258
258
|
help="Decimal hours per day. Omit for full-day from membership.",
|
|
259
259
|
),
|
|
260
260
|
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Optional free-text notes."),
|
|
261
|
+
paid: Optional[bool] = typer.Option(
|
|
262
|
+
None, "--paid/--unpaid",
|
|
263
|
+
help="Whether these hours are paid. Omit to follow the organization's "
|
|
264
|
+
"policy for this kind. Account managers and above only — the API "
|
|
265
|
+
"rejects the request from anyone below, it is not ignored.",
|
|
266
|
+
),
|
|
261
267
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
262
268
|
) -> None:
|
|
263
|
-
"""
|
|
269
|
+
"""On-behalf PTO entry, auto-approved. Project manager+, within their scope."""
|
|
264
270
|
_validate_kind(kind)
|
|
265
271
|
start, end = _parse_range_or_exit(dates)
|
|
266
272
|
hpd = _hours_payload(hours_per_day)
|
|
@@ -283,12 +289,18 @@ def add_cmd(
|
|
|
283
289
|
payload["hours_per_day"] = str(hpd)
|
|
284
290
|
if notes is not None:
|
|
285
291
|
payload["notes"] = notes
|
|
292
|
+
# Declared since the flag was added but never sent, so --unpaid silently
|
|
293
|
+
# booked paid leave. The backend defaults every kind to paid, so the
|
|
294
|
+
# omission was not neutral.
|
|
295
|
+
if paid is not None:
|
|
296
|
+
payload["is_paid"] = paid
|
|
286
297
|
|
|
287
298
|
try:
|
|
288
299
|
data = client.post("/time-off/", data=payload)
|
|
289
300
|
except APIError as e:
|
|
290
|
-
# Surface
|
|
291
|
-
#
|
|
301
|
+
# Surface the rejection as a friendly error — the backend refuses
|
|
302
|
+
# `user` values outside the caller's visible set, refuses it entirely
|
|
303
|
+
# below project manager, and refuses `is_paid` below account manager.
|
|
292
304
|
format_error(e.message)
|
|
293
305
|
raise typer.Exit(1)
|
|
294
306
|
|
|
@@ -516,7 +528,7 @@ def cancel_cmd(
|
|
|
516
528
|
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation."),
|
|
517
529
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
518
530
|
) -> None:
|
|
519
|
-
"""Cancel a PTO entry (self if pending; manager+ for any state
|
|
531
|
+
"""Cancel a PTO entry (self if pending; project manager+ for any non-canceled state)."""
|
|
520
532
|
if not force:
|
|
521
533
|
if not typer.confirm(f"Cancel PTO entry {pto_id}?"):
|
|
522
534
|
console.print("[dim]Cancelled.[/dim]")
|
|
@@ -544,7 +556,7 @@ def approve_cmd(
|
|
|
544
556
|
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Optional decision notes."),
|
|
545
557
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
546
558
|
) -> None:
|
|
547
|
-
"""Approve a pending PTO entry (manager
|
|
559
|
+
"""Approve a pending PTO entry (project manager+, within their scope)."""
|
|
548
560
|
payload: dict = {}
|
|
549
561
|
if notes is not None:
|
|
550
562
|
payload["notes"] = notes
|
|
@@ -571,7 +583,7 @@ def reject_cmd(
|
|
|
571
583
|
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Decision notes (REQUIRED)."),
|
|
572
584
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
573
585
|
) -> None:
|
|
574
|
-
"""Reject a pending PTO entry (manager
|
|
586
|
+
"""Reject a pending PTO entry (project manager+, within scope). --notes is required."""
|
|
575
587
|
if not notes or not notes.strip():
|
|
576
588
|
format_error("--notes is required when rejecting a PTO entry.")
|
|
577
589
|
raise typer.Exit(1)
|
|
@@ -378,7 +378,15 @@ def format_parse_result(result: ParseResult) -> None:
|
|
|
378
378
|
console.print(f" Task: {result.task_name}")
|
|
379
379
|
if result.hours is not None:
|
|
380
380
|
console.print(f" Duration: [yellow]{format_duration(result.hours)}[/yellow]")
|
|
381
|
-
|
|
381
|
+
occurrences = result.occurrence_dates
|
|
382
|
+
if len(occurrences) > 1:
|
|
383
|
+
console.print(
|
|
384
|
+
f" Dates: [yellow]{len(occurrences)} entries[/yellow] "
|
|
385
|
+
f"({occurrences[0]} to {occurrences[-1]})"
|
|
386
|
+
)
|
|
387
|
+
for d in occurrences:
|
|
388
|
+
console.print(f" - {d:%a %d %b}")
|
|
389
|
+
elif result.date:
|
|
382
390
|
console.print(f" Date: {result.date}")
|
|
383
391
|
console.print(f" Billable: {'Yes' if result.is_billable else 'No'}")
|
|
384
392
|
|
|
@@ -23,6 +23,7 @@ from .commands import (
|
|
|
23
23
|
ai_cmd,
|
|
24
24
|
auth_cmd,
|
|
25
25
|
billing_cmd,
|
|
26
|
+
calendar_cmd,
|
|
26
27
|
clients_cmd,
|
|
27
28
|
config_cmd,
|
|
28
29
|
expense_cmd,
|
|
@@ -74,6 +75,7 @@ app.add_typer(expense_cmd.app, name="expense")
|
|
|
74
75
|
app.add_typer(insights_cmd.app, name="insights")
|
|
75
76
|
app.add_typer(team_cmd.app, name="team")
|
|
76
77
|
app.add_typer(pto_cmd.app, name="pto", help="Manage PTO (paid time off) requests")
|
|
78
|
+
app.add_typer(calendar_cmd.app, name="calendar", help="Google Calendar integration")
|
|
77
79
|
app.add_typer(version_cmd.app, name="version")
|
|
78
80
|
|
|
79
81
|
|
|
@@ -158,18 +160,30 @@ def _show_status(output_json: bool = False) -> None:
|
|
|
158
160
|
params={"status": "pending", "page_size": 50},
|
|
159
161
|
)
|
|
160
162
|
pending_items = extract_results(pending) if pending is not None else []
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
own_pending = [
|
|
164
|
+
row for row in pending_items if row.get("user") == self_user_id
|
|
165
|
+
] if self_user_id else []
|
|
166
|
+
|
|
167
|
+
# Approvers get the server's routed count — it excludes requests that
|
|
168
|
+
# were routed to someone else, which listing and filtering here cannot
|
|
169
|
+
# know. Falls back to the local count when the caller isn't an approver
|
|
170
|
+
# (the endpoint is project manager+ and 403s for everyone below).
|
|
171
|
+
routed_count = None
|
|
172
|
+
try:
|
|
173
|
+
routed = client.get("/time-off/pending-count/")
|
|
174
|
+
if isinstance(routed, dict):
|
|
175
|
+
routed_count = routed.get("count")
|
|
176
|
+
except APIError:
|
|
177
|
+
routed_count = None
|
|
178
|
+
|
|
179
|
+
if routed_count:
|
|
180
|
+
pto_pending_count = routed_count
|
|
181
|
+
pto_pending_label = "PTO requests pending review"
|
|
182
|
+
elif own_pending:
|
|
183
|
+
pto_pending_count = len(own_pending)
|
|
184
|
+
pto_pending_label = "of your PTO requests pending"
|
|
185
|
+
else:
|
|
186
|
+
pto_pending_count = 0
|
|
173
187
|
except APIError:
|
|
174
188
|
pto_pending_count = None
|
|
175
189
|
except Exception:
|
|
@@ -136,11 +136,22 @@ class ParseResult(BaseModel):
|
|
|
136
136
|
task_id: str | None = None
|
|
137
137
|
task_name: str | None = None
|
|
138
138
|
date: Optional[dt.date] = None
|
|
139
|
+
# Every date the description covers. A one-off entry holds a single date
|
|
140
|
+
# equal to ``date``; recurring input ("daily standup all week") expands to
|
|
141
|
+
# one date per occurrence.
|
|
142
|
+
dates: list[dt.date] = Field(default_factory=list)
|
|
139
143
|
is_billable: bool = True
|
|
140
144
|
confidence: float = 0.0
|
|
141
145
|
ambiguities: list[str] = Field(default_factory=list)
|
|
142
146
|
parse_log_id: str | None = None
|
|
143
147
|
|
|
148
|
+
@property
|
|
149
|
+
def occurrence_dates(self) -> list[dt.date]:
|
|
150
|
+
"""The dates to create entries for, never empty when a date parsed."""
|
|
151
|
+
if self.dates:
|
|
152
|
+
return self.dates
|
|
153
|
+
return [self.date] if self.date else []
|
|
154
|
+
|
|
144
155
|
@property
|
|
145
156
|
def parsed_fields(self) -> dict[str, Any]:
|
|
146
157
|
"""Return the parsed entry fields as a dict (for confirm endpoint)."""
|
|
@@ -160,6 +171,8 @@ class ParseResult(BaseModel):
|
|
|
160
171
|
fields["task_name"] = self.task_name
|
|
161
172
|
if self.date:
|
|
162
173
|
fields["date"] = str(self.date)
|
|
174
|
+
if self.occurrence_dates:
|
|
175
|
+
fields["dates"] = [str(d) for d in self.occurrence_dates]
|
|
163
176
|
fields["is_billable"] = self.is_billable
|
|
164
177
|
return fields
|
|
165
178
|
|
|
@@ -269,6 +282,11 @@ class TimeOffEntryListItem(BaseModel):
|
|
|
269
282
|
days_count: int = 0
|
|
270
283
|
kind: str = "vacation"
|
|
271
284
|
kind_display: str = ""
|
|
285
|
+
# Explicit override; None follows the org policy for the kind.
|
|
286
|
+
is_paid: bool | None = None
|
|
287
|
+
# Effective paid/unpaid after resolution. Unpaid hours explain the absence
|
|
288
|
+
# but do not count towards the hours the person is paid for.
|
|
289
|
+
resolved_is_paid: bool = True
|
|
272
290
|
status: str = "pending"
|
|
273
291
|
status_display: str = ""
|
|
274
292
|
resolved_hours_per_day: Decimal | None = None
|
|
@@ -294,6 +312,8 @@ class TimeOffEntry(BaseModel):
|
|
|
294
312
|
total_hours: Decimal | None = None
|
|
295
313
|
kind: str = "vacation"
|
|
296
314
|
kind_display: str = ""
|
|
315
|
+
is_paid: bool | None = None
|
|
316
|
+
resolved_is_paid: bool = True
|
|
297
317
|
status: str = "pending"
|
|
298
318
|
status_display: str = ""
|
|
299
319
|
notes: str = ""
|
|
@@ -34,6 +34,19 @@ ct s # Short alias
|
|
|
34
34
|
ct s --json # Machine-readable output
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### Recurring Entries
|
|
38
|
+
|
|
39
|
+
`ct ai parse` expands a repeating description into one entry per day, so a
|
|
40
|
+
whole week goes in with a single command:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ct ai parse "daily 15 min standup on argo all week" # → 5 entries, one per weekday
|
|
44
|
+
ct ai parse "1h code review on acme monday and wednesday"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It shows every date before creating anything. Prefer this over looping `ct log`
|
|
48
|
+
when the user describes repeating work; use `ct log` for one-off entries.
|
|
49
|
+
|
|
37
50
|
### Timers
|
|
38
51
|
```bash
|
|
39
52
|
ct timer start "description" -p project-slug -t "task name" # Start a timer
|
|
@@ -52,6 +65,20 @@ ct t # timer status
|
|
|
52
65
|
ct c # timer continue
|
|
53
66
|
```
|
|
54
67
|
|
|
68
|
+
### Billable Status
|
|
69
|
+
|
|
70
|
+
Billability comes from the **project and task configuration**, not from whoever
|
|
71
|
+
logs the time:
|
|
72
|
+
|
|
73
|
+
1. A non-billable project always wins — nothing on it is ever billable.
|
|
74
|
+
2. Otherwise the task's billable setting decides, when a task is set.
|
|
75
|
+
3. Otherwise the project's setting.
|
|
76
|
+
|
|
77
|
+
`--billable/--no-billable` is an override that only account managers and above
|
|
78
|
+
can use, and never on a non-billable project. For everyone else the flag is
|
|
79
|
+
ignored, so do not pass it and do not tell the user an entry is billable
|
|
80
|
+
because they asked for it — check the project/task setup instead.
|
|
81
|
+
|
|
55
82
|
### Logging Time
|
|
56
83
|
|
|
57
84
|
**Options MUST come BEFORE positional arguments (duration, description).**
|
|
@@ -137,10 +164,21 @@ ct p -a # include archived
|
|
|
137
164
|
|
|
138
165
|
#### Project Members
|
|
139
166
|
```bash
|
|
167
|
+
ct projects assignable-users <project-id> # Who can still be added (gives you the user IDs)
|
|
140
168
|
ct projects update-member <project-id> <user-id> --role project_manager --rate 150 # Set per-project role & rate
|
|
141
169
|
ct projects add-members <project-id> --user <uid1> --user <uid2> --role member --rate 120 # Bulk add
|
|
142
170
|
```
|
|
143
171
|
|
|
172
|
+
**Adding someone to a project:** run `assignable-users` first to get the user
|
|
173
|
+
IDs — it lists every active org member not already on the project. Do not use
|
|
174
|
+
`ct team list` for this; it is scoped to who you can review time for, which is
|
|
175
|
+
a different and narrower set.
|
|
176
|
+
|
|
177
|
+
**Per-project roles:** setting `--role project_manager` on a project member
|
|
178
|
+
grants them management of *that project only* (adding members, editing it,
|
|
179
|
+
managing its tasks), whatever their organization role is. That is the way to
|
|
180
|
+
let a member run one project without making them an org-wide project manager.
|
|
181
|
+
|
|
144
182
|
#### Bulk Task Assignment
|
|
145
183
|
```bash
|
|
146
184
|
ct projects add-tasks <project-id> --task <tid1> --task <tid2> # Bulk assign tasks to project
|
|
@@ -343,22 +381,44 @@ ct billing events -n 50 # Show last 50 events
|
|
|
343
381
|
ct b # billing status
|
|
344
382
|
```
|
|
345
383
|
|
|
384
|
+
### Calendar (Google)
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
ct calendar status # Is a calendar connected?
|
|
388
|
+
ct calendar events # Today's meetings, fetched live from Google
|
|
389
|
+
ct calendar events --date 2026-06-01 # A specific day
|
|
390
|
+
ct calendar disconnect # Revoke access
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Connecting requires the Google consent screen, so it happens in the web app
|
|
394
|
+
(Settings → Integrations); the CLI can only read and disconnect. Events are
|
|
395
|
+
fetched live on every call and are never stored by CrowdTime.
|
|
396
|
+
|
|
397
|
+
**Turning a meeting into an entry:** read the events, then log the time
|
|
398
|
+
explicitly — there is no automatic conversion:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
ct calendar events # e.g. "Daily standup", 0.25h
|
|
402
|
+
ct log 0.25 --project argo --task BE --notes "Daily standup"
|
|
403
|
+
```
|
|
404
|
+
|
|
346
405
|
### Time Off (PTO)
|
|
347
406
|
|
|
348
|
-
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
|
|
407
|
+
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).
|
|
349
408
|
|
|
350
409
|
> **IMPORTANT**: Never use magic routing (`ct "..."`) or `ct ai parse` for PTO operations. Always construct explicit `ct pto ...` commands.
|
|
351
410
|
> Full reference: `references/pto.md` and `references/commands.md` § PTO.
|
|
352
411
|
|
|
353
412
|
```bash
|
|
354
413
|
ct pto request <dates> # Submit your own PTO request (status=pending)
|
|
355
|
-
ct pto add <user> <dates> #
|
|
414
|
+
ct pto add <user> <dates> # Add PTO on behalf of user, auto-approved (project manager+, in scope)
|
|
415
|
+
ct pto add <user> <dates> --unpaid # ... marked unpaid (account manager+; 400 for anyone below, not ignored)
|
|
356
416
|
ct pto list [--mine|--team] # List PTO entries with optional filters
|
|
357
417
|
ct pto show <id> # Detail view of a single PTO entry (NEVER truncate the ID)
|
|
358
418
|
ct pto edit <id> [fields] # Edit date range, kind, notes, hours/day (pending entries)
|
|
359
|
-
ct pto cancel <id> [--force] # Cancel a PTO entry (own pending; or manager+ any)
|
|
360
|
-
ct pto approve <id> [--notes "..."] # Approve a pending entry (manager+)
|
|
361
|
-
ct pto reject <id> --notes "..." # Reject a pending entry — notes required (manager+)
|
|
419
|
+
ct pto cancel <id> [--force] # Cancel a PTO entry (own pending; or project manager+ any)
|
|
420
|
+
ct pto approve <id> [--notes "..."] # Approve a pending entry (project manager+)
|
|
421
|
+
ct pto reject <id> --notes "..." # Reject a pending entry — notes required (project manager+)
|
|
362
422
|
ct pto calendar [--from --to --user --status] # PTO slice for a date window (approved only unless --status)
|
|
363
423
|
ct pto summary [--from --to --user] # Aggregate approved PTO hours/days, by kind and by user
|
|
364
424
|
```
|
|
@@ -481,8 +541,8 @@ Read `references/workflows.md` for detailed multi-step workflow patterns includi
|
|
|
481
541
|
- Checking project budgets
|
|
482
542
|
- Retainer management and withdrawals
|
|
483
543
|
- **Requesting your own PTO** (member submits, awaits approval)
|
|
484
|
-
- **Reviewing and approving team PTO** (manager+ reviews pending requests)
|
|
485
|
-
- **Adding PTO on behalf of someone** (
|
|
544
|
+
- **Reviewing and approving team PTO** (project manager+ reviews pending requests)
|
|
545
|
+
- **Adding PTO on behalf of someone** (approver logs pre-agreed leave, auto-approved)
|
|
486
546
|
- **Reconciling missing hours with PTO** (diagnose low-hour weeks: check PTO coverage, then anomaly report)
|
|
487
547
|
|
|
488
548
|
## Complete Command Reference
|
|
@@ -508,4 +568,4 @@ Read `references/commands.md` for the full reference of every command, subcomman
|
|
|
508
568
|
15. **Invoices include expenses** — when creating invoices with `ct invoice create --period`, billable expenses from the same period are automatically included as line items. Mention this to users so they know their expenses will appear on the invoice. Invoices can be grouped by `project`, `task`, `user`, `date`, or `none` via `--group-by`. Line items support discounts (`--discount-type fixed|percent|hours --discount-value N`)
|
|
509
569
|
16. **Client contacts** — use `ct clients contacts` and `ct clients add-contact` to manage multiple contacts per client. Set `--primary` for the main point of contact. Use `--receives-invoices` to flag contacts who should receive invoice emails
|
|
510
570
|
17. **Payroll** — payroll data is strictly confidential. Only HR managers, admins, and owners can access it. When the user asks about payroll, always use the `ct payroll` commands. Compensation types are `hourly` (rate × hours) or `salary` (fixed monthly). Rate changes are always effective from the 1st of a month. The payroll workflow is: configure compensation → run liquidation → approve → mark paid
|
|
511
|
-
18. **PTO / Time Off** — always construct explicit `ct pto ...` commands. Never use magic routing or `ct ai parse` for PTO. Before requesting PTO, always confirm the exact date range with the user — date ambiguity is the most common source of mistakes. Use `YYYY-MM-DD` for a single day or `YYYY-MM-DD:YYYY-MM-DD` for a range. `ct pto request` creates a pending entry (
|
|
571
|
+
18. **PTO / Time Off** — always construct explicit `ct pto ...` commands. Never use magic routing or `ct ai parse` for PTO. Before requesting PTO, always confirm the exact date range with the user — date ambiguity is the most common source of mistakes. Use `YYYY-MM-DD` for a single day or `YYYY-MM-DD:YYYY-MM-DD` for a range. `ct pto request` creates a pending entry (the requester's approvers are notified by email); `ct pto add <user>` requires project manager+ with the subject in their scope, and creates an auto-approved entry. When reconciling missing hours for a user, always check `ct pto summary --user <email> --from X --to Y` before assuming the hours are missing. See `references/pto.md` for the full status lifecycle, kind definitions, and LLM guidance.
|
|
@@ -1119,6 +1119,20 @@ ct projects update-member <project-id> <user-id> --rate 120
|
|
|
1119
1119
|
|
|
1120
1120
|
Endpoint: `PATCH /projects/{id}/members/{user_id}/`
|
|
1121
1121
|
|
|
1122
|
+
### ct projects assignable-users
|
|
1123
|
+
|
|
1124
|
+
```
|
|
1125
|
+
ct projects assignable-users PROJECT_ID [--json]
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
| Option | Type | Description |
|
|
1129
|
+
|--------|------|-------------|
|
|
1130
|
+
| `--json` | flag | JSON output |
|
|
1131
|
+
|
|
1132
|
+
Lists every **active organization member not already on the project**, with their user IDs and org roles. This is the correct source for the user IDs that `ct projects add-members` needs.
|
|
1133
|
+
|
|
1134
|
+
It is deliberately not filtered by review visibility — `ct team list` and the timesheet endpoints answer "whose time can I review", which is a narrower set and the wrong question when staffing a project. Requires permission to manage the project (org project_manager+ assigned to it, an account manager or above, or a per-project `project_role` of `project_manager`). Endpoint: `GET /projects/<id>/assignable-users/`
|
|
1135
|
+
|
|
1122
1136
|
### ct projects add-members
|
|
1123
1137
|
|
|
1124
1138
|
```
|
|
@@ -1373,8 +1387,21 @@ ct ai parse TEXT [--dry-run] [--force/-f] [--json]
|
|
|
1373
1387
|
| `--force`, `-f` | flag | Skip confirmation |
|
|
1374
1388
|
| `--json` | flag | JSON output |
|
|
1375
1389
|
|
|
1376
|
-
Shows parsed: description, project, task, duration, date, billable, confidence, ambiguities.
|
|
1377
|
-
|
|
1390
|
+
Shows parsed: description, project, task, duration, date(s), billable, confidence, ambiguities.
|
|
1391
|
+
|
|
1392
|
+
**Recurring input creates several entries.** When the text describes work repeating over
|
|
1393
|
+
several days ("daily standup all week", "1h every day this week", "2h monday and
|
|
1394
|
+
wednesday"), the parse expands it into explicit dates and lists them all; confirming
|
|
1395
|
+
creates one entry per date in a single transaction. A one-off description still produces
|
|
1396
|
+
exactly one entry.
|
|
1397
|
+
|
|
1398
|
+
```
|
|
1399
|
+
ct ai parse "daily 15 min standup on argo all week" # → 5 entries, one per weekday
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
The confirm response is a plain time entry for a single date, and `{count, entries}` when
|
|
1403
|
+
several were created. Endpoints: `POST /ai/parse/` then `POST /ai/parse/confirm/`
|
|
1404
|
+
(the latter accepts `dates` to create a subset of what was parsed).
|
|
1378
1405
|
|
|
1379
1406
|
### ct ai suggest
|
|
1380
1407
|
|
|
@@ -1699,7 +1726,9 @@ ct timesheet submit --from DATE --to DATE [--force/-f] [--json]
|
|
|
1699
1726
|
| `--force`, `-f` | flag | Skip confirmation prompt |
|
|
1700
1727
|
| `--json` | flag | JSON output |
|
|
1701
1728
|
|
|
1702
|
-
Submits a timesheet for the given period (any date range — not limited to full weeks). Supports partial-week submissions for end-of-month splits. Shows a preview of entries and hours before confirming (use `--force` to skip). Rejects
|
|
1729
|
+
Submits a timesheet for the given period (any date range — not limited to full weeks). Supports partial-week submissions for end-of-month splits. Shows a preview of entries and hours before confirming (use `--force` to skip). Rejects periods that overlap with already submitted/approved timesheets.
|
|
1730
|
+
|
|
1731
|
+
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/`
|
|
1703
1732
|
|
|
1704
1733
|
### ct timesheet approve
|
|
1705
1734
|
|
|
@@ -2703,9 +2732,9 @@ ct pto request 2026-08-04:2026-08-08 --kind vacation --json
|
|
|
2703
2732
|
ct pto add <user> <dates> [--kind/-k KIND] [--hours-per-day/-h HOURS] [--notes/-n TEXT] [--json]
|
|
2704
2733
|
```
|
|
2705
2734
|
|
|
2706
|
-
Add a PTO entry on behalf of another user (manager
|
|
2735
|
+
Add a PTO entry on behalf of another user (project manager and above). The entry is created with `status=approved` immediately — no pending state. Use when an employee notified you via Slack or email and you are logging it for them.
|
|
2707
2736
|
|
|
2708
|
-
**Who can call it:** `manager`, `admin`, `owner
|
|
2737
|
+
**Who can call it:** `project_manager`, `manager`, `admin`, `owner`. Project managers and account managers may only name a user inside their visibility scope — a subject outside it returns `400` on the `user` field, not `403`. Admins and owners may name any active member. See `references/pto.md`.
|
|
2709
2738
|
|
|
2710
2739
|
| Argument/Option | Type | Required | Description |
|
|
2711
2740
|
|-----------------|------|----------|-------------|
|
|
@@ -2714,6 +2743,7 @@ Add a PTO entry on behalf of another user (manager, admin, or owner only). The e
|
|
|
2714
2743
|
| `--kind`, `-k` | string | no | `vacation` (default), `sick`, `holiday`, `personal`, `other` |
|
|
2715
2744
|
| `--hours-per-day`, `-h` | decimal | no | Hours/day. Empty = capacity-based default |
|
|
2716
2745
|
| `--notes`, `-n` | string | no | Reason or context notes |
|
|
2746
|
+
| `--paid` / `--unpaid` | flag | no | Whether the hours are paid. **Account manager+ only** — sending it as a project manager returns `400`, it is NOT ignored. Omit to follow the org policy for the kind |
|
|
2717
2747
|
| `--json` | flag | no | JSON output |
|
|
2718
2748
|
|
|
2719
2749
|
**Response (201):** full entry serializer with `status=approved`, `decided_by=<your user>`, `decided_at=<now>`.
|
|
@@ -2721,7 +2751,11 @@ Add a PTO entry on behalf of another user (manager, admin, or owner only). The e
|
|
|
2721
2751
|
**Errors:**
|
|
2722
2752
|
- `400` — date overlap, invalid dates, or hours_per_day > 24.
|
|
2723
2753
|
- `400` — user is not an active member of this organization.
|
|
2724
|
-
- `
|
|
2754
|
+
- `400` on the `user` field — caller is below `project_manager`, or the named
|
|
2755
|
+
user is outside the caller's visibility scope. This is a `400`, not a `403`:
|
|
2756
|
+
the check lives in the serializer, so treat it as a validation error.
|
|
2757
|
+
- `400` on the `is_paid` field — caller is below account manager and sent
|
|
2758
|
+
`--paid`/`--unpaid`.
|
|
2725
2759
|
|
|
2726
2760
|
**Usage examples:**
|
|
2727
2761
|
|
|
@@ -2743,6 +2777,22 @@ ct pto add jane@example.com 2026-06-09:2026-06-13 --kind vacation --json
|
|
|
2743
2777
|
|
|
2744
2778
|
---
|
|
2745
2779
|
|
|
2780
|
+
### PTO pending count (no dedicated command)
|
|
2781
|
+
|
|
2782
|
+
`ct` with no arguments surfaces a "PTO requests pending review" line driven by
|
|
2783
|
+
`GET /organizations/<slug>/time-off/pending-count/` (project manager+). There is
|
|
2784
|
+
no `ct pto pending-count` subcommand — use `ct pto list --team --status pending`
|
|
2785
|
+
when you need the entries themselves.
|
|
2786
|
+
|
|
2787
|
+
The endpoint returns `{"count": N}` for the requests **routed to the caller**,
|
|
2788
|
+
excluding their own. For admins and owners that is not every pending request in
|
|
2789
|
+
the org — requests with a named approver (a direct manager, or a PM on the
|
|
2790
|
+
person's project) are excluded, because those were emailed to that person
|
|
2791
|
+
instead. Never recompute this by listing and filtering; you cannot see the
|
|
2792
|
+
routing from the list payload.
|
|
2793
|
+
|
|
2794
|
+
---
|
|
2795
|
+
|
|
2746
2796
|
### ct pto list
|
|
2747
2797
|
|
|
2748
2798
|
```
|
|
@@ -2800,7 +2850,7 @@ ct pto show <id> [--json]
|
|
|
2800
2850
|
|
|
2801
2851
|
Show full details of a single PTO entry.
|
|
2802
2852
|
|
|
2803
|
-
**Who can call it:** own entries (any role); manager+ for entries within their visibility scope; admin/owner for any entry.
|
|
2853
|
+
**Who can call it:** own entries (any role); project manager+ for entries within their visibility scope; admin/owner for any entry.
|
|
2804
2854
|
|
|
2805
2855
|
| Argument | Type | Required | Description |
|
|
2806
2856
|
|----------|------|----------|-------------|
|
|
@@ -2987,7 +3037,7 @@ ct pto calendar --from DATE --to DATE [--user/-u USER] [--status/-s STATUSES] [-
|
|
|
2987
3037
|
|
|
2988
3038
|
Render-ready calendar slice of PTO entries overlapping the specified date window, scoped to visible users. **Returns approved entries only unless `--status` is passed.**
|
|
2989
3039
|
|
|
2990
|
-
**Who can call it:** any active member (sees own entries); manager+ sees entries for their visibility scope; admin/owner sees all.
|
|
3040
|
+
**Who can call it:** any active member (sees own entries); project manager+ sees entries for their visibility scope; admin/owner sees all.
|
|
2991
3041
|
|
|
2992
3042
|
| Option | Type | Required | Description |
|
|
2993
3043
|
|--------|------|----------|-------------|
|
|
@@ -3036,7 +3086,7 @@ ct pto summary --from DATE --to DATE [--user/-u USER] [--json]
|
|
|
3036
3086
|
|
|
3037
3087
|
Aggregate **approved** PTO hours and days within the specified window, clipping partial overlaps to the window boundaries. Shows totals, breakdown by kind, and breakdown by user.
|
|
3038
3088
|
|
|
3039
|
-
**Who can call it:** any active member (sees own data); manager+ sees summaries for their visibility scope; admin/owner sees all.
|
|
3089
|
+
**Who can call it:** any active member (sees own data); project manager+ sees summaries for their visibility scope; admin/owner sees all.
|
|
3040
3090
|
|
|
3041
3091
|
| Option | Type | Required | Description |
|
|
3042
3092
|
|--------|------|----------|-------------|
|
|
@@ -3148,3 +3198,43 @@ Displays all available shortcut commands in a formatted table. Shows each alias
|
|
|
3148
3198
|
| `ct f <id>` | `ct favorites start <id>` |
|
|
3149
3199
|
| `ct ex` | `ct expense` |
|
|
3150
3200
|
| `ct "text"` | `ct ai parse "text"` |
|
|
3201
|
+
|
|
3202
|
+
|
|
3203
|
+
---
|
|
3204
|
+
|
|
3205
|
+
## ct calendar
|
|
3206
|
+
|
|
3207
|
+
Google Calendar integration. Events are fetched live from Google on every call and are never stored by CrowdTime — only a time entry the user creates from one is.
|
|
3208
|
+
|
|
3209
|
+
Connecting a calendar needs the Google consent screen, so it happens in the web app (Settings → Integrations). The CLI can read and disconnect only.
|
|
3210
|
+
|
|
3211
|
+
The connect flow is `POST /calendar/authorize/` → Google → `POST /calendar/callback/`. The authorize step returns a single-use `state` bound to the caller; the callback requires that exact value back and rejects anything else, so an authorization code cannot be redeemed by or planted on a different user.
|
|
3212
|
+
|
|
3213
|
+
### ct calendar status
|
|
3214
|
+
|
|
3215
|
+
```
|
|
3216
|
+
ct calendar status [--json]
|
|
3217
|
+
```
|
|
3218
|
+
|
|
3219
|
+
Reports whether a calendar is connected, which Google account, when it was last read, and the last error if the connection has broken. Endpoint: `GET /calendar/`
|
|
3220
|
+
|
|
3221
|
+
### ct calendar events
|
|
3222
|
+
|
|
3223
|
+
```
|
|
3224
|
+
ct calendar events [--date DATE] [--json]
|
|
3225
|
+
```
|
|
3226
|
+
|
|
3227
|
+
| Option | Type | Description |
|
|
3228
|
+
|--------|------|-------------|
|
|
3229
|
+
| `--date`, `-d` | string | Day to read (YYYY-MM-DD). Defaults to today |
|
|
3230
|
+
| `--json` | flag | JSON output |
|
|
3231
|
+
|
|
3232
|
+
Lists that day's events with durations rounded to the nearest quarter hour. All-day events show `—` for hours; declined invitations are marked. There is **no automatic conversion** — read the events, then create entries explicitly with `ct log`. Endpoint: `GET /calendar/events/`
|
|
3233
|
+
|
|
3234
|
+
### ct calendar disconnect
|
|
3235
|
+
|
|
3236
|
+
```
|
|
3237
|
+
ct calendar disconnect [--force/-f]
|
|
3238
|
+
```
|
|
3239
|
+
|
|
3240
|
+
Deletes the stored connection and asks Google to revoke the grant. Endpoint: `DELETE /calendar/`
|
{crowdtime_cli-0.15.0 → crowdtime_cli-0.17.0}/src/crowdtime_cli/skills/crowdtime/references/pto.md
RENAMED
|
@@ -31,6 +31,25 @@ If the user doesn't specify a kind, default to `vacation` but **confirm before s
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## Paid vs unpaid
|
|
35
|
+
|
|
36
|
+
Every entry resolves to paid or unpaid, exposed as `resolved_is_paid`:
|
|
37
|
+
|
|
38
|
+
1. The entry's own `is_paid`, when set.
|
|
39
|
+
2. Otherwise the organization's policy for that kind (`time_off_paid_kinds`).
|
|
40
|
+
3. Otherwise paid.
|
|
41
|
+
|
|
42
|
+
**Paid** hours count towards the hours a person is paid for that month. **Unpaid**
|
|
43
|
+
hours still explain the absence — reports and anomaly checks treat the person as
|
|
44
|
+
away either way — but do not count towards what they are paid.
|
|
45
|
+
|
|
46
|
+
Only account managers and above can set `is_paid`, via `ct pto add --paid/--unpaid`. A project manager passing the flag gets a `400` on the `is_paid` field — the request is refused, not silently booked under the org default. Omit the flag and the organization's policy for the kind applies.
|
|
47
|
+
A member submitting their own request never chooses; it follows the org policy.
|
|
48
|
+
Do not tell a user their leave is paid because they asked for it — read
|
|
49
|
+
`resolved_is_paid` back from the entry.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
34
53
|
## Status lifecycle
|
|
35
54
|
|
|
36
55
|
```
|
|
@@ -80,7 +99,7 @@ The resolved value is surfaced as `resolved_hours_per_day` in API responses —
|
|
|
80
99
|
| `admin` | All entries in the organization |
|
|
81
100
|
| `owner` | All entries in the organization |
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
Project managers and account managers using `ct pto list --team` or `ct pto add <user>` can only target users in their visibility scope. On `ct pto add` an out-of-scope user returns a **`400`** on the `user` field (the check is in the serializer, so it is a validation error, not a permission error); the detail-route actions (`approve`/`reject`/`cancel`) return `403` or `404` instead, because scope there is enforced by the queryset.
|
|
84
103
|
|
|
85
104
|
---
|
|
86
105
|
|
|
@@ -89,7 +108,7 @@ Managers using `ct pto list --team` or `ct pto add <user>` can only target users
|
|
|
89
108
|
| Operation | member/viewer | project_manager / manager | admin / owner |
|
|
90
109
|
|-----------|--------------|--------------------------|---------------|
|
|
91
110
|
| `ct pto request` (own) | Own, → pending | Own, → pending | Own, → pending |
|
|
92
|
-
| `ct pto add <user>` (on-behalf) | No | Within scope → approved | Any user → approved |
|
|
111
|
+
| `ct pto add <user>` (on-behalf) | No | Within scope → approved (400 outside it) | Any user → approved |
|
|
93
112
|
| `ct pto list / show` | Own only | Within scope | Any |
|
|
94
113
|
| `ct pto edit` | Own + pending only | Within scope (any status) | Any |
|
|
95
114
|
| `ct pto cancel` | Own + pending only | Within scope (any non-canceled) | Any |
|
|
@@ -99,6 +118,25 @@ Managers using `ct pto list --team` or `ct pto add <user>` can only target users
|
|
|
99
118
|
|
|
100
119
|
---
|
|
101
120
|
|
|
121
|
+
## Approver notifications and the pending count
|
|
122
|
+
|
|
123
|
+
Creating a **pending** entry (`ct pto request`, or the web request form) emails
|
|
124
|
+
the requester's approvers automatically — one message each. Recipients are
|
|
125
|
+
resolved per person, not by role sweep: whoever manages them directly, or runs
|
|
126
|
+
a project they work on; org admins and owners only when nobody else does. An
|
|
127
|
+
on-behalf `ct pto add` lands approved and notifies nobody.
|
|
128
|
+
|
|
129
|
+
`GET /organizations/<slug>/time-off/pending-count/` (project manager+) returns
|
|
130
|
+
`{"count": N}` — the requests **routed to the caller**, excluding their own. It
|
|
131
|
+
is not "all pending in the org": for an admin or owner it counts only the
|
|
132
|
+
requests with no named approver, so it agrees with who was actually emailed.
|
|
133
|
+
`ct` (bare status) uses it for the "PTO requests pending review" line.
|
|
134
|
+
|
|
135
|
+
Do not compute this by listing and filtering — that cannot tell which requests
|
|
136
|
+
were routed elsewhere, and will over-count for admins and owners.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
102
140
|
## LLM guidelines for PTO
|
|
103
141
|
|
|
104
142
|
1. **Never use magic routing or `ct ai parse` for PTO.** Always construct explicit `ct pto ...` commands. You are an LLM capable of building the correct command — no AI-parse intermediary is needed.
|
|
@@ -110,8 +148,8 @@ Managers using `ct pto list --team` or `ct pto add <user>` can only target users
|
|
|
110
148
|
4. **Check for overlap before requesting.** Run `ct pto list --mine --json` first to confirm there are no conflicting pending or approved entries for the same user and date range. The API will reject overlaps, but catching them proactively avoids a confusing 400 error.
|
|
111
149
|
|
|
112
150
|
5. **`ct pto request` vs `ct pto add`:**
|
|
113
|
-
- `ct pto request` → creates `status=pending`, requires
|
|
114
|
-
- `ct pto add <user>` → creates `status=approved` immediately. Use when
|
|
151
|
+
- `ct pto request` → creates `status=pending`, requires approval. Use for the member's own PTO. Their approvers are emailed automatically — do not tell the user to chase someone manually.
|
|
152
|
+
- `ct pto add <user>` → creates `status=approved` immediately, and notifies nobody. Use when an approver is logging pre-agreed PTO on behalf of someone else. Requires `project_manager+` **and** the subject must be inside the caller's scope; a `400` on the `user` field means they are out of scope, not that the role is wrong.
|
|
115
153
|
|
|
116
154
|
6. **Rejection notes are required.** `ct pto reject` will fail with `400` if `--notes` is empty or omitted. Always ask the manager for a reason before running the command.
|
|
117
155
|
|
|
@@ -1472,11 +1472,11 @@ ct pto request 2026-07-15 --kind sick --notes "Cita médica"
|
|
|
1472
1472
|
ct pto request 2026-08-20 --kind personal --hours-per-day 4 --notes "Trámites personales"
|
|
1473
1473
|
```
|
|
1474
1474
|
|
|
1475
|
-
After running: confirm the output shows `status: pending`. Explain to the user that the entry is queued for
|
|
1475
|
+
After running: confirm the output shows `status: pending`. Explain to the user that the entry is queued for approval — it is NOT yet in effect. Their approvers are emailed automatically, so there is no need to chase anyone manually.
|
|
1476
1476
|
|
|
1477
1477
|
---
|
|
1478
1478
|
|
|
1479
|
-
### Workflow B: Review and Approve Team PTO (manager+)
|
|
1479
|
+
### Workflow B: Review and Approve Team PTO (project manager+)
|
|
1480
1480
|
|
|
1481
1481
|
Use this when a manager wants to review pending requests from their team and approve or reject them.
|
|
1482
1482
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|