From 119d326eea753bb4139fedf94ae2d93ccaeefdd4 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Nov 2025 20:50:04 +1100 Subject: [PATCH] Expose note in time log dialog and reports --- bouquin/db.py | 6 ++++- bouquin/locales/en.json | 1 + bouquin/time_log.py | 58 +++++++++++++++++++++++++++-------------- tests/test_time_log.py | 38 +++++++++++++-------------- 4 files changed, 64 insertions(+), 39 deletions(-) diff --git a/bouquin/db.py b/bouquin/db.py index 10492d5..c6846e9 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -987,6 +987,7 @@ class DBManager: SELECT {bucket_expr} AS bucket, a.name AS activity_name, + t.note AS note, SUM(t.minutes) AS total_minutes FROM time_log t JOIN activities a ON a.id = t.activity_id @@ -998,7 +999,10 @@ class DBManager: (project_id, start_date_iso, end_date_iso), ).fetchall() - return [(r["bucket"], r["activity_name"], r["total_minutes"]) for r in rows] + return [ + (r["bucket"], r["activity_name"], r["note"], r["total_minutes"]) + for r in rows + ] def close(self) -> None: if self.conn is not None: diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index 3ab2d08..c6334af 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -164,6 +164,7 @@ "toolbar_alarm": "Set reminder alarm", "activities": "Activities", "activity": "Activity", + "note": "Note", "activity_delete_error_message": "A problem occurred deleting the activity", "activity_delete_error_title": "Problem deleting activity", "activity_rename_error_message": "A problem occurred renaming the activity", diff --git a/bouquin/time_log.py b/bouquin/time_log.py index 997fa2c..8dac971 100644 --- a/bouquin/time_log.py +++ b/bouquin/time_log.py @@ -183,7 +183,7 @@ class TimeLogDialog(QDialog): self._current_entry_id: Optional[int] = None self.setWindowTitle(strings._("time_log_for").format(date=date_iso)) - self.resize(600, 400) + self.resize(900, 600) root = QVBoxLayout(self) @@ -211,6 +211,12 @@ class TimeLogDialog(QDialog): act_row.addWidget(self.manage_activities_btn) form.addRow(strings._("activity"), act_row) + # Optional Note + note_row = QHBoxLayout() + self.note = QLineEdit() + note_row.addWidget(self.note, 1) + form.addRow(strings._("note"), note_row) + # Hours (decimal) self.hours_spin = QDoubleSpinBox() self.hours_spin.setRange(0.0, 24.0) @@ -240,18 +246,20 @@ class TimeLogDialog(QDialog): # --- Table of entries for this date self.table = QTableWidget() - self.table.setColumnCount(3) + self.table.setColumnCount(4) self.table.setHorizontalHeaderLabels( [ strings._("project"), strings._("activity"), + strings._("note"), strings._("hours"), ] ) self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( - 2, QHeaderView.ResizeToContents + 3, QHeaderView.ResizeToContents ) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setSelectionMode(QAbstractItemView.SingleSelection) @@ -291,11 +299,13 @@ class TimeLogDialog(QDialog): entry_id = r[0] project_name = r[3] activity_name = r[5] + note = r[7] or "" minutes = r[6] hours = minutes / 60.0 item_proj = QTableWidgetItem(project_name) item_act = QTableWidgetItem(activity_name) + item_note = QTableWidgetItem(note) item_hours = QTableWidgetItem(f"{hours:.2f}") # store the entry id on the first column @@ -303,7 +313,8 @@ class TimeLogDialog(QDialog): self.table.setItem(row_idx, 0, item_proj) self.table.setItem(row_idx, 1, item_act) - self.table.setItem(row_idx, 2, item_hours) + self.table.setItem(row_idx, 2, item_note) + self.table.setItem(row_idx, 3, item_hours) self._current_entry_id = None self.delete_btn.setEnabled(False) @@ -338,6 +349,10 @@ class TimeLogDialog(QDialog): ) return + note = self.note.text().strip() + if not note: + note = None + hours = float(self.hours_spin.value()) minutes = int(round(hours * 60)) @@ -346,16 +361,14 @@ class TimeLogDialog(QDialog): if self._current_entry_id is None: # New entry - self._db.add_time_log(self._date_iso, proj_id, activity_id, minutes) + self._db.add_time_log(self._date_iso, proj_id, activity_id, minutes, note) else: # Update existing self._db.update_time_log( - self._current_entry_id, - proj_id, - activity_id, - minutes, + self._current_entry_id, proj_id, activity_id, minutes, note ) + self.note.setText("") self._reload_entries() def _on_row_selected(self) -> None: @@ -369,7 +382,8 @@ class TimeLogDialog(QDialog): row = items[0].row() proj_item = self.table.item(row, 0) act_item = self.table.item(row, 1) - hours_item = self.table.item(row, 2) + note_item = self.table.item(row, 2) + hours_item = self.table.item(row, 3) entry_id = proj_item.data(Qt.ItemDataRole.UserRole) self._current_entry_id = int(entry_id) @@ -379,6 +393,7 @@ class TimeLogDialog(QDialog): # push values into the editors proj_name = proj_item.text() act_name = act_item.text() + note = note_item.text() hours = float(hours_item.text()) # Set project combo by name @@ -387,6 +402,7 @@ class TimeLogDialog(QDialog): self.project_combo.setCurrentIndex(idx) self.activity_edit.setText(act_name) + self.note.setText(note) self.hours_spin.setValue(hours) def _on_delete_entry(self) -> None: @@ -787,18 +803,20 @@ class TimeReportDialog(QDialog): # Table self.table = QTableWidget() - self.table.setColumnCount(3) + self.table.setColumnCount(4) self.table.setHorizontalHeaderLabels( [ strings._("time_period"), strings._("activity"), + strings._("note"), strings._("hours"), ] ) self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode( - 2, QHeaderView.ResizeToContents + 3, QHeaderView.ResizeToContents ) root.addWidget(self.table, 1) @@ -833,14 +851,15 @@ class TimeReportDialog(QDialog): rows = self._db.time_report(proj_id, start, end, gran) self._last_rows = rows - self._last_total_minutes = sum(r[2] for r in rows) + self._last_total_minutes = sum(r[3] for r in rows) self.table.setRowCount(len(rows)) - for i, (time_period, activity_name, minutes) in enumerate(rows): + for i, (time_period, activity_name, note, minutes) in enumerate(rows): hrs = minutes / 60.0 self.table.setItem(i, 0, QTableWidgetItem(time_period)) self.table.setItem(i, 1, QTableWidgetItem(activity_name)) - self.table.setItem(i, 2, QTableWidgetItem(f"{hrs:.2f}")) + self.table.setItem(i, 2, QTableWidgetItem(note)) + self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}")) total_hours = self._last_total_minutes / 60.0 self.total_label.setText( @@ -874,14 +893,15 @@ class TimeReportDialog(QDialog): [ strings._("time_period"), strings._("activity"), + strings._("note"), strings._("hours"), ] ) # Data rows - for time_period, activity_name, minutes in self._last_rows: + for time_period, activity_name, note, minutes in self._last_rows: hours = minutes / 60.0 - writer.writerow([time_period, activity_name, f"{hours:.2f}"]) + writer.writerow([time_period, activity_name, note, f"{hours:.2f}"]) # Blank line + total total_hours = self._last_total_minutes / 60.0 @@ -914,7 +934,7 @@ class TimeReportDialog(QDialog): # ---------- Build chart image (hours per period) ---------- per_period_minutes: dict[str, int] = defaultdict(int) - for period, _activity, minutes in self._last_rows: + for period, _activity, note, minutes in self._last_rows: per_period_minutes[period] += minutes periods = sorted(per_period_minutes.keys()) @@ -1015,7 +1035,7 @@ class TimeReportDialog(QDialog): # Table rows (period, activity, hours) row_html_parts: list[str] = [] - for period, activity, minutes in self._last_rows: + for period, activity, note, minutes in self._last_rows: hours = minutes / 60.0 row_html_parts.append( "" diff --git a/tests/test_time_log.py b/tests/test_time_log.py index 6cca759..28a25c9 100644 --- a/tests/test_time_log.py +++ b/tests/test_time_log.py @@ -370,10 +370,10 @@ def test_time_report_by_day(fresh_db): yesterday_act1 = next( r for r in report if r[0] == _yesterday() and r[1] == "Activity 1" ) - assert yesterday_act1[2] == 60 + assert yesterday_act1[3] == 60 today_act1 = next(r for r in report if r[0] == _today() and r[1] == "Activity 1") - assert today_act1[2] == 90 + assert today_act1[3] == 90 def test_time_report_by_week(fresh_db): @@ -396,9 +396,9 @@ def test_time_report_by_week(fresh_db): assert len(report) == 2 # First week total - assert report[0][2] == 90 # 60 + 30 + assert report[0][3] == 90 # 60 + 30 # Second week total - assert report[1][2] == 45 + assert report[1][3] == 45 def test_time_report_by_month(fresh_db): @@ -422,11 +422,11 @@ def test_time_report_by_month(fresh_db): # January total jan_row = next(r for r in report if r[0] == "2024-01") - assert jan_row[2] == 90 # 60 + 30 + assert jan_row[3] == 90 # 60 + 30 # February total feb_row = next(r for r in report if r[0] == "2024-02") - assert feb_row[2] == 45 + assert feb_row[3] == 45 def test_time_report_multiple_activities(fresh_db): @@ -443,10 +443,10 @@ def test_time_report_multiple_activities(fresh_db): assert len(report) == 2 act1_row = next(r for r in report if r[1] == "Activity 1") - assert act1_row[2] == 90 # 60 + 30 aggregated + assert act1_row[3] == 90 # 60 + 30 aggregated act2_row = next(r for r in report if r[1] == "Activity 2") - assert act2_row[2] == 45 + assert act2_row[3] == 45 def test_time_report_filters_by_project(fresh_db): @@ -461,7 +461,7 @@ def test_time_report_filters_by_project(fresh_db): report = fresh_db.time_report(proj1_id, _today(), _today(), "day") assert len(report) == 1 - assert report[0][2] == 60 + assert report[0][3] == 60 def test_time_report_empty(fresh_db): @@ -658,8 +658,8 @@ def test_time_log_dialog_loads_existing_entries(qtbot, fresh_db): assert "Project" in dialog.table.item(0, 0).text() assert "Activity" in dialog.table.item(0, 1).text() assert ( - "1.5" in dialog.table.item(0, 2).text() - or "1.50" in dialog.table.item(0, 2).text() + "1.5" in dialog.table.item(0, 3).text() + or "1.50" in dialog.table.item(0, 3).text() ) @@ -725,8 +725,8 @@ def test_time_log_dialog_add_entry_success(qtbot, fresh_db): assert dialog.table.rowCount() == 1 assert "New Activity" in dialog.table.item(0, 1).text() assert ( - "2.5" in dialog.table.item(0, 2).text() - or "2.50" in dialog.table.item(0, 2).text() + "2.5" in dialog.table.item(0, 3).text() + or "2.50" in dialog.table.item(0, 3).text() ) @@ -774,8 +774,8 @@ def test_time_log_dialog_update_entry(qtbot, fresh_db): assert dialog.table.rowCount() == 1 assert "Project 2" in dialog.table.item(0, 0).text() assert ( - "2.0" in dialog.table.item(0, 2).text() - or "2.00" in dialog.table.item(0, 2).text() + "2.0" in dialog.table.item(0, 3).text() + or "2.00" in dialog.table.item(0, 3).text() ) @@ -1429,7 +1429,7 @@ def test_time_report_dialog_granularity_week(qtbot, fresh_db): # Should aggregate to single week assert dialog.table.rowCount() == 1 - hours_text = dialog.table.item(0, 2).text() + hours_text = dialog.table.item(0, 3).text() assert "2.5" in hours_text or "2.50" in hours_text @@ -1457,7 +1457,7 @@ def test_time_report_dialog_granularity_month(qtbot, fresh_db): # Should aggregate to single month assert dialog.table.rowCount() == 1 - hours_text = dialog.table.item(0, 2).text() + hours_text = dialog.table.item(0, 3).text() assert "2.5" in hours_text or "2.50" in hours_text @@ -2196,8 +2196,8 @@ def test_full_workflow_add_project_activity_log_report( assert report_dialog.table.rowCount() == 1 assert "Test Activity" in report_dialog.table.item(0, 1).text() assert ( - "2.5" in report_dialog.table.item(0, 2).text() - or "2.50" in report_dialog.table.item(0, 2).text() + "2.5" in report_dialog.table.item(0, 3).text() + or "2.50" in report_dialog.table.item(0, 3).text() ) # 5. Export CSV