diff --git a/apps/reports/services/aggregation.py b/apps/reports/services/aggregation.py index a92ceba..187c72d 100644 --- a/apps/reports/services/aggregation.py +++ b/apps/reports/services/aggregation.py @@ -418,7 +418,11 @@ def _scope_payload(filters: ReportFilters) -> dict: } return { - "workspace": {"id": str(filters.workspace.id), "name": filters.workspace.name}, + "workspace": { + "id": str(filters.workspace.id), + "name": filters.workspace.name, + "thumbnail_path": filters.workspace.thumbnail.path if getattr(filters.workspace, "thumbnail", None) else None, + }, "period": filters.period, "from_date": filters.from_date.isoformat(), "to_date": filters.to_date.isoformat(), diff --git a/apps/reports/services/exporters.py b/apps/reports/services/exporters.py index a4915e9..2f0c6b7 100644 --- a/apps/reports/services/exporters.py +++ b/apps/reports/services/exporters.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +import os from datetime import datetime from openpyxl import Workbook @@ -12,6 +13,7 @@ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import mm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import Image as RLImage from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle from apps.reports.services.export_i18n import ExportLocale, safe_sheet_title, user_label @@ -229,6 +231,11 @@ def _paragraph(text: str, style: ParagraphStyle, locale: ExportLocale) -> Paragr return Paragraph(locale.shape(text), style) +def _workspace_initial(name: str) -> str: + stripped = (name or "").strip() + return stripped[0].upper() if stripped else "W" + + def _styled_table(data: list[list[str]], *, locale: ExportLocale, column_widths: list[float]) -> Table: shaped_data = [ [locale.shape(cell) if cell is not None else "" for cell in row] @@ -318,10 +325,35 @@ def build_pdf_report(*, report_data: dict, locale: ExportLocale) -> bytes: scope = report_data["scope"] summary = report_data["summary"] - story = [ - _paragraph(f"{locale.t('report_title')} - {scope['workspace']['name']}", title_style, locale), - Spacer(1, 6 * mm), - ] + workspace_name = scope["workspace"]["name"] + workspace_thumbnail_path = scope["workspace"].get("thumbnail_path") + title_text = f"{locale.t('report_title')} - {workspace_name}" + + title_cell = _paragraph(title_text, title_style, locale) + badge_cell = _paragraph(_workspace_initial(workspace_name), title_style, locale) + if workspace_thumbnail_path and os.path.exists(workspace_thumbnail_path): + try: + badge_cell = RLImage(workspace_thumbnail_path, width=14 * mm, height=14 * mm) + except Exception: # noqa: BLE001 + badge_cell = _paragraph(_workspace_initial(workspace_name), title_style, locale) + + header_row = [badge_cell, title_cell] if locale.is_rtl else [title_cell, badge_cell] + header_col_widths = [doc.width * 0.08, doc.width * 0.92] if locale.is_rtl else [doc.width * 0.92, doc.width * 0.08] + header_table = Table([header_row], colWidths=header_col_widths) + header_table.setStyle( + TableStyle( + [ + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("ALIGN", (0, 0), (-1, -1), "RIGHT" if locale.is_rtl else "LEFT"), + ("LEFTPADDING", (0, 0), (-1, -1), 0), + ("RIGHTPADDING", (0, 0), (-1, -1), 0), + ("TOPPADDING", (0, 0), (-1, -1), 0), + ("BOTTOMPADDING", (0, 0), (-1, -1), 0), + ] + ) + ) + + story = [header_table, Spacer(1, 6 * mm)] meta_rows = [ [locale.t("workspace"), scope["workspace"]["name"]],