- Add comprehensive WorktimeTracker service with worktime estimation and cost distribution - Implement full CLI interface with log, list, daily, estimate, distribute, report, delete, update commands - Support flexible duration parsing (90, 1h30m, 2.5h) and time tracking with start/end times - Add worktime estimation with equal and activity-based distribution methods - Implement proportional cost distribution based on actual time spent on issues - Create worktime database schema with entries, summaries, and cost distribution logging - Add 24 comprehensive test cases covering all functionality with integration tests - Support multiple output formats (table/JSON) and comprehensive reporting features - Enable precise cost allocation per minute with audit trail for financial tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
677 lines
25 KiB
Python
677 lines
25 KiB
Python
"""
|
|
Daily Worktime Tracking and Cost Distribution System
|
|
|
|
This module provides comprehensive worktime tracking functionality that estimates
|
|
daily work time spent on issues and distributes costs proportionally based on
|
|
time allocation rather than equal distribution.
|
|
"""
|
|
|
|
import sqlite3
|
|
import json
|
|
from datetime import datetime, date, timedelta
|
|
from typing import List, Dict, Any, Optional, Tuple
|
|
from dataclasses import dataclass, asdict
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
from .models import FinanceModels
|
|
|
|
|
|
@dataclass
|
|
class WorktimeEntry:
|
|
"""Data class representing a worktime tracking entry."""
|
|
id: Optional[int] = None
|
|
issue_id: int = None
|
|
work_date: date = None
|
|
start_time: Optional[datetime] = None
|
|
end_time: Optional[datetime] = None
|
|
duration_minutes: int = None
|
|
description: Optional[str] = None
|
|
entry_type: str = "manual" # manual, estimated, tracked
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
|
|
@dataclass
|
|
class DailySummary:
|
|
"""Data class for daily worktime summary."""
|
|
work_date: date
|
|
total_minutes: int
|
|
issue_count: int
|
|
entries: List[WorktimeEntry]
|
|
cost_per_minute: Optional[Decimal] = None
|
|
total_cost_allocated: Optional[Decimal] = None
|
|
|
|
|
|
class WorktimeTracker:
|
|
"""
|
|
Service for tracking worktime and distributing costs based on time allocation.
|
|
|
|
Provides functionality to log work time, estimate daily effort, and calculate
|
|
proportional cost distribution across issues based on time spent.
|
|
"""
|
|
|
|
def __init__(self, db_path: str = "markitect.db"):
|
|
"""
|
|
Initialize the worktime tracker.
|
|
|
|
Args:
|
|
db_path: Path to the SQLite database file
|
|
"""
|
|
self.db_path = db_path
|
|
self.finance_models = FinanceModels(db_path)
|
|
self._ensure_schema()
|
|
|
|
def _ensure_schema(self):
|
|
"""Ensure the database schema is properly initialized."""
|
|
self.finance_models.initialize_finance_schema()
|
|
self._create_worktime_tables()
|
|
|
|
def _create_worktime_tables(self):
|
|
"""Create worktime-specific database tables."""
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Enable foreign key constraints
|
|
cursor.execute('PRAGMA foreign_keys = ON')
|
|
|
|
# Create worktime entries table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS worktime_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
issue_id INTEGER NOT NULL,
|
|
work_date DATE NOT NULL,
|
|
start_time TIMESTAMP,
|
|
end_time TIMESTAMP,
|
|
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
|
|
description TEXT,
|
|
entry_type TEXT CHECK (entry_type IN ('manual', 'estimated', 'tracked')) DEFAULT 'manual',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT unique_issue_date_time UNIQUE (issue_id, work_date, start_time)
|
|
)
|
|
''')
|
|
|
|
# Create daily worktime summaries table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS daily_worktime_summaries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
work_date DATE NOT NULL UNIQUE,
|
|
total_minutes INTEGER NOT NULL DEFAULT 0,
|
|
issue_count INTEGER NOT NULL DEFAULT 0,
|
|
cost_per_minute DECIMAL(10,6),
|
|
total_cost_allocated DECIMAL(10,2),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
# Create cost distribution log table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS worktime_cost_distributions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
work_date DATE NOT NULL,
|
|
issue_id INTEGER NOT NULL,
|
|
time_minutes INTEGER NOT NULL,
|
|
cost_allocated DECIMAL(10,2) NOT NULL,
|
|
cost_per_minute DECIMAL(10,6) NOT NULL,
|
|
period_id INTEGER REFERENCES cost_periods(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT unique_date_issue_distribution UNIQUE (work_date, issue_id)
|
|
)
|
|
''')
|
|
|
|
def log_worktime(self,
|
|
issue_id: int,
|
|
duration_minutes: int,
|
|
work_date: Optional[date] = None,
|
|
start_time: Optional[datetime] = None,
|
|
end_time: Optional[datetime] = None,
|
|
description: Optional[str] = None,
|
|
entry_type: str = "manual") -> int:
|
|
"""
|
|
Log worktime for an issue.
|
|
|
|
Args:
|
|
issue_id: ID of the issue
|
|
duration_minutes: Duration of work in minutes
|
|
work_date: Date when work was performed (defaults to today)
|
|
start_time: Optional start timestamp
|
|
end_time: Optional end timestamp
|
|
description: Description of work performed
|
|
entry_type: Type of entry (manual, estimated, tracked)
|
|
|
|
Returns:
|
|
ID of the created worktime entry
|
|
|
|
Raises:
|
|
ValueError: If duration is invalid
|
|
sqlite3.Error: If database operation fails
|
|
"""
|
|
if duration_minutes <= 0:
|
|
raise ValueError("Duration must be positive")
|
|
|
|
if work_date is None:
|
|
work_date = date.today()
|
|
|
|
# Validate time consistency
|
|
if start_time and end_time:
|
|
calculated_duration = int((end_time - start_time).total_seconds() / 60)
|
|
if abs(calculated_duration - duration_minutes) > 5: # Allow 5-minute tolerance
|
|
print(f"⚠️ Warning: Calculated duration ({calculated_duration}min) differs from specified ({duration_minutes}min)")
|
|
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
INSERT INTO worktime_entries
|
|
(issue_id, work_date, start_time, end_time, duration_minutes, description, entry_type)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', (issue_id, work_date, start_time, end_time, duration_minutes, description, entry_type))
|
|
|
|
entry_id = cursor.lastrowid
|
|
|
|
# Update daily summary
|
|
self._update_daily_summary(work_date, conn)
|
|
|
|
return entry_id
|
|
|
|
def _update_daily_summary(self, work_date: date, conn: sqlite3.Connection):
|
|
"""Update the daily worktime summary for a given date."""
|
|
cursor = conn.cursor()
|
|
|
|
# Calculate daily totals
|
|
cursor.execute('''
|
|
SELECT
|
|
COUNT(DISTINCT issue_id) as issue_count,
|
|
SUM(duration_minutes) as total_minutes
|
|
FROM worktime_entries
|
|
WHERE work_date = ?
|
|
''', (work_date,))
|
|
|
|
result = cursor.fetchone()
|
|
issue_count = result[0] or 0
|
|
total_minutes = result[1] or 0
|
|
|
|
# Insert or update summary
|
|
cursor.execute('''
|
|
INSERT OR REPLACE INTO daily_worktime_summaries
|
|
(work_date, total_minutes, issue_count)
|
|
VALUES (?, ?, ?)
|
|
''', (work_date, total_minutes, issue_count))
|
|
|
|
def get_worktime_entries(self,
|
|
issue_id: Optional[int] = None,
|
|
work_date: Optional[date] = None,
|
|
start_date: Optional[date] = None,
|
|
end_date: Optional[date] = None,
|
|
limit: Optional[int] = None) -> List[WorktimeEntry]:
|
|
"""
|
|
Get worktime entries with optional filtering.
|
|
|
|
Args:
|
|
issue_id: Filter by specific issue
|
|
work_date: Filter by specific date
|
|
start_date: Filter by date range start
|
|
end_date: Filter by date range end
|
|
limit: Maximum number of entries to return
|
|
|
|
Returns:
|
|
List of worktime entries
|
|
"""
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = '''
|
|
SELECT id, issue_id, work_date, start_time, end_time,
|
|
duration_minutes, description, entry_type, created_at, updated_at
|
|
FROM worktime_entries
|
|
WHERE 1=1
|
|
'''
|
|
params = []
|
|
|
|
if issue_id:
|
|
query += ' AND issue_id = ?'
|
|
params.append(issue_id)
|
|
|
|
if work_date:
|
|
query += ' AND work_date = ?'
|
|
params.append(work_date)
|
|
elif start_date and end_date:
|
|
query += ' AND work_date BETWEEN ? AND ?'
|
|
params.extend([start_date, end_date])
|
|
elif start_date:
|
|
query += ' AND work_date >= ?'
|
|
params.append(start_date)
|
|
elif end_date:
|
|
query += ' AND work_date <= ?'
|
|
params.append(end_date)
|
|
|
|
query += ' ORDER BY work_date DESC, start_time DESC'
|
|
|
|
if limit:
|
|
query += ' LIMIT ?'
|
|
params.append(limit)
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
entries = []
|
|
for row in rows:
|
|
entry = WorktimeEntry(
|
|
id=row[0],
|
|
issue_id=row[1],
|
|
work_date=datetime.strptime(row[2], '%Y-%m-%d').date() if row[2] else None,
|
|
start_time=datetime.fromisoformat(row[3]) if row[3] else None,
|
|
end_time=datetime.fromisoformat(row[4]) if row[4] else None,
|
|
duration_minutes=row[5],
|
|
description=row[6],
|
|
entry_type=row[7],
|
|
created_at=datetime.fromisoformat(row[8]) if row[8] else None,
|
|
updated_at=datetime.fromisoformat(row[9]) if row[9] else None
|
|
)
|
|
entries.append(entry)
|
|
|
|
return entries
|
|
|
|
def get_daily_summary(self, work_date: date) -> Optional[DailySummary]:
|
|
"""
|
|
Get daily worktime summary for a specific date.
|
|
|
|
Args:
|
|
work_date: Date to get summary for
|
|
|
|
Returns:
|
|
Daily summary or None if no data
|
|
"""
|
|
entries = self.get_worktime_entries(work_date=work_date)
|
|
if not entries:
|
|
return None
|
|
|
|
total_minutes = sum(entry.duration_minutes for entry in entries)
|
|
issue_count = len(set(entry.issue_id for entry in entries))
|
|
|
|
# Get cost allocation if available
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT cost_per_minute, total_cost_allocated
|
|
FROM daily_worktime_summaries
|
|
WHERE work_date = ?
|
|
''', (work_date,))
|
|
result = cursor.fetchone()
|
|
|
|
cost_per_minute = Decimal(str(result[0])) if result and result[0] else None
|
|
total_cost_allocated = Decimal(str(result[1])) if result and result[1] else None
|
|
|
|
return DailySummary(
|
|
work_date=work_date,
|
|
total_minutes=total_minutes,
|
|
issue_count=issue_count,
|
|
entries=entries,
|
|
cost_per_minute=cost_per_minute,
|
|
total_cost_allocated=total_cost_allocated
|
|
)
|
|
|
|
def estimate_daily_worktime(self,
|
|
work_date: date,
|
|
total_hours: float = 8.0,
|
|
issues: Optional[List[int]] = None,
|
|
distribution_method: str = "equal") -> Dict[str, Any]:
|
|
"""
|
|
Estimate worktime distribution for a day when specific times aren't tracked.
|
|
|
|
Args:
|
|
work_date: Date to estimate for
|
|
total_hours: Total work hours for the day
|
|
issues: List of issue IDs that were worked on
|
|
distribution_method: How to distribute time (equal, activity_based, manual)
|
|
|
|
Returns:
|
|
Dictionary with estimation results
|
|
"""
|
|
total_minutes = int(total_hours * 60)
|
|
|
|
if not issues:
|
|
# Try to identify issues from activity log
|
|
issues = self._get_active_issues_for_date(work_date)
|
|
|
|
if not issues:
|
|
raise ValueError("No issues specified and none found in activity log")
|
|
|
|
estimates = {}
|
|
|
|
if distribution_method == "equal":
|
|
# Equal distribution across all issues
|
|
minutes_per_issue = total_minutes // len(issues)
|
|
remainder = total_minutes % len(issues)
|
|
|
|
for i, issue_id in enumerate(issues):
|
|
minutes = minutes_per_issue + (1 if i < remainder else 0)
|
|
estimates[issue_id] = minutes
|
|
|
|
elif distribution_method == "activity_based":
|
|
# Distribute based on activity frequency
|
|
activity_weights = self._get_activity_weights_for_date(work_date, issues)
|
|
total_weight = sum(activity_weights.values())
|
|
|
|
if total_weight == 0:
|
|
# Fallback to equal distribution
|
|
minutes_per_issue = total_minutes // len(issues)
|
|
for issue_id in issues:
|
|
estimates[issue_id] = minutes_per_issue
|
|
else:
|
|
for issue_id in issues:
|
|
weight = activity_weights.get(issue_id, 0)
|
|
minutes = int((weight / total_weight) * total_minutes)
|
|
estimates[issue_id] = minutes
|
|
|
|
# Log estimated entries
|
|
for issue_id, minutes in estimates.items():
|
|
if minutes > 0:
|
|
self.log_worktime(
|
|
issue_id=issue_id,
|
|
duration_minutes=minutes,
|
|
work_date=work_date,
|
|
description=f"Estimated worktime ({distribution_method} distribution)",
|
|
entry_type="estimated"
|
|
)
|
|
|
|
return {
|
|
"work_date": work_date,
|
|
"total_minutes": total_minutes,
|
|
"distribution_method": distribution_method,
|
|
"issue_estimates": estimates,
|
|
"issues_count": len(issues)
|
|
}
|
|
|
|
def _get_active_issues_for_date(self, work_date: date) -> List[int]:
|
|
"""Get issues that had activity on a specific date."""
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT DISTINCT issue_id
|
|
FROM issue_activity_log
|
|
WHERE activity_date = ?
|
|
''', (work_date,))
|
|
return [row[0] for row in cursor.fetchall()]
|
|
|
|
def _get_activity_weights_for_date(self, work_date: date, issues: List[int]) -> Dict[int, int]:
|
|
"""Get activity weights for issues on a specific date."""
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
placeholders = ','.join(['?' for _ in issues])
|
|
cursor.execute(f'''
|
|
SELECT issue_id, COUNT(*) as activity_count
|
|
FROM issue_activity_log
|
|
WHERE activity_date = ? AND issue_id IN ({placeholders})
|
|
GROUP BY issue_id
|
|
''', [work_date] + issues)
|
|
return dict(cursor.fetchall())
|
|
|
|
def distribute_daily_costs(self,
|
|
work_date: date,
|
|
total_daily_cost: Decimal,
|
|
period_id: Optional[int] = None) -> Dict[str, Any]:
|
|
"""
|
|
Distribute daily costs proportionally based on time spent on each issue.
|
|
|
|
Args:
|
|
work_date: Date to distribute costs for
|
|
total_daily_cost: Total cost to distribute
|
|
period_id: Cost period ID for tracking
|
|
|
|
Returns:
|
|
Dictionary with distribution results
|
|
"""
|
|
# Get worktime entries for the date
|
|
entries = self.get_worktime_entries(work_date=work_date)
|
|
if not entries:
|
|
return {
|
|
"work_date": work_date,
|
|
"total_cost": float(total_daily_cost),
|
|
"distributions": {},
|
|
"message": "No worktime entries found for this date"
|
|
}
|
|
|
|
# Calculate time per issue
|
|
issue_minutes = {}
|
|
for entry in entries:
|
|
issue_minutes[entry.issue_id] = issue_minutes.get(entry.issue_id, 0) + entry.duration_minutes
|
|
|
|
total_minutes = sum(issue_minutes.values())
|
|
if total_minutes == 0:
|
|
return {
|
|
"work_date": work_date,
|
|
"total_cost": float(total_daily_cost),
|
|
"distributions": {},
|
|
"message": "No time tracked for this date"
|
|
}
|
|
|
|
cost_per_minute = total_daily_cost / total_minutes
|
|
|
|
# Calculate cost distribution
|
|
distributions = {}
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
for issue_id, minutes in issue_minutes.items():
|
|
cost_allocated = cost_per_minute * minutes
|
|
distributions[issue_id] = {
|
|
"minutes": minutes,
|
|
"percentage": (minutes / total_minutes) * 100,
|
|
"cost_allocated": float(cost_allocated),
|
|
"cost_per_minute": float(cost_per_minute)
|
|
}
|
|
|
|
# Log the distribution
|
|
cursor.execute('''
|
|
INSERT OR REPLACE INTO worktime_cost_distributions
|
|
(work_date, issue_id, time_minutes, cost_allocated, cost_per_minute, period_id)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', (work_date, issue_id, minutes, float(cost_allocated), float(cost_per_minute), period_id))
|
|
|
|
# Update daily summary with cost information
|
|
cursor.execute('''
|
|
UPDATE daily_worktime_summaries
|
|
SET cost_per_minute = ?, total_cost_allocated = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE work_date = ?
|
|
''', (float(cost_per_minute), float(total_daily_cost), work_date))
|
|
|
|
return {
|
|
"work_date": work_date,
|
|
"total_cost": float(total_daily_cost),
|
|
"total_minutes": total_minutes,
|
|
"cost_per_minute": float(cost_per_minute),
|
|
"distributions": distributions,
|
|
"issues_count": len(distributions)
|
|
}
|
|
|
|
def get_worktime_report(self,
|
|
start_date: Optional[date] = None,
|
|
end_date: Optional[date] = None,
|
|
issue_id: Optional[int] = None) -> Dict[str, Any]:
|
|
"""
|
|
Generate comprehensive worktime report.
|
|
|
|
Args:
|
|
start_date: Report start date
|
|
end_date: Report end date
|
|
issue_id: Filter by specific issue
|
|
|
|
Returns:
|
|
Comprehensive worktime report
|
|
"""
|
|
if not start_date:
|
|
start_date = date.today() - timedelta(days=30) # Last 30 days
|
|
if not end_date:
|
|
end_date = date.today()
|
|
|
|
entries = self.get_worktime_entries(
|
|
issue_id=issue_id,
|
|
start_date=start_date,
|
|
end_date=end_date
|
|
)
|
|
|
|
if not entries:
|
|
return {
|
|
"period": f"{start_date} to {end_date}",
|
|
"total_entries": 0,
|
|
"total_time": {"hours": 0, "minutes": 0},
|
|
"summary": "No worktime entries found"
|
|
}
|
|
|
|
# Calculate summary statistics
|
|
total_minutes = sum(entry.duration_minutes for entry in entries)
|
|
issue_breakdown = {}
|
|
daily_breakdown = {}
|
|
|
|
for entry in entries:
|
|
# Issue breakdown
|
|
if entry.issue_id not in issue_breakdown:
|
|
issue_breakdown[entry.issue_id] = {
|
|
"total_minutes": 0,
|
|
"entry_count": 0,
|
|
"dates": set()
|
|
}
|
|
issue_breakdown[entry.issue_id]["total_minutes"] += entry.duration_minutes
|
|
issue_breakdown[entry.issue_id]["entry_count"] += 1
|
|
issue_breakdown[entry.issue_id]["dates"].add(entry.work_date)
|
|
|
|
# Daily breakdown
|
|
date_str = entry.work_date.isoformat()
|
|
if date_str not in daily_breakdown:
|
|
daily_breakdown[date_str] = {
|
|
"total_minutes": 0,
|
|
"issues": set(),
|
|
"entries": 0
|
|
}
|
|
daily_breakdown[date_str]["total_minutes"] += entry.duration_minutes
|
|
daily_breakdown[date_str]["issues"].add(entry.issue_id)
|
|
daily_breakdown[date_str]["entries"] += 1
|
|
|
|
# Convert sets to counts for JSON serialization
|
|
for issue_id in issue_breakdown:
|
|
issue_breakdown[issue_id]["unique_dates"] = len(issue_breakdown[issue_id]["dates"])
|
|
del issue_breakdown[issue_id]["dates"]
|
|
|
|
for date_str in daily_breakdown:
|
|
daily_breakdown[date_str]["unique_issues"] = len(daily_breakdown[date_str]["issues"])
|
|
del daily_breakdown[date_str]["issues"]
|
|
|
|
return {
|
|
"period": f"{start_date} to {end_date}",
|
|
"total_entries": len(entries),
|
|
"total_time": {
|
|
"hours": total_minutes // 60,
|
|
"minutes": total_minutes % 60,
|
|
"total_minutes": total_minutes
|
|
},
|
|
"unique_issues": len(issue_breakdown),
|
|
"unique_dates": len(daily_breakdown),
|
|
"average_minutes_per_day": total_minutes / len(daily_breakdown) if daily_breakdown else 0,
|
|
"issue_breakdown": issue_breakdown,
|
|
"daily_breakdown": daily_breakdown
|
|
}
|
|
|
|
def delete_worktime_entry(self, entry_id: int) -> bool:
|
|
"""
|
|
Delete a worktime entry and update summaries.
|
|
|
|
Args:
|
|
entry_id: ID of the entry to delete
|
|
|
|
Returns:
|
|
True if deleted successfully, False otherwise
|
|
"""
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get the entry details before deletion for summary update
|
|
cursor.execute('''
|
|
SELECT work_date FROM worktime_entries WHERE id = ?
|
|
''', (entry_id,))
|
|
result = cursor.fetchone()
|
|
|
|
if not result:
|
|
return False
|
|
|
|
work_date = datetime.strptime(result[0], '%Y-%m-%d').date()
|
|
|
|
# Delete the entry
|
|
cursor.execute('DELETE FROM worktime_entries WHERE id = ?', (entry_id,))
|
|
|
|
if cursor.rowcount > 0:
|
|
# Update daily summary
|
|
self._update_daily_summary(work_date, conn)
|
|
return True
|
|
|
|
return False
|
|
|
|
def update_worktime_entry(self,
|
|
entry_id: int,
|
|
duration_minutes: Optional[int] = None,
|
|
description: Optional[str] = None,
|
|
start_time: Optional[datetime] = None,
|
|
end_time: Optional[datetime] = None) -> bool:
|
|
"""
|
|
Update an existing worktime entry.
|
|
|
|
Args:
|
|
entry_id: ID of the entry to update
|
|
duration_minutes: New duration in minutes
|
|
description: New description
|
|
start_time: New start time
|
|
end_time: New end time
|
|
|
|
Returns:
|
|
True if updated successfully, False otherwise
|
|
"""
|
|
updates = {}
|
|
params = []
|
|
|
|
if duration_minutes is not None:
|
|
updates["duration_minutes"] = "?"
|
|
params.append(duration_minutes)
|
|
if description is not None:
|
|
updates["description"] = "?"
|
|
params.append(description)
|
|
if start_time is not None:
|
|
updates["start_time"] = "?"
|
|
params.append(start_time)
|
|
if end_time is not None:
|
|
updates["end_time"] = "?"
|
|
params.append(end_time)
|
|
|
|
if not updates:
|
|
return False
|
|
|
|
updates["updated_at"] = "CURRENT_TIMESTAMP"
|
|
|
|
with self.finance_models.get_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get work date for summary update
|
|
cursor.execute('SELECT work_date FROM worktime_entries WHERE id = ?', (entry_id,))
|
|
result = cursor.fetchone()
|
|
if not result:
|
|
return False
|
|
|
|
work_date = datetime.strptime(result[0], '%Y-%m-%d').date()
|
|
|
|
# Build update query
|
|
set_clause = ", ".join(f"{col} = {val}" for col, val in updates.items())
|
|
params.append(entry_id)
|
|
|
|
cursor.execute(f'''
|
|
UPDATE worktime_entries
|
|
SET {set_clause}
|
|
WHERE id = ?
|
|
''', params)
|
|
|
|
if cursor.rowcount > 0:
|
|
# Update daily summary
|
|
self._update_daily_summary(work_date, conn)
|
|
return True
|
|
|
|
return False |