""" CLI commands for issue activity tracking. This module provides command-line interface for logging, viewing, and managing issue activities for cost allocation and project management purposes. """ import click from datetime import datetime, date from typing import List, Optional from tabulate import tabulate from markitect.issues.activity_tracker import IssueActivityTracker, ActivityType, IssueActivity @click.group() def activity(): """Issue activity tracking commands.""" pass @activity.command() @click.argument('issue_id', type=int) @click.argument('activity_type', type=click.Choice([at.value for at in ActivityType])) @click.option('--date', '-d', type=click.DateTime(formats=['%Y-%m-%d']), help='Activity date (defaults to today)') @click.option('--details', '-m', help='Activity details/message') @click.option('--period-id', type=int, help='Cost period ID for allocation') def log(issue_id: int, activity_type: str, date: Optional[datetime], details: Optional[str], period_id: Optional[int]): """Log an activity for an issue.""" tracker = IssueActivityTracker() activity_date = date.date() if date else None activity_enum = ActivityType(activity_type) try: activity_id = tracker.log_activity( issue_id=issue_id, activity_type=activity_enum, activity_date=activity_date, activity_details=details, period_id=period_id ) click.echo(f"āœ… Logged {activity_type} activity for issue #{issue_id} (ID: {activity_id})") if details: click.echo(f" Details: {details}") except Exception as e: click.echo(f"āŒ Error logging activity: {e}", err=True) raise click.Abort() @activity.command() @click.argument('issue_id', type=int) @click.option('--limit', '-l', type=int, default=20, help='Maximum number of activities to show') @click.option('--offset', type=int, default=0, help='Number of activities to skip') @click.option('--format', 'output_format', type=click.Choice(['table', 'json']), default='table', help='Output format') def show(issue_id: int, limit: int, offset: int, output_format: str): """Show activities for a specific issue.""" tracker = IssueActivityTracker() try: activities = tracker.get_issue_activities(issue_id, limit=limit, offset=offset) if not activities: click.echo(f"šŸ“ No activities found for issue #{issue_id}") return if output_format == 'json': import json activity_data = [activity.to_dict() for activity in activities] click.echo(json.dumps(activity_data, indent=2)) else: # Table format click.echo(f"\nšŸ“‹ Activities for Issue #{issue_id}\n") headers = ['ID', 'Type', 'Date', 'Period', 'Details', 'Logged'] rows = [] for activity in activities: rows.append([ activity.id, activity.activity_type_display, activity.formatted_date, activity.period_id or 'N/A', activity.truncated_details, activity.formatted_datetime ]) click.echo(tabulate(rows, headers=headers, tablefmt='grid')) if len(activities) == limit: click.echo(f"\nšŸ’” Showing {limit} most recent activities. Use --limit and --offset for pagination.") except Exception as e: click.echo(f"āŒ Error retrieving activities: {e}", err=True) raise click.Abort() @activity.command() @click.option('--period-id', type=int, help='Filter by cost period ID') @click.option('--activity-type', type=click.Choice([at.value for at in ActivityType]), multiple=True, help='Filter by activity types (can specify multiple)') @click.option('--format', 'output_format', type=click.Choice(['table', 'json']), default='table', help='Output format') def list(period_id: Optional[int], activity_type: List[str], output_format: str): """List activities across issues.""" tracker = IssueActivityTracker() try: if period_id: activity_types = [ActivityType(at) for at in activity_type] if activity_type else None activities = tracker.get_activities_by_period(period_id, activity_types) title = f"Activities for Period #{period_id}" else: # For now, show recent activities across all issues (could be enhanced) click.echo("āŒ Currently only period-based listing is supported. Use --period-id option.") return if not activities: click.echo(f"šŸ“ No activities found for the specified criteria") return if output_format == 'json': import json activity_data = [activity.to_dict() for activity in activities] click.echo(json.dumps(activity_data, indent=2)) else: # Table format click.echo(f"\nšŸ“Š {title}\n") headers = ['ID', 'Issue', 'Type', 'Date', 'Details'] rows = [] for activity in activities: rows.append([ activity.id, f"#{activity.issue_id}", activity.activity_type.value.title(), activity.activity_date.strftime('%Y-%m-%d') if activity.activity_date else 'N/A', (activity.activity_details[:50] + '...') if activity.activity_details and len(activity.activity_details) > 50 else (activity.activity_details or '') ]) click.echo(tabulate(rows, headers=headers, tablefmt='grid')) click.echo(f"\nšŸ“ˆ Total: {len(activities)} activities") except Exception as e: click.echo(f"āŒ Error retrieving activities: {e}", err=True) raise click.Abort() @activity.command() @click.option('--issue-id', type=int, help='Filter by specific issue ID') @click.option('--start-date', type=click.DateTime(formats=['%Y-%m-%d']), help='Start date for summary period') @click.option('--end-date', type=click.DateTime(formats=['%Y-%m-%d']), help='End date for summary period') def summary(issue_id: Optional[int], start_date: Optional[datetime], end_date: Optional[datetime]): """Show activity summary statistics.""" tracker = IssueActivityTracker() try: summary_data = tracker.get_activity_summary( issue_id=issue_id, start_date=start_date.date() if start_date else None, end_date=end_date.date() if end_date else None ) click.echo("\nšŸ“Š Issue Activity Summary\n") # Basic stats click.echo(f"Total Activities: {summary_data['total_activities']}") click.echo(f"Unique Issues: {summary_data['unique_issues']}") # Date range date_range = summary_data['date_range'] if date_range['start'] and date_range['end']: click.echo(f"Date Range: {date_range['start']} to {date_range['end']}") elif date_range['start']: click.echo(f"Since: {date_range['start']}") # Activity breakdown if summary_data['activities_by_type']: click.echo("\nActivity Breakdown:") for activity_type, count in summary_data['activities_by_type'].items(): percentage = (count / summary_data['total_activities']) * 100 click.echo(f" {activity_type.title()}: {count} ({percentage:.1f}%)") # Filters applied filters = summary_data['filters'] applied_filters = [] if filters['issue_id']: applied_filters.append(f"Issue #{filters['issue_id']}") if filters['start_date']: applied_filters.append(f"From {filters['start_date']}") if filters['end_date']: applied_filters.append(f"Until {filters['end_date']}") if applied_filters: click.echo(f"\nFilters Applied: {', '.join(applied_filters)}") except Exception as e: click.echo(f"āŒ Error generating summary: {e}", err=True) raise click.Abort() @activity.command() @click.argument('activity_id', type=int) @click.confirmation_option(prompt='Are you sure you want to delete this activity?') def delete(activity_id: int): """Delete an activity record.""" tracker = IssueActivityTracker() try: if tracker.delete_activity(activity_id): click.echo(f"āœ… Deleted activity #{activity_id}") else: click.echo(f"āŒ Activity #{activity_id} not found") raise click.Abort() except Exception as e: click.echo(f"āŒ Error deleting activity: {e}", err=True) raise click.Abort() @activity.command() @click.argument('file_path', type=click.Path(exists=True)) @click.option('--format', 'input_format', type=click.Choice(['json', 'csv']), default='json', help='Input file format') def import_activities(file_path: str, input_format: str): """Import activities from a file.""" tracker = IssueActivityTracker() try: if input_format == 'json': import json with open(file_path, 'r') as f: activities = json.load(f) elif input_format == 'csv': import csv activities = [] with open(file_path, 'r') as f: reader = csv.DictReader(f) for row in reader: activity = { 'issue_id': int(row['issue_id']), 'activity_type': row['activity_type'], 'activity_date': datetime.strptime(row['activity_date'], '%Y-%m-%d').date() if row.get('activity_date') else None, 'activity_details': row.get('activity_details'), 'period_id': int(row['period_id']) if row.get('period_id') else None } activities.append(activity) activity_ids = tracker.bulk_log_activities(activities) click.echo(f"āœ… Successfully imported {len(activity_ids)} activities") except Exception as e: click.echo(f"āŒ Error importing activities: {e}", err=True) raise click.Abort() if __name__ == '__main__': activity()