From 15aa43a0e1238c79eff67bdb4e9fbea341d0c3c6 Mon Sep 17 00:00:00 2001 From: MarioYang Date: Wed, 21 Jan 2026 14:59:10 +0800 Subject: [PATCH] feat: update weekly report generation to handle individual work entries and improve data processing --- .claude/skills/week_report_gen/SKILL.md | 91 ++++--- .../skills/week_report_gen/generate_report.py | 256 ++++++++++++------ 2 files changed, 219 insertions(+), 128 deletions(-) diff --git a/.claude/skills/week_report_gen/SKILL.md b/.claude/skills/week_report_gen/SKILL.md index 82fa41c..f851af2 100644 --- a/.claude/skills/week_report_gen/SKILL.md +++ b/.claude/skills/week_report_gen/SKILL.md @@ -25,7 +25,7 @@ Use this skill when: Expected columns in the input file: - `日期` - Date of the work entry - `使用者` - User/person name -- `活動` - Activity type (Development, Testing, Specification, Support, etc.) +- `登錄為` - Login as type (Development, Testing, Specification, Support, etc.) - `專案` - Project name - `留言` - Comments/notes (optional) - `單位` - Work hours/units @@ -66,7 +66,7 @@ def read_cost_report(file_path): df = pd.read_excel(file_path, header=1) # Verify expected columns exist - required_cols = ['日期', '使用者', '活動', '專案', '單位'] + required_cols = ['日期', '使用者', '登錄為', '專案', '單位'] if not all(col in df.columns for col in required_cols): # Try header=0 if header=1 doesn't work df = pd.read_excel(file_path, header=0) @@ -77,77 +77,78 @@ def read_cost_report(file_path): return df ``` -### Step 2: Aggregate Data by Person and Project +### Step 2: Process Individual Work Entries by Project ```python def aggregate_work_hours(df): """ - Group work hours by user and project. - Returns a summary DataFrame. + Process work hours data by project, keeping individual entries separate. + Returns a processed DataFrame and date range. """ # Extract date range for report period df['日期'] = pd.to_datetime(df['日期']) start_date = df['日期'].min() end_date = df['日期'].max() - # Group by user and project - summary = df.groupby(['使用者', '專案', '活動']).agg({ - '單位': 'sum', - '留言': lambda x: '\n'.join(x.dropna().unique()) if '留言' in df.columns else '' - }).reset_index() + # Keep individual work entries separate (no grouping/aggregation) + summary = df[['使用者', '專案', '單位', '登錄為', '留言']].copy() + summary.columns = ['使用者', '專案', '工時', '登錄為', '備註'] - summary.columns = ['使用者', '專案', '活動', '工時', '備註'] + # Clean up empty or NaN values + summary['登錄為'] = summary['登錄為'].fillna('') + summary['備註'] = summary['備註'].fillna('') - # Sort by user and hours (descending) - summary = summary.sort_values(['使用者', '工時'], ascending=[True, False]) + # Sort by project name and hours (descending) + summary = summary.sort_values(['專案', '工時'], ascending=[True, False]) return summary, start_date, end_date ``` -### Step 3: Format Data for Weekly Report +### Step 3: Format Individual Entries for Weekly Report ```python def format_for_weekly_report(summary_df): """ - Transform aggregated data into weekly report format. - Group entries by project and list participants. + Transform individual work entries into weekly report format. + Group by project but keep individual work entries separate. """ - # Group by project report_data = [] for project in summary_df['專案'].unique(): project_data = summary_df[summary_df['專案'] == project] - # Get all participants and their hours - participants = [] - total_hours = 0 - activities = [] - notes = [] - + # Process each work entry individually for _, row in project_data.iterrows(): - participants.append(f"{row['使用者']}") - total_hours += row['工時'] - if row['活動'] and row['活動'] != '-': - activities.append(row['活動']) + user_name = row['使用者'] + user_hours = row['工時'] + + # Format progress text from activity and notes + progress_parts = [] + + if row['登錄為'] and row['登錄為'] != '-': + # Extract project name after the colon from format like "Task #838: AXEL CommLib 重構" + login_as_text = str(row['登錄為']) + if ':' in login_as_text: + project_name = login_as_text.split(':', 1)[1].strip() + progress_parts.append(f"{project_name}") + else: + progress_parts.append(f"{login_as_text}") + if row['備註']: - notes.append(row['備註']) - - # Format main progress - progress_text = '\n'.join(set(notes)) if notes else '' - if activities: - activity_text = '\n'.join(set(activities)) - progress_text = f"{activity_text}\n{progress_text}" if progress_text else activity_text - - report_data.append({ - '專案名稱': project, - '子項目名稱': None, - '進度': None, # User should fill this in - '本周主要進展': progress_text, - '參與人員': ', '.join(set(participants)), - '工時': total_hours, - '交付物': None, - '代碼上傳': None, - '下周計畫': None + progress_parts.append(str(row['備註'])) + + progress_text = ' '.join(progress_parts) if progress_parts else None + + report_data.append({ + '專案名稱': project, + '子項目名稱': None, + '進度': None, # User should fill this in + '本周主要進展': progress_text, + '參與人員': user_name, + '工時': user_hours, + '交付物': None, + '代碼上傳': None, + '下周計畫': None }) return pd.DataFrame(report_data) diff --git a/.claude/skills/week_report_gen/generate_report.py b/.claude/skills/week_report_gen/generate_report.py index ce65843..97ea78d 100644 --- a/.claude/skills/week_report_gen/generate_report.py +++ b/.claude/skills/week_report_gen/generate_report.py @@ -153,31 +153,39 @@ def read_cost_report(file_path): def aggregate_work_hours(df): """ - Group work hours by user and project. - Returns a summary DataFrame and date range. + Process work hours data by project, keeping individual entries separate. + Returns a processed DataFrame and date range. """ # Extract date range for report period df['日期'] = pd.to_datetime(df['日期']) start_date = df['日期'].min() end_date = df['日期'].max() - # Group by user and project - agg_dict = {'單位': 'sum'} + # Prepare the summary DataFrame with individual entries + summary_columns = ['使用者', '專案', '工時'] - # Add 留言 (comments) if column exists - if '留言' in df.columns: - agg_dict['留言'] = lambda x: '\n'.join([str(i) for i in x.dropna().unique() if str(i) != 'nan']) - - # Add 活動 (activity) if column exists - if '活動' in df.columns: - summary = df.groupby(['使用者', '專案', '活動']).agg(agg_dict).reset_index() - summary.columns = ['使用者', '專案', '活動', '工時'] + (['備註'] if '留言' in df.columns else []) + # Add 登錄為 (login as) column if exists + if '登錄為' in df.columns: + summary_columns.append('登錄為') else: - summary = df.groupby(['使用者', '專案']).agg(agg_dict).reset_index() - summary.columns = ['使用者', '專案', '工時'] + (['備註'] if '留言' in df.columns else []) - summary['活動'] = '' + df['登錄為'] = '' + summary_columns.append('登錄為') - # Sort by user and hours (descending) + # Add 備註 (comments) column if exists + if '留言' in df.columns: + summary_columns.append('備註') + summary = df[['使用者', '專案', '單位', '登錄為', '留言']].copy() + summary.columns = ['使用者', '專案', '工時', '登錄為', '備註'] + else: + summary = df[['使用者', '專案', '單位', '登錄為']].copy() + summary.columns = ['使用者', '專案', '工時', '登錄為'] + + # Clean up empty or NaN values + summary['登錄為'] = summary['登錄為'].fillna('') + if '備註' in summary.columns: + summary['備註'] = summary['備註'].fillna('') + + # Sort by project name first, then by hours (descending) within each project summary = summary.sort_values(['專案', '工時'], ascending=[True, False]) return summary, start_date, end_date @@ -185,90 +193,123 @@ def aggregate_work_hours(df): def format_for_weekly_report(summary_df, user_manager=None, project_manager=None): """ - Transform aggregated data into weekly report format. - Group entries by project and list participants. + Transform individual work entries into weekly report format. + Group by project and merge entries with same (user, task name) but different comments. Uses project hierarchy to organize parent-child project relationships. Args: - summary_df: Aggregated work data + summary_df: Individual work data entries user_manager: Optional UserManager instance for name mapping project_manager: Optional ProjectManager instance for hierarchy """ report_data = [] + # Process projects in order to maintain grouping for project in summary_df['專案'].unique(): project_data = summary_df[summary_df['專案'] == project] - # Get all participants and their hours - participants = [] - total_hours = 0 - activities = [] - notes = [] + # Group by user and task name for merging + grouped_entries = {} + # First pass: collect and group entries for _, row in project_data.iterrows(): user_name = row['使用者'] - user_hours = row['工時'] # Map nickname to formal name if user manager is available if user_manager: formal_name = user_manager.get_formal_name(user_name, fallback='keep_original') - participants.append(formal_name) + participant = formal_name else: - participants.append(f"{user_name}") + participant = user_name - total_hours += user_hours + # Extract task name from 登錄為 + task_name = None + if '登錄為' in row and row['登錄為'] and row['登錄為'] != '-' and pd.notna(row['登錄為']): + login_as_text = str(row['登錄為']) + if ':' in login_as_text: + task_name = login_as_text.split(':', 1)[1].strip() + else: + task_name = login_as_text - if '活動' in row and row['活動'] and row['活動'] != '-' and pd.notna(row['活動']): - activities.append(row['活動']) + # Create grouping key + group_key = (participant, task_name) + # Initialize group if not exists + if group_key not in grouped_entries: + grouped_entries[group_key] = { + 'participant': participant, + 'task_name': task_name, + 'total_hours': 0, + 'comments': [], + 'project': project + } + + # Add to group + grouped_entries[group_key]['total_hours'] += row['工時'] + + # Add comment if exists and not already added if '備註' in row and row['備註'] and pd.notna(row['備註']): - notes.append(row['備註']) + comment = str(row['備註']).strip() + if comment and comment != "-" and comment not in grouped_entries[group_key]['comments']: + grouped_entries[group_key]['comments'].append(comment) - # Format main progress - progress_text = '' - if activities: - activity_text = '\n'.join([f"{act}" for act in set(activities)]) - progress_text = activity_text - - if notes: - note_text = '\n'.join(set(notes)) - progress_text = f"{progress_text}\n{note_text}" if progress_text else note_text - - # Determine if this is a sub-project using project hierarchy - parent_project = None - project_name = project - sub_project_name = None - - if project_manager: - # Try to find the project by exact name match - project_id = project_manager.find_project_by_name(project) + # Second pass: generate report entries from grouped data + for group_key, group_data in grouped_entries.items(): + participant = group_data['participant'] + task_name = group_data['task_name'] + total_hours = group_data['total_hours'] + comments = group_data['comments'] - if project_id: - parent_id = project_manager.get_parent(project_id) + # Format progress text + progress_parts = [] + if task_name: + progress_parts.append(f"{task_name}") + + # Format comments as bullet points if multiple + if comments: + if len(comments) == 1: + progress_parts.append(comments[0]) + else: + # Multiple comments - format as bullet list + bullet_comments = '\n'.join([f" - {comment}" for comment in comments]) + progress_parts.append(f"\n{bullet_comments}") + + progress_text = ' '.join(progress_parts) if progress_parts else None + + # Determine if this is a sub-project using project hierarchy + parent_project = None + project_name = project + sub_project_name = None + + if project_manager: + # Try to find the project by exact name match + project_id = project_manager.find_project_by_name(project) - if parent_id: - # This is a sub-project - parent_info = project_manager.get_project(parent_id) - if parent_info: - parent_project = parent_info['name'] - sub_project_name = project - project_name = parent_project - else: - # Project not found in hierarchy, log for debugging - pass - - report_data.append({ - '專案名稱': project_name, - '子項目名稱': sub_project_name, - '進度': None, # User should fill this in - '本周主要進展': progress_text if progress_text else None, - '參與人員': ', '.join(set(participants)), - '工時': total_hours, - '交付物': None, - '代碼上傳': None, - '下周計畫': None - }) - + if project_id: + parent_id = project_manager.get_parent(project_id) + + if parent_id: + # This is a sub-project + parent_info = project_manager.get_project(parent_id) + if parent_info: + parent_project = parent_info['name'] + sub_project_name = project + project_name = parent_project + else: + # Project not found in hierarchy, log for debugging + pass + + report_data.append({ + '專案名稱': project_name, + '子項目名稱': sub_project_name, + '進度': None, # User should fill this in + '本周主要進展': progress_text if progress_text else None, + '參與人員': participant, + '工時': total_hours, + '交付物': None, + '代碼上傳': None, + '下周計畫': None + }) return pd.DataFrame(report_data) @@ -318,10 +359,10 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam summary, start_date, end_date = aggregate_work_hours(df) print(f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") - print(f"Found {len(summary)} work entries across {summary['專案'].nunique()} projects") + print(f"Found {len(summary)} individual work entries across {summary['專案'].nunique()} projects") report_df = format_for_weekly_report(summary, user_manager=user_manager, project_manager=project_manager) - print(f"Formatted {len(report_df)} project entries for report") + print(f"Formatted {len(report_df)} individual work entries for report") # Load template or create new workbook if template_file and os.path.exists(template_file): @@ -378,8 +419,31 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam # Write data starting from row 5 current_row = 5 + project_start_rows = {} # Track where each project starts for merging + current_project = None + project_start_row = None + for idx, row_data in report_df.iterrows(): - ws.cell(row=current_row, column=2, value=row_data['專案名稱']) + project_name = row_data['專案名稱'] + + # Check if we're starting a new project + if project_name != current_project: + # Merge cells for the previous project if it had multiple rows + if current_project is not None and project_start_row is not None and current_row > project_start_row + 1: + try: + ws.merge_cells(f'B{project_start_row}:B{current_row - 1}') + # Center the merged cell content + merged_cell = ws.cell(row=project_start_row, column=2) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + except: + pass # Skip if merge fails + + # Start tracking new project + current_project = project_name + project_start_row = current_row + + # Write data for current row + ws.cell(row=current_row, column=2, value=project_name) ws.cell(row=current_row, column=3, value=row_data['子項目名稱']) ws.cell(row=current_row, column=4, value=row_data['進度']) ws.cell(row=current_row, column=5, value=row_data['本周主要進展']) @@ -396,12 +460,38 @@ def generate_weekly_report(input_file, output_file, template_file=None, team_nam # Add separator row (except after last entry) if idx < len(report_df) - 1: - ws.cell(row=current_row, column=2, value='專案名稱') - for col_idx, header in enumerate(headers[1:], start=3): - ws.cell(row=current_row, column=col_idx, value=header) - # Apply blue header styling to separator row (no blank white spacer) - style_header_row(ws, row_idx=current_row, template_cell=template_header_cell) - current_row += 1 + # Check if next entry is different project + next_project = report_df.iloc[idx + 1]['專案名稱'] if idx + 1 < len(report_df) else None + if next_project != project_name: + # Merge cells for current project before adding separator + if project_start_row is not None and current_row > project_start_row: + try: + ws.merge_cells(f'B{project_start_row}:B{current_row - 1}') + merged_cell = ws.cell(row=project_start_row, column=2) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + except: + pass + + # Add separator row + ws.cell(row=current_row, column=2, value='專案名稱') + for col_idx, header in enumerate(headers[1:], start=3): + ws.cell(row=current_row, column=col_idx, value=header) + # Apply blue header styling to separator row (no blank white spacer) + style_header_row(ws, row_idx=current_row, template_cell=template_header_cell) + current_row += 1 + + # Reset project tracking for next group + current_project = None + project_start_row = None + + # Handle the last project merging (if needed) + if current_project is not None and project_start_row is not None and current_row > project_start_row: + try: + ws.merge_cells(f'B{project_start_row}:B{current_row - 1}') + merged_cell = ws.cell(row=project_start_row, column=2) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + except: + pass # Apply formatting (only if no template was used) if not template_sheet: