NEtraAi / backend /app /services /reports.py
093xpku
Clean project layout deployment
9bc686b
Raw
History Blame Contribute Delete
6.61 kB
import io
from typing import List, Dict, Any
import pandas as pd
from datetime import datetime, date
# ReportLab imports for PDF generation
from reportlab.lib.pagesizes import letter
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
class ReportGenerator:
@staticmethod
def to_csv(data: List[Dict[str, Any]]) -> str:
"""
Converts list of dicts to CSV string.
"""
if not data:
return ""
df = pd.DataFrame(data)
return df.to_csv(index=False)
@staticmethod
def to_xlsx(data: List[Dict[str, Any]], sheet_name: str = "Report") -> bytes:
"""
Converts list of dicts to Excel binary stream.
"""
output = io.BytesIO()
if not data:
# Create an empty excel file
df = pd.DataFrame([{"Message": "No data available"}])
else:
df = pd.DataFrame(data)
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name=sheet_name)
# Format sheet with openpyxl
workbook = writer.book
worksheet = writer.sheets[sheet_name]
# Auto-fit columns
for col in worksheet.columns:
max_len = max(len(str(cell.value or '')) for cell in col)
col_letter = col[0].column_letter
worksheet.column_dimensions[col_letter].width = max(max_len + 3, 10)
output.seek(0)
return output.getvalue()
@staticmethod
def to_pdf(title: str, headers: List[str], rows: List[List[Any]], metadata: Dict[str, str] = None) -> bytes:
"""
Generates a premium, clean PDF report using ReportLab.
"""
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=letter,
rightMargin=36,
leftMargin=36,
topMargin=36,
bottomMargin=36
)
styles = getSampleStyleSheet()
# Custom styles
title_style = ParagraphStyle(
name='ReportTitle',
parent=styles['Heading1'],
fontName='Helvetica-Bold',
fontSize=22,
leading=26,
textColor=colors.HexColor('#0F172A'), # Slate 900
spaceAfter=15
)
meta_label_style = ParagraphStyle(
name='MetaLabel',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=10,
leading=14,
textColor=colors.HexColor('#475569') # Slate 600
)
meta_val_style = ParagraphStyle(
name='MetaVal',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=10,
leading=14,
textColor=colors.HexColor('#0F172A')
)
table_header_style = ParagraphStyle(
name='TableHeader',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=9,
leading=12,
textColor=colors.white
)
table_cell_style = ParagraphStyle(
name='TableCell',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=9,
leading=12,
textColor=colors.HexColor('#334155') # Slate 700
)
elements = []
# Add Title
elements.append(Paragraph(title, title_style))
elements.append(Spacer(1, 10))
# Add Metadata Block
if metadata:
meta_data = []
keys = list(metadata.keys())
for i in range(0, len(keys), 2):
row = []
# First col
k1 = keys[i]
row.extend([Paragraph(k1, meta_label_style), Paragraph(metadata[k1], meta_val_style)])
# Second col
if i + 1 < len(keys):
k2 = keys[i+1]
row.extend([Paragraph(k2, meta_label_style), Paragraph(metadata[k2], meta_val_style)])
else:
row.extend([Paragraph("", meta_label_style), Paragraph("", meta_val_style)])
meta_data.append(row)
meta_table = Table(meta_data, colWidths=[100, 170, 100, 170])
meta_table.setStyle(TableStyle([
('ALIGN', (0,0), (-1,-1), 'LEFT'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('TOPPADDING', (0,0), (-1,-1), 4),
]))
elements.append(meta_table)
elements.append(Spacer(1, 20))
# Draw a line
line_table = Table([[""]], colWidths=[540], rowHeights=[1])
line_table.setStyle(TableStyle([
('LINEABOVE', (0,0), (-1,-1), 1, colors.HexColor('#E2E8F0')),
]))
elements.append(line_table)
elements.append(Spacer(1, 15))
# Prepare Data Table
table_data = []
# Header row
table_data.append([Paragraph(h, table_header_style) for h in headers])
# Data rows
for r in rows:
table_data.append([Paragraph(str(cell), table_cell_style) for cell in r])
# Calculate widths dynamically
col_width = 540 / len(headers)
data_table = Table(table_data, colWidths=[col_width] * len(headers))
# Table Styling
grid_style = TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0F172A')), # Dark Header
('ALIGN', (0,0), (-1,-1), 'LEFT'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 6),
('LEFTPADDING', (0,0), (-1,-1), 8),
('RIGHTPADDING', (0,0), (-1,-1), 8),
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#F1F5F9')),
])
# Alternating row colors
for i in range(1, len(table_data)):
if i % 2 == 0:
grid_style.add('BACKGROUND', (0, i), (-1, i), colors.HexColor('#F8FAFC'))
data_table.setStyle(grid_style)
elements.append(data_table)
# Build Document
doc.build(elements)
buffer.seek(0)
return buffer.getvalue()