mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-20 05:10:26 -05:00
b4486a627f
- Webhook models: remove duplicate index definitions so db.create_all() no longer raises 'index already exists' (columns already have index=True) - ImportService: fix circular import by late-importing ClientService, ProjectService, TimeTrackingService in __init__ - reports: fix F823 by renaming unpack variable _ to _entry_count to avoid shadowing gettext _ in export_task_excel() - Code quality: add .flake8 with extend-ignore so flake8 CI passes; simplify pyproject.toml isort config (drop unsupported options) - Format: run black and isort on app/ - tests: restore minimal app fixture in test_import_export_models
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
"""
|
||
Summary report PDF export – one-page PDF with today/week/month hours and top projects.
|
||
Uses ReportLab (same as time_entries_pdf).
|
||
"""
|
||
|
||
from io import BytesIO
|
||
|
||
from reportlab.lib import colors
|
||
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||
from reportlab.lib.pagesizes import A4
|
||
from reportlab.lib.styles import ParagraphStyle
|
||
from reportlab.lib.units import cm
|
||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||
|
||
# Colors (aligned with time_entries_pdf)
|
||
BRAND_COLOR = colors.HexColor("#1e3a5f")
|
||
HEADER_BG = colors.HexColor("#1e3a5f")
|
||
HEADER_FG = colors.HexColor("#ffffff")
|
||
ROW_ALT_BG = colors.HexColor("#f0f4f8")
|
||
GRID_LIGHT = colors.HexColor("#dde3ea")
|
||
MUTED_TEXT = colors.HexColor("#64748b")
|
||
|
||
MARGIN = 1.5 * cm
|
||
PAGE_SIZE = A4
|
||
|
||
|
||
def build_summary_report_pdf(today_hours, week_hours, month_hours, project_stats):
|
||
"""
|
||
Build a one-page Summary Report PDF.
|
||
|
||
Args:
|
||
today_hours: float
|
||
week_hours: float
|
||
month_hours: float
|
||
project_stats: list of dicts with keys "project" (object with .name) and "hours" (float)
|
||
|
||
Returns:
|
||
bytes: PDF file content
|
||
"""
|
||
buffer = BytesIO()
|
||
doc = SimpleDocTemplate(
|
||
buffer,
|
||
pagesize=PAGE_SIZE,
|
||
leftMargin=MARGIN,
|
||
rightMargin=MARGIN,
|
||
topMargin=MARGIN,
|
||
bottomMargin=MARGIN,
|
||
)
|
||
elements = []
|
||
|
||
# Title
|
||
title_style = ParagraphStyle(
|
||
"SummaryTitle",
|
||
fontName="Helvetica-Bold",
|
||
fontSize=18,
|
||
leading=22,
|
||
textColor=BRAND_COLOR,
|
||
)
|
||
elements.append(Paragraph("Summary Report", title_style))
|
||
elements.append(Spacer(1, 6))
|
||
|
||
# Stats row: Today | Week | Month
|
||
stats_data = [
|
||
["Today's Hours", "Week's Hours", "Month's Hours"],
|
||
[f"{today_hours:.2f} h", f"{week_hours:.2f} h", f"{month_hours:.2f} h"],
|
||
]
|
||
stats_table = Table(
|
||
stats_data,
|
||
colWidths=[5 * cm, 5 * cm, 5 * cm],
|
||
rowHeights=[0.7 * cm, 0.9 * cm],
|
||
)
|
||
stats_table.setStyle(
|
||
TableStyle(
|
||
[
|
||
("BACKGROUND", (0, 0), (-1, 0), HEADER_BG),
|
||
("TEXTCOLOR", (0, 0), (-1, 0), HEADER_FG),
|
||
("ALIGN", (0, 0), (-1, -1), TA_CENTER),
|
||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||
("FONTNAME", (0, 1), (-1, 1), "Helvetica"),
|
||
("FONTSIZE", (0, 1), (-1, 1), 12),
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("TOPPADDING", (0, 0), (-1, -1), 8),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
||
("GRID", (0, 0), (-1, -1), 0.5, GRID_LIGHT),
|
||
]
|
||
)
|
||
)
|
||
elements.append(stats_table)
|
||
elements.append(Spacer(1, 12))
|
||
|
||
# Top projects table
|
||
sub_title_style = ParagraphStyle(
|
||
"SubTitle",
|
||
fontName="Helvetica-Bold",
|
||
fontSize=12,
|
||
leading=14,
|
||
textColor=BRAND_COLOR,
|
||
)
|
||
elements.append(Paragraph("Top Projects (Last 30 Days)", sub_title_style))
|
||
elements.append(Spacer(1, 6))
|
||
|
||
if project_stats:
|
||
table_data = [["Project", "Total Hours"]]
|
||
for stat in project_stats:
|
||
project_name = stat.get("project")
|
||
name = getattr(project_name, "name", str(project_name)) if project_name else ""
|
||
hours = stat.get("hours", 0)
|
||
table_data.append([name, f"{hours:.2f}"])
|
||
|
||
col_widths = [12 * cm, 4 * cm]
|
||
projects_table = Table(table_data, colWidths=col_widths, repeatRows=1)
|
||
projects_table.setStyle(
|
||
TableStyle(
|
||
[
|
||
("BACKGROUND", (0, 0), (-1, 0), HEADER_BG),
|
||
("TEXTCOLOR", (0, 0), (-1, 0), HEADER_FG),
|
||
("ALIGN", (0, 0), (0, -1), TA_LEFT),
|
||
("ALIGN", (1, 0), (1, -1), "RIGHT"),
|
||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("TOPPADDING", (0, 0), (-1, -1), 6),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
|
||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [ROW_ALT_BG, colors.white]),
|
||
("GRID", (0, 0), (-1, -1), 0.5, GRID_LIGHT),
|
||
]
|
||
)
|
||
)
|
||
elements.append(projects_table)
|
||
else:
|
||
no_data_style = ParagraphStyle(
|
||
"NoData",
|
||
fontName="Helvetica",
|
||
fontSize=10,
|
||
leading=12,
|
||
textColor=MUTED_TEXT,
|
||
)
|
||
elements.append(Paragraph("No project data for the last 30 days.", no_data_style))
|
||
|
||
doc.build(elements)
|
||
return buffer.getvalue()
|