crowdtime-cli 0.12.1__tar.gz → 0.13.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.12.1 → crowdtime_cli-0.13.0}/PKG-INFO +1 -1
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/pyproject.toml +1 -1
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/__init__.py +1 -1
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/client.py +2 -2
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/timesheet_cmd.py +46 -26
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/skills/crowdtime/references/commands.md +1 -1
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/.gitignore +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/LICENSE +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/README.md +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/auth.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/__init__.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/ai_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/auth_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/billing_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/clients_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/config_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/expense_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/favorites_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/insights_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/invoice_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/log_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/org_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/payroll_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/projects_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/report_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/skill_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/tasks_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/team_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/timer_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/commands/version_cmd.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/config.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/formatters.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/main.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/models.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/oauth.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/resolvers.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/skills/crowdtime/SKILL.md +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/skills/crowdtime/references/workflows.md +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.0}/src/crowdtime_cli/utils.py +0 -0
- {crowdtime_cli-0.12.1 → crowdtime_cli-0.13.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.13.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
|
|
@@ -40,7 +40,7 @@ class CrowdTimeClient:
|
|
|
40
40
|
|
|
41
41
|
if require_auth and not self.token:
|
|
42
42
|
console.print(
|
|
43
|
-
"[red]Not authenticated.[/red] Please run [bold]ct login[/bold] first."
|
|
43
|
+
"[red]Not authenticated.[/red] Please run [bold]ct auth login[/bold] first."
|
|
44
44
|
)
|
|
45
45
|
raise SystemExit(1)
|
|
46
46
|
|
|
@@ -133,7 +133,7 @@ class CrowdTimeClient:
|
|
|
133
133
|
|
|
134
134
|
if response.status_code == 401:
|
|
135
135
|
console.print(
|
|
136
|
-
"[red]Authentication failed.[/red] Please run [bold]ct login[/bold] to re-authenticate."
|
|
136
|
+
"[red]Authentication failed.[/red] Please run [bold]ct auth login[/bold] to re-authenticate."
|
|
137
137
|
)
|
|
138
138
|
raise SystemExit(1)
|
|
139
139
|
|
|
@@ -90,14 +90,21 @@ def _print_team_overview(data: dict) -> None:
|
|
|
90
90
|
members = data.get("members", [])
|
|
91
91
|
period_start = data.get("period_start", "")
|
|
92
92
|
period_end = data.get("period_end", "")
|
|
93
|
+
project_id = data.get("project_id")
|
|
93
94
|
|
|
94
95
|
console.print(f"\n[dim]Period: {period_start} to {period_end}[/dim]")
|
|
96
|
+
if project_id:
|
|
97
|
+
console.print(f"[dim]Scoped to project: {project_id}[/dim]")
|
|
95
98
|
|
|
96
99
|
if not members:
|
|
97
100
|
console.print("[dim]No team members found.[/dim]")
|
|
98
101
|
return
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
title = "Team Timesheet Overview"
|
|
104
|
+
if project_id:
|
|
105
|
+
title += " (project-scoped)"
|
|
106
|
+
|
|
107
|
+
table = Table(show_header=True, header_style="bold", title=title)
|
|
101
108
|
table.add_column("Member", width=22)
|
|
102
109
|
table.add_column("Hours", justify="right", width=8)
|
|
103
110
|
table.add_column("Capacity", justify="right", width=10)
|
|
@@ -107,7 +114,13 @@ def _print_team_overview(data: dict) -> None:
|
|
|
107
114
|
table.add_column("Status", width=14)
|
|
108
115
|
|
|
109
116
|
for m in members:
|
|
110
|
-
|
|
117
|
+
# When project filter is active, surface the per-project approval
|
|
118
|
+
# status rather than the whole-timesheet status.
|
|
119
|
+
if project_id:
|
|
120
|
+
project_status = m.get("project_approval_status")
|
|
121
|
+
status = project_status if project_status else m.get("timesheet_status")
|
|
122
|
+
else:
|
|
123
|
+
status = m.get("timesheet_status")
|
|
111
124
|
style = _status_style(status)
|
|
112
125
|
|
|
113
126
|
capacity_pct = m.get("capacity_percent", 0)
|
|
@@ -422,6 +435,12 @@ def team_overview(
|
|
|
422
435
|
),
|
|
423
436
|
week: bool = typer.Option(False, "--week", "-w", help="This week (default if no period specified)."),
|
|
424
437
|
last_week: bool = typer.Option(False, "--last-week", help="Last week."),
|
|
438
|
+
project: Optional[str] = typer.Option(
|
|
439
|
+
None,
|
|
440
|
+
"--project",
|
|
441
|
+
"-p",
|
|
442
|
+
help="Scope hours, breakdown, and approval status to a single project.",
|
|
443
|
+
),
|
|
425
444
|
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
426
445
|
) -> None:
|
|
427
446
|
"""Show team timesheet overview for a period (manager+).
|
|
@@ -429,10 +448,15 @@ def team_overview(
|
|
|
429
448
|
Displays all org members with their hours and timesheet status,
|
|
430
449
|
including those who haven't submitted yet.
|
|
431
450
|
|
|
451
|
+
With --project, hours and breakdown are scoped to that project, the status
|
|
452
|
+
column reflects the per-project approval status (PENDING/APPROVED/REJECTED),
|
|
453
|
+
and members with no story for that project are excluded.
|
|
454
|
+
|
|
432
455
|
Examples:
|
|
433
456
|
ct timesheet team
|
|
434
457
|
ct timesheet team --last-week
|
|
435
458
|
ct timesheet team --from 2026-03-09 --to 2026-03-15
|
|
459
|
+
ct timesheet team --project <project-id>
|
|
436
460
|
"""
|
|
437
461
|
client = CrowdTimeClient(require_auth=True, require_org=True)
|
|
438
462
|
|
|
@@ -450,6 +474,8 @@ def team_overview(
|
|
|
450
474
|
except ValueError as e:
|
|
451
475
|
format_error(str(e))
|
|
452
476
|
raise typer.Exit(1)
|
|
477
|
+
if project:
|
|
478
|
+
params["project_id"] = project
|
|
453
479
|
|
|
454
480
|
if last_week and not period_start:
|
|
455
481
|
today = date_type.today()
|
|
@@ -540,35 +566,29 @@ def bulk_approve_timesheets(
|
|
|
540
566
|
|
|
541
567
|
try:
|
|
542
568
|
if project:
|
|
543
|
-
# Per-project bulk
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
client.post(
|
|
553
|
-
f"/timesheets/{ts_id}/approvals/{project}/approve/",
|
|
554
|
-
data=data or None,
|
|
555
|
-
)
|
|
556
|
-
approved += 1
|
|
557
|
-
except APIError as e:
|
|
558
|
-
skipped += 1
|
|
559
|
-
errors.append(f" {ts_id}: {e.message}")
|
|
560
|
-
|
|
561
|
-
result = {"total_approved": approved, "total_skipped": skipped}
|
|
569
|
+
# Per-project bulk via single transactional endpoint (added with
|
|
570
|
+
# CRO-44). One request approves N portions, with row-level locks
|
|
571
|
+
# and a single auto-approve pass per affected timesheet.
|
|
572
|
+
payload: dict = {
|
|
573
|
+
"items": [{"timesheet_id": ts_id, "project_id": project} for ts_id in timesheet_ids],
|
|
574
|
+
}
|
|
575
|
+
if notes:
|
|
576
|
+
payload["notes"] = notes
|
|
577
|
+
result = client.post("/timesheets/bulk-approve-projects/", data=payload)
|
|
562
578
|
if output_json:
|
|
563
579
|
print_json(result)
|
|
564
580
|
else:
|
|
565
|
-
|
|
566
|
-
|
|
581
|
+
approved = result.get("total_approved", 0)
|
|
582
|
+
skipped_count = result.get("total_skipped", 0)
|
|
583
|
+
format_success(f"Approved {approved} project portion(s).")
|
|
584
|
+
if skipped_count:
|
|
567
585
|
console.print(
|
|
568
|
-
f"[yellow]Skipped {
|
|
586
|
+
f"[yellow]Skipped {skipped_count} (not found, not submitted, already reviewed, self, or no permission):[/yellow]"
|
|
569
587
|
)
|
|
570
|
-
for
|
|
571
|
-
|
|
588
|
+
for item in result.get("skipped", []):
|
|
589
|
+
ts_id = item.get("timesheet_id", "")
|
|
590
|
+
reason = item.get("reason", "")
|
|
591
|
+
console.print(f"[yellow] {ts_id}: {reason}[/yellow]")
|
|
572
592
|
else:
|
|
573
593
|
payload: dict = {"timesheet_ids": timesheet_ids}
|
|
574
594
|
if notes:
|
|
@@ -1763,7 +1763,7 @@ ct timesheet bulk-approve ID1 ID2 [ID3 ...] [--project PROJECT_ID] [--notes NOTE
|
|
|
1763
1763
|
| `--json` | flag | JSON output |
|
|
1764
1764
|
|
|
1765
1765
|
Without `--project`: approves all project portions in each timesheet (manager+ role). With `--project`: approves only that project's portion in each timesheet (PM for that project). Skips timesheets that are not found or not in submitted status.
|
|
1766
|
-
Endpoint: `POST /timesheets/bulk-approve/` (without --project),
|
|
1766
|
+
Endpoint: `POST /timesheets/bulk-approve/` (without --project), `POST /timesheets/bulk-approve-projects/` (with --project — single transactional bulk request, max 200 items).
|
|
1767
1767
|
|
|
1768
1768
|
### ct timesheet bulk-reject
|
|
1769
1769
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|