diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d83088a..f08a3e1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,7 +28,7 @@ If applicable, add screenshots to help explain your problem. ## Environment - **OS**: [e.g., Windows 10, macOS 11.0, Ubuntu 20.04] - **Python Version**: [e.g., 3.9.5] -- **Worklog Manager Version**: [e.g., 1.6.0] +- **Worklog Manager Version**: [e.g., 1.7.0] ## Log Files Please attach relevant log files from the `logs/` directory. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e34d2d..d367c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to Worklog Manager will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.6.0] - 2025-10-22 +## [1.7.0] - 2025-10-22 ### Added - Scroll-enabled main window container so every control stays reachable on compact displays without sacrificing theming. diff --git a/DOCUMENTATION_SUMMARY.md b/DOCUMENTATION_SUMMARY.md index c4299ac..17b1496 100644 --- a/DOCUMENTATION_SUMMARY.md +++ b/DOCUMENTATION_SUMMARY.md @@ -89,7 +89,7 @@ Configure these in GitHub: 1. **Description**: Add a clear one-line description 2. **Topics**: Add tags like `time-tracking`, `python`, `tkinter`, `productivity` 3. **Website**: Link to documentation or project page -4. **Releases**: Create your first release (v1.6.0) +4. **Releases**: Create your first release (v1.7.0) 5. **About**: Fill in the About section with description and topics 6. **Discussions**: Enable GitHub Discussions for community 7. **Issues**: Ensure issues are enabled @@ -97,7 +97,7 @@ Configure these in GitHub: ### Before Publishing - [ ] Replace `your-username` in all markdown files with actual GitHub username -- [ ] Create first release (v1.6.0) on GitHub +- [ ] Create first release (v1.7.0) on GitHub - [ ] Add repository description and topics - [ ] Enable GitHub Discussions - [ ] Consider adding a social preview image diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md index f27e97e..76b7781 100644 --- a/PROJECT_OVERVIEW.md +++ b/PROJECT_OVERVIEW.md @@ -7,7 +7,7 @@ Worklog Manager is a comprehensive desktop application for precise work time tracking. Built with Python and Tkinter, it provides professionals and teams with an offline-first solution for monitoring work hours, managing breaks, and generating detailed productivity reports. ### Key Statistics -- **Version**: 1.6.0 (Production Ready) +- **Version**: 1.7.0 (Production Ready) - **Language**: Python 3.7+ - **License**: MIT - **Platform**: Cross-platform (Windows, macOS, Linux) @@ -114,7 +114,7 @@ worklog-manager/ ✅ **Phase 1**: Core time tracking (v1.0.0) ✅ **Phase 2**: Action history and revoke (v1.2.0) ✅ **Phase 3**: Export and reporting (v1.3.0) -✅ **Phase 4**: Advanced features (v1.6.0) +✅ **Phase 4**: Advanced features (v1.7.0) ### Current State - **Production ready** for professional use diff --git a/README.md b/README.md index 3f56726..a492aeb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Python Version](https://img.shields.io/badge/python-3.7%2B-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-1.6.0-green.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-1.7.0-green.svg)](CHANGELOG.md) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](INSTALL.md) [![Code Style](https://img.shields.io/badge/code%20style-PEP8-orange.svg)](https://www.python.org/dev/peps/pep-0008/) @@ -144,7 +144,7 @@ Daily logs stored in `logs/` directory with format `worklog_YYYYMMDD.log`. Inclu ## Development Status -**Current Version: 1.6.0** - All phases completed +**Current Version: 1.7.0** - All phases completed - ✅ Phase 1: Core time tracking and break management - ✅ Phase 2: Action history and revoke system - ✅ Phase 3: Export and reporting (CSV/JSON/PDF) diff --git a/ROADMAP.md b/ROADMAP.md index 735893b..393a7a9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ Future development plans and feature ideas for Worklog Manager. -## Version 1.6.0 - CURRENT ✅ +## Version 1.7.0 - CURRENT ✅ Latest highlights: - ✅ Appearance settings for window size and maximized state @@ -11,7 +11,7 @@ Latest highlights: - ✅ Geometry preservation when hiding to system tray - ✅ Stability fixes for shutdown and tray lifecycle -## Version 1.6.0 - Data Management (Planned) +## Version 1.7.0 - Data Management (Planned) **Focus**: Enhanced data capabilities and historical analysis @@ -32,7 +32,7 @@ Latest highlights: **Target Release**: Q1 2026 -## Version 1.7.0 - Collaboration Features (Under Consideration) +## Version 1.8.0 - Collaboration Features (Under Consideration) **Focus**: Team and multi-user capabilities @@ -46,7 +46,7 @@ Latest highlights: **Target Release**: Q2 2026 -## Version 1.8.0 - Integration & Automation (Future) +## Version 1.9.0 - Integration & Automation (Future) **Focus**: External integrations and automation @@ -80,8 +80,8 @@ Latest highlights: Features requested by users (in order of popularity): -1. **Edit Historical Data** - High demand, planned for v1.6.0 -2. **Project Tracking** - Under consideration for v1.7.0 +1. **Edit Historical Data** - High demand, planned for v1.7.0 +2. **Project Tracking** - Under consideration for v1.8.0 3. **Calendar Integration** - Investigating feasibility 4. **Mobile App** - Long-term goal (v2.0.0) 5. **Multi-language Support** - Medium priority diff --git a/core/action_history.py b/core/action_history.py index 4c0a4fa..fbca897 100644 --- a/core/action_history.py +++ b/core/action_history.py @@ -7,6 +7,7 @@ from dataclasses import dataclass, asdict from data.models import WorklogState, ActionType, BreakType +from utils.datetime_compat import datetime_fromisoformat @dataclass @@ -278,9 +279,9 @@ def import_history(self, history_data: List[Dict[str, Any]]): data['state_after'] = WorklogState(data['state_after']) # Convert timestamp strings back to datetime - data['timestamp'] = datetime.fromisoformat(data['timestamp']) + data['timestamp'] = datetime_fromisoformat(data['timestamp']) if data.get('revoke_timestamp'): - data['revoke_timestamp'] = datetime.fromisoformat(data['revoke_timestamp']) + data['revoke_timestamp'] = datetime_fromisoformat(data['revoke_timestamp']) action = ActionSnapshot(**data) imported_actions.append(action) diff --git a/core/data_aggregator.py b/core/data_aggregator.py index fe4b867..fffb4ef 100644 --- a/core/data_aggregator.py +++ b/core/data_aggregator.py @@ -8,6 +8,7 @@ from data.database import Database from data.models import WorkSession, BreakPeriod, ActionLog, BreakType from core.export_models import DailyStats, WeeklyStats, ExportData, ExportOptions, DateRange +from utils.datetime_compat import datetime_fromisoformat class DataAggregator: @@ -130,7 +131,7 @@ def _calculate_daily_stats(self, sessions: List[WorkSession], # Group sessions by date sessions_by_date = defaultdict(list) for session in sessions: - session_date = datetime.fromisoformat(session.date).date() + session_date = datetime_fromisoformat(session.date).date() sessions_by_date[session_date].append(session) # Group breaks by session @@ -159,8 +160,8 @@ def _calculate_daily_stats(self, sessions: List[WorkSession], lunch_breaks=len([b for b in session_breaks if b.break_type == BreakType.LUNCH]), general_breaks=len([b for b in session_breaks if b.break_type == BreakType.GENERAL]), sessions_count=len(day_sessions), - first_start=datetime.fromisoformat(main_session.start_time) if main_session.start_time else None, - last_end=datetime.fromisoformat(main_session.end_time) if main_session.end_time else None + first_start=datetime_fromisoformat(main_session.start_time) if main_session.start_time else None, + last_end=datetime_fromisoformat(main_session.end_time) if main_session.end_time else None ) else: # No session for this day - create empty stats diff --git a/core/simple_backup_manager.py b/core/simple_backup_manager.py index f499611..5a8cfb9 100644 --- a/core/simple_backup_manager.py +++ b/core/simple_backup_manager.py @@ -15,6 +15,8 @@ from typing import List, Dict, Optional from pathlib import Path +from utils.datetime_compat import datetime_fromisoformat + class BackupManager: """Simple backup manager using only built-in Python libraries.""" @@ -233,7 +235,7 @@ def get_backup_list(self) -> List[Dict]: info_created = info_data.get('created') if info_created: try: - entry['created'] = datetime.fromisoformat(info_created) + entry['created'] = datetime_fromisoformat(info_created) except ValueError: pass except Exception as info_error: diff --git a/core/time_calculator.py b/core/time_calculator.py index 8ecd74b..00bd7ce 100644 --- a/core/time_calculator.py +++ b/core/time_calculator.py @@ -5,6 +5,7 @@ import logging from data.models import ActionLog, BreakPeriod, TimeCalculation, ActionType, WorklogState +from utils.datetime_compat import datetime_fromisoformat class TimeCalculator: @@ -36,7 +37,7 @@ def parse_time(time_str: str) -> datetime: Returns: datetime object """ - return datetime.fromisoformat(time_str) + return datetime_fromisoformat(time_str) @staticmethod def format_time(dt: datetime) -> str: diff --git a/data/database.py b/data/database.py index ad6023a..998cd3e 100644 --- a/data/database.py +++ b/data/database.py @@ -8,6 +8,7 @@ from contextlib import contextmanager from data.models import WorkSession, ActionLog, BreakPeriod, WorklogState, ActionType, BreakType +from utils.datetime_compat import datetime_fromisoformat class Database: @@ -151,8 +152,8 @@ def get_session_by_date(self, date: str) -> Optional[WorkSession]: productive_minutes=row['productive_minutes'], overtime_minutes=row['overtime_minutes'], status=WorklogState(row['status']), - created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None, - updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None + created_at=datetime_fromisoformat(row['created_at']) if row['created_at'] else None, + updated_at=datetime_fromisoformat(row['updated_at']) if row['updated_at'] else None ) return None @@ -236,7 +237,7 @@ def get_session_actions(self, session_id: int) -> List[ActionLog]: break_type=BreakType(row['break_type']) if row['break_type'] else None, notes=row['notes'], revoked=bool(row['revoked']), - created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None + created_at=datetime_fromisoformat(row['created_at']) if row['created_at'] else None )) return actions @@ -346,7 +347,7 @@ def get_session_breaks(self, session_id: int) -> List[BreakPeriod]: start_time=row['start_time'], end_time=row['end_time'], duration_minutes=row['duration_minutes'], - created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None + created_at=datetime_fromisoformat(row['created_at']) if row['created_at'] else None )) return breaks diff --git a/docs/COMPLETION_SUMMARY.md b/docs/COMPLETION_SUMMARY.md index 3a9aecd..16e29ed 100644 --- a/docs/COMPLETION_SUMMARY.md +++ b/docs/COMPLETION_SUMMARY.md @@ -1,4 +1,4 @@ -# Worklog Manager v1.6.0 - Application Completed! +# Worklog Manager v1.7.0 - Application Completed! 🎉 **CONGRATULATIONS!** Your comprehensive Worklog Manager application is now complete and ready to use! @@ -60,7 +60,7 @@ Your complete application includes: ``` worklog-manager/ -├── 🎯 main.py # Integrated application entry point (v1.6.0) +├── 🎯 main.py # Integrated application entry point (v1.7.0) ├── 🚀 start_worklog.py # Smart startup script with dependency checking ├── 🪟 start_worklog.bat # Windows batch file ├── 🐧 start_worklog.sh # Linux/Mac shell script @@ -154,11 +154,11 @@ The application has been successfully tested and shows: ## 📞 Next Steps -Your Worklog Manager v1.6.0 is **COMPLETE AND READY TO USE!** +Your Worklog Manager v1.7.0 is **COMPLETE AND READY TO USE!** Simply run the application and start tracking your work time with all the advanced features you requested. The application will grow with you as you use it, and all the Phase 4 foundations are in place for future enhancements. **Enjoy your new professional work time tracking system!** 🎉 --- -*Worklog Manager v1.6.0 - Built with ❤️ by GitHub Copilot* \ No newline at end of file +*Worklog Manager v1.7.0 - Built with ❤️ by GitHub Copilot* \ No newline at end of file diff --git a/docs/TECHNICAL_SPECIFICATION.md b/docs/TECHNICAL_SPECIFICATION.md index cd80ef3..3a4791a 100644 --- a/docs/TECHNICAL_SPECIFICATION.md +++ b/docs/TECHNICAL_SPECIFICATION.md @@ -26,7 +26,7 @@ #### Window Properties - **Size**: 700x500 pixels (resizable) -- **Title**: "Worklog Manager v1.6.0" +- **Title**: "Worklog Manager v1.7.0" - **Icon**: Custom work timer icon - **Position**: Center screen on startup diff --git a/exporters/csv_exporter.py b/exporters/csv_exporter.py index d2e0989..9705e4a 100644 --- a/exporters/csv_exporter.py +++ b/exporters/csv_exporter.py @@ -9,6 +9,7 @@ from core.export_models import ExportData, ExportResult, ReportType from core.data_aggregator import DataAggregator +from utils.datetime_compat import datetime_fromisoformat class CSVExporter: @@ -224,7 +225,7 @@ def _generate_detailed_log_csv(self, export_data: ExportData) -> str: session_date = session.date break - timestamp = datetime.fromisoformat(action.timestamp) + timestamp = datetime_fromisoformat(action.timestamp) row = [ session_date, timestamp.strftime('%H:%M:%S'), @@ -289,7 +290,7 @@ def _generate_break_analysis_csv(self, export_data: ExportData) -> str: start_time = '' if break_period.start_time: - start_time = datetime.fromisoformat(break_period.start_time).strftime('%H:%M:%S') + start_time = datetime_fromisoformat(break_period.start_time).strftime('%H:%M:%S') row = [ session_date, diff --git a/gui/components/break_tracker.py b/gui/components/break_tracker.py index a22ed46..f02d5ec 100644 --- a/gui/components/break_tracker.py +++ b/gui/components/break_tracker.py @@ -68,7 +68,7 @@ def _create_summary_grid(self, parent): lunch_frame.grid(row=1, column=0, columnspan=3, sticky="ew", pady=1) lunch_frame.columnconfigure(2, weight=1) - tk.Label(lunch_frame, text="🍽️ Lunch", bg="#FFE4B5").grid(row=0, column=0, sticky="w", padx=5, pady=2) + tk.Label(lunch_frame, text="[L] Lunch", bg="#FFE4B5").grid(row=0, column=0, sticky="w", padx=5, pady=2) tk.Label(lunch_frame, textvariable=self.lunch_count_var, bg="#FFE4B5").grid(row=0, column=1, padx=20, pady=2) tk.Label(lunch_frame, textvariable=self.lunch_time_var, bg="#FFE4B5").grid(row=0, column=2, sticky="e", padx=5, pady=2) @@ -77,7 +77,7 @@ def _create_summary_grid(self, parent): coffee_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=1) coffee_frame.columnconfigure(2, weight=1) - tk.Label(coffee_frame, text="☕ Coffee", bg="#D2B48C").grid(row=0, column=0, sticky="w", padx=5, pady=2) + tk.Label(coffee_frame, text="[C] Coffee", bg="#D2B48C").grid(row=0, column=0, sticky="w", padx=5, pady=2) tk.Label(coffee_frame, textvariable=self.coffee_count_var, bg="#D2B48C").grid(row=0, column=1, padx=20, pady=2) tk.Label(coffee_frame, textvariable=self.coffee_time_var, bg="#D2B48C").grid(row=0, column=2, sticky="e", padx=5, pady=2) @@ -86,7 +86,7 @@ def _create_summary_grid(self, parent): general_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=1) general_frame.columnconfigure(2, weight=1) - tk.Label(general_frame, text="⏱️ General", bg="#F0F0F0").grid(row=0, column=0, sticky="w", padx=5, pady=2) + tk.Label(general_frame, text="[B] General", bg="#F0F0F0").grid(row=0, column=0, sticky="w", padx=5, pady=2) tk.Label(general_frame, textvariable=self.general_count_var, bg="#F0F0F0").grid(row=0, column=1, padx=20, pady=2) tk.Label(general_frame, textvariable=self.general_time_var, bg="#F0F0F0").grid(row=0, column=2, sticky="e", padx=5, pady=2) @@ -166,14 +166,14 @@ def _update_recent_list(self): else: status = f"{start_time}-ongoing" - # Add emoji based on break type - emoji = { - BreakType.LUNCH: "🍽️", - BreakType.COFFEE: "☕", - BreakType.GENERAL: "⏱️" - }.get(break_period.break_type, "⏱️") + # Add ASCII symbol based on break type for broad interpreter support + symbol = { + BreakType.LUNCH: "[L]", + BreakType.COFFEE: "[C]", + BreakType.GENERAL: "[B]" + }.get(break_period.break_type, "[B]") - break_text = f"{emoji} {break_period.break_type.value.title()}: {status}" + break_text = f"{symbol} {break_period.break_type.value.title()}: {status}" self.breaks_listbox.insert(tk.END, break_text) def _break_duration_seconds(self, break_period: BreakPeriod) -> int: diff --git a/gui/main_window.py b/gui/main_window.py index e2c6a87..feda9cf 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -37,7 +37,7 @@ def __init__(self): # Create main window self.root = tk.Tk() - self.root.title("Worklog Manager v1.6.0") + self.root.title("Worklog Manager v1.7.0") # Load window settings from configuration appearance_settings = self.settings_manager.settings.appearance @@ -245,38 +245,48 @@ def _create_control_buttons(self, parent): button_frame = ttk.Frame(parent, style="Themed.TFrame") button_frame.pack(fill="x", pady=10) - # First row - Start Day and End Day + # First row - Start Work and End Work row1_frame = ttk.Frame(button_frame, style="Themed.TFrame") row1_frame.pack(fill="x", pady=(0, 10)) self.start_day_btn = ttk.Button( row1_frame, - text="Start Day", + text="Start Work", command=self._start_day, - style="StartDay.TButton" + style="StartDay.TButton", + width=15 ) self.start_day_btn.pack(side="left", padx=(0, 10), ipadx=20, ipady=10) self.end_day_btn = ttk.Button( row1_frame, - text="End Day", + text="End Work", command=self._end_day, - style="EndDay.TButton" + style="EndDay.TButton", + width=15 ) self.end_day_btn.pack(side="right", padx=(10, 0), ipadx=20, ipady=10) - - # Second row - Stop and Continue + + # Second row - Take a Break and Resume Work row2_frame = ttk.Frame(button_frame, style="Themed.TFrame") row2_frame.pack(fill="x") - - self.stop_btn = ttk.Button(row2_frame, text="Stop", - command=self._stop_work, - style="Stop.TButton") + + self.stop_btn = ttk.Button( + row2_frame, + text="Take a Break", + command=self._stop_work, + style="Stop.TButton", + width=15 + ) self.stop_btn.pack(side="left", padx=(0, 10), ipadx=20, ipady=10) - - self.continue_btn = ttk.Button(row2_frame, text="Continue", - command=self._continue_work, - style="Continue.TButton") + + self.continue_btn = ttk.Button( + row2_frame, + text="Resume Work", + command=self._continue_work, + style="Continue.TButton", + width=15 + ) self.continue_btn.pack(side="right", padx=(10, 0), ipadx=20, ipady=10) def _create_break_selection(self, parent): @@ -452,38 +462,38 @@ def _end_day(self): messagebox.showerror("Error", f"Failed to end day: {e}") def _stop_work(self): - """Handle Stop button click.""" + """Handle Take a Break button click.""" try: if self.worklog_manager.can_perform_action(ActionType.STOP): break_type = BreakType(self.break_type_var.get()) - if messagebox.askyesno("Confirm", f"Stop work for {break_type.value} break?"): + if messagebox.askyesno("Confirm", f"Start a {break_type.value.lower()} break?"): if self.worklog_manager.stop_work(break_type): self._update_display() - self.logger.info(f"Work stopped for {break_type.value} break") + self.logger.info(f"Work paused for {break_type.value} break") else: - messagebox.showerror("Error", "Failed to stop work") + messagebox.showerror("Error", "Failed to start break") else: - messagebox.showwarning("Warning", "Cannot stop work in current state") + messagebox.showwarning("Warning", "Cannot start a break in the current state") except Exception as e: - self.logger.error(f"Error stopping work: {e}") - messagebox.showerror("Error", f"Failed to stop work: {e}") + self.logger.error(f"Error starting break: {e}") + messagebox.showerror("Error", f"Failed to start break: {e}") def _continue_work(self): - """Handle Continue button click.""" + """Handle Resume Work button click.""" try: if self.worklog_manager.can_perform_action(ActionType.CONTINUE): - if messagebox.askyesno("Confirm", "Continue work?"): + if messagebox.askyesno("Confirm", "Resume working?"): if self.worklog_manager.continue_work(): self._update_display() - self.logger.info("Work continued") + self.logger.info("Work resumed") else: - messagebox.showerror("Error", "Failed to continue work") + messagebox.showerror("Error", "Failed to resume work") else: - messagebox.showwarning("Warning", "Cannot continue work in current state") + messagebox.showwarning("Warning", "Cannot resume work in the current state") except Exception as e: - self.logger.error(f"Error continuing work: {e}") - messagebox.showerror("Error", f"Failed to continue work: {e}") + self.logger.error(f"Error resuming work: {e}") + messagebox.showerror("Error", f"Failed to resume work: {e}") def _export_data(self): """Handle Export Data button click.""" diff --git a/gui/settings_dialog.py b/gui/settings_dialog.py index d567d73..70ae6c6 100644 --- a/gui/settings_dialog.py +++ b/gui/settings_dialog.py @@ -15,6 +15,21 @@ from gui.theme_manager import ThemeManager, ThemePreview from core.simple_backup_manager import BackupManager +# Compatibility wrapper for ttk.Spinbox on Python versions that lack it. +if not hasattr(ttk, 'Spinbox'): + class TtkSpinboxCompat(tk.Spinbox): + """Compatibility wrapper for ttk.Spinbox using tk.Spinbox.""" + + def __init__(self, parent, **kwargs): + tk_kwargs = kwargs.copy() + if 'from' in tk_kwargs and 'from_' not in tk_kwargs: + tk_kwargs['from_'] = tk_kwargs.pop('from') + tk_kwargs.setdefault('width', 10) + tk_kwargs.setdefault('justify', 'center') + super().__init__(parent, **tk_kwargs) + + ttk.Spinbox = TtkSpinboxCompat + class SettingsDialog: """Main settings dialog with tabbed interface for all configuration options.""" diff --git a/gui/system_tray.py b/gui/system_tray.py index 296ba43..ba3e544 100644 --- a/gui/system_tray.py +++ b/gui/system_tray.py @@ -509,8 +509,8 @@ def _show_about_dialog(self): • Keyboard shortcuts • Automatic backups -Version: 1.0 -© 2024 Worklog Manager""" +Version: 1.7.0 +© 2025 Worklog Manager""" messagebox.showinfo("About Worklog Manager", about_text) diff --git a/main.py b/main.py index e47fdc2..5ef4dcb 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ - Cross-platform compatibility Author: GitHub Copilot -Version: 1.6.0 +Version: 1.7.0 """ import sys @@ -35,12 +35,14 @@ import threading import atexit from datetime import datetime -from pathlib import Path # Add the project root to Python path project_root = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, project_root) +# Import datetime compatibility for Python 3.6 support (must be imported early) +from utils.datetime_compat import datetime_fromisoformat, fromisoformat_compat # noqa: F401 + # Import core application components from gui.main_window import MainWindow from core.settings import SettingsManager @@ -204,7 +206,7 @@ def run(self): main_window = self.create_main_window() # Start the application - self.logger.info("Starting Worklog Manager Application v1.6.0") + self.logger.info("Starting Worklog Manager Application v1.7.0") main_window.run() except Exception as e: @@ -266,7 +268,7 @@ def setup_logging(): logger = logging.getLogger(__name__) logger.info("="*50) logger.info("Worklog Manager Application Starting") - logger.info(f"Version: 1.6.0") + logger.info(f"Version: 1.7.0") logger.info(f"Python: {sys.version}") logger.info(f"Working Directory: {os.getcwd()}") logger.info(f"Project Root: {project_root}") @@ -280,15 +282,15 @@ def main(): # Setup logging setup_logging() logger = logging.getLogger(__name__) - - logger.info("Initializing Worklog Manager v1.6.0 with advanced features...") - + + logger.info("Initializing Worklog Manager v1.7.0 with advanced features...") + # Create and run the comprehensive application app = WorklogApplication() app.run() - + logger.info("Application shutdown normally") - + except KeyboardInterrupt: print("\nApplication interrupted by user") logging.getLogger(__name__).info("Application interrupted by user") diff --git a/requirements.txt b/requirements.txt index 8d626df..fd9c14f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Worklog Manager - Python Dependencies -# Version 1.6.0 - Advanced Features +# Version 1.7.0 - Advanced Features # Core Dependencies (included with Python standard library): # - tkinter (GUI framework) @@ -22,6 +22,9 @@ plyer>=2.0 # PDF export functionality (Phase 3 & 4) reportlab>=3.6.0 +# Python 3.6 compatibility +dataclasses; python_version < "3.7" + # Optional Dependencies for Enhanced Experience: # Windows-specific system integration (Windows only) diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 0000000..7e29616 --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,164 @@ +"""Utility script to bump the Worklog Manager version across project files.""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +SETTINGS_PATH = PROJECT_ROOT / "settings.json" + +# Files updated via simple string replacement of the current version string. +REPLACEMENT_FILES = ( + "README.md", + "CHANGELOG.md", + "DOCUMENTATION_SUMMARY.md", + "PROJECT_OVERVIEW.md", + "requirements.txt", + "start_worklog.py", + "start_worklog.sh", + "start_worklog.bat", + "main.py", + "gui/main_window.py", + "gui/system_tray.py", + "docs/TECHNICAL_SPECIFICATION.md", + "docs/COMPLETION_SUMMARY.md", + ".github/ISSUE_TEMPLATE/bug_report.md", +) + + +@dataclass +class UpdateResult: + path: Path + updated: bool + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Bump project version strings") + parser.add_argument("new_version", help="New semantic version, e.g. 1.8.0") + parser.add_argument( + "--current", + dest="current_version", + help="Override detected current version", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show planned updates without writing files", + ) + parser.add_argument( + "--skip-roadmap", + action="store_true", + help="Do not adjust roadmap version sections", + ) + return parser.parse_args() + + +def load_current_version(override: str | None) -> str: + if override: + return override + with SETTINGS_PATH.open("r", encoding="utf-8") as fh: + data = json.load(fh) + return data["version"] + + +def write_file(path: Path, content: str, *, dry_run: bool) -> None: + if dry_run: + return + path.write_text(content, encoding="utf-8") + + +def replace_version(path: Path, current: str, new: str, *, dry_run: bool) -> UpdateResult: + text = path.read_text(encoding="utf-8") + if current not in text: + return UpdateResult(path, False) + updated = text.replace(current, new) + write_file(path, updated, dry_run=dry_run) + return UpdateResult(path, True) + + +def bump_minor(version: str) -> str: + major, minor, patch = version.split(".") + return f"{int(major)}.{int(minor) + 1}.{int(patch)}" + + +def update_roadmap(current: str, new: str, *, dry_run: bool) -> UpdateResult: + roadmap_path = PROJECT_ROOT / "ROADMAP.md" + text = roadmap_path.read_text(encoding="utf-8") + + old_collab = bump_minor(current) + old_integration = bump_minor(old_collab) + new_collab = bump_minor(new) + new_integration = bump_minor(new_collab) + + replacements = { + f"## Version {current} - CURRENT ✅": f"## Version {new} - CURRENT ✅", + f"## Version {current} - Data Management (Planned)": f"## Version {new} - Data Management (Planned)", + f"## Version {old_collab} - Collaboration Features (Under Consideration)": f"## Version {new_collab} - Collaboration Features (Under Consideration)", + f"## Version {old_integration} - Integration & Automation (Future)": f"## Version {new_integration} - Integration & Automation (Future)", + f"planned for v{current}": f"planned for v{new}", + f"for v{old_collab}": f"for v{new_collab}", + } + + updated = text + changed = False + for old, new_value in replacements.items(): + if old in updated: + updated = updated.replace(old, new_value) + changed = True + + if changed: + write_file(roadmap_path, updated, dry_run=dry_run) + return UpdateResult(roadmap_path, changed) + + +def update_files(paths: Iterable[str], current: str, new: str, *, dry_run: bool) -> list[UpdateResult]: + results: list[UpdateResult] = [] + for relative in paths: + path = PROJECT_ROOT / relative + results.append(replace_version(path, current, new, dry_run=dry_run)) + return results + + +def main() -> None: + args = parse_args() + current_version = load_current_version(args.current_version) + new_version = args.new_version + + if current_version == new_version: + print(f"Version already set to {new_version}") + return + + results = update_files(REPLACEMENT_FILES, current_version, new_version, dry_run=args.dry_run) + + if not args.skip_roadmap: + results.append(update_roadmap(current_version, new_version, dry_run=args.dry_run)) + + touched = [res for res in results if res.updated] + missing = [res for res in results if not res.updated] + + for res in touched: + print(f"Updated {res.path.relative_to(PROJECT_ROOT)}") + for res in missing: + print(f"Skipped {res.path.relative_to(PROJECT_ROOT)} (no '{current_version}' found)") + + if args.dry_run: + print("Dry-run complete; no files were modified.") + else: + # Update settings.json explicitly to keep the JSON value in sync even if + # the replacement list omitted it for some reason. + settings = json.loads(SETTINGS_PATH.read_text(encoding="utf-8")) + settings["version"] = new_version + write_file( + SETTINGS_PATH, + json.dumps(settings, indent=2, ensure_ascii=False) + "\n", + dry_run=False, + ) + print(f"Bumped version {current_version} -> {new_version}") + + +if __name__ == "__main__": + main() diff --git a/start_worklog.bat b/start_worklog.bat index 6257b02..0e46c5d 100644 --- a/start_worklog.bat +++ b/start_worklog.bat @@ -1,6 +1,6 @@ @echo off REM Worklog Manager - Windows Startup Script -REM Version 1.6.0 +REM Version 1.7.0 REM REM This batch file provides an easy way to start Worklog Manager on Windows diff --git a/start_worklog.py b/start_worklog.py index 77045dd..3814459 100644 --- a/start_worklog.py +++ b/start_worklog.py @@ -11,7 +11,7 @@ double-click this file if Python is properly configured Author: GitHub Copilot -Version: 1.6.0 +Version: 1.7.0 """ import sys @@ -117,7 +117,7 @@ def start_application(): def main(): """Main startup function.""" - print("Worklog Manager - Startup Script v1.6.0") + print("Worklog Manager - Startup Script v1.7.0") print("=" * 50) # Check system requirements diff --git a/start_worklog.sh b/start_worklog.sh index e9f6e05..596eb77 100644 --- a/start_worklog.sh +++ b/start_worklog.sh @@ -1,6 +1,6 @@ #!/bin/bash # Worklog Manager - Unix/Linux/Mac Startup Script -# Version 1.6.0 +# Version 1.7.0 # # This shell script provides an easy way to start Worklog Manager on Unix-like systems diff --git a/utils/__init__.py b/utils/__init__.py index b8795e3..757710b 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1 +1,4 @@ -"""Utility functions and helpers.""" \ No newline at end of file +"""Utility functions and helpers.""" + +# Import datetime compatibility shim so Python 3.6 environments load helpers early. +from . import datetime_compat # noqa: F401 \ No newline at end of file diff --git a/utils/datetime_compat.py b/utils/datetime_compat.py new file mode 100644 index 0000000..f89a0bf --- /dev/null +++ b/utils/datetime_compat.py @@ -0,0 +1,163 @@ +"""DateTime compatibility utilities for Python 3.6 support.""" + +import re +from datetime import datetime + + +def fromisoformat_compat(date_string: str) -> datetime: + """ + Compatibility function for datetime.fromisoformat() which was introduced in Python 3.7. + + This function provides equivalent functionality for Python 3.6. + + Args: + date_string: ISO format date string + + Returns: + datetime object + + Raises: + ValueError: If the date string format is invalid + """ + # Handle various ISO format patterns + patterns = [ + # Full datetime with microseconds and timezone (T separator) + r'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d+)([+-]\d{2}:\d{2}|Z)?$', + # Full datetime with timezone (T separator) + r'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]\d{2}:\d{2}|Z)?$', + # Full datetime without timezone (T separator) + r'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d+)$', + r'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$', + # Full datetime with microseconds and timezone (space separator) + r'^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d+)([+-]\d{2}:\d{2}|Z)?$', + # Full datetime with timezone (space separator) + r'^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})([+-]\d{2}:\d{2}|Z)?$', + # Full datetime without timezone (space separator) + r'^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d+)$', + r'^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$', + # Date only + r'^(\d{4})-(\d{2})-(\d{2})$', + # Time only (assuming today's date) + r'^(\d{2}):(\d{2}):(\d{2})\.(\d+)$', + r'^(\d{2}):(\d{2}):(\d{2})$', + ] + + date_string = date_string.strip() + + # Try each pattern + for index, pattern in enumerate(patterns): + match = re.match(pattern, date_string) + if match: + groups = match.groups() + + if index == 0: + year, month, day, hour, minute, second, microsecond, tz = groups + return datetime( + int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), + int(microsecond.ljust(6, '0')[:6]), + ) + + if index == 1: + year, month, day, hour, minute, second, tz = groups + return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) + + if index == 2: + year, month, day, hour, minute, second, microsecond = groups + return datetime( + int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), + int(microsecond.ljust(6, '0')[:6]), + ) + + if index == 3: + year, month, day, hour, minute, second = groups + return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) + + if index == 4: + year, month, day, hour, minute, second, microsecond, tz = groups + return datetime( + int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), + int(microsecond.ljust(6, '0')[:6]), + ) + + if index == 5: + year, month, day, hour, minute, second, tz = groups + return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) + + if index == 6: + year, month, day, hour, minute, second, microsecond = groups + return datetime( + int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), + int(microsecond.ljust(6, '0')[:6]), + ) + + if index == 7: + year, month, day, hour, minute, second = groups + return datetime(int(year), int(month), int(day), int(hour), int(minute), int(second)) + + if index == 8: + year, month, day = groups + return datetime(int(year), int(month), int(day)) + + if index == 9: + hour, minute, second, microsecond = groups + today = datetime.today().date() + return datetime.combine( + today, + datetime.min.time().replace( + hour=int(hour), + minute=int(minute), + second=int(second), + microsecond=int(microsecond.ljust(6, '0')[:6]), + ), + ) + + if index == 10: + hour, minute, second = groups + today = datetime.today().date() + return datetime.combine( + today, + datetime.min.time().replace( + hour=int(hour), + minute=int(minute), + second=int(second), + ), + ) + + # If no pattern matches, try the native Python 3.6 strptime as fallback + try: + for fmt in ['%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d', '%H:%M:%S']: + try: + return datetime.strptime(date_string, fmt) + except ValueError: + continue + except Exception: + pass + + raise ValueError(f"Invalid isoformat string: '{date_string}'") + + +def datetime_fromisoformat(date_string: str) -> datetime: + """Compatibility wrapper for datetime.fromisoformat().""" + if hasattr(datetime, 'fromisoformat'): + return datetime.fromisoformat(date_string) + return fromisoformat_compat(date_string) diff --git a/utils/validators.py b/utils/validators.py index 556bfc7..16a0703 100644 --- a/utils/validators.py +++ b/utils/validators.py @@ -6,6 +6,7 @@ from data.models import WorklogState, ActionType, BreakType, WorkSession from core.action_history import ActionHistory, ActionSnapshot +from utils.datetime_compat import datetime_fromisoformat class WorklogValidator: @@ -56,22 +57,22 @@ def validate_session_data(self, session: WorkSession) -> Tuple[bool, str]: # Validate date format try: - datetime.fromisoformat(session.date) + datetime_fromisoformat(session.date) except ValueError: return False, f"Invalid date format: {session.date}" # Validate times if present if session.start_time: try: - datetime.fromisoformat(session.start_time) + datetime_fromisoformat(session.start_time) except ValueError: return False, f"Invalid start time format: {session.start_time}" if session.end_time: try: - end_time = datetime.fromisoformat(session.end_time) + end_time = datetime_fromisoformat(session.end_time) if session.start_time: - start_time = datetime.fromisoformat(session.start_time) + start_time = datetime_fromisoformat(session.start_time) if end_time < start_time: return False, "End time cannot be before start time" except ValueError: @@ -195,8 +196,8 @@ def validate_date_range(self, start_date: str, end_date: str) -> Tuple[bool, str Tuple of (is_valid, error_message) """ try: - start = datetime.fromisoformat(start_date) - end = datetime.fromisoformat(end_date) + start = datetime_fromisoformat(start_date) + end = datetime_fromisoformat(end_date) except ValueError as e: return False, f"Invalid date format: {e}" @@ -259,7 +260,7 @@ def validate_time_string(time_str: str) -> Tuple[bool, str]: return False, "Time string is empty" try: - datetime.fromisoformat(time_str) + datetime_fromisoformat(time_str) return True, "" except ValueError: return False, f"Invalid time format: {time_str}"