diff --git a/.claude/skills/week_report_gen_from_json/README.md b/.claude/skills/week_report_gen_from_json/README.md new file mode 100644 index 0000000..d7a9e7d --- /dev/null +++ b/.claude/skills/week_report_gen_from_json/README.md @@ -0,0 +1,102 @@ +# Week Report Generator from JSON + +這個技能基於 `week_work_package_log` 的 JSON 輸出生成項目週報,與原始的 `week_report_gen` 功能相同但輸入來源不同。 + +## 主要差異 + +### 輸入格式 +- **原始 week_report_gen**: Excel 成本報告檔案 (cost-report-*.xls) +- **新 week_report_gen_from_json**: JSON 格式的工時數據 + +### 數據來源 +- **原始**: 手動匯出的 Excel 檔案 +- **新**: 直接從 OpenProject API 獲取的結構化數據 + +### 欄位邏輯 +按照新需求實現的邏輯: + +#### B欄 (專案名稱) +顯示最上級專案名稱(`type: "project"` 的節點) + +#### C欄 (子項目名稱) +- 如果底層任務屬於子專案 → 顯示最上級的 `work_package` +- 如果沒有中間層級 → 顯示 `"-"` + +#### D欄 (進度) +跟隨 C 欄合併,因為進度是針對整個子項目的狀態 + +#### E欄 (本周主要進展) +顯示最底層工作包名稱(有 `time_entries` 的節點),如果有 comment 也一併顯示 + +## 使用方法 + +### 1. 指定日期範圍(自動獲取數據) +```bash +python generate_report.py "2026-01-20" "2026-01-22" "output.xlsx" +``` + +### 2. 使用已有的 JSON 檔案 +```bash +python generate_report.py "data.json" "output.xlsx" +``` + +### 3. 使用預設值(本週數據) +```bash +python generate_report.py +``` + +## 功能特色 + +- ✓ 自動從 OpenProject API 獲取數據 +- ✓ 支援使用者名稱映射 (UserManager) +- ✓ 階層專案結構識別 +- ✓ 工時聚合和評論合併 +- ✓ 智能儲存格合併邏輯 + - **B 欄**: 相同專案名稱合併 + - **C 欄**: 相同子項目名稱合併("-" 不合併) + - **D 欄**: 跟隨 C 欄合併(進度針對子項目) +- ✓ 使用相同模板格式 +- ✓ 完整的樣式和格式化 + +## 排序邏輯 + +數據按以下順序排序: +1. **專案名稱** (升序) +2. **子項目名稱** (升序) +3. **工時** (降序) + +這確保相同專案和子項目的條目會聚集在一起,便於合併儲存格。 + +## 合併邏輯 + +### 專案層級合併 (B 欄) +- 相同專案名稱的多行會合併 B 欄儲存格 +- 合併儲存格垂直置中對齊 + +### 子項目層級合併 (C & D 欄) +- 相同子項目名稱的多行會同時合併 C 欄和 D 欄儲存格 +- **特殊處理**: 子項目名稱為 `"-"` 的條目不會合併(因為 "-" 表示沒有子項目,每個都是獨立的) +- 合併儲存格垂直置中對齊 + +## 依賴技能 + +- `week_work_package_log`: 獲取 JSON 數據 +- `list_user`: 使用者名稱映射 (可選) +- `week_report_gen`: 模板檔案共用 + +## 輸出格式 + +生成的 Excel 檔案格式與原始 `week_report_gen` 完全相同: +- 公司標題和日期範圍 +- 標準化的欄位結構 +- 專案分組和合併儲存格 +- 一致的顏色和邊框樣式 +- 專業的視覺呈現效果 + +## 範例輸出 + +生成的週報會包含: +- **專案分組**: 每個專案的條目會聚集在一起 +- **子項目合併**: 相同子項目的進度欄位統一顯示 +- **清晰的層次**: 從專案 → 子項目 → 具體任務的清晰結構 +- **工時統計**: 每個任務的詳細工時記錄 \ No newline at end of file diff --git a/.claude/skills/week_report_gen_from_json/examples.py b/.claude/skills/week_report_gen_from_json/examples.py new file mode 100644 index 0000000..a7078d0 --- /dev/null +++ b/.claude/skills/week_report_gen_from_json/examples.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Week Report Generator from JSON 使用範例腳本 +展示基於 JSON 數據生成週報的各種使用場景 +""" + +import sys +import os +from datetime import datetime, timedelta + +# Add the skill to Python path +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_dir) + +from generate_report import generate_weekly_report_from_json + + +def example_this_week(): + """範例1: 生成本週週報(自動獲取數據)""" + + today = datetime.now() + days_since_monday = today.weekday() + this_monday = today - timedelta(days=days_since_monday) + this_friday = this_monday + timedelta(days=4) + + start_date = this_monday.strftime("%Y-%m-%d") + end_date = this_friday.strftime("%Y-%m-%d") + + print(f"📅 範例1: 本週週報生成 ({start_date} ~ {end_date})") + print("=" * 70) + + try: + output_file = f"temp/週報-本週-{datetime.now().strftime('%Y%m%d')}.xlsx" + + output_path, report_df = generate_weekly_report_from_json( + json_input=(start_date, end_date), + output_file=output_file + ) + + print(f"\n✅ 週報生成成功!") + print(f"📁 輸出檔案: {output_path}") + print(f"📊 包含 {len(report_df)} 個工時記錄") + print(f"🏢 涵蓋 {report_df['專案名稱'].nunique()} 個專案") + + except Exception as e: + print(f"❌ 錯誤: {e}") + + +def example_specific_dates(): + """範例2: 生成指定日期範圍的週報""" + + start_date = "2026-01-20" + end_date = "2026-01-22" + + print(f"📅 範例2: 指定日期週報 ({start_date} ~ {end_date})") + print("=" * 70) + + try: + output_file = f"temp/週報-指定日期-{end_date.replace('-', '')}.xlsx" + + output_path, report_df = generate_weekly_report_from_json( + json_input=(start_date, end_date), + output_file=output_file, + team_name="智能控制組" + ) + + print(f"\n✅ 週報生成成功!") + print(f"📁 輸出檔案: {output_path}") + + # 顯示專案摘要 + print(f"\n📋 專案摘要:") + for project in report_df['專案名稱'].unique(): + project_data = report_df[report_df['專案名稱'] == project] + total_hours = project_data['工時'].sum() + print(f" • {project}: {total_hours} 小時 ({len(project_data)} 個任務)") + + except Exception as e: + print(f"❌ 錯誤: {e}") + + +def example_with_json_file(): + """範例3: 從現有 JSON 檔案生成週報""" + + print("📅 範例3: 從 JSON 檔案生成週報") + print("=" * 70) + + # 先生成 JSON 數據(模擬已有的數據文件) + from week_work_package_log.work_package_logger import WeekWorkPackageLogger + + try: + # 獲取 JSON 數據 + logger = WeekWorkPackageLogger() + json_data = logger.fetch_and_display_time_entries("2026-01-20", "2026-01-22", 'json') + + if json_data: + # 保存為檔案 + import json + json_file = "temp/work_data.json" + with open(json_file, 'w', encoding='utf-8') as f: + json.dump(json_data, f, ensure_ascii=False, indent=2) + + print(f"💾 JSON 數據已保存到: {json_file}") + + # 從 JSON 檔案生成週報 + output_file = "temp/週報-from-json-file.xlsx" + output_path, report_df = generate_weekly_report_from_json( + json_input=json_file, + output_file=output_file + ) + + print(f"\n✅ 從 JSON 檔案生成週報成功!") + print(f"📁 輸出檔案: {output_path}") + + except Exception as e: + print(f"❌ 錯誤: {e}") + + +def show_features(): + """展示主要功能特色""" + + print("🚀 Week Report Generator from JSON 功能特色") + print("=" * 70) + print("✓ 直接從 OpenProject API 獲取數據") + print("✓ 智能儲存格合併:") + print(" - B 欄:相同專案名稱自動合併") + print(" - C 欄:相同子項目名稱自動合併") + print(" - D 欄:跟隨子項目進度合併") + print("✓ 階層專案結構識別") + print("✓ 使用者名稱自動映射") + print("✓ 專業週報模板格式") + print("✓ 支援多種輸入方式:日期範圍、JSON檔案、預設值") + print("✓ 自動排序和分組") + print("✓ 工時聚合和評論合併") + + +def main(): + """主函數 - 展示所有範例""" + + print("🎯 Week Report Generator from JSON 使用範例") + print("=" * 70) + + # 確保 temp 目錄存在 + os.makedirs("temp", exist_ok=True) + + # 展示功能特色 + show_features() + print("\n") + + # 執行範例 + example_this_week() + print("\n") + + example_specific_dates() + print("\n") + + example_with_json_file() + print("\n") + + print("🎉 所有範例執行完畢!") + print("📁 請檢查 temp/ 目錄中生成的週報檔案") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.claude/skills/week_report_gen_from_json/generate_report.py b/.claude/skills/week_report_gen_from_json/generate_report.py new file mode 100644 index 0000000..0956f09 --- /dev/null +++ b/.claude/skills/week_report_gen_from_json/generate_report.py @@ -0,0 +1,721 @@ +""" +Weekly Report Generator from JSON + +This script generates weekly project reports from week_work_package_log JSON output. +""" + +import pandas as pd +import openpyxl +import json +from datetime import datetime +from openpyxl.styles import Font, Alignment, PatternFill, Border, Side +from copy import copy +import os +import sys + +# Add list_user skill to path for user mapping +script_dir = os.path.dirname(os.path.abspath(__file__)) +skills_dir = os.path.dirname(script_dir) # .../skills +list_user_path = os.path.join(skills_dir, 'list_user') +if list_user_path not in sys.path: + sys.path.insert(0, list_user_path) + +try: + from user_manager import UserManager + USER_MANAGER_AVAILABLE = True +except ImportError as e: + USER_MANAGER_AVAILABLE = False + +# Add week_work_package_log skill to path for JSON data generation +work_package_log_path = os.path.join(skills_dir, 'week_work_package_log') +if work_package_log_path not in sys.path: + sys.path.insert(0, work_package_log_path) + +try: + from work_package_logger import WeekWorkPackageLogger + WORK_PACKAGE_LOG_AVAILABLE = True +except ImportError as e: + WORK_PACKAGE_LOG_AVAILABLE = False + + +# Style constants for consistent coloring +HEADER_FILL_COLOR = 'FF2F75B5' # Blue header background +DATA_FILL_COLOR = 'FFE7E6E6' # Light gray data background +WHITE_BORDER = Border( + left=Side(style='thin', color='FFFFFFFF'), + right=Side(style='thin', color='FFFFFFFF'), + top=Side(style='thin', color='FFFFFFFF'), + bottom=Side(style='thin', color='FFFFFFFF') +) + + +def copy_cell_style(source_cell, target_cell): + """ + Copy all styling from source cell to target cell. + """ + if source_cell.has_style: + target_cell.font = copy(source_cell.font) + target_cell.border = copy(source_cell.border) + target_cell.fill = copy(source_cell.fill) + target_cell.number_format = copy(source_cell.number_format) + target_cell.protection = copy(source_cell.protection) + target_cell.alignment = copy(source_cell.alignment) + + +def copy_sheet_with_format(source_sheet, target_sheet, max_rows=10): + """ + Copy formatting from source sheet template to target sheet. + Only copies the first few rows (headers) to preserve template styling. + """ + # Copy row heights + for row_idx in range(1, max_rows + 1): + if row_idx in source_sheet.row_dimensions: + target_sheet.row_dimensions[row_idx].height = source_sheet.row_dimensions[row_idx].height + + # Copy column widths + for col_letter in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']: + if col_letter in source_sheet.column_dimensions: + target_sheet.column_dimensions[col_letter].width = source_sheet.column_dimensions[col_letter].width + + # Copy cell formatting for header rows (first max_rows) + for row_idx in range(1, max_rows + 1): + for col_idx in range(1, 12): # Columns A-K + source_cell = source_sheet.cell(row=row_idx, column=col_idx) + target_cell = target_sheet.cell(row=row_idx, column=col_idx) + + # Copy style + copy_cell_style(source_cell, target_cell) + + # Copy merged cells info (we'll handle merging separately) + + # Copy merged cells from template (only header rows) + for merged_range in source_sheet.merged_cells.ranges: + if merged_range.min_row <= max_rows and merged_range.max_row <= max_rows: + try: + target_sheet.merge_cells(str(merged_range)) + except: + pass # Skip if already merged or invalid + + +def style_header_row(ws, row_idx, template_cell=None, col_start=2, col_end=10): + """ + Apply header styling (blue fill, bold, centered, white grid) to a row. + If template_cell is provided, its style is copied before applying borders. + """ + for col_idx in range(col_start, col_end + 1): + cell = ws.cell(row=row_idx, column=col_idx) + if template_cell and template_cell.has_style: + copy_cell_style(template_cell, cell) + else: + cell.fill = PatternFill(start_color=HEADER_FILL_COLOR, end_color=HEADER_FILL_COLOR, fill_type='solid') + cell.font = Font(bold=True) + cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + cell.border = WHITE_BORDER + + +def style_data_row(ws, row_idx, fill_color=DATA_FILL_COLOR, col_start=2, col_end=10): + """ + Apply data row styling (fill, wrap text, white grid) to a row. + """ + data_fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid') + data_align = Alignment(vertical='top', wrap_text=True) + for col_idx in range(col_start, col_end + 1): + cell = ws.cell(row=row_idx, column=col_idx) + cell.fill = data_fill + cell.alignment = data_align + cell.border = WHITE_BORDER + + +def read_json_data(json_input): + """ + Read JSON data from file or string and parse the structure. + + Args: + json_input: Either file path to JSON file or JSON string + + Returns: + dict: Parsed JSON data + """ + try: + if os.path.exists(json_input): + # Input is a file path + with open(json_input, 'r', encoding='utf-8') as f: + data = json.load(f) + else: + # Input is a JSON string + data = json.loads(json_input) + + # Validate JSON structure + if 'projects' not in data or 'summary' not in data: + raise ValueError("Invalid JSON format: missing 'projects' or 'summary' key") + + return data + + except (json.JSONDecodeError, FileNotFoundError) as e: + print(f"Error reading JSON data: {e}") + raise + except Exception as e: + print(f"Error processing JSON data: {e}") + raise + + +def find_leaf_work_packages(node, path=[]): + """ + Recursively find all leaf work packages (those with time_entries). + + Args: + node: Current node in the tree + path: Current path from root to this node + + Returns: + list: List of (leaf_node, path_to_leaf) tuples + """ + current_path = path + [node] + leaf_packages = [] + + # If this node has time_entries, it's a leaf + if node.get('time_entries', []): + leaf_packages.append((node, current_path)) + + # Recursively search children + for child in node.get('children', []): + leaf_packages.extend(find_leaf_work_packages(child, current_path)) + + return leaf_packages + + +def extract_report_data_from_json(json_data, user_manager=None): + """ + Extract report data from JSON structure following the new logic. + + Args: + json_data: Parsed JSON data from week_work_package_log + user_manager: Optional UserManager for name mapping + + Returns: + tuple: (report_data_list, start_date, end_date) + """ + report_data = [] + + # Extract date range from summary + date_range = json_data['summary']['date_range'] + start_date = datetime.strptime(date_range.split(' to ')[0], '%Y-%m-%d') + end_date = datetime.strptime(date_range.split(' to ')[1], '%Y-%m-%d') + + # Process each project + for project in json_data['projects']: + # Find all leaf work packages in this project + leaf_packages = find_leaf_work_packages(project) + + for leaf_node, path_to_leaf in leaf_packages: + # Process each time entry in the leaf node + for time_entry in leaf_node['time_entries']: + user_name = time_entry['user'] + + # 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') + participant = formal_name + else: + participant = user_name + + # Determine project name (B column) - always the root project + project_name = path_to_leaf[0]['name'] # First element is always the project + + # Determine sub-project name (C column) + sub_project_name = "-" + if len(path_to_leaf) > 2: # project -> work_package -> ... -> leaf + # Find the highest level work_package (first work_package in path) + for node in path_to_leaf[1:]: # Skip project node + if node['type'] == 'work_package': + sub_project_name = node['name'] + break + + # Task name (E column) - leaf work package name + task_name = leaf_node['name'] + + # Format progress text with comments + progress_parts = [task_name] + if time_entry.get('comment') and time_entry['comment'].strip(): + progress_parts.append(time_entry['comment'].strip()) + + progress_text = '\n'.join(progress_parts) + + report_data.append({ + '專案名稱': project_name, + '子項目名稱': sub_project_name, + '進度': None, # User should fill this in + '本周主要進展': progress_text, + '參與人員': participant, + '工時': time_entry['hours'], + '交付物': None, + '代碼上傳': None, + '下周計畫': None + }) + + return report_data, start_date, end_date + + +def format_for_weekly_report(json_data, user_manager=None): + """ + Transform JSON data into weekly report format with aggregation. + Groups by project, sub-project, user, and task, merging comments. + + Args: + json_data: Parsed JSON data from week_work_package_log + user_manager: Optional UserManager instance for name mapping + + Returns: + pd.DataFrame: Formatted report data + """ + # Extract individual entries + report_entries, start_date, end_date = extract_report_data_from_json(json_data, user_manager) + + # Group entries for aggregation + grouped_entries = {} + + for entry in report_entries: + # Create grouping key: project + sub-project + user + task + group_key = ( + entry['專案名稱'], + entry['子項目名稱'], + entry['參與人員'], + entry['本周主要進展'].split('\n')[0] # Task name (first line) + ) + + if group_key not in grouped_entries: + grouped_entries[group_key] = { + '專案名稱': entry['專案名稱'], + '子項目名稱': entry['子項目名稱'], + '進度': None, + '本周主要進展': [entry['本周主要進展'].split('\n')[0]], # Task name + '參與人員': entry['參與人員'], + '工時': 0, + '交付物': None, + '代碼上傳': None, + '下周計畫': None, + 'comments': [] + } + + # Aggregate hours + grouped_entries[group_key]['工時'] += entry['工時'] + + # Collect comments (skip task name) + entry_lines = entry['本周主要進展'].split('\n') + if len(entry_lines) > 1: # Has comments beyond task name + for comment in entry_lines[1:]: + comment = comment.strip() + if comment and comment not in grouped_entries[group_key]['comments']: + grouped_entries[group_key]['comments'].append(comment) + + # Format final entries + final_report_data = [] + for group_key, group_data in grouped_entries.items(): + # Format progress text: task name + comments + progress_parts = group_data['本周主要進展'] # Task name + if group_data['comments']: + progress_parts.extend(group_data['comments']) + + progress_text = '\n'.join(progress_parts) + + final_report_data.append({ + '專案名稱': group_data['專案名稱'], + '子項目名稱': group_data['子項目名稱'], + '進度': group_data['進度'], + '本周主要進展': progress_text, + '參與人員': group_data['參與人員'], + '工時': group_data['工時'], + '交付物': group_data['交付物'], + '代碼上傳': group_data['代碼上傳'], + '下周計畫': group_data['下周計畫'] + }) + + # Sort by project name first, then by sub-project name, then by hours (descending) + final_report_data.sort(key=lambda x: (x['專案名稱'], x['子項目名稱'], -x['工時'])) + + return pd.DataFrame(final_report_data), start_date, end_date + + +def fetch_json_data(start_date, end_date): + """ + Fetch JSON data using week_work_package_log skill. + + Args: + start_date: Start date in YYYY-MM-DD format + end_date: End date in YYYY-MM-DD format + + Returns: + dict: JSON data from work package logger + """ + if not WORK_PACKAGE_LOG_AVAILABLE: + raise ImportError("week_work_package_log skill not available") + + try: + logger = WeekWorkPackageLogger() + json_data = logger.fetch_and_display_time_entries(start_date, end_date, 'json') + + if not json_data: + raise ValueError(f"No data returned for date range {start_date} to {end_date}") + + return json_data + + except Exception as e: + print(f"Error fetching work package data: {e}") + raise + + +def generate_weekly_report_from_json(json_input, output_file, template_file=None, team_name="智能控制組", use_user_mapping=True): + """ + Main function to generate weekly report from JSON data. + + Args: + json_input: JSON file path, JSON string, or tuple (start_date, end_date) to fetch data + output_file: Path to output weekly report Excel file + template_file: Optional path to template file + team_name: Team name for sheet title (default: "智能控制組") + use_user_mapping: Whether to apply user name mapping (default: True) + + Returns: + Tuple of (output_file_path, report_dataframe) + """ + # Initialize user manager for name mapping + user_manager = None + if use_user_mapping and USER_MANAGER_AVAILABLE: + try: + user_manager = UserManager() + print("✓ User name mapping enabled") + except Exception as e: + print(f"⚠ Warning: Could not load user mapping: {e}") + print(" Continuing without user name mapping...") + elif use_user_mapping and not USER_MANAGER_AVAILABLE: + print("⚠ Warning: list_user skill not available, skipping user name mapping") + + # Get JSON data + if isinstance(json_input, tuple) and len(json_input) == 2: + # Fetch data using dates + start_date, end_date = json_input + print(f"Fetching work package data from {start_date} to {end_date}...") + json_data = fetch_json_data(start_date, end_date) + else: + # Read from file or string + print(f"Reading JSON data from: {json_input}") + json_data = read_json_data(json_input) + + print(f"Loaded JSON data with {len(json_data['projects'])} projects") + + # Process data + report_df, start_date, end_date = format_for_weekly_report(json_data, user_manager=user_manager) + print(f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") + print(f"Formatted {len(report_df)} work entries for report") + + # Load template or create new workbook + if template_file and os.path.exists(template_file): + print(f"Loading template from: {template_file}") + wb = openpyxl.load_workbook(template_file) + + # Get the template sheet + template_sheet = None + for sheet_name_candidate in ['周报示例', wb.sheetnames[0]]: + if sheet_name_candidate in wb.sheetnames: + template_sheet = wb[sheet_name_candidate] + break + + # Create new sheet for this week + sheet_name = f"{end_date.strftime('%Y%m%d')}-{team_name}" + ws = wb.create_sheet(sheet_name, 0) # Insert at beginning + + # Copy formatting from template if available + if template_sheet: + print(f"Copying formatting from template sheet: {template_sheet.title}") + copy_sheet_with_format(template_sheet, ws, max_rows=4) + + # Unmerge cells beyond row 4 to allow data writing + merged_ranges_to_remove = [] + for merged_range in ws.merged_cells.ranges: + if merged_range.min_row > 4: + merged_ranges_to_remove.append(merged_range) + + for merged_range in merged_ranges_to_remove: + ws.unmerge_cells(str(merged_range)) + else: + print("Creating new workbook (no template provided)") + wb = openpyxl.Workbook() + ws = wb.active + sheet_name = f"{end_date.strftime('%Y%m%d')}-{team_name}" + ws.title = sheet_name + template_sheet = None + + # Write header + ws['B1'] = '弘訊科技股份有限公司' + ws['B2'] = f"2026年度週報({start_date.strftime('%m月%d日')}-{end_date.strftime('%m月%d日')})" + ws['B3'] = '建議用可視化素材(如有)輔助報告:設計圖、產品架構圖、產品實物照片等等相關材料' + + # Write column headers (row 4) + headers = ['專案名稱', '子項目名稱', '進度', '本周主要進展 (請條列說明)', + '參與人員', '工时(个人投入研发工时', '交付物', '代碼(如有)是否上傳', '下周計畫'] + for col_idx, header in enumerate(headers, start=2): # Start from column B + ws.cell(row=4, column=col_idx, value=header) + + # Ensure header row is uniformly blue + template_header_cell = template_sheet.cell(row=4, column=2) if template_sheet else None + style_header_row(ws, row_idx=4, template_cell=template_header_cell) + + # Write data starting from row 5 + current_row = 5 + current_project = None + current_sub_project = None + project_start_row = None + sub_project_start_row = None + + for idx, row_data in report_df.iterrows(): + project_name = row_data['專案名稱'] + sub_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 + + # Merge cells for the previous sub-project if it had multiple rows and is not "-" + if (current_sub_project is not None and current_sub_project != "-" and + sub_project_start_row is not None and current_row > sub_project_start_row + 1): + try: + # Merge C column (sub-project name) + ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}') + merged_cell = ws.cell(row=sub_project_start_row, column=3) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + + # Merge D column (progress) as well + ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}') + merged_cell_d = ws.cell(row=sub_project_start_row, column=4) + merged_cell_d.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 + current_sub_project = sub_project_name + sub_project_start_row = current_row + + # Check if we're starting a new sub-project (within same project) + elif sub_project_name != current_sub_project: + # Merge cells for the previous sub-project if it had multiple rows and is not "-" + if (current_sub_project is not None and current_sub_project != "-" and + sub_project_start_row is not None and current_row > sub_project_start_row + 1): + try: + # Merge C column (sub-project name) + ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}') + merged_cell = ws.cell(row=sub_project_start_row, column=3) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + + # Merge D column (progress) as well + ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}') + merged_cell_d = ws.cell(row=sub_project_start_row, column=4) + merged_cell_d.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + except: + pass # Skip if merge fails + + # Start tracking new sub-project + current_sub_project = sub_project_name + sub_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['本周主要進展']) + ws.cell(row=current_row, column=6, value=row_data['參與人員']) + ws.cell(row=current_row, column=7, value=row_data['工時']) + ws.cell(row=current_row, column=8, value=row_data['交付物']) + ws.cell(row=current_row, column=9, value=row_data['代碼上傳']) + ws.cell(row=current_row, column=10, value=row_data['下周計畫']) + + # Apply consistent gray fill and white borders to data row + style_data_row(ws, row_idx=current_row) + + current_row += 1 + + # Add separator row (except after last entry) + if idx < len(report_df) - 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 + + # Merge cells for current sub-project before adding separator + if (current_sub_project is not None and current_sub_project != "-" and + sub_project_start_row is not None and current_row > sub_project_start_row): + try: + # Merge C column (sub-project name) + ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}') + merged_cell = ws.cell(row=sub_project_start_row, column=3) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + + # Merge D column (progress) as well + ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}') + merged_cell_d = ws.cell(row=sub_project_start_row, column=4) + merged_cell_d.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 + 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 + current_sub_project = None + sub_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 + + # Handle the last sub-project merging (if needed) + if (current_sub_project is not None and current_sub_project != "-" and + sub_project_start_row is not None and current_row > sub_project_start_row): + try: + # Merge C column (sub-project name) + ws.merge_cells(f'C{sub_project_start_row}:C{current_row - 1}') + merged_cell = ws.cell(row=sub_project_start_row, column=3) + merged_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + + # Merge D column (progress) as well + ws.merge_cells(f'D{sub_project_start_row}:D{current_row - 1}') + merged_cell_d = ws.cell(row=sub_project_start_row, column=4) + merged_cell_d.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + except: + pass + + # Apply formatting (only if no template was used) + if not template_sheet: + # Format title rows + ws['B1'].font = Font(bold=True, size=14) + ws['B1'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + ws['B2'].font = Font(bold=True, size=12) + ws['B2'].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True) + ws['B3'].font = Font(size=10, italic=True) + ws['B3'].alignment = Alignment(vertical='top', wrap_text=True) + + # Merge cells for headers + ws.merge_cells('B1:J1') + ws.merge_cells('B2:J2') + ws.merge_cells('B3:J3') + + # Set column widths + column_widths = { + 'B': 20, # 專案名稱 + 'C': 20, # 子項目名稱 + 'D': 8, # 進度 + 'E': 35, # 本周主要進展 + 'F': 15, # 參與人員 + 'G': 12, # 工時 + 'H': 15, # 交付物 + 'I': 10, # 代碼上傳 + 'J': 20 # 下周計畫 + } + + for col, width in column_widths.items(): + ws.column_dimensions[col].width = width + + # Apply wrap text to all data cells + wrap_align = Alignment(vertical='top', wrap_text=True) + for row in range(5, current_row): + for col in range(2, 11): + ws.cell(row=row, column=col).alignment = wrap_align + + # Save workbook + wb.save(output_file) + print(f"\n✓ Weekly report saved to: {output_file}") + + return output_file, report_df + + +if __name__ == "__main__": + # Example usage + import sys + + # Default paths + script_dir = os.path.dirname(os.path.abspath(__file__)) + + if len(sys.argv) > 3: + # Date range provided: start_date end_date output_file + start_date = sys.argv[1] + end_date = sys.argv[2] + output_file = sys.argv[3] + json_input = (start_date, end_date) + elif len(sys.argv) > 2: + # JSON file provided: json_file output_file + json_input = sys.argv[1] + output_file = sys.argv[2] + else: + # Default: use dates and auto-generate output file + from datetime import datetime, timedelta + today = datetime.now() + days_since_monday = today.weekday() + this_monday = today - timedelta(days=days_since_monday) + this_friday = this_monday + timedelta(days=4) + + start_date = this_monday.strftime("%Y-%m-%d") + end_date = this_friday.strftime("%Y-%m-%d") + json_input = (start_date, end_date) + + timestamp = datetime.now().strftime('%Y%m%d') + output_file = os.path.join(script_dir, 'references', f'項目週報-智能控制組-{timestamp}.xlsx') + + # Find template file + template_file = os.path.join(skills_dir, 'week_report_gen', 'references', '項目週報-模板.xlsx') + + # Generate the report + try: + output_path, summary_data = generate_weekly_report_from_json( + json_input=json_input, + output_file=output_file, + template_file=template_file if os.path.exists(template_file) else None + ) + + print("\n" + "="*60) + print("SUMMARY OF PROJECTS:") + print("="*60) + for idx, row in summary_data.iterrows(): + print(f"\nProject: {row['專案名稱']}") + print(f" Sub-project: {row['子項目名稱']}") + print(f" Team: {row['參與人員']}") + print(f" Hours: {row['工時']}") + if row['本周主要進展']: + print(f" Progress:\n {row['本周主要進展'].replace(chr(10), chr(10) + ' ')}") + + print("\n" + "="*60) + print("✓ Report generation completed successfully!") + print("="*60) + + except Exception as e: + print(f"\n✗ Error generating report: {e}") + import traceback + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/README.md b/README.md index 1b5ad09..0d71075 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Report Generation Skills Experiment -This repository contains AI skills for automated report generation. Currently includes a weekly project report generator that transforms time tracking data into formatted management reports. +This repository contains AI skills for automated report generation. Currently includes weekly project report generators that transform time tracking data into formatted management reports. ## Available Skills @@ -12,6 +12,17 @@ Automatically generates weekly project reports (項目週報) from exported Exce Fetches and displays time entries grouped by work packages for specified date ranges via OpenProject API. Provides detailed analysis of work package time logging patterns with user, project, and activity information. +### 🚀 Week Report Generator from JSON (`week_report_gen_from_json`) + +**New Enhanced Version**: Automatically generates weekly project reports directly from OpenProject API data via JSON format. Features intelligent cell merging, hierarchical project structure recognition, and streamlined workflow. + +**Key Advantages:** +- 🔄 **Direct API Integration**: No manual Excel export required +- 🎯 **Smart Cell Merging**: Automatic B/C/D column merging for cleaner presentation +- 📊 **Hierarchical Structure**: Proper project → sub-project → task organization +- ⚡ **Automated Workflow**: From data fetch to formatted report in one command +- 🎨 **Professional Layout**: Enhanced visual grouping and formatting + ## Getting Started ### Prerequisites @@ -20,9 +31,25 @@ Fetches and displays time entries grouped by work packages for specified date ra # Create virtual environment and install dependencies uv sync +# Verify OpenProject API connection uv run --env-file .env python -c 'import os; print(os.getenv("OPENPROJECT_TOKEN"))' ``` +### Usage Examples + +#### Original Excel-based Generator ```bash uv run --env-file .env python .claude\skills\week_report_gen\generate_report.py "temp\cost-report-2026-01-16-T-16-22-3620260116-7-1r1n4h.xls" "temp\項目週報-智能控制組-20260119.xlsx" ``` + +#### New JSON-based Generator (Recommended) +```bash +# Generate report for specific date range +uv run --env-file .env python .claude\skills\week_report_gen_from_json\generate_report.py "2026-01-20" "2026-01-22" "temp\週報-智能控制組-20260123.xlsx" + +# Generate report for current week (default) +uv run --env-file .env python .claude\skills\week_report_gen_from_json\generate_report.py + +# Use existing JSON data file +uv run --env-file .env python .claude\skills\week_report_gen_from_json\generate_report.py "data.json" "output.xlsx" +```