Guilherme Silberfarb Costa commited on
Commit
9a6f968
·
1 Parent(s): 8718a07

inclusao de equacoes para excel

Browse files
backend/app/api/elaboracao.py CHANGED
@@ -355,6 +355,17 @@ def evaluation_export(session_id: str) -> FileResponse:
355
  )
356
 
357
 
 
 
 
 
 
 
 
 
 
 
 
358
  @router.get("/avaliadores")
359
  def listar_avaliadores() -> dict[str, Any]:
360
  return {"avaliadores": elaboracao_service.list_avaliadores()}
 
355
  )
356
 
357
 
358
+ @router.get("/equation/export")
359
+ def equation_export(session_id: str, mode: str = "excel") -> FileResponse:
360
+ session = session_store.get(session_id)
361
+ caminho, nome = elaboracao_service.exportar_equacao(session, mode)
362
+ return FileResponse(
363
+ path=caminho,
364
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
365
+ filename=nome,
366
+ )
367
+
368
+
369
  @router.get("/avaliadores")
370
  def listar_avaliadores() -> dict[str, Any]:
371
  return {"avaliadores": elaboracao_service.list_avaliadores()}
backend/app/api/visualizacao.py CHANGED
@@ -145,6 +145,17 @@ def evaluation_export(session_id: str) -> FileResponse:
145
  )
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
148
  @router.post("/clear")
149
  def clear(payload: SessionPayload) -> dict[str, Any]:
150
  session = session_store.get(payload.session_id)
 
145
  )
146
 
147
 
148
+ @router.get("/equation/export")
149
+ def equation_export(session_id: str, mode: str = "excel") -> FileResponse:
150
+ session = session_store.get(session_id)
151
+ caminho, nome = visualizacao_service.exportar_equacao(session, mode)
152
+ return FileResponse(
153
+ path=caminho,
154
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
155
+ filename=nome,
156
+ )
157
+
158
+
159
  @router.post("/clear")
160
  def clear(payload: SessionPayload) -> dict[str, Any]:
161
  session = session_store.get(payload.session_id)
backend/app/core/elaboracao/formatadores.py CHANGED
@@ -158,10 +158,6 @@ def formatar_diagnosticos_html(diagnosticos):
158
  html += f'''<div class="field-row"><span class="field-row-label">P-valor</span><span class="field-row-value">{diagnosticos["bp_p"]:.4f}</span></div>'''
159
  html += f'''<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value">{diagnosticos["interp_BP"]}</span></div>'''
160
 
161
- # Equação do Modelo
162
- html += '<div class="section-title-orange">Equação do Modelo</div>'
163
- html += f'<div class="equation-box">{diagnosticos["equacao"]}</div>'
164
-
165
  html += '</div>'
166
  return html
167
 
 
158
  html += f'''<div class="field-row"><span class="field-row-label">P-valor</span><span class="field-row-value">{diagnosticos["bp_p"]:.4f}</span></div>'''
159
  html += f'''<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value">{diagnosticos["interp_BP"]}</span></div>'''
160
 
 
 
 
 
161
  html += '</div>'
162
  return html
163
 
backend/app/core/visualizacao/app.py CHANGED
@@ -636,12 +636,6 @@ def formatar_resumo_html(resumo_reorganizado):
636
  if teste.get("interpretacao"):
637
  linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
638
 
639
- # Equação
640
- equacao = resumo_reorganizado.get("equacao")
641
- if equacao:
642
- linhas_html.append(criar_titulo_secao("Equação do Modelo"))
643
- linhas_html.append(f'<div class="equation-box">{equacao}</div>')
644
-
645
  return f"""<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>"""
646
 
647
  # ============================================================
 
636
  if teste.get("interpretacao"):
637
  linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
638
 
 
 
 
 
 
 
639
  return f"""<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>"""
640
 
641
  # ============================================================
backend/app/services/elaboracao_service.py CHANGED
@@ -41,6 +41,7 @@ from app.core.elaboracao.formatadores import (
41
  )
42
  from app.models.session import SessionState
43
  from app.services import model_repository
 
44
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
45
 
46
  _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboracao" / "avaliadores.json"
@@ -768,6 +769,14 @@ def fit_model(
768
  session.avaliacoes_elaboracao = []
769
 
770
  diagnosticos_html = formatar_diagnosticos_html(resultado["diagnosticos"])
 
 
 
 
 
 
 
 
771
 
772
  try:
773
  fig_dispersao_transf = charts.criar_graficos_dispersao(
@@ -827,6 +836,7 @@ def fit_model(
827
  "transformacao_y": session.transformacao_y,
828
  "transformacoes_x": sanitize_value(session.transformacoes_x),
829
  },
 
830
  "tabela_coef": dataframe_to_payload(resultado["tabela_coef"], decimals=4),
831
  "tabela_obs_calc": dataframe_to_payload(resultado["tabela_obs_calc"], decimals=4),
832
  "grafico_dispersao_modelo": figure_to_payload(fig_dispersao_transf),
@@ -1277,6 +1287,26 @@ def exportar_base(session: SessionState, usar_filtrado: bool = True) -> str:
1277
  return caminho
1278
 
1279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1280
  def atualizar_mapa(session: SessionState, var_mapa: str | None) -> dict[str, Any]:
1281
  df = session.df_filtrado if session.df_filtrado is not None else session.df_original
1282
  if df is None:
 
41
  )
42
  from app.models.session import SessionState
43
  from app.services import model_repository
44
+ from app.services.equacao_service import build_equacoes_payload, exportar_planilha_equacao
45
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
46
 
47
  _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboracao" / "avaliadores.json"
 
769
  session.avaliacoes_elaboracao = []
770
 
771
  diagnosticos_html = formatar_diagnosticos_html(resultado["diagnosticos"])
772
+ equacoes = build_equacoes_payload(
773
+ modelo_sm=resultado.get("modelo_sm"),
774
+ coluna_y=session.coluna_y or str(resultado.get("coluna_y") or ""),
775
+ transformacao_y=session.transformacao_y,
776
+ transformacoes_x=session.transformacoes_x,
777
+ colunas_x=session.colunas_x,
778
+ equacao_visual=str(resultado.get("diagnosticos", {}).get("equacao") or ""),
779
+ )
780
 
781
  try:
782
  fig_dispersao_transf = charts.criar_graficos_dispersao(
 
836
  "transformacao_y": session.transformacao_y,
837
  "transformacoes_x": sanitize_value(session.transformacoes_x),
838
  },
839
+ "equacoes": sanitize_value(equacoes),
840
  "tabela_coef": dataframe_to_payload(resultado["tabela_coef"], decimals=4),
841
  "tabela_obs_calc": dataframe_to_payload(resultado["tabela_obs_calc"], decimals=4),
842
  "grafico_dispersao_modelo": figure_to_payload(fig_dispersao_transf),
 
1287
  return caminho
1288
 
1289
 
1290
+ def exportar_equacao(session: SessionState, mode: str) -> tuple[str, str]:
1291
+ if session.resultado_modelo is None:
1292
+ raise HTTPException(status_code=400, detail="Ajuste um modelo antes de exportar a equacao")
1293
+ if not session.coluna_y or not session.colunas_x:
1294
+ raise HTTPException(status_code=400, detail="Modelo sem variaveis para montar equacao")
1295
+
1296
+ diagnosticos = session.resultado_modelo.get("diagnosticos", {}) or {}
1297
+ caminho, nome = exportar_planilha_equacao(
1298
+ mode=mode,
1299
+ modelo_sm=session.resultado_modelo.get("modelo_sm"),
1300
+ coluna_y=session.coluna_y,
1301
+ transformacao_y=session.transformacao_y,
1302
+ transformacoes_x=session.transformacoes_x,
1303
+ colunas_x=session.colunas_x,
1304
+ equacao_visual=str(diagnosticos.get("equacao") or ""),
1305
+ nome_base="equacao_modelo",
1306
+ )
1307
+ return caminho, nome
1308
+
1309
+
1310
  def atualizar_mapa(session: SessionState, var_mapa: str | None) -> dict[str, Any]:
1311
  df = session.df_filtrado if session.df_filtrado is not None else session.df_original
1312
  if df is None:
backend/app/services/equacao_service.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import tempfile
6
+ import uuid
7
+ from typing import Any, Callable
8
+
9
+ from fastapi import HTTPException
10
+ from openpyxl import Workbook
11
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
12
+
13
+
14
+ _E_CONST_BR = "2,718281828459045"
15
+ _E_CONST_DOT = "2.718281828459045"
16
+ _MODE_EXCEL = "excel"
17
+ _MODE_EXCEL_SAB = "excel_sab"
18
+
19
+
20
+ def _normalize_mode(mode: str | None) -> str:
21
+ value = str(mode or "").strip().lower()
22
+ if value in {"excel", "formato_excel"}:
23
+ return _MODE_EXCEL
24
+ if value in {"excel_sab", "sab", "estilo_sab", "formato_excel_sab"}:
25
+ return _MODE_EXCEL_SAB
26
+ raise HTTPException(status_code=400, detail="Modo de equacao invalido. Use 'excel' ou 'excel_sab'.")
27
+
28
+
29
+ def _format_number_br(value: Any) -> str:
30
+ try:
31
+ num = float(value)
32
+ except Exception:
33
+ num = 0.0
34
+ if not math.isfinite(num):
35
+ num = 0.0
36
+ text = f"{num:.12f}".rstrip("0").rstrip(".")
37
+ if text in {"", "-0"}:
38
+ text = "0"
39
+ return text.replace(".", ",")
40
+
41
+
42
+ def _normalize_transform(value: Any) -> str:
43
+ return str(value or "(x)").strip() or "(x)"
44
+
45
+
46
+ def _format_number_calc(value: Any) -> str:
47
+ try:
48
+ num = float(value)
49
+ except Exception:
50
+ num = 0.0
51
+ if not math.isfinite(num):
52
+ num = 0.0
53
+ text = f"{num:.12f}".rstrip("0").rstrip(".")
54
+ if text in {"", "-0"}:
55
+ text = "0"
56
+ return text
57
+
58
+
59
+ def _resolve_const(params: Any) -> float:
60
+ if params is None:
61
+ return 0.0
62
+ for key in ("const", "Intercept", "intercept", "(Intercept)"):
63
+ try:
64
+ if key in params:
65
+ return float(params[key])
66
+ except Exception:
67
+ continue
68
+ try:
69
+ if hasattr(params, "iloc") and len(params) > 0:
70
+ return float(params.iloc[0])
71
+ except Exception:
72
+ pass
73
+ return 0.0
74
+
75
+
76
+ def _resolve_coef(params: Any, column: str) -> float:
77
+ if params is None:
78
+ return 0.0
79
+ try:
80
+ return float(params[column])
81
+ except Exception:
82
+ return 0.0
83
+
84
+
85
+ def _term_expr_excel(transformacao: str, var_expr: str, mode: str) -> str:
86
+ transf = _normalize_transform(transformacao)
87
+ if transf == "ln(x)":
88
+ if mode == _MODE_EXCEL_SAB:
89
+ return f"LOG({var_expr};{_E_CONST_BR})"
90
+ return f"LN({var_expr})"
91
+ if transf == "exp(x)":
92
+ if mode == _MODE_EXCEL_SAB:
93
+ return f"({_E_CONST_BR}^({var_expr}))"
94
+ return f"EXP({var_expr})"
95
+ if transf == "1/(x)":
96
+ return f"(1/({var_expr}))"
97
+ if transf == "(x)^2":
98
+ return f"(({var_expr})^2)"
99
+ if transf == "raiz(x)":
100
+ return f"RAIZ({var_expr})"
101
+ if transf == "1/raiz(x)":
102
+ return f"(1/RAIZ({var_expr}))"
103
+ return var_expr
104
+
105
+
106
+ def _term_expr_calc(transformacao: str, var_expr: str, mode: str) -> str:
107
+ transf = _normalize_transform(transformacao)
108
+ if transf == "ln(x)":
109
+ if mode == _MODE_EXCEL_SAB:
110
+ return f"LOG({var_expr},{_E_CONST_DOT})"
111
+ return f"LN({var_expr})"
112
+ if transf == "exp(x)":
113
+ if mode == _MODE_EXCEL_SAB:
114
+ return f"({_E_CONST_DOT}^({var_expr}))"
115
+ return f"EXP({var_expr})"
116
+ if transf == "1/(x)":
117
+ return f"(1/({var_expr}))"
118
+ if transf == "(x)^2":
119
+ return f"(({var_expr})^2)"
120
+ if transf == "raiz(x)":
121
+ return f"SQRT({var_expr})"
122
+ if transf == "1/raiz(x)":
123
+ return f"(1/SQRT({var_expr}))"
124
+ return var_expr
125
+
126
+
127
+ def _compose_linear_expression(
128
+ *,
129
+ params: Any,
130
+ colunas_x: list[str],
131
+ transformacoes_x: dict[str, str],
132
+ var_resolver: Callable[[str], str],
133
+ mode: str,
134
+ include_ln_terms: bool = True,
135
+ ) -> str:
136
+ const_value = _resolve_const(params)
137
+ expr = _format_number_br(const_value)
138
+ for coluna in colunas_x:
139
+ coef = _resolve_coef(params, coluna)
140
+ if abs(coef) < 1e-15:
141
+ continue
142
+ transf = _normalize_transform(transformacoes_x.get(coluna, "(x)"))
143
+ if not include_ln_terms and transf == "ln(x)":
144
+ continue
145
+ termo = _term_expr_excel(transf, var_resolver(coluna), mode)
146
+ sinal = "+" if coef >= 0 else "-"
147
+ expr += f"{sinal}{_format_number_br(abs(coef))}*{termo}"
148
+ return expr
149
+
150
+
151
+ def _compose_linear_expression_calc(
152
+ *,
153
+ params: Any,
154
+ colunas_x: list[str],
155
+ transformacoes_x: dict[str, str],
156
+ var_resolver: Callable[[str], str],
157
+ mode: str,
158
+ include_ln_terms: bool = True,
159
+ ) -> str:
160
+ const_value = _resolve_const(params)
161
+ expr = _format_number_calc(const_value)
162
+ for coluna in colunas_x:
163
+ coef = _resolve_coef(params, coluna)
164
+ if abs(coef) < 1e-15:
165
+ continue
166
+ transf = _normalize_transform(transformacoes_x.get(coluna, "(x)"))
167
+ if not include_ln_terms and transf == "ln(x)":
168
+ continue
169
+ termo = _term_expr_calc(transf, var_resolver(coluna), mode)
170
+ sinal = "+" if coef >= 0 else "-"
171
+ expr += f"{sinal}{_format_number_calc(abs(coef))}*{termo}"
172
+ return expr
173
+
174
+
175
+ def _invert_y_expr(expr: str, transformacao_y: str, mode: str) -> str:
176
+ transf_y = _normalize_transform(transformacao_y)
177
+ if transf_y == "(x)":
178
+ return f"({expr})"
179
+ if transf_y == "ln(x)":
180
+ if mode == _MODE_EXCEL_SAB:
181
+ return f"({_E_CONST_BR}^({expr}))"
182
+ return f"EXP({expr})"
183
+ if transf_y == "1/(x)":
184
+ return f"(1/({expr}))"
185
+ if transf_y == "(x)^2":
186
+ return f"RAIZ({expr})"
187
+ if transf_y == "raiz(x)":
188
+ return f"(({expr})^2)"
189
+ if transf_y == "1/raiz(x)":
190
+ return f"(1/(({expr})^2))"
191
+ if transf_y == "exp(x)":
192
+ if mode == _MODE_EXCEL_SAB:
193
+ return f"LOG({expr};{_E_CONST_BR})"
194
+ return f"LN({expr})"
195
+ return f"({expr})"
196
+
197
+
198
+ def _invert_y_expr_calc(expr: str, transformacao_y: str, mode: str) -> str:
199
+ transf_y = _normalize_transform(transformacao_y)
200
+ if transf_y == "(x)":
201
+ return f"({expr})"
202
+ if transf_y == "ln(x)":
203
+ if mode == _MODE_EXCEL_SAB:
204
+ return f"({_E_CONST_DOT}^({expr}))"
205
+ return f"EXP({expr})"
206
+ if transf_y == "1/(x)":
207
+ return f"(1/({expr}))"
208
+ if transf_y == "(x)^2":
209
+ return f"SQRT({expr})"
210
+ if transf_y == "raiz(x)":
211
+ return f"(({expr})^2)"
212
+ if transf_y == "1/raiz(x)":
213
+ return f"(1/(({expr})^2))"
214
+ if transf_y == "exp(x)":
215
+ if mode == _MODE_EXCEL_SAB:
216
+ return f"LOG({expr},{_E_CONST_DOT})"
217
+ return f"LN({expr})"
218
+ return f"({expr})"
219
+
220
+
221
+ def _build_formula_excel(
222
+ *,
223
+ params: Any,
224
+ transformacao_y: str,
225
+ transformacoes_x: dict[str, str],
226
+ colunas_x: list[str],
227
+ var_resolver: Callable[[str], str],
228
+ ) -> str:
229
+ linear = _compose_linear_expression(
230
+ params=params,
231
+ colunas_x=colunas_x,
232
+ transformacoes_x=transformacoes_x,
233
+ var_resolver=var_resolver,
234
+ mode=_MODE_EXCEL,
235
+ )
236
+ return "=" + _invert_y_expr(linear, transformacao_y, _MODE_EXCEL)
237
+
238
+
239
+ def _build_formula_excel_sab(
240
+ *,
241
+ params: Any,
242
+ transformacao_y: str,
243
+ transformacoes_x: dict[str, str],
244
+ colunas_x: list[str],
245
+ var_resolver: Callable[[str], str],
246
+ ) -> str:
247
+ transf_y = _normalize_transform(transformacao_y)
248
+ # Forma direta sem EXP/LN quando y esta em log.
249
+ if transf_y == "ln(x)":
250
+ base_linear = _compose_linear_expression(
251
+ params=params,
252
+ colunas_x=colunas_x,
253
+ transformacoes_x=transformacoes_x,
254
+ var_resolver=var_resolver,
255
+ mode=_MODE_EXCEL_SAB,
256
+ include_ln_terms=False,
257
+ )
258
+ expr = f"({_E_CONST_BR}^({base_linear}))"
259
+ for coluna in colunas_x:
260
+ transf = _normalize_transform(transformacoes_x.get(coluna, "(x)"))
261
+ if transf != "ln(x)":
262
+ continue
263
+ coef = _resolve_coef(params, coluna)
264
+ if abs(coef) < 1e-15:
265
+ continue
266
+ expr += f"*(({var_resolver(coluna)})^{_format_number_br(coef)})"
267
+ return "=" + expr
268
+
269
+ linear = _compose_linear_expression(
270
+ params=params,
271
+ colunas_x=colunas_x,
272
+ transformacoes_x=transformacoes_x,
273
+ var_resolver=var_resolver,
274
+ mode=_MODE_EXCEL_SAB,
275
+ )
276
+ return "=" + _invert_y_expr(linear, transformacao_y, _MODE_EXCEL_SAB)
277
+
278
+
279
+ def _build_formula_calc_excel(
280
+ *,
281
+ params: Any,
282
+ transformacao_y: str,
283
+ transformacoes_x: dict[str, str],
284
+ colunas_x: list[str],
285
+ var_resolver: Callable[[str], str],
286
+ ) -> str:
287
+ linear = _compose_linear_expression_calc(
288
+ params=params,
289
+ colunas_x=colunas_x,
290
+ transformacoes_x=transformacoes_x,
291
+ var_resolver=var_resolver,
292
+ mode=_MODE_EXCEL,
293
+ )
294
+ return "=" + _invert_y_expr_calc(linear, transformacao_y, _MODE_EXCEL)
295
+
296
+
297
+ def _build_formula_calc_excel_sab(
298
+ *,
299
+ params: Any,
300
+ transformacao_y: str,
301
+ transformacoes_x: dict[str, str],
302
+ colunas_x: list[str],
303
+ var_resolver: Callable[[str], str],
304
+ ) -> str:
305
+ transf_y = _normalize_transform(transformacao_y)
306
+ if transf_y == "ln(x)":
307
+ base_linear = _compose_linear_expression_calc(
308
+ params=params,
309
+ colunas_x=colunas_x,
310
+ transformacoes_x=transformacoes_x,
311
+ var_resolver=var_resolver,
312
+ mode=_MODE_EXCEL_SAB,
313
+ include_ln_terms=False,
314
+ )
315
+ expr = f"({_E_CONST_DOT}^({base_linear}))"
316
+ for coluna in colunas_x:
317
+ transf = _normalize_transform(transformacoes_x.get(coluna, "(x)"))
318
+ if transf != "ln(x)":
319
+ continue
320
+ coef = _resolve_coef(params, coluna)
321
+ if abs(coef) < 1e-15:
322
+ continue
323
+ expr += f"*(({var_resolver(coluna)})^{_format_number_calc(coef)})"
324
+ return "=" + expr
325
+
326
+ linear = _compose_linear_expression_calc(
327
+ params=params,
328
+ colunas_x=colunas_x,
329
+ transformacoes_x=transformacoes_x,
330
+ var_resolver=var_resolver,
331
+ mode=_MODE_EXCEL_SAB,
332
+ )
333
+ return "=" + _invert_y_expr_calc(linear, transformacao_y, _MODE_EXCEL_SAB)
334
+
335
+
336
+ def _build_formula_by_mode(
337
+ *,
338
+ mode: str,
339
+ params: Any,
340
+ transformacao_y: str,
341
+ transformacoes_x: dict[str, str],
342
+ colunas_x: list[str],
343
+ var_resolver: Callable[[str], str],
344
+ ) -> str:
345
+ mode_norm = _normalize_mode(mode)
346
+ if mode_norm == _MODE_EXCEL:
347
+ return _build_formula_excel(
348
+ params=params,
349
+ transformacao_y=transformacao_y,
350
+ transformacoes_x=transformacoes_x,
351
+ colunas_x=colunas_x,
352
+ var_resolver=var_resolver,
353
+ )
354
+ return _build_formula_excel_sab(
355
+ params=params,
356
+ transformacao_y=transformacao_y,
357
+ transformacoes_x=transformacoes_x,
358
+ colunas_x=colunas_x,
359
+ var_resolver=var_resolver,
360
+ )
361
+
362
+
363
+ def _build_formula_calc_by_mode(
364
+ *,
365
+ mode: str,
366
+ params: Any,
367
+ transformacao_y: str,
368
+ transformacoes_x: dict[str, str],
369
+ colunas_x: list[str],
370
+ var_resolver: Callable[[str], str],
371
+ ) -> str:
372
+ mode_norm = _normalize_mode(mode)
373
+ if mode_norm == _MODE_EXCEL:
374
+ return _build_formula_calc_excel(
375
+ params=params,
376
+ transformacao_y=transformacao_y,
377
+ transformacoes_x=transformacoes_x,
378
+ colunas_x=colunas_x,
379
+ var_resolver=var_resolver,
380
+ )
381
+ return _build_formula_calc_excel_sab(
382
+ params=params,
383
+ transformacao_y=transformacao_y,
384
+ transformacoes_x=transformacoes_x,
385
+ colunas_x=colunas_x,
386
+ var_resolver=var_resolver,
387
+ )
388
+
389
+
390
+ def _set_formula_as_text(cell: Any, formula_text: str) -> None:
391
+ cell.value = str(formula_text or "")
392
+ cell.data_type = "s"
393
+ cell.number_format = "@"
394
+
395
+
396
+ def build_equacoes_payload(
397
+ *,
398
+ modelo_sm: Any,
399
+ coluna_y: str,
400
+ transformacao_y: str,
401
+ transformacoes_x: dict[str, str] | None,
402
+ colunas_x: list[str] | None,
403
+ equacao_visual: str | None = None,
404
+ ) -> dict[str, str]:
405
+ if modelo_sm is None:
406
+ return {
407
+ "visual_apresentacao": str(equacao_visual or "").strip(),
408
+ "excel": "",
409
+ "excel_sab": "",
410
+ }
411
+
412
+ params = getattr(modelo_sm, "params", None)
413
+ if params is None:
414
+ return {
415
+ "visual_apresentacao": str(equacao_visual or "").strip(),
416
+ "excel": "",
417
+ "excel_sab": "",
418
+ }
419
+
420
+ lista_x = [str(c) for c in (colunas_x or [])]
421
+ mapa_transf = {str(k): _normalize_transform(v) for k, v in (transformacoes_x or {}).items()}
422
+ visual = str(equacao_visual or "").strip()
423
+ if not visual:
424
+ visual = f"{coluna_y} = {str(_build_formula_excel(params=params, transformacao_y=transformacao_y, transformacoes_x=mapa_transf, colunas_x=lista_x, var_resolver=lambda c: c)).lstrip('=')}"
425
+
426
+ formula_excel = _build_formula_excel(
427
+ params=params,
428
+ transformacao_y=transformacao_y,
429
+ transformacoes_x=mapa_transf,
430
+ colunas_x=lista_x,
431
+ var_resolver=lambda c: c,
432
+ )
433
+ formula_excel_sab = _build_formula_excel_sab(
434
+ params=params,
435
+ transformacao_y=transformacao_y,
436
+ transformacoes_x=mapa_transf,
437
+ colunas_x=lista_x,
438
+ var_resolver=lambda c: c,
439
+ )
440
+ return {
441
+ "visual_apresentacao": visual,
442
+ "excel": formula_excel,
443
+ "excel_sab": formula_excel_sab,
444
+ }
445
+
446
+
447
+ def exportar_planilha_equacao(
448
+ *,
449
+ mode: str,
450
+ modelo_sm: Any,
451
+ coluna_y: str,
452
+ transformacao_y: str,
453
+ transformacoes_x: dict[str, str] | None,
454
+ colunas_x: list[str] | None,
455
+ equacao_visual: str | None = None,
456
+ nome_base: str = "equacao_modelo",
457
+ ) -> tuple[str, str]:
458
+ mode_norm = _normalize_mode(mode)
459
+ lista_x = [str(c) for c in (colunas_x or [])]
460
+ mapa_transf = {str(k): _normalize_transform(v) for k, v in (transformacoes_x or {}).items()}
461
+
462
+ if modelo_sm is None or getattr(modelo_sm, "params", None) is None:
463
+ raise HTTPException(status_code=400, detail="Modelo sem parametros para gerar equacao.")
464
+
465
+ params = modelo_sm.params
466
+ equacoes = build_equacoes_payload(
467
+ modelo_sm=modelo_sm,
468
+ coluna_y=coluna_y,
469
+ transformacao_y=transformacao_y,
470
+ transformacoes_x=mapa_transf,
471
+ colunas_x=lista_x,
472
+ equacao_visual=equacao_visual,
473
+ )
474
+ formula_texto = equacoes["excel"] if mode_norm == _MODE_EXCEL else equacoes["excel_sab"]
475
+
476
+ inicio_linhas = 2
477
+ mapa_refs = {coluna: f"B{inicio_linhas + idx}" for idx, coluna in enumerate(lista_x)}
478
+ formula_celulas_exec = _build_formula_calc_by_mode(
479
+ mode=mode_norm,
480
+ params=params,
481
+ transformacao_y=transformacao_y,
482
+ transformacoes_x=mapa_transf,
483
+ colunas_x=lista_x,
484
+ var_resolver=lambda c: mapa_refs[c],
485
+ )
486
+
487
+ wb = Workbook()
488
+ ws = wb.active
489
+ ws.title = "Equacao"
490
+
491
+ ws["A1"] = "Variável"
492
+ ws["B1"] = "Valor"
493
+ ws["D1"] = "Variável dependente"
494
+ ws["E1"] = str(coluna_y or "")
495
+ ws["D2"] = "Fórmula utilizada"
496
+ _set_formula_as_text(ws["E2"], formula_texto)
497
+ ws["D3"] = "Fórmula aplicada"
498
+ ws["E3"] = formula_celulas_exec
499
+
500
+ for idx, coluna in enumerate(lista_x):
501
+ row = inicio_linhas + idx
502
+ ws[f"A{row}"] = coluna
503
+ ws[f"B{row}"] = 1
504
+
505
+ for cell in ("A1", "B1", "D1", "D2", "D3"):
506
+ ws[cell].font = Font(bold=True)
507
+
508
+ section_fill = PatternFill(fill_type="solid", start_color="EEF5FC", end_color="EEF5FC")
509
+ table_header_fill = PatternFill(fill_type="solid", start_color="E6EEF8", end_color="E6EEF8")
510
+ border = Border(
511
+ left=Side(style="thin", color="D8E2EE"),
512
+ right=Side(style="thin", color="D8E2EE"),
513
+ top=Side(style="thin", color="D8E2EE"),
514
+ bottom=Side(style="thin", color="D8E2EE"),
515
+ )
516
+
517
+ for col in ("A1", "B1"):
518
+ ws[col].fill = table_header_fill
519
+ ws[col].border = border
520
+ ws[col].alignment = Alignment(horizontal="center", vertical="top")
521
+
522
+ for row in range(inicio_linhas, inicio_linhas + len(lista_x)):
523
+ ws[f"A{row}"].border = border
524
+ ws[f"B{row}"].border = border
525
+ ws[f"A{row}"].alignment = Alignment(vertical="top")
526
+ ws[f"B{row}"].alignment = Alignment(horizontal="center", vertical="top")
527
+
528
+ for row in (1, 2, 3):
529
+ ws[f"D{row}"].fill = section_fill
530
+ ws[f"D{row}"].border = border
531
+ ws[f"D{row}"].alignment = Alignment(vertical="top")
532
+ ws[f"E{row}"].border = border
533
+ ws[f"E{row}"].alignment = Alignment(wrap_text=True, vertical="top")
534
+
535
+ ws.column_dimensions["A"].width = 32
536
+ ws.column_dimensions["B"].width = 64
537
+ ws.column_dimensions["D"].width = 28
538
+ ws.column_dimensions["E"].width = 74
539
+ ws.freeze_panes = "A2"
540
+
541
+ modo_sufixo = "excel" if mode_norm == _MODE_EXCEL else "excel_estilo_sab"
542
+ safe_nome = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in (nome_base or "equacao_modelo")).strip("_")
543
+ if not safe_nome:
544
+ safe_nome = "equacao_modelo"
545
+ file_name = f"{safe_nome}_{modo_sufixo}.xlsx"
546
+ caminho = os.path.join(tempfile.gettempdir(), f"{safe_nome}_{modo_sufixo}_{uuid.uuid4().hex[:8]}.xlsx")
547
+ wb.save(caminho)
548
+ return caminho, file_name
backend/app/services/visualizacao_service.py CHANGED
@@ -13,6 +13,7 @@ from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, avaliar_imovel,
13
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
14
  from app.models.session import SessionState
15
  from app.services import model_repository
 
16
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
17
 
18
 
@@ -141,6 +142,15 @@ def exibir_modelo(session: SessionState) -> dict[str, Any]:
141
  figs = viz_app.gerar_todos_graficos(pacote)
142
 
143
  info = _extrair_modelo_info(pacote)
 
 
 
 
 
 
 
 
 
144
  mapa_html = viz_app.criar_mapa(dados, col_y=info["nome_y"])
145
 
146
  colunas_numericas = [
@@ -169,6 +179,7 @@ def exibir_modelo(session: SessionState) -> dict[str, Any]:
169
  "mapa_choices": choices_mapa,
170
  "campos_avaliacao": campos_avaliacao(session),
171
  "meta_modelo": sanitize_value(info),
 
172
  }
173
 
174
 
@@ -397,6 +408,26 @@ def exportar_avaliacoes(session: SessionState) -> str:
397
  return caminho
398
 
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  def limpar_tudo_visualizacao(session: SessionState) -> dict[str, Any]:
401
  session.reset_visualizacao()
402
  return {
 
13
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
14
  from app.models.session import SessionState
15
  from app.services import model_repository
16
+ from app.services.equacao_service import build_equacoes_payload, exportar_planilha_equacao
17
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
18
 
19
 
 
142
  figs = viz_app.gerar_todos_graficos(pacote)
143
 
144
  info = _extrair_modelo_info(pacote)
145
+ diagnosticos = pacote.get("modelo", {}).get("diagnosticos", {}) if isinstance(pacote.get("modelo"), dict) else {}
146
+ equacoes = build_equacoes_payload(
147
+ modelo_sm=pacote.get("modelo", {}).get("sm"),
148
+ coluna_y=info["nome_y"],
149
+ transformacao_y=info["transformacao_y"],
150
+ transformacoes_x=info["transformacoes_x"],
151
+ colunas_x=info["colunas_x"],
152
+ equacao_visual=str(diagnosticos.get("equacao") or ""),
153
+ )
154
  mapa_html = viz_app.criar_mapa(dados, col_y=info["nome_y"])
155
 
156
  colunas_numericas = [
 
179
  "mapa_choices": choices_mapa,
180
  "campos_avaliacao": campos_avaliacao(session),
181
  "meta_modelo": sanitize_value(info),
182
+ "equacoes": sanitize_value(equacoes),
183
  }
184
 
185
 
 
408
  return caminho
409
 
410
 
411
+ def exportar_equacao(session: SessionState, mode: str) -> tuple[str, str]:
412
+ pacote = session.pacote_visualizacao
413
+ if pacote is None:
414
+ raise HTTPException(status_code=400, detail="Carregue um modelo primeiro")
415
+
416
+ info = _extrair_modelo_info(pacote)
417
+ diagnosticos = pacote.get("modelo", {}).get("diagnosticos", {}) if isinstance(pacote.get("modelo"), dict) else {}
418
+ caminho, nome = exportar_planilha_equacao(
419
+ mode=mode,
420
+ modelo_sm=pacote.get("modelo", {}).get("sm"),
421
+ coluna_y=info["nome_y"],
422
+ transformacao_y=info["transformacao_y"],
423
+ transformacoes_x=info["transformacoes_x"],
424
+ colunas_x=info["colunas_x"],
425
+ equacao_visual=str(diagnosticos.get("equacao") or ""),
426
+ nome_base="equacao_modelo",
427
+ )
428
+ return caminho, nome
429
+
430
+
431
  def limpar_tudo_visualizacao(session: SessionState) -> dict[str, Any]:
432
  session.reset_visualizacao()
433
  return {
frontend/src/api.js CHANGED
@@ -190,6 +190,7 @@ export const api = {
190
  indice_base: indiceBase,
191
  }),
192
  exportEvaluationElab: async (sessionId) => getBlob(`/api/elaboracao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
 
193
  exportModel: async (sessionId, nomeArquivo, elaborador) => {
194
  const response = await fetch(`${API_BASE}/api/elaboracao/export-model`, {
195
  method: 'POST',
@@ -232,6 +233,7 @@ export const api = {
232
  indice_base: indiceBase,
233
  }),
234
  exportEvaluationViz: (sessionId) => getBlob(`/api/visualizacao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
 
235
  clearVisualizacao: (sessionId) => postJson('/api/visualizacao/clear', { session_id: sessionId }),
236
 
237
  repositorioListar: () => getJson('/api/repositorio/modelos'),
 
190
  indice_base: indiceBase,
191
  }),
192
  exportEvaluationElab: async (sessionId) => getBlob(`/api/elaboracao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
193
+ exportEquationElab: async (sessionId, mode = 'excel') => getBlob(`/api/elaboracao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`),
194
  exportModel: async (sessionId, nomeArquivo, elaborador) => {
195
  const response = await fetch(`${API_BASE}/api/elaboracao/export-model`, {
196
  method: 'POST',
 
233
  indice_base: indiceBase,
234
  }),
235
  exportEvaluationViz: (sessionId) => getBlob(`/api/visualizacao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
236
+ exportEquationViz: (sessionId, mode = 'excel') => getBlob(`/api/visualizacao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`),
237
  clearVisualizacao: (sessionId) => postJson('/api/visualizacao/clear', { session_id: sessionId }),
238
 
239
  repositorioListar: () => getJson('/api/repositorio/modelos'),
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import Plotly from 'plotly.js-dist-min'
4
  import DataTable from './DataTable'
 
5
  import LoadingOverlay from './LoadingOverlay'
6
  import MapFrame from './MapFrame'
7
  import PlotFigure from './PlotFigure'
@@ -1876,6 +1877,21 @@ export default function ElaboracaoTab({ sessionId }) {
1876
  })
1877
  }
1878
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1879
  async function onMapVarChange(value) {
1880
  setMapaVariavel(value)
1881
  if (!sessionId || !mapaGerado) return
@@ -3078,6 +3094,14 @@ export default function ElaboracaoTab({ sessionId }) {
3078
 
3079
  <SectionBlock step="13" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
3080
  <div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
 
 
 
 
 
 
 
 
3081
  <div className="download-actions-bar">
3082
  <span className="download-actions-label">Fazer download:</span>
3083
  <button
 
2
  import { api, downloadBlob } from '../api'
3
  import Plotly from 'plotly.js-dist-min'
4
  import DataTable from './DataTable'
5
+ import EquationFormatsPanel from './EquationFormatsPanel'
6
  import LoadingOverlay from './LoadingOverlay'
7
  import MapFrame from './MapFrame'
8
  import PlotFigure from './PlotFigure'
 
1877
  })
1878
  }
1879
 
1880
+ async function onDownloadEquacao(mode) {
1881
+ if (!sessionId || !mode) return
1882
+ setDownloadingAssets(true)
1883
+ setError('')
1884
+ try {
1885
+ const blob = await api.exportEquationElab(sessionId, mode)
1886
+ const sufixo = String(mode) === 'excel_sab' ? 'estilo_sab' : 'excel'
1887
+ downloadBlob(blob, `equacao_modelo_${sufixo}.xlsx`)
1888
+ } catch (err) {
1889
+ setError(err.message || 'Falha ao baixar equacao.')
1890
+ } finally {
1891
+ setDownloadingAssets(false)
1892
+ }
1893
+ }
1894
+
1895
  async function onMapVarChange(value) {
1896
  setMapaVariavel(value)
1897
  if (!sessionId || !mapaGerado) return
 
3094
 
3095
  <SectionBlock step="13" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
3096
  <div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
3097
+ <div className="equation-formats-section">
3098
+ <h4>Equações do Modelo</h4>
3099
+ <EquationFormatsPanel
3100
+ equacoes={fit.equacoes}
3101
+ onDownload={(mode) => void onDownloadEquacao(mode)}
3102
+ disabled={loading || downloadingAssets}
3103
+ />
3104
+ </div>
3105
  <div className="download-actions-bar">
3106
  <span className="download-actions-label">Fazer download:</span>
3107
  <button
frontend/src/components/EquationFormatsPanel.jsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo, useState } from 'react'
2
+
3
+ const GROUPS = [
4
+ { key: 'visual_apresentacao', label: 'Formato para Apresentação', canExport: false },
5
+ { key: 'excel', label: 'Formato Excel', canExport: true, exportMode: 'excel' },
6
+ { key: 'excel_sab', label: 'Formato Excel (estilo SAB)', canExport: true, exportMode: 'excel_sab' },
7
+ ]
8
+
9
+ function fallbackCopy(text) {
10
+ const area = document.createElement('textarea')
11
+ area.value = text
12
+ area.setAttribute('readonly', '')
13
+ area.style.position = 'fixed'
14
+ area.style.opacity = '0'
15
+ area.style.pointerEvents = 'none'
16
+ document.body.appendChild(area)
17
+ area.focus()
18
+ area.select()
19
+ try {
20
+ document.execCommand('copy')
21
+ } finally {
22
+ area.remove()
23
+ }
24
+ }
25
+
26
+ async function copyText(text) {
27
+ const value = String(text || '').trim()
28
+ if (!value) return false
29
+ try {
30
+ if (navigator?.clipboard?.writeText) {
31
+ await navigator.clipboard.writeText(value)
32
+ return true
33
+ }
34
+ } catch {
35
+ // fallback below
36
+ }
37
+ try {
38
+ fallbackCopy(value)
39
+ return true
40
+ } catch {
41
+ return false
42
+ }
43
+ }
44
+
45
+ export default function EquationFormatsPanel({ equacoes, onDownload, disabled = false }) {
46
+ const [lastCopied, setLastCopied] = useState('')
47
+ const groups = useMemo(() => GROUPS, [])
48
+
49
+ async function onCopy(value, key) {
50
+ const ok = await copyText(value)
51
+ setLastCopied(ok ? key : '')
52
+ if (ok) {
53
+ window.setTimeout(() => setLastCopied(''), 1800)
54
+ }
55
+ }
56
+
57
+ return (
58
+ <div className="equation-formats-wrap">
59
+ {groups.map((group) => {
60
+ const value = String(equacoes?.[group.key] || '').trim()
61
+ const hasValue = Boolean(value)
62
+ return (
63
+ <div key={group.key} className="equation-format-card">
64
+ <div className="equation-format-head">
65
+ <h5>{group.label}</h5>
66
+ {group.canExport ? (
67
+ <div className="equation-format-actions">
68
+ <button
69
+ type="button"
70
+ className="btn-download-subtle"
71
+ onClick={() => void onCopy(value, group.key)}
72
+ disabled={disabled || !hasValue}
73
+ >
74
+ {lastCopied === group.key ? 'Copiado' : 'Copiar fórmula'}
75
+ </button>
76
+ <button
77
+ type="button"
78
+ className="btn-download-subtle"
79
+ onClick={() => onDownload?.(group.exportMode)}
80
+ disabled={disabled || !hasValue || typeof onDownload !== 'function'}
81
+ >
82
+ Baixar Excel
83
+ </button>
84
+ </div>
85
+ ) : null}
86
+ </div>
87
+ <div className="equation-box equation-box-plain">
88
+ {hasValue ? value : 'Equação indisponível.'}
89
+ </div>
90
+ </div>
91
+ )
92
+ })}
93
+ </div>
94
+ )
95
+ }
96
+
frontend/src/components/VisualizacaoTab.jsx CHANGED
@@ -1,6 +1,7 @@
1
  import React, { useEffect, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import DataTable from './DataTable'
 
4
  import LoadingOverlay from './LoadingOverlay'
5
  import MapFrame from './MapFrame'
6
  import PlotFigure from './PlotFigure'
@@ -37,6 +38,7 @@ export default function VisualizacaoTab({ sessionId }) {
37
  const [escalasHtml, setEscalasHtml] = useState('')
38
  const [dadosTransformados, setDadosTransformados] = useState(null)
39
  const [resumoHtml, setResumoHtml] = useState('')
 
40
  const [coeficientes, setCoeficientes] = useState(null)
41
  const [obsCalc, setObsCalc] = useState(null)
42
 
@@ -67,6 +69,7 @@ export default function VisualizacaoTab({ sessionId }) {
67
  setEscalasHtml('')
68
  setDadosTransformados(null)
69
  setResumoHtml('')
 
70
  setCoeficientes(null)
71
  setObsCalc(null)
72
 
@@ -96,6 +99,7 @@ export default function VisualizacaoTab({ sessionId }) {
96
  setEscalasHtml(resp.escalas_html || '')
97
  setDadosTransformados(resp.dados_transformados || null)
98
  setResumoHtml(resp.resumo_html || '')
 
99
  setCoeficientes(resp.coeficientes || null)
100
  setObsCalc(resp.obs_calc || null)
101
 
@@ -359,6 +363,15 @@ export default function VisualizacaoTab({ sessionId }) {
359
  })
360
  }
361
 
 
 
 
 
 
 
 
 
 
362
  return (
363
  <div className="tab-content">
364
  <SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
@@ -468,7 +481,17 @@ export default function VisualizacaoTab({ sessionId }) {
468
  ) : null}
469
 
470
  {activeInnerTab === 'resumo' ? (
471
- <div dangerouslySetInnerHTML={{ __html: resumoHtml }} />
 
 
 
 
 
 
 
 
 
 
472
  ) : null}
473
 
474
  {activeInnerTab === 'coeficientes' ? (
 
1
  import React, { useEffect, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import DataTable from './DataTable'
4
+ import EquationFormatsPanel from './EquationFormatsPanel'
5
  import LoadingOverlay from './LoadingOverlay'
6
  import MapFrame from './MapFrame'
7
  import PlotFigure from './PlotFigure'
 
38
  const [escalasHtml, setEscalasHtml] = useState('')
39
  const [dadosTransformados, setDadosTransformados] = useState(null)
40
  const [resumoHtml, setResumoHtml] = useState('')
41
+ const [equacoes, setEquacoes] = useState(null)
42
  const [coeficientes, setCoeficientes] = useState(null)
43
  const [obsCalc, setObsCalc] = useState(null)
44
 
 
69
  setEscalasHtml('')
70
  setDadosTransformados(null)
71
  setResumoHtml('')
72
+ setEquacoes(null)
73
  setCoeficientes(null)
74
  setObsCalc(null)
75
 
 
99
  setEscalasHtml(resp.escalas_html || '')
100
  setDadosTransformados(resp.dados_transformados || null)
101
  setResumoHtml(resp.resumo_html || '')
102
+ setEquacoes(resp.equacoes || null)
103
  setCoeficientes(resp.coeficientes || null)
104
  setObsCalc(resp.obs_calc || null)
105
 
 
363
  })
364
  }
365
 
366
+ async function onDownloadEquacao(mode) {
367
+ if (!sessionId || !mode) return
368
+ await withBusy(async () => {
369
+ const blob = await api.exportEquationViz(sessionId, mode)
370
+ const sufixo = String(mode) === 'excel_sab' ? 'estilo_sab' : 'excel'
371
+ downloadBlob(blob, `equacao_modelo_${sufixo}.xlsx`)
372
+ })
373
+ }
374
+
375
  return (
376
  <div className="tab-content">
377
  <SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
 
481
  ) : null}
482
 
483
  {activeInnerTab === 'resumo' ? (
484
+ <>
485
+ <div className="equation-formats-section">
486
+ <h4>Equações do Modelo</h4>
487
+ <EquationFormatsPanel
488
+ equacoes={equacoes}
489
+ onDownload={(mode) => void onDownloadEquacao(mode)}
490
+ disabled={loading}
491
+ />
492
+ </div>
493
+ <div dangerouslySetInnerHTML={{ __html: resumoHtml }} />
494
+ </>
495
  ) : null}
496
 
497
  {activeInnerTab === 'coeficientes' ? (
frontend/src/styles.css CHANGED
@@ -3006,6 +3006,56 @@ button.btn-download-subtle {
3006
  word-break: break-word;
3007
  }
3008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3009
  .busca-container,
3010
  .dai-cards-grid {
3011
  display: grid;
 
3006
  word-break: break-word;
3007
  }
3008
 
3009
+ .equation-box-plain {
3010
+ margin-top: 0;
3011
+ background: #fff;
3012
+ border-color: #dbe7f1;
3013
+ border-left-color: #b9ccde;
3014
+ }
3015
+
3016
+ .equation-formats-section {
3017
+ margin: 14px 0 10px;
3018
+ }
3019
+
3020
+ .equation-formats-section h4 {
3021
+ margin: 0 0 8px;
3022
+ color: #2d4358;
3023
+ }
3024
+
3025
+ .equation-formats-wrap {
3026
+ display: grid;
3027
+ gap: 10px;
3028
+ }
3029
+
3030
+ .equation-format-card {
3031
+ border: 1px solid #dbe6f0;
3032
+ border-radius: 10px;
3033
+ background: #fbfdff;
3034
+ padding: 10px;
3035
+ }
3036
+
3037
+ .equation-format-head {
3038
+ display: flex;
3039
+ align-items: center;
3040
+ justify-content: space-between;
3041
+ gap: 10px;
3042
+ flex-wrap: wrap;
3043
+ margin-bottom: 8px;
3044
+ }
3045
+
3046
+ .equation-format-head h5 {
3047
+ margin: 0;
3048
+ color: #355067;
3049
+ font-size: 0.9rem;
3050
+ }
3051
+
3052
+ .equation-format-actions {
3053
+ display: inline-flex;
3054
+ align-items: center;
3055
+ gap: 8px;
3056
+ flex-wrap: wrap;
3057
+ }
3058
+
3059
  .busca-container,
3060
  .dai-cards-grid {
3061
  display: grid;