Expose note in time log dialog and reports
This commit is contained in:
parent
f41ec9a5a9
commit
119d326eea
4 changed files with 64 additions and 39 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"<tr>"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue