Implement Dashboard consolidation + Performance logging
Features: - Add unified "Dashboard Complet" sheet (Excel) with all 9 sections - Add unified "Dashboard Complet" page (PDF) with key metrics - Fix VALOARE_ANTERIOARA NULL bug (use sumar_executiv_yoy directly) - Add PerformanceLogger class for timing analysis - Remove redundant consolidated sheets (keep only Dashboard Complet) Bug fixes: - Fix Excel formula error (=== interpreted as formula, changed to >>>) - Fix args.output → args.output_dir in perf.summary() Performance analysis: - Add PERFORMANCE_ANALYSIS.md with detailed breakdown - SQL queries take 94% of runtime (31 min), Excel/PDF only 1% - Identified slow queries for optimization Documentation: - Update CLAUDE.md with new structure - Add context handover for query optimization task 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -262,6 +262,156 @@ class ExcelReportGenerator:
|
||||
|
||||
ws.freeze_panes = ws.cell(row=5, column=1)
|
||||
|
||||
def add_consolidated_sheet(self, name: str, sections: list, sheet_title: str = None,
|
||||
sheet_description: str = None):
|
||||
"""
|
||||
Add a consolidated sheet with multiple sections separated visually.
|
||||
|
||||
Args:
|
||||
name: Sheet name (max 31 chars)
|
||||
sections: List of dicts with keys:
|
||||
- 'title': Section title (str)
|
||||
- 'df': DataFrame with data
|
||||
- 'description': Optional section description (str)
|
||||
- 'legend': Optional dict with column explanations
|
||||
sheet_title: Overall sheet title
|
||||
sheet_description: Overall sheet description
|
||||
"""
|
||||
sheet_name = name[:31]
|
||||
ws = self.wb.create_sheet(title=sheet_name)
|
||||
|
||||
start_row = 1
|
||||
|
||||
# Add overall sheet title
|
||||
if sheet_title:
|
||||
ws.cell(row=start_row, column=1, value=sheet_title)
|
||||
ws.cell(row=start_row, column=1).font = Font(bold=True, size=16)
|
||||
start_row += 1
|
||||
|
||||
# Add overall description
|
||||
if sheet_description:
|
||||
ws.cell(row=start_row, column=1, value=sheet_description)
|
||||
ws.cell(row=start_row, column=1).font = Font(italic=True, size=10, color='666666')
|
||||
start_row += 1
|
||||
|
||||
# Add timestamp
|
||||
ws.cell(row=start_row, column=1, value=f"Generat: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
ws.cell(row=start_row, column=1).font = Font(size=9, color='999999')
|
||||
start_row += 2
|
||||
|
||||
# Process each section
|
||||
for section in sections:
|
||||
section_title = section.get('title', '')
|
||||
df = section.get('df')
|
||||
description = section.get('description', '')
|
||||
legend = section.get('legend', {})
|
||||
|
||||
# Section separator
|
||||
separator_fill = PatternFill(start_color='2C3E50', end_color='2C3E50', fill_type='solid')
|
||||
for col in range(1, 10): # Wide separator
|
||||
# Use >>> instead of === to avoid Excel formula interpretation
|
||||
cell = ws.cell(row=start_row, column=col, value='' if col > 1 else f'>>> {section_title}')
|
||||
cell.fill = separator_fill
|
||||
cell.font = Font(bold=True, color='FFFFFF', size=11)
|
||||
start_row += 1
|
||||
|
||||
# Section description
|
||||
if description:
|
||||
ws.cell(row=start_row, column=1, value=description)
|
||||
ws.cell(row=start_row, column=1).font = Font(italic=True, size=9, color='666666')
|
||||
start_row += 1
|
||||
|
||||
start_row += 1
|
||||
|
||||
# Check for empty data
|
||||
if df is None or df.empty:
|
||||
ws.cell(row=start_row, column=1, value="Nu există date pentru această secțiune.")
|
||||
ws.cell(row=start_row, column=1).font = Font(italic=True, color='999999')
|
||||
start_row += 3
|
||||
continue
|
||||
|
||||
# Write headers
|
||||
for col_idx, col_name in enumerate(df.columns, 1):
|
||||
cell = ws.cell(row=start_row, column=col_idx, value=col_name)
|
||||
cell.font = self.header_font
|
||||
cell.fill = self.header_fill
|
||||
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
cell.border = self.border
|
||||
|
||||
# Write data
|
||||
for row_idx, row in enumerate(df.itertuples(index=False), start_row + 1):
|
||||
for col_idx, value in enumerate(row, 1):
|
||||
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
||||
cell.border = self.border
|
||||
|
||||
# Format numbers
|
||||
if isinstance(value, (int, float)):
|
||||
cell.number_format = '#,##0.00' if isinstance(value, float) else '#,##0'
|
||||
cell.alignment = Alignment(horizontal='right')
|
||||
|
||||
# Highlight based on column name
|
||||
col_name = df.columns[col_idx - 1].lower()
|
||||
|
||||
# Status coloring
|
||||
if col_name == 'status' or col_name == 'acoperire':
|
||||
if isinstance(value, str):
|
||||
if value == 'OK':
|
||||
cell.fill = self.good_fill
|
||||
elif value in ('ATENTIE', 'NECESAR'):
|
||||
cell.fill = self.warning_fill
|
||||
elif value in ('ALERTA', 'DEFICIT', 'RISC MARE'):
|
||||
cell.fill = self.alert_fill
|
||||
|
||||
# Trend coloring
|
||||
if col_name == 'trend':
|
||||
if isinstance(value, str):
|
||||
if value in ('CRESTERE', 'IMBUNATATIRE', 'DIVERSIFICARE'):
|
||||
cell.fill = self.good_fill
|
||||
elif value in ('SCADERE', 'DETERIORARE', 'CONCENTRARE', 'PIERDUT'):
|
||||
cell.fill = self.alert_fill
|
||||
elif value == 'ATENTIE':
|
||||
cell.fill = self.warning_fill
|
||||
|
||||
# Variatie coloring
|
||||
if 'variatie' in col_name:
|
||||
if isinstance(value, (int, float)):
|
||||
if value > 0:
|
||||
cell.fill = self.good_fill
|
||||
elif value < 0:
|
||||
cell.fill = self.alert_fill
|
||||
|
||||
# Margin coloring
|
||||
if 'procent' in col_name or 'marja' in col_name:
|
||||
if isinstance(value, (int, float)):
|
||||
if value < 10:
|
||||
cell.fill = self.alert_fill
|
||||
elif value < 15:
|
||||
cell.fill = self.warning_fill
|
||||
elif value > 25:
|
||||
cell.fill = self.good_fill
|
||||
|
||||
start_row = start_row + len(df) + 2
|
||||
|
||||
# Add legend for this section
|
||||
if legend:
|
||||
ws.cell(row=start_row, column=1, value="Legendă:")
|
||||
ws.cell(row=start_row, column=1).font = Font(bold=True, size=8, color='336699')
|
||||
start_row += 1
|
||||
for col_name, explanation in legend.items():
|
||||
ws.cell(row=start_row, column=1, value=f"• {col_name}: {explanation}")
|
||||
ws.cell(row=start_row, column=1).font = Font(size=8, color='666666')
|
||||
start_row += 1
|
||||
|
||||
# Space between sections
|
||||
start_row += 2
|
||||
|
||||
# Auto-adjust column widths
|
||||
for col_idx in range(1, 12):
|
||||
ws.column_dimensions[get_column_letter(col_idx)].width = 18
|
||||
|
||||
# Freeze title row
|
||||
ws.freeze_panes = ws.cell(row=5, column=1)
|
||||
|
||||
def save(self):
|
||||
"""Save the workbook"""
|
||||
self.wb.save(self.output_path)
|
||||
@@ -497,6 +647,108 @@ class PDFReportGenerator:
|
||||
"""Add page break"""
|
||||
self.elements.append(PageBreak())
|
||||
|
||||
def add_consolidated_page(self, page_title: str, sections: list):
|
||||
"""
|
||||
Add a consolidated PDF page with multiple sections.
|
||||
|
||||
Args:
|
||||
page_title: Main title for the page
|
||||
sections: List of dicts with keys:
|
||||
- 'title': Section title (str)
|
||||
- 'df': DataFrame with data
|
||||
- 'columns': List of columns to display (optional)
|
||||
- 'max_rows': Max rows to display (default 15)
|
||||
"""
|
||||
# Page title
|
||||
self.elements.append(Paragraph(page_title, self.styles['SectionHeader']))
|
||||
self.elements.append(Spacer(1, 0.3*cm))
|
||||
|
||||
for section in sections:
|
||||
section_title = section.get('title', '')
|
||||
df = section.get('df')
|
||||
columns = section.get('columns')
|
||||
max_rows = section.get('max_rows', 15)
|
||||
|
||||
# Sub-section title
|
||||
subsection_style = ParagraphStyle(
|
||||
name='SubSection',
|
||||
parent=self.styles['Heading2'],
|
||||
fontSize=11,
|
||||
spaceBefore=10,
|
||||
spaceAfter=5,
|
||||
textColor=colors.HexColor('#2C3E50')
|
||||
)
|
||||
self.elements.append(Paragraph(section_title, subsection_style))
|
||||
|
||||
if df is None or df.empty:
|
||||
self.elements.append(Paragraph("Nu există date.", self.styles['Normal']))
|
||||
self.elements.append(Spacer(1, 0.3*cm))
|
||||
continue
|
||||
|
||||
# Select columns
|
||||
if columns:
|
||||
cols = [c for c in columns if c in df.columns]
|
||||
else:
|
||||
cols = list(df.columns)[:6] # Max 6 columns
|
||||
|
||||
if not cols:
|
||||
continue
|
||||
|
||||
# Prepare data
|
||||
data = [cols]
|
||||
for _, row in df.head(max_rows).iterrows():
|
||||
row_data = []
|
||||
for col in cols:
|
||||
val = row.get(col, '')
|
||||
if isinstance(val, float):
|
||||
row_data.append(f"{val:,.2f}")
|
||||
elif isinstance(val, int):
|
||||
row_data.append(f"{val:,}")
|
||||
else:
|
||||
row_data.append(str(val)[:30]) # Truncate long strings
|
||||
data.append(row_data)
|
||||
|
||||
# Calculate column widths
|
||||
n_cols = len(cols)
|
||||
col_width = 16*cm / n_cols
|
||||
|
||||
table = Table(data, colWidths=[col_width] * n_cols)
|
||||
|
||||
# Build style with conditional row colors for status
|
||||
table_style = [
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 7),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')])
|
||||
]
|
||||
|
||||
# Color status cells if STATUS column exists
|
||||
if 'STATUS' in cols:
|
||||
status_col_idx = cols.index('STATUS')
|
||||
for row_idx, row in enumerate(df.head(max_rows).itertuples(index=False), 1):
|
||||
status_val = str(row[df.columns.get_loc('STATUS')]) if 'STATUS' in df.columns else ''
|
||||
if status_val == 'ALERTA':
|
||||
table_style.append(('BACKGROUND', (status_col_idx, row_idx), (status_col_idx, row_idx), colors.HexColor('#FF6B6B')))
|
||||
elif status_val == 'ATENTIE':
|
||||
table_style.append(('BACKGROUND', (status_col_idx, row_idx), (status_col_idx, row_idx), colors.HexColor('#FFE66D')))
|
||||
elif status_val == 'OK':
|
||||
table_style.append(('BACKGROUND', (status_col_idx, row_idx), (status_col_idx, row_idx), colors.HexColor('#4ECDC4')))
|
||||
|
||||
table.setStyle(TableStyle(table_style))
|
||||
self.elements.append(table)
|
||||
|
||||
if len(df) > max_rows:
|
||||
self.elements.append(Paragraph(
|
||||
f"... și încă {len(df) - max_rows} înregistrări",
|
||||
self.styles['SmallText']
|
||||
))
|
||||
|
||||
self.elements.append(Spacer(1, 0.4*cm))
|
||||
|
||||
def add_recommendations_section(self, recommendations_df: pd.DataFrame):
|
||||
"""Add recommendations section with status colors"""
|
||||
self.elements.append(Paragraph("Recomandari Cheie", self.styles['SectionHeader']))
|
||||
|
||||
Reference in New Issue
Block a user