Time Log Report fixes
* Time reports: Fix report 'group by' logic to not show ambiguous 'note' data. * Time reports: Add default option to 'don't group'. This gives every individual time log row (and so the 'note' is shown in this case)
This commit is contained in:
parent
2464147a59
commit
778d988ebd
5 changed files with 225 additions and 35 deletions
|
|
@ -1108,8 +1108,8 @@ class DBManager:
|
|||
project_id: int,
|
||||
start_date_iso: str,
|
||||
end_date_iso: str,
|
||||
granularity: str = "day", # 'day' | 'week' | 'month'
|
||||
) -> list[tuple[str, str, int]]:
|
||||
granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
|
||||
) -> list[tuple[str, str, str, int]]:
|
||||
"""
|
||||
Return (time_period, activity_name, total_minutes) tuples between start and end
|
||||
for a project, grouped by period and activity.
|
||||
|
|
@ -1117,7 +1117,33 @@ class DBManager:
|
|||
- 'YYYY-MM-DD' for day
|
||||
- 'YYYY-WW' for week
|
||||
- 'YYYY-MM' for month
|
||||
For 'none' granularity, each individual time log entry becomes a row.
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
|
||||
if granularity == "none":
|
||||
# No grouping: one row per entry
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
t.page_date AS period,
|
||||
a.name AS activity_name,
|
||||
t.note AS note,
|
||||
t.minutes AS total_minutes
|
||||
FROM time_log t
|
||||
JOIN activities a ON a.id = t.activity_id
|
||||
WHERE t.project_id = ?
|
||||
AND t.page_date BETWEEN ? AND ?
|
||||
ORDER BY period, LOWER(a.name), t.id;
|
||||
""",
|
||||
(project_id, start_date_iso, end_date_iso),
|
||||
).fetchall()
|
||||
|
||||
return [
|
||||
(r["period"], r["activity_name"], r["note"], r["total_minutes"])
|
||||
for r in rows
|
||||
]
|
||||
|
||||
if granularity == "day":
|
||||
bucket_expr = "page_date"
|
||||
elif granularity == "week":
|
||||
|
|
@ -1126,13 +1152,11 @@ class DBManager:
|
|||
else: # month
|
||||
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
||||
|
||||
cur = self.conn.cursor()
|
||||
rows = cur.execute(
|
||||
f"""
|
||||
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
|
||||
|
|
@ -1144,21 +1168,50 @@ class DBManager:
|
|||
(project_id, start_date_iso, end_date_iso),
|
||||
).fetchall()
|
||||
|
||||
return [
|
||||
(r["bucket"], r["activity_name"], r["note"], r["total_minutes"])
|
||||
for r in rows
|
||||
]
|
||||
return [(r["bucket"], r["activity_name"], "", r["total_minutes"]) for r in rows]
|
||||
|
||||
def time_report_all(
|
||||
self,
|
||||
start_date_iso: str,
|
||||
end_date_iso: str,
|
||||
granularity: str = "day", # 'day' | 'week' | 'month'
|
||||
granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
|
||||
) -> list[tuple[str, str, str, str, int]]:
|
||||
"""
|
||||
Return (project_name, time_period, activity_name, note, total_minutes)
|
||||
across *all* projects between start and end, grouped by project + period + activity.
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
|
||||
if granularity == "none":
|
||||
# No grouping – one row per time_log record
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
p.name AS project_name,
|
||||
t.page_date AS period,
|
||||
a.name AS activity_name,
|
||||
t.note AS note,
|
||||
t.minutes AS total_minutes
|
||||
FROM time_log t
|
||||
JOIN projects p ON p.id = t.project_id
|
||||
JOIN activities a ON a.id = t.activity_id
|
||||
WHERE t.page_date BETWEEN ? AND ?
|
||||
ORDER BY LOWER(p.name), period, LOWER(activity_name), t.id;
|
||||
""",
|
||||
(start_date_iso, end_date_iso),
|
||||
).fetchall()
|
||||
|
||||
return [
|
||||
(
|
||||
r["project_name"],
|
||||
r["period"],
|
||||
r["activity_name"],
|
||||
r["note"],
|
||||
r["total_minutes"],
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
if granularity == "day":
|
||||
bucket_expr = "page_date"
|
||||
elif granularity == "week":
|
||||
|
|
@ -1166,14 +1219,12 @@ class DBManager:
|
|||
else: # month
|
||||
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
||||
|
||||
cur = self.conn.cursor()
|
||||
rows = cur.execute(
|
||||
f"""
|
||||
SELECT
|
||||
p.name AS project_name,
|
||||
{bucket_expr} AS bucket,
|
||||
a.name AS activity_name,
|
||||
t.note AS note,
|
||||
SUM(t.minutes) AS total_minutes
|
||||
FROM time_log t
|
||||
JOIN projects p ON p.id = t.project_id
|
||||
|
|
@ -1190,7 +1241,7 @@ class DBManager:
|
|||
r["project_name"],
|
||||
r["bucket"],
|
||||
r["activity_name"],
|
||||
r["note"],
|
||||
"",
|
||||
r["total_minutes"],
|
||||
)
|
||||
for r in rows
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@
|
|||
"add_project": "Add project",
|
||||
"add_time_entry": "Add time entry",
|
||||
"time_period": "Time period",
|
||||
"dont_group": "Don't group",
|
||||
"by_day": "by day",
|
||||
"by_month": "by month",
|
||||
"by_week": "by week",
|
||||
|
|
|
|||
|
|
@ -986,7 +986,7 @@ class TimeReportDialog(QDialog):
|
|||
self._db = db
|
||||
|
||||
# state for last run
|
||||
self._last_rows: list[tuple[str, str, int]] = []
|
||||
self._last_rows: list[tuple[str, str, str, str, int]] = []
|
||||
self._last_total_minutes: int = 0
|
||||
self._last_project_name: str = ""
|
||||
self._last_start: str = ""
|
||||
|
|
@ -1038,6 +1038,7 @@ class TimeReportDialog(QDialog):
|
|||
|
||||
# Granularity
|
||||
self.granularity = QComboBox()
|
||||
self.granularity.addItem(strings._("dont_group"), "none")
|
||||
self.granularity.addItem(strings._("by_day"), "day")
|
||||
self.granularity.addItem(strings._("by_week"), "week")
|
||||
self.granularity.addItem(strings._("by_month"), "month")
|
||||
|
|
@ -1095,6 +1096,43 @@ class TimeReportDialog(QDialog):
|
|||
close_row.addWidget(close_btn)
|
||||
root.addLayout(close_row)
|
||||
|
||||
def _configure_table_columns(self, granularity: str) -> None:
|
||||
if granularity == "none":
|
||||
# Show notes
|
||||
self.table.setColumnCount(5)
|
||||
self.table.setHorizontalHeaderLabels(
|
||||
[
|
||||
strings._("project"),
|
||||
strings._("time_period"),
|
||||
strings._("activity"),
|
||||
strings._("note"),
|
||||
strings._("hours"),
|
||||
]
|
||||
)
|
||||
# project, period, activity, note stretch; hours shrink
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
||||
else:
|
||||
# Grouped: no note column
|
||||
self.table.setColumnCount(4)
|
||||
self.table.setHorizontalHeaderLabels(
|
||||
[
|
||||
strings._("project"),
|
||||
strings._("time_period"),
|
||||
strings._("activity"),
|
||||
strings._("hours"),
|
||||
]
|
||||
)
|
||||
header = self.table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
|
||||
def _on_range_preset_changed(self, index: int) -> None:
|
||||
preset = self.range_preset.currentData()
|
||||
today = QDate.currentDate()
|
||||
|
|
@ -1140,6 +1178,9 @@ class TimeReportDialog(QDialog):
|
|||
self._last_start = start
|
||||
self._last_end = end
|
||||
self._last_gran_label = self.granularity.currentText()
|
||||
self._last_gran = gran # remember which grouping was used
|
||||
|
||||
self._configure_table_columns(gran)
|
||||
|
||||
rows_for_table: list[tuple[str, str, str, str, int]] = []
|
||||
|
||||
|
|
@ -1179,8 +1220,13 @@ class TimeReportDialog(QDialog):
|
|||
self.table.setItem(i, 0, QTableWidgetItem(project))
|
||||
self.table.setItem(i, 1, QTableWidgetItem(time_period))
|
||||
self.table.setItem(i, 2, QTableWidgetItem(activity_name))
|
||||
self.table.setItem(i, 3, QTableWidgetItem(note))
|
||||
self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
|
||||
|
||||
if self._last_gran == "none":
|
||||
self.table.setItem(i, 3, QTableWidgetItem(note or ""))
|
||||
self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
|
||||
else:
|
||||
# no note column
|
||||
self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
|
||||
|
||||
# Summary label – include per-project totals when in "all projects" mode
|
||||
total_hours = self._last_total_minutes / 60.0
|
||||
|
|
@ -1224,16 +1270,18 @@ class TimeReportDialog(QDialog):
|
|||
with open(filename, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
show_note = getattr(self, "_last_gran", "day") == "none"
|
||||
|
||||
# Header
|
||||
writer.writerow(
|
||||
[
|
||||
strings._("project"),
|
||||
strings._("time_period"),
|
||||
strings._("activity"),
|
||||
strings._("note"),
|
||||
strings._("hours"),
|
||||
]
|
||||
)
|
||||
header = [
|
||||
strings._("project"),
|
||||
strings._("time_period"),
|
||||
strings._("activity"),
|
||||
]
|
||||
if show_note:
|
||||
header.append(strings._("note"))
|
||||
header.append(strings._("hours"))
|
||||
writer.writerow(header)
|
||||
|
||||
# Data rows
|
||||
for (
|
||||
|
|
@ -1244,9 +1292,11 @@ class TimeReportDialog(QDialog):
|
|||
minutes,
|
||||
) in self._last_rows:
|
||||
hours = minutes / 60.0
|
||||
writer.writerow(
|
||||
[project, time_period, activity_name, note, f"{hours:.2f}"]
|
||||
)
|
||||
row = [project, time_period, activity_name]
|
||||
if show_note:
|
||||
row.append(note)
|
||||
row.append(f"{hours:.2f}")
|
||||
writer.writerow(row)
|
||||
|
||||
# Blank line + total
|
||||
total_hours = self._last_total_minutes / 60.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue