feat(stories): add stories subcommand
- Add stories subcommand listing stories worked on, grouped by project - Preserve story_raw in parser row dicts alongside the stripped story, so markdown links are available for display - print_stories() filters to rows with a non-empty story field, deduplicates by stripped story text (preferring the linked version), sums hours per story, and outputs an indented Markdown list - Project names resolved through project_map (same as csv/summary) - -w/--weekly flag aggregates stories across the full week - Add tests for print_stories covering deduplication, link preservation, grouping, empty rows, and story-less row exclusion - Fix flex daily target in status: use projected hours per prior day rather than fixed 8h when computing remaining hours for today
This commit is contained in:
parent
f372a691d4
commit
2d60624e0e
5 changed files with 206 additions and 17 deletions
|
|
@ -13,6 +13,7 @@ from .config import (
|
|||
)
|
||||
from .output import (
|
||||
print_status,
|
||||
print_stories,
|
||||
print_summary,
|
||||
print_summary_short,
|
||||
print_summary_weekly,
|
||||
|
|
@ -135,6 +136,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
)
|
||||
_add_shared_args(status_parser)
|
||||
|
||||
stories_parser = subparsers.add_parser(
|
||||
"stories",
|
||||
help="List stories worked on, grouped by project.",
|
||||
)
|
||||
_add_shared_args(stories_parser)
|
||||
stories_parser.add_argument(
|
||||
"--weekly",
|
||||
"-w",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show stories for the entire week containing the given day.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
|
@ -319,16 +333,56 @@ def _cmd_status(args: argparse.Namespace, config: dict) -> None:
|
|||
sys.exit(1)
|
||||
|
||||
from .parser import filter_rows_by_date, filter_week_sections
|
||||
from .status import projected_hours_for_day
|
||||
|
||||
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)
|
||||
# Adjust daily target to account for over/under time earlier this week.
|
||||
# Only applies when the target date is within the same week as today.
|
||||
# days_remaining includes today (weekday 0=Mon, so Mon has 5 days remaining).
|
||||
from datetime import date as _date
|
||||
|
||||
days_remaining = 5 - target_date.weekday() # 1 on Friday, 5 on Monday
|
||||
if week_start <= _date.today() and target_date.weekday() > 0:
|
||||
# Sum projected hours for days before today in the week
|
||||
hours_before_today = sum(
|
||||
projected_hours_for_day(rows)
|
||||
for day, rows in day_sections
|
||||
if day < target_date
|
||||
)
|
||||
effective_daily_target = max(
|
||||
0.0, (weekly_target - hours_before_today) / days_remaining
|
||||
)
|
||||
else:
|
||||
effective_daily_target = daily_target
|
||||
|
||||
day_status = compute_day_status(day_rows, target_date, effective_daily_target)
|
||||
week_status = compute_week_status(day_sections, target_date, weekly_target)
|
||||
|
||||
print_status(day_status, week_status)
|
||||
|
||||
|
||||
def _cmd_stories(args: argparse.Namespace, config: dict) -> None:
|
||||
from datetime import timedelta
|
||||
|
||||
target_date, _ = _resolve_date(args)
|
||||
|
||||
if args.weekly:
|
||||
week_start = target_date - timedelta(days=target_date.weekday())
|
||||
day_sections = _resolve_week_sections(args, config, target_date)
|
||||
rows = [row for _, day_rows in day_sections for row in day_rows]
|
||||
else:
|
||||
rows = _resolve_rows(args, config, target_date)
|
||||
|
||||
if not rows:
|
||||
print("Warning: no timesheet rows found in input.", file=sys.stderr)
|
||||
return
|
||||
|
||||
project_map = _resolve_project_map(args, config)
|
||||
print_stories(rows, project_map)
|
||||
|
||||
|
||||
def _cmd_csv(args: argparse.Namespace, config: dict) -> None:
|
||||
target_date, date_str = _resolve_date(args)
|
||||
rows = _resolve_rows(args, config, target_date)
|
||||
|
|
@ -362,3 +416,5 @@ def main() -> None:
|
|||
_cmd_csv(args, config)
|
||||
elif args.command == "status":
|
||||
_cmd_status(args, config)
|
||||
elif args.command == "stories":
|
||||
_cmd_stories(args, config)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue