feat(status): add status subcommand with day and week metrics
- Add status.py with compute_day_status() and compute_week_status() - Add projected_hours_for_day(): for each entry, use its duration if closed, or the next entry's start time as the close time if open - Open entries (start present, end absent) are preserved by the parser instead of being skipped; aggregate_rows() skips them for summaries - Expected end is computed by filling remaining hours into available time slots from the open entry's start; clamped to latest_end if a pre-logged entry ends later, with a note explaining why - Projected week total sums projected_hours_for_day() across all days - Add status subcommand to cli.py with shared source/day arguments - Add [work] daily_hours / weekly_hours config keys (default 8 / 40) - Add timesheets.example.toml [work] section - Add tests for projected_hours_for_day, compute_day_status, compute_week_status and all DayStatus/WeekStatus fields
This commit is contained in:
parent
d5dbe8791b
commit
f372a691d4
8 changed files with 799 additions and 7 deletions
|
|
@ -3,8 +3,16 @@ import os
|
|||
import sys
|
||||
from datetime import date
|
||||
|
||||
from .config import find_default_config, get_map_path, get_token, load_config
|
||||
from .config import (
|
||||
find_default_config,
|
||||
get_daily_target,
|
||||
get_map_path,
|
||||
get_token,
|
||||
get_weekly_target,
|
||||
load_config,
|
||||
)
|
||||
from .output import (
|
||||
print_status,
|
||||
print_summary,
|
||||
print_summary_short,
|
||||
print_summary_weekly,
|
||||
|
|
@ -121,6 +129,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
)
|
||||
_add_shared_args(csv_parser)
|
||||
|
||||
status_parser = subparsers.add_parser(
|
||||
"status",
|
||||
help="Show how many hours remain today and this week.",
|
||||
)
|
||||
_add_shared_args(status_parser)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
|
@ -272,6 +286,49 @@ def _cmd_summary(args: argparse.Namespace, config: dict) -> None:
|
|||
_write(print_summary, aggregated, project_map)
|
||||
|
||||
|
||||
def _cmd_status(args: argparse.Namespace, config: dict) -> None:
|
||||
from datetime import timedelta
|
||||
|
||||
from .status import compute_day_status, compute_week_status
|
||||
|
||||
target_date, _ = _resolve_date(args)
|
||||
daily_target = get_daily_target(config)
|
||||
weekly_target = get_weekly_target(config)
|
||||
week_start = target_date - timedelta(days=target_date.weekday())
|
||||
|
||||
# Fetch the full week document once for both day and week status
|
||||
if args.joplin:
|
||||
from .joplin import fetch_week_note
|
||||
|
||||
token = _resolve_token(args, config)
|
||||
try:
|
||||
content = fetch_week_note(token, target_date)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
lines = content.splitlines()
|
||||
else:
|
||||
if args.input == "-":
|
||||
lines = sys.stdin.read().splitlines()
|
||||
else:
|
||||
try:
|
||||
with open(args.input, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: file not found: {args.input}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from .parser import filter_rows_by_date, filter_week_sections
|
||||
|
||||
day_rows = filter_rows_by_date(lines, target_date)
|
||||
day_sections = filter_week_sections(lines, week_start)
|
||||
|
||||
day_status = compute_day_status(day_rows, target_date, daily_target)
|
||||
week_status = compute_week_status(day_sections, target_date, weekly_target)
|
||||
|
||||
print_status(day_status, week_status)
|
||||
|
||||
|
||||
def _cmd_csv(args: argparse.Namespace, config: dict) -> None:
|
||||
target_date, date_str = _resolve_date(args)
|
||||
rows = _resolve_rows(args, config, target_date)
|
||||
|
|
@ -303,3 +360,5 @@ def main() -> None:
|
|||
_cmd_summary(args, config)
|
||||
elif args.command == "csv":
|
||||
_cmd_csv(args, config)
|
||||
elif args.command == "status":
|
||||
_cmd_status(args, config)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue