Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
1b337a7
1
Parent(s): 5c5c634
Refine elaboracao and trabalhos tecnicos flows
Browse files- backend/app/api/elaboracao.py +20 -0
- backend/app/api/trabalhos_tecnicos.py +25 -2
- backend/app/models/session.py +1 -0
- backend/app/services/elaboracao_service.py +103 -1
- backend/app/services/trabalhos_tecnicos_service.py +284 -1
- backend/run_backend.sh +46 -3
- frontend/src/api.js +3 -0
- frontend/src/components/ElaboracaoTab.jsx +407 -20
- frontend/src/components/TrabalhosTecnicosTab.jsx +126 -39
- frontend/src/styles.css +279 -0
backend/app/api/elaboracao.py
CHANGED
|
@@ -23,6 +23,14 @@ class ConfirmSheetPayload(SessionPayload):
|
|
| 23 |
sheet_name: str
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
class MapCoordsPayload(SessionPayload):
|
| 27 |
col_lat: str
|
| 28 |
col_lon: str
|
|
@@ -213,6 +221,18 @@ def confirm_sheet(payload: ConfirmSheetPayload) -> dict[str, Any]:
|
|
| 213 |
return elaboracao_service.load_uploaded_file(session, selected_sheet=payload.sheet_name)
|
| 214 |
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
@router.post("/map-coords")
|
| 217 |
def map_coords(payload: MapCoordsPayload) -> dict[str, Any]:
|
| 218 |
session = session_store.get(payload.session_id)
|
|
|
|
| 23 |
sheet_name: str
|
| 24 |
|
| 25 |
|
| 26 |
+
class PastedTablePayload(SessionPayload):
|
| 27 |
+
pasted_text: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ApplyImportColumnsPayload(SessionPayload):
|
| 31 |
+
colunas: list[str] = Field(default_factory=list)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
class MapCoordsPayload(SessionPayload):
|
| 35 |
col_lat: str
|
| 36 |
col_lon: str
|
|
|
|
| 221 |
return elaboracao_service.load_uploaded_file(session, selected_sheet=payload.sheet_name)
|
| 222 |
|
| 223 |
|
| 224 |
+
@router.post("/paste-table")
|
| 225 |
+
def paste_table(payload: PastedTablePayload) -> dict[str, Any]:
|
| 226 |
+
session = session_store.get(payload.session_id)
|
| 227 |
+
return elaboracao_service.load_pasted_table(session, payload.pasted_text)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@router.post("/apply-import-columns")
|
| 231 |
+
def apply_import_columns(payload: ApplyImportColumnsPayload) -> dict[str, Any]:
|
| 232 |
+
session = session_store.get(payload.session_id)
|
| 233 |
+
return elaboracao_service.apply_import_columns(session, payload.colunas)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
@router.post("/map-coords")
|
| 237 |
def map_coords(payload: MapCoordsPayload) -> dict[str, Any]:
|
| 238 |
session = session_store.get(payload.session_id)
|
backend/app/api/trabalhos_tecnicos.py
CHANGED
|
@@ -25,8 +25,7 @@ class TrabalhoImovelEdicaoPayload(BaseModel):
|
|
| 25 |
modelos: list[str] | None = None
|
| 26 |
|
| 27 |
|
| 28 |
-
class
|
| 29 |
-
trabalho_id: str
|
| 30 |
nome: str | None = None
|
| 31 |
tipo_codigo: str | None = None
|
| 32 |
ano: int | None = None
|
|
@@ -35,6 +34,14 @@ class TrabalhoSalvarPayload(BaseModel):
|
|
| 35 |
imoveis: list[TrabalhoImovelEdicaoPayload] | None = None
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
class TrabalhosMapaPayload(BaseModel):
|
| 39 |
trabalhos_ids: list[str] | None = None
|
| 40 |
modo_exibicao: str | None = None
|
|
@@ -109,3 +116,19 @@ def salvar_trabalho(payload: TrabalhoSalvarPayload, request: Request) -> dict[st
|
|
| 109 |
details={"trabalho_id": payload.trabalho_id},
|
| 110 |
)
|
| 111 |
return resposta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
modelos: list[str] | None = None
|
| 26 |
|
| 27 |
|
| 28 |
+
class TrabalhoPayloadBase(BaseModel):
|
|
|
|
| 29 |
nome: str | None = None
|
| 30 |
tipo_codigo: str | None = None
|
| 31 |
ano: int | None = None
|
|
|
|
| 34 |
imoveis: list[TrabalhoImovelEdicaoPayload] | None = None
|
| 35 |
|
| 36 |
|
| 37 |
+
class TrabalhoCadastrarPayload(TrabalhoPayloadBase):
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class TrabalhoSalvarPayload(TrabalhoPayloadBase):
|
| 42 |
+
trabalho_id: str
|
| 43 |
+
|
| 44 |
+
|
| 45 |
class TrabalhosMapaPayload(BaseModel):
|
| 46 |
trabalhos_ids: list[str] | None = None
|
| 47 |
modo_exibicao: str | None = None
|
|
|
|
| 116 |
details={"trabalho_id": payload.trabalho_id},
|
| 117 |
)
|
| 118 |
return resposta
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@router.post("/cadastrar")
|
| 122 |
+
def cadastrar_trabalho(payload: TrabalhoCadastrarPayload, request: Request) -> dict[str, Any]:
|
| 123 |
+
user = auth_service.require_user(request)
|
| 124 |
+
resposta = trabalhos_tecnicos_service.cadastrar_trabalho_tecnico(payload.model_dump())
|
| 125 |
+
trabalho = resposta.get("trabalho") if isinstance(resposta, dict) else {}
|
| 126 |
+
log_event(
|
| 127 |
+
"trabalhos_tecnicos",
|
| 128 |
+
"cadastrar_trabalho",
|
| 129 |
+
user=user,
|
| 130 |
+
status="ok",
|
| 131 |
+
request=request,
|
| 132 |
+
details={"trabalho_id": trabalho.get("id") if isinstance(trabalho, dict) else None},
|
| 133 |
+
)
|
| 134 |
+
return resposta
|
backend/app/models/session.py
CHANGED
|
@@ -16,6 +16,7 @@ class SessionState:
|
|
| 16 |
uploaded_filename: str | None = None
|
| 17 |
available_sheets: list[str] = field(default_factory=list)
|
| 18 |
|
|
|
|
| 19 |
df_original: pd.DataFrame | None = None
|
| 20 |
df_filtrado: pd.DataFrame | None = None
|
| 21 |
df_geo_origem: pd.DataFrame | None = None
|
|
|
|
| 16 |
uploaded_filename: str | None = None
|
| 17 |
available_sheets: list[str] = field(default_factory=list)
|
| 18 |
|
| 19 |
+
df_import_original: pd.DataFrame | None = None
|
| 20 |
df_original: pd.DataFrame | None = None
|
| 21 |
df_filtrado: pd.DataFrame | None = None
|
| 22 |
df_geo_origem: pd.DataFrame | None = None
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -1047,6 +1047,56 @@ def _set_dataframe_base(
|
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1050 |
def save_uploaded_file(session: SessionState, filename: str, content: bytes) -> str:
|
| 1051 |
safe_name = os.path.basename(filename)
|
| 1052 |
destino = session.workdir / safe_name
|
|
@@ -1080,7 +1130,7 @@ def load_uploaded_file(session: SessionState, selected_sheet: str | None = None)
|
|
| 1080 |
if not sucesso or df is None:
|
| 1081 |
raise HTTPException(status_code=400, detail=msg)
|
| 1082 |
|
| 1083 |
-
|
| 1084 |
sheet_selected_result = ""
|
| 1085 |
if selected_sheet is not None:
|
| 1086 |
sheet_selected_result = str(selected_sheet)
|
|
@@ -1089,8 +1139,60 @@ def load_uploaded_file(session: SessionState, selected_sheet: str | None = None)
|
|
| 1089 |
return {
|
| 1090 |
"status": msg,
|
| 1091 |
"requires_sheet": False,
|
|
|
|
| 1092 |
"sheets": [],
|
| 1093 |
"sheet_selected": sheet_selected_result,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
**base,
|
| 1095 |
"contexto": _selection_context(session),
|
| 1096 |
}
|
|
|
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
|
| 1050 |
+
def _build_import_preview(df: pd.DataFrame, max_rows: int = 15) -> dict[str, Any]:
|
| 1051 |
+
total_rows = len(df.index)
|
| 1052 |
+
total_columns = len(df.columns)
|
| 1053 |
+
sample_size = min(max_rows, total_rows)
|
| 1054 |
+
if sample_size > 0:
|
| 1055 |
+
preview_df = df.sample(n=sample_size)
|
| 1056 |
+
else:
|
| 1057 |
+
preview_df = df.head(0)
|
| 1058 |
+
|
| 1059 |
+
rows: list[dict[str, Any]] = []
|
| 1060 |
+
columns = [str(c) for c in df.columns]
|
| 1061 |
+
for tuple_row in preview_df.itertuples(index=True, name=None):
|
| 1062 |
+
row = {"_index": sanitize_value(tuple_row[0])}
|
| 1063 |
+
for col, value in zip(columns, tuple_row[1:]):
|
| 1064 |
+
row[col] = sanitize_value(value)
|
| 1065 |
+
rows.append(row)
|
| 1066 |
+
|
| 1067 |
+
return {
|
| 1068 |
+
"columns": columns,
|
| 1069 |
+
"rows": rows,
|
| 1070 |
+
"total_rows": total_rows,
|
| 1071 |
+
"total_columns": total_columns,
|
| 1072 |
+
"returned_rows": len(rows),
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
+
def _set_import_pending_base(session: SessionState, df: pd.DataFrame) -> None:
|
| 1077 |
+
session.df_import_original = df.copy()
|
| 1078 |
+
session.df_original = None
|
| 1079 |
+
session.df_filtrado = None
|
| 1080 |
+
session.df_geo_origem = df.copy()
|
| 1081 |
+
session.geo_falhas_df = None
|
| 1082 |
+
session.geo_col_cdlog = None
|
| 1083 |
+
session.geo_col_num = None
|
| 1084 |
+
session.mapa_habilitado = False
|
| 1085 |
+
session.reset_modelo()
|
| 1086 |
+
session.coluna_y = None
|
| 1087 |
+
session.tipo_y = None
|
| 1088 |
+
session.coluna_area = None
|
| 1089 |
+
session.colunas_x = []
|
| 1090 |
+
session.dicotomicas = []
|
| 1091 |
+
session.codigo_alocado = []
|
| 1092 |
+
session.percentuais = []
|
| 1093 |
+
session.outliers_anteriores = []
|
| 1094 |
+
session.iteracao = 1
|
| 1095 |
+
session.coluna_data_mercado = None
|
| 1096 |
+
session.periodo_dados_mercado_inicio = None
|
| 1097 |
+
session.periodo_dados_mercado_fim = None
|
| 1098 |
+
|
| 1099 |
+
|
| 1100 |
def save_uploaded_file(session: SessionState, filename: str, content: bytes) -> str:
|
| 1101 |
safe_name = os.path.basename(filename)
|
| 1102 |
destino = session.workdir / safe_name
|
|
|
|
| 1130 |
if not sucesso or df is None:
|
| 1131 |
raise HTTPException(status_code=400, detail=msg)
|
| 1132 |
|
| 1133 |
+
_set_import_pending_base(session, df)
|
| 1134 |
sheet_selected_result = ""
|
| 1135 |
if selected_sheet is not None:
|
| 1136 |
sheet_selected_result = str(selected_sheet)
|
|
|
|
| 1139 |
return {
|
| 1140 |
"status": msg,
|
| 1141 |
"requires_sheet": False,
|
| 1142 |
+
"requires_column_selection": True,
|
| 1143 |
"sheets": [],
|
| 1144 |
"sheet_selected": sheet_selected_result,
|
| 1145 |
+
"import_preview": _build_import_preview(df),
|
| 1146 |
+
"contexto": _selection_context(session),
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
|
| 1150 |
+
def load_pasted_table(session: SessionState, pasted_text: str) -> dict[str, Any]:
|
| 1151 |
+
texto = str(pasted_text or "").replace("\r\n", "\n").replace("\r", "\n").strip("\n")
|
| 1152 |
+
if not texto.strip():
|
| 1153 |
+
raise HTTPException(status_code=400, detail="Cole os dados copiados do Excel antes de carregar.")
|
| 1154 |
+
|
| 1155 |
+
linhas_validas = [linha for linha in texto.split("\n") if linha.strip()]
|
| 1156 |
+
if len(linhas_validas) < 2:
|
| 1157 |
+
raise HTTPException(status_code=400, detail="Cole pelo menos uma linha de cabeçalho e uma linha de dados.")
|
| 1158 |
+
|
| 1159 |
+
nome_arquivo = f"dados_colados_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 1160 |
+
save_uploaded_file(session, nome_arquivo, texto.encode("utf-8-sig"))
|
| 1161 |
+
resposta = load_uploaded_file(session)
|
| 1162 |
+
resposta["status"] = resposta.get("status") or "Dados colados carregados."
|
| 1163 |
+
resposta["pasted_table"] = True
|
| 1164 |
+
resposta["uploaded_filename"] = nome_arquivo
|
| 1165 |
+
return resposta
|
| 1166 |
+
|
| 1167 |
+
|
| 1168 |
+
def apply_import_columns(session: SessionState, colunas: list[str]) -> dict[str, Any]:
|
| 1169 |
+
df_origem = session.df_import_original
|
| 1170 |
+
if df_origem is None:
|
| 1171 |
+
df_origem = session.df_original
|
| 1172 |
+
if df_origem is None:
|
| 1173 |
+
raise HTTPException(status_code=400, detail="Carregue um arquivo tabular primeiro.")
|
| 1174 |
+
|
| 1175 |
+
disponiveis = {str(col): col for col in df_origem.columns}
|
| 1176 |
+
selecionadas: list[str] = []
|
| 1177 |
+
vistos: set[str] = set()
|
| 1178 |
+
for item in colunas or []:
|
| 1179 |
+
coluna = str(item or "").strip()
|
| 1180 |
+
if not coluna or coluna in vistos or coluna not in disponiveis:
|
| 1181 |
+
continue
|
| 1182 |
+
selecionadas.append(coluna)
|
| 1183 |
+
vistos.add(coluna)
|
| 1184 |
+
|
| 1185 |
+
if len(selecionadas) < 2:
|
| 1186 |
+
raise HTTPException(status_code=400, detail="Selecione pelo menos 2 colunas para seguir.")
|
| 1187 |
+
|
| 1188 |
+
df_selecionado = df_origem.loc[:, [disponiveis[coluna] for coluna in selecionadas]].copy()
|
| 1189 |
+
base = _set_dataframe_base(session, df_selecionado, clear_models=True)
|
| 1190 |
+
session.df_import_original = df_origem.copy()
|
| 1191 |
+
|
| 1192 |
+
return {
|
| 1193 |
+
"status": f"Seleção aplicada: {len(selecionadas)} colunas farão parte do modelo.",
|
| 1194 |
+
"requires_column_selection": False,
|
| 1195 |
+
"import_columns_selected": selecionadas,
|
| 1196 |
**base,
|
| 1197 |
"contexto": _selection_context(session),
|
| 1198 |
}
|
backend/app/services/trabalhos_tecnicos_service.py
CHANGED
|
@@ -3,7 +3,9 @@ from __future__ import annotations
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
import json
|
| 5 |
import math
|
|
|
|
| 6 |
import sqlite3
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
from statistics import median
|
| 9 |
from threading import Lock
|
|
@@ -42,6 +44,14 @@ _MODEL_MATCH_CACHE_CATALOGO: dict[str, dict[str, str]] = {}
|
|
| 42 |
_MODEL_MATCH_CACHE_TRABALHOS: dict[str, set[str]] = {}
|
| 43 |
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def _connect_database(path: Path) -> sqlite3.Connection:
|
| 46 |
conn = sqlite3.connect(str(path))
|
| 47 |
conn.row_factory = sqlite3.Row
|
|
@@ -745,6 +755,246 @@ def _normalizar_edicao_payload(
|
|
| 745 |
)
|
| 746 |
|
| 747 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
def _listar_avaliandos_tecnicos(chaves_modelo: list[str] | None = None) -> list[dict[str, Any]]:
|
| 749 |
aliases = [str(item or "").strip() for item in (chaves_modelo or []) if str(item or "").strip()]
|
| 750 |
filtrar_por_modelo = chaves_modelo is not None
|
|
@@ -779,7 +1029,7 @@ def _listar_avaliandos_tecnicos(chaves_modelo: list[str] | None = None) -> list[
|
|
| 779 |
FROM trabalho_imoveis ti
|
| 780 |
JOIN trabalhos t
|
| 781 |
ON t.trabalho_id = ti.trabalho_id
|
| 782 |
-
JOIN trabalho_imovel_modelos tim
|
| 783 |
ON tim.imovel_id = ti.imovel_id
|
| 784 |
ORDER BY LOWER(t.nome), ti.imovel_id, LOWER(tim.modelo_nome)
|
| 785 |
"""
|
|
@@ -1393,6 +1643,38 @@ def detalhar_trabalho(trabalho_id: str) -> dict[str, Any]:
|
|
| 1393 |
)
|
| 1394 |
|
| 1395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1396 |
def salvar_edicao_trabalho(trabalho_id: str, payload: dict[str, Any] | None) -> dict[str, Any]:
|
| 1397 |
chave = str(trabalho_id or "").strip()
|
| 1398 |
if not chave:
|
|
@@ -1432,4 +1714,5 @@ def salvar_edicao_trabalho(trabalho_id: str, payload: dict[str, Any] | None) ->
|
|
| 1432 |
except Exception:
|
| 1433 |
pass
|
| 1434 |
|
|
|
|
| 1435 |
return detalhar_trabalho(chave)
|
|
|
|
| 3 |
from datetime import datetime, timezone
|
| 4 |
import json
|
| 5 |
import math
|
| 6 |
+
import re
|
| 7 |
import sqlite3
|
| 8 |
+
import unicodedata
|
| 9 |
from pathlib import Path
|
| 10 |
from statistics import median
|
| 11 |
from threading import Lock
|
|
|
|
| 44 |
_MODEL_MATCH_CACHE_TRABALHOS: dict[str, set[str]] = {}
|
| 45 |
|
| 46 |
|
| 47 |
+
def _clear_model_match_cache() -> None:
|
| 48 |
+
global _MODEL_MATCH_CACHE_SIGNATURE, _MODEL_MATCH_CACHE_CATALOGO, _MODEL_MATCH_CACHE_TRABALHOS
|
| 49 |
+
with _MODEL_MATCH_CACHE_LOCK:
|
| 50 |
+
_MODEL_MATCH_CACHE_SIGNATURE = None
|
| 51 |
+
_MODEL_MATCH_CACHE_CATALOGO = {}
|
| 52 |
+
_MODEL_MATCH_CACHE_TRABALHOS = {}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
def _connect_database(path: Path) -> sqlite3.Connection:
|
| 56 |
conn = sqlite3.connect(str(path))
|
| 57 |
conn.row_factory = sqlite3.Row
|
|
|
|
| 755 |
)
|
| 756 |
|
| 757 |
|
| 758 |
+
def _slug_trabalho_fragment(value: Any) -> str:
|
| 759 |
+
texto = str(value or "").strip()
|
| 760 |
+
if not texto:
|
| 761 |
+
return ""
|
| 762 |
+
sem_acentos = unicodedata.normalize("NFKD", texto).encode("ascii", "ignore").decode("ascii")
|
| 763 |
+
slug = re.sub(r"[^A-Za-z0-9]+", "_", sem_acentos).strip("_")
|
| 764 |
+
return slug.upper()
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def _gerar_trabalho_id_unico(
|
| 768 |
+
conn: sqlite3.Connection,
|
| 769 |
+
nome: str,
|
| 770 |
+
tipo_codigo: str,
|
| 771 |
+
ano: int | None,
|
| 772 |
+
) -> str:
|
| 773 |
+
partes = ["MANUAL", tipo_codigo, str(ano or ""), _slug_trabalho_fragment(nome)]
|
| 774 |
+
base = "_".join([parte for parte in partes if parte]).strip("_")
|
| 775 |
+
base = (base or "MANUAL_TRABALHO")[:96].rstrip("_") or "MANUAL_TRABALHO"
|
| 776 |
+
|
| 777 |
+
def existe(candidate: str) -> bool:
|
| 778 |
+
row = conn.execute(
|
| 779 |
+
"SELECT 1 FROM trabalhos WHERE LOWER(trabalho_id) = LOWER(?) LIMIT 1",
|
| 780 |
+
(candidate,),
|
| 781 |
+
).fetchone()
|
| 782 |
+
if row is not None:
|
| 783 |
+
return True
|
| 784 |
+
_ensure_overrides_table(conn)
|
| 785 |
+
row = conn.execute(
|
| 786 |
+
"SELECT 1 FROM trabalho_edicoes WHERE LOWER(trabalho_id) = LOWER(?) LIMIT 1",
|
| 787 |
+
(candidate,),
|
| 788 |
+
).fetchone()
|
| 789 |
+
return row is not None
|
| 790 |
+
|
| 791 |
+
candidate = base
|
| 792 |
+
suffix = 2
|
| 793 |
+
while existe(candidate):
|
| 794 |
+
suffix_text = f"_{suffix}"
|
| 795 |
+
candidate = f"{base[:96 - len(suffix_text)]}{suffix_text}"
|
| 796 |
+
suffix += 1
|
| 797 |
+
return candidate
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
def _normalizar_imoveis_cadastro(values: Any, modelos_padrao: list[str]) -> list[dict[str, Any]]:
|
| 801 |
+
if not isinstance(values, list):
|
| 802 |
+
return []
|
| 803 |
+
|
| 804 |
+
filtrados: list[dict[str, Any]] = []
|
| 805 |
+
for raw in values:
|
| 806 |
+
if not isinstance(raw, dict):
|
| 807 |
+
continue
|
| 808 |
+
tem_texto = any(
|
| 809 |
+
str(raw.get(key) or "").strip()
|
| 810 |
+
for key in ("label", "endereco", "numero")
|
| 811 |
+
)
|
| 812 |
+
tem_coord = _to_float_or_none(raw.get("coord_x")) is not None or _to_float_or_none(raw.get("coord_y")) is not None
|
| 813 |
+
if tem_texto or tem_coord:
|
| 814 |
+
filtrados.append(raw)
|
| 815 |
+
return _normalizar_imoveis_edicao(filtrados, modelos_padrao)
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
def _normalizar_cadastro_payload(
|
| 819 |
+
payload: dict[str, Any],
|
| 820 |
+
catalogo_modelos: dict[str, dict[str, str]],
|
| 821 |
+
) -> dict[str, Any]:
|
| 822 |
+
nome = str(payload.get("nome") or "").strip()
|
| 823 |
+
if not nome:
|
| 824 |
+
raise HTTPException(status_code=400, detail="Informe o nome do trabalho tecnico.")
|
| 825 |
+
|
| 826 |
+
tipo_codigo = str(payload.get("tipo_codigo") or "").strip().upper()
|
| 827 |
+
if not tipo_codigo:
|
| 828 |
+
raise HTTPException(status_code=400, detail="Informe o tipo do trabalho tecnico.")
|
| 829 |
+
if tipo_codigo not in TIPO_LABELS:
|
| 830 |
+
raise HTTPException(status_code=400, detail="Selecione um tipo valido para o trabalho tecnico.")
|
| 831 |
+
|
| 832 |
+
ano = _to_int_or_none(payload.get("ano"))
|
| 833 |
+
processos_sei = _dedupe_text_list(payload.get("processos_sei"))
|
| 834 |
+
modelos = _dedupe_text_list(payload.get("modelos"))
|
| 835 |
+
imoveis = _normalizar_imoveis_cadastro(payload.get("imoveis"), modelos)
|
| 836 |
+
if not imoveis:
|
| 837 |
+
raise HTTPException(status_code=400, detail="Informe pelo menos um imovel vinculado ao trabalho tecnico.")
|
| 838 |
+
|
| 839 |
+
for imovel in imoveis:
|
| 840 |
+
modelos_imovel = _dedupe_text_list(imovel.get("modelos"))
|
| 841 |
+
imovel["modelos"] = modelos_imovel or list(modelos)
|
| 842 |
+
|
| 843 |
+
_enriquecer_modelos(modelos, catalogo_modelos)
|
| 844 |
+
|
| 845 |
+
return sanitize_value(
|
| 846 |
+
{
|
| 847 |
+
"nome": nome,
|
| 848 |
+
"tipo_codigo": tipo_codigo,
|
| 849 |
+
"ano": ano,
|
| 850 |
+
"processos_sei": processos_sei,
|
| 851 |
+
"modelos": modelos,
|
| 852 |
+
"imoveis": imoveis,
|
| 853 |
+
}
|
| 854 |
+
)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def _inserir_trabalho_manual(
|
| 858 |
+
conn: sqlite3.Connection,
|
| 859 |
+
trabalho_id: str,
|
| 860 |
+
normalized: dict[str, Any],
|
| 861 |
+
) -> None:
|
| 862 |
+
nome = str(normalized.get("nome") or "").strip()
|
| 863 |
+
tipo_codigo = str(normalized.get("tipo_codigo") or "").strip().upper()
|
| 864 |
+
ano = _to_int_or_none(normalized.get("ano"))
|
| 865 |
+
processos_sei = _dedupe_text_list(normalized.get("processos_sei"))
|
| 866 |
+
modelos = _dedupe_text_list(normalized.get("modelos"))
|
| 867 |
+
imoveis = list(normalized.get("imoveis") or [])
|
| 868 |
+
primeiro_imovel = imoveis[0] if imoveis else {}
|
| 869 |
+
endereco_resumo = _resumo_enderecos(imoveis) or "Endereco nao informado"
|
| 870 |
+
modelo_resumo = _resumo_modelos(modelos) or "Sem modelo informado"
|
| 871 |
+
tem_coordenadas = any(
|
| 872 |
+
_coordenada_valida(item.get("coord_x")) and _coordenada_valida(item.get("coord_y"))
|
| 873 |
+
for item in imoveis
|
| 874 |
+
)
|
| 875 |
+
|
| 876 |
+
registros: list[tuple[dict[str, Any], str | None, str | None]] = []
|
| 877 |
+
processos_registro = processos_sei or [None]
|
| 878 |
+
modelos_registro = modelos or [None]
|
| 879 |
+
for imovel in imoveis or [{}]:
|
| 880 |
+
for modelo_nome in modelos_registro:
|
| 881 |
+
for processo in processos_registro:
|
| 882 |
+
registros.append((imovel, modelo_nome, processo))
|
| 883 |
+
|
| 884 |
+
conn.execute(
|
| 885 |
+
"""
|
| 886 |
+
INSERT INTO trabalhos (
|
| 887 |
+
trabalho_id,
|
| 888 |
+
nome,
|
| 889 |
+
nome_original,
|
| 890 |
+
codigo_trabalho,
|
| 891 |
+
nome_pasta,
|
| 892 |
+
tipo_codigo,
|
| 893 |
+
tipo_label,
|
| 894 |
+
ano,
|
| 895 |
+
endereco_principal,
|
| 896 |
+
numero_principal,
|
| 897 |
+
endereco_resumo,
|
| 898 |
+
modelo_resumo,
|
| 899 |
+
tecnico_resumo,
|
| 900 |
+
processo_resumo,
|
| 901 |
+
finalidade_processo_resumo,
|
| 902 |
+
total_registros,
|
| 903 |
+
total_imoveis,
|
| 904 |
+
total_modelos,
|
| 905 |
+
tem_coordenadas
|
| 906 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 907 |
+
""",
|
| 908 |
+
(
|
| 909 |
+
trabalho_id,
|
| 910 |
+
nome,
|
| 911 |
+
nome,
|
| 912 |
+
None,
|
| 913 |
+
None,
|
| 914 |
+
tipo_codigo,
|
| 915 |
+
_tipo_label_from_codigo(tipo_codigo),
|
| 916 |
+
ano,
|
| 917 |
+
str(primeiro_imovel.get("endereco") or "").strip() or None,
|
| 918 |
+
str(primeiro_imovel.get("numero") or "").strip() or None,
|
| 919 |
+
endereco_resumo,
|
| 920 |
+
modelo_resumo,
|
| 921 |
+
None,
|
| 922 |
+
_resumo_textos(processos_sei, limite_inline=2) or None,
|
| 923 |
+
None,
|
| 924 |
+
max(1, len(registros)),
|
| 925 |
+
len(imoveis),
|
| 926 |
+
len(modelos),
|
| 927 |
+
1 if tem_coordenadas else 0,
|
| 928 |
+
),
|
| 929 |
+
)
|
| 930 |
+
|
| 931 |
+
for ordem, modelo_nome in enumerate(modelos, start=1):
|
| 932 |
+
conn.execute(
|
| 933 |
+
"INSERT INTO trabalho_modelos (trabalho_id, modelo_nome, ordem) VALUES (?, ?, ?)",
|
| 934 |
+
(trabalho_id, modelo_nome, ordem),
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
+
for imovel in imoveis:
|
| 938 |
+
cursor = conn.execute(
|
| 939 |
+
"""
|
| 940 |
+
INSERT INTO trabalho_imoveis (trabalho_id, endereco, numero, label, coord_x, coord_y)
|
| 941 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 942 |
+
""",
|
| 943 |
+
(
|
| 944 |
+
trabalho_id,
|
| 945 |
+
str(imovel.get("endereco") or "").strip() or None,
|
| 946 |
+
str(imovel.get("numero") or "").strip() or None,
|
| 947 |
+
str(imovel.get("label") or "").strip() or "Imovel",
|
| 948 |
+
_to_float_or_none(imovel.get("coord_x")),
|
| 949 |
+
_to_float_or_none(imovel.get("coord_y")),
|
| 950 |
+
),
|
| 951 |
+
)
|
| 952 |
+
imovel_id = int(cursor.lastrowid)
|
| 953 |
+
for modelo_nome in _dedupe_text_list(imovel.get("modelos")):
|
| 954 |
+
conn.execute(
|
| 955 |
+
"INSERT INTO trabalho_imovel_modelos (imovel_id, modelo_nome) VALUES (?, ?)",
|
| 956 |
+
(imovel_id, modelo_nome),
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
for source_row, (imovel, modelo_nome, processo) in enumerate(registros, start=1):
|
| 960 |
+
conn.execute(
|
| 961 |
+
"""
|
| 962 |
+
INSERT INTO trabalho_registros (
|
| 963 |
+
trabalho_id,
|
| 964 |
+
source_row,
|
| 965 |
+
ano,
|
| 966 |
+
nome_original,
|
| 967 |
+
codigo_trabalho,
|
| 968 |
+
nome_pasta,
|
| 969 |
+
tecnico,
|
| 970 |
+
processo,
|
| 971 |
+
finalidade_processo,
|
| 972 |
+
modelo_nome,
|
| 973 |
+
endereco,
|
| 974 |
+
numero,
|
| 975 |
+
coord_x,
|
| 976 |
+
coord_y
|
| 977 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 978 |
+
""",
|
| 979 |
+
(
|
| 980 |
+
trabalho_id,
|
| 981 |
+
source_row,
|
| 982 |
+
ano,
|
| 983 |
+
nome,
|
| 984 |
+
None,
|
| 985 |
+
None,
|
| 986 |
+
None,
|
| 987 |
+
processo,
|
| 988 |
+
None,
|
| 989 |
+
modelo_nome,
|
| 990 |
+
str(imovel.get("endereco") or "").strip() or None,
|
| 991 |
+
str(imovel.get("numero") or "").strip() or None,
|
| 992 |
+
_to_float_or_none(imovel.get("coord_x")),
|
| 993 |
+
_to_float_or_none(imovel.get("coord_y")),
|
| 994 |
+
),
|
| 995 |
+
)
|
| 996 |
+
|
| 997 |
+
|
| 998 |
def _listar_avaliandos_tecnicos(chaves_modelo: list[str] | None = None) -> list[dict[str, Any]]:
|
| 999 |
aliases = [str(item or "").strip() for item in (chaves_modelo or []) if str(item or "").strip()]
|
| 1000 |
filtrar_por_modelo = chaves_modelo is not None
|
|
|
|
| 1029 |
FROM trabalho_imoveis ti
|
| 1030 |
JOIN trabalhos t
|
| 1031 |
ON t.trabalho_id = ti.trabalho_id
|
| 1032 |
+
LEFT JOIN trabalho_imovel_modelos tim
|
| 1033 |
ON tim.imovel_id = ti.imovel_id
|
| 1034 |
ORDER BY LOWER(t.nome), ti.imovel_id, LOWER(tim.modelo_nome)
|
| 1035 |
"""
|
|
|
|
| 1643 |
)
|
| 1644 |
|
| 1645 |
|
| 1646 |
+
def cadastrar_trabalho_tecnico(payload: dict[str, Any] | None) -> dict[str, Any]:
|
| 1647 |
+
resolved = trabalhos_tecnicos_repository.resolve_database()
|
| 1648 |
+
if resolved.provider != "local":
|
| 1649 |
+
raise HTTPException(
|
| 1650 |
+
status_code=403,
|
| 1651 |
+
detail="Cadastro de trabalhos tecnicos disponivel apenas na base local neste momento",
|
| 1652 |
+
)
|
| 1653 |
+
|
| 1654 |
+
catalogo_modelos = _catalogo_modelos_mesa()
|
| 1655 |
+
conn = _connect_database(resolved.db_path)
|
| 1656 |
+
trabalho_id = ""
|
| 1657 |
+
try:
|
| 1658 |
+
normalized = _normalizar_cadastro_payload(payload or {}, catalogo_modelos)
|
| 1659 |
+
_ensure_overrides_table(conn)
|
| 1660 |
+
trabalho_id = _gerar_trabalho_id_unico(
|
| 1661 |
+
conn,
|
| 1662 |
+
str(normalized.get("nome") or ""),
|
| 1663 |
+
str(normalized.get("tipo_codigo") or ""),
|
| 1664 |
+
_to_int_or_none(normalized.get("ano")),
|
| 1665 |
+
)
|
| 1666 |
+
_inserir_trabalho_manual(conn, trabalho_id, normalized)
|
| 1667 |
+
conn.commit()
|
| 1668 |
+
finally:
|
| 1669 |
+
try:
|
| 1670 |
+
conn.close()
|
| 1671 |
+
except Exception:
|
| 1672 |
+
pass
|
| 1673 |
+
|
| 1674 |
+
_clear_model_match_cache()
|
| 1675 |
+
return detalhar_trabalho(trabalho_id)
|
| 1676 |
+
|
| 1677 |
+
|
| 1678 |
def salvar_edicao_trabalho(trabalho_id: str, payload: dict[str, Any] | None) -> dict[str, Any]:
|
| 1679 |
chave = str(trabalho_id or "").strip()
|
| 1680 |
if not chave:
|
|
|
|
| 1714 |
except Exception:
|
| 1715 |
pass
|
| 1716 |
|
| 1717 |
+
_clear_model_match_cache()
|
| 1718 |
return detalhar_trabalho(chave)
|
backend/run_backend.sh
CHANGED
|
@@ -4,10 +4,53 @@ set -euo pipefail
|
|
| 4 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
cd "${SCRIPT_DIR}"
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
# shellcheck disable=SC1091
|
| 10 |
-
source "${
|
| 11 |
fi
|
| 12 |
|
| 13 |
uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" --reload --reload-dir "${SCRIPT_DIR}"
|
|
|
|
| 4 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
cd "${SCRIPT_DIR}"
|
| 6 |
|
| 7 |
+
VENV_DIR="${SCRIPT_DIR}/.venv"
|
| 8 |
+
VENV_PYTHON="${VENV_DIR}/bin/python"
|
| 9 |
+
VENV_UVICORN="${VENV_DIR}/bin/uvicorn"
|
| 10 |
+
|
| 11 |
+
find_bootstrap_python() {
|
| 12 |
+
local candidate
|
| 13 |
+
for candidate in python3.12 /Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12 python3 python; do
|
| 14 |
+
if command -v "${candidate}" >/dev/null 2>&1; then
|
| 15 |
+
command -v "${candidate}"
|
| 16 |
+
return 0
|
| 17 |
+
fi
|
| 18 |
+
done
|
| 19 |
+
|
| 20 |
+
return 1
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
ensure_backend_venv() {
|
| 24 |
+
local bootstrap_python
|
| 25 |
+
|
| 26 |
+
bootstrap_python="$(find_bootstrap_python)" || {
|
| 27 |
+
cat >&2 <<'EOF'
|
| 28 |
+
[backend] Nenhum interpretador Python compativel foi encontrado.
|
| 29 |
+
[backend] Instale o Python 3.12 e rode ./run_backend.sh novamente.
|
| 30 |
+
EOF
|
| 31 |
+
return 1
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
if [[ ! -x "${VENV_PYTHON}" ]]; then
|
| 35 |
+
echo "[backend] Recriando .venv com ${bootstrap_python}..." >&2
|
| 36 |
+
rm -rf "${VENV_DIR}"
|
| 37 |
+
"${bootstrap_python}" -m venv "${VENV_DIR}"
|
| 38 |
+
fi
|
| 39 |
+
|
| 40 |
+
# shellcheck disable=SC1091
|
| 41 |
+
source "${VENV_DIR}/bin/activate"
|
| 42 |
+
|
| 43 |
+
if [[ ! -x "${VENV_UVICORN}" ]]; then
|
| 44 |
+
echo "[backend] Instalando dependencias do backend..." >&2
|
| 45 |
+
python -m pip install --retries 5 --timeout 60 -r "${SCRIPT_DIR}/requirements.txt" >&2
|
| 46 |
+
fi
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
ensure_backend_venv
|
| 50 |
+
|
| 51 |
+
if [[ -f "${VENV_DIR}/bin/activate" ]]; then
|
| 52 |
# shellcheck disable=SC1091
|
| 53 |
+
source "${VENV_DIR}/bin/activate"
|
| 54 |
fi
|
| 55 |
|
| 56 |
uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" --reload --reload-dir "${SCRIPT_DIR}"
|
frontend/src/api.js
CHANGED
|
@@ -235,7 +235,9 @@ export const api = {
|
|
| 235 |
elaboracaoRepositorioModelos: () => getJson('/api/elaboracao/repositorio-modelos'),
|
| 236 |
elaboracaoRepositorioCarregar: (sessionId, modeloId) => postJson('/api/elaboracao/repositorio-carregar', { session_id: sessionId, modelo_id: modeloId }),
|
| 237 |
|
|
|
|
| 238 |
confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }),
|
|
|
|
| 239 |
mapCoords: (sessionId, colLat, colLon) => postJson('/api/elaboracao/map-coords', { session_id: sessionId, col_lat: colLat, col_lon: colLon }),
|
| 240 |
geocodificar: (sessionId, colCdlog, colNum, auto200) => postJson('/api/elaboracao/geocodificar', { session_id: sessionId, col_cdlog: colCdlog, col_num: colNum, auto_200: auto200 }),
|
| 241 |
geocodificarCorrecoes: (sessionId, correcoes, auto200) => postJson('/api/elaboracao/geocodificar-correcoes', { session_id: sessionId, correcoes, auto_200: auto200 }),
|
|
@@ -431,6 +433,7 @@ export const api = {
|
|
| 431 |
avaliando_lon: avaliando?.lon ?? null,
|
| 432 |
}),
|
| 433 |
trabalhosTecnicosDetalhe: (trabalhoId) => postJson('/api/trabalhos-tecnicos/detalhe', { trabalho_id: trabalhoId }),
|
|
|
|
| 434 |
trabalhosTecnicosSalvar: (payload = {}) => postJson('/api/trabalhos-tecnicos/salvar', payload),
|
| 435 |
|
| 436 |
logsStatus: () => getJson('/api/logs/status'),
|
|
|
|
| 235 |
elaboracaoRepositorioModelos: () => getJson('/api/elaboracao/repositorio-modelos'),
|
| 236 |
elaboracaoRepositorioCarregar: (sessionId, modeloId) => postJson('/api/elaboracao/repositorio-carregar', { session_id: sessionId, modelo_id: modeloId }),
|
| 237 |
|
| 238 |
+
pasteElaboracaoTable: (sessionId, pastedText) => postJson('/api/elaboracao/paste-table', { session_id: sessionId, pasted_text: pastedText }),
|
| 239 |
confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }),
|
| 240 |
+
applyImportColumns: (sessionId, colunas) => postJson('/api/elaboracao/apply-import-columns', { session_id: sessionId, colunas }),
|
| 241 |
mapCoords: (sessionId, colLat, colLon) => postJson('/api/elaboracao/map-coords', { session_id: sessionId, col_lat: colLat, col_lon: colLon }),
|
| 242 |
geocodificar: (sessionId, colCdlog, colNum, auto200) => postJson('/api/elaboracao/geocodificar', { session_id: sessionId, col_cdlog: colCdlog, col_num: colNum, auto_200: auto200 }),
|
| 243 |
geocodificarCorrecoes: (sessionId, correcoes, auto200) => postJson('/api/elaboracao/geocodificar-correcoes', { session_id: sessionId, correcoes, auto_200: auto200 }),
|
|
|
|
| 433 |
avaliando_lon: avaliando?.lon ?? null,
|
| 434 |
}),
|
| 435 |
trabalhosTecnicosDetalhe: (trabalhoId) => postJson('/api/trabalhos-tecnicos/detalhe', { trabalho_id: trabalhoId }),
|
| 436 |
+
trabalhosTecnicosCadastrar: (payload = {}) => postJson('/api/trabalhos-tecnicos/cadastrar', payload),
|
| 437 |
trabalhosTecnicosSalvar: (payload = {}) => postJson('/api/trabalhos-tecnicos/salvar', payload),
|
| 438 |
|
| 439 |
logsStatus: () => getJson('/api/logs/status'),
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -908,6 +908,7 @@ function buildArquivoCarregadoInfo(resp, options = {}) {
|
|
| 908 |
const isDai = tipo === 'dai' || fileNameLower.endsWith('.dai')
|
| 909 |
if (isDai) return null
|
| 910 |
|
|
|
|
| 911 |
const extMatch = fileName.match(/\.([^.]+)$/)
|
| 912 |
const tipoArquivo = extMatch?.[1] ? String(extMatch[1]).toUpperCase() : '-'
|
| 913 |
const sheetName = String(options.sheetName || resp?.sheet_selected || '').trim()
|
|
@@ -915,8 +916,16 @@ function buildArquivoCarregadoInfo(resp, options = {}) {
|
|
| 915 |
? Number(resp.dados.total_rows)
|
| 916 |
: Array.isArray(resp?.dados?.rows)
|
| 917 |
? resp.dados.rows.length
|
| 918 |
-
:
|
| 919 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
const totalAbas = Array.isArray(resp?.sheets) ? resp.sheets.length : 0
|
| 921 |
|
| 922 |
if (!fileName && totalLinhas === null && totalColunas === null && !sheetName) return null
|
|
@@ -1026,6 +1035,12 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 1026 |
const [requiresSheet, setRequiresSheet] = useState(false)
|
| 1027 |
const [sheetOptions, setSheetOptions] = useState([])
|
| 1028 |
const [selectedSheet, setSelectedSheet] = useState('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1029 |
const [elaborador, setElaborador] = useState(null)
|
| 1030 |
const [modeloCarregadoInfo, setModeloCarregadoInfo] = useState(null)
|
| 1031 |
const [repoModelos, setRepoModelos] = useState([])
|
|
@@ -1192,6 +1207,22 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 1192 |
|
| 1193 |
const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
|
| 1194 |
const mapaModoDisponivel = mapaVariavel !== MAPA_VARIAVEL_PADRAO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1195 |
const colunasXDisponiveis = useMemo(
|
| 1196 |
() => (colunaY ? colunasNumericas.filter((coluna) => coluna !== colunaY) : []),
|
| 1197 |
[colunasNumericas, colunaY],
|
|
@@ -1932,7 +1963,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 1932 |
}
|
| 1933 |
observer.disconnect()
|
| 1934 |
}
|
| 1935 |
-
}, [sectionsMountKey, baseCarregada, hasSelection, hasFit])
|
| 1936 |
|
| 1937 |
useEffect(() => {
|
| 1938 |
if (typeof window === 'undefined') return undefined
|
|
@@ -2331,6 +2362,81 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2331 |
}
|
| 2332 |
}
|
| 2333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2334 |
function applyBaseResponse(resp, options = {}) {
|
| 2335 |
const resetXSelection = Boolean(options.resetXSelection)
|
| 2336 |
const colunaYPadrao = String(resp.coluna_y_padrao || '')
|
|
@@ -2602,6 +2708,19 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2602 |
setImportacaoErro('')
|
| 2603 |
setRequiresSheet(Boolean(resp.requires_sheet))
|
| 2604 |
setSheetOptions(resp.sheets || [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2605 |
const nomeModeloCarregado = String(resp?.nome_modelo || '').trim()
|
| 2606 |
if (Boolean(resp.requires_sheet)) {
|
| 2607 |
setSelectedSheet('')
|
|
@@ -2617,6 +2736,12 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2617 |
? 'dai'
|
| 2618 |
: 'tabular'
|
| 2619 |
setTipoFonteDados(tipoFonte)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2620 |
if (tipoFonte === 'dai' && nomeModeloCarregado) {
|
| 2621 |
setNomeArquivoExport(nomeModeloCarregado)
|
| 2622 |
} else if (tipoFonte !== 'dai') {
|
|
@@ -2630,6 +2755,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2630 |
|
| 2631 |
setTipoFonteDados('tabular')
|
| 2632 |
setNomeArquivoExport('')
|
|
|
|
| 2633 |
setDados(null)
|
| 2634 |
setColunasNumericas([])
|
| 2635 |
setColunaY('')
|
|
@@ -2710,6 +2836,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2710 |
setSelectedSheet('')
|
| 2711 |
setRequiresSheet(false)
|
| 2712 |
setSheetOptions([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2713 |
const nomeArquivo = String(arquivoUpload?.name || '').toLowerCase()
|
| 2714 |
const uploadEhDai = nomeArquivo.endsWith('.dai')
|
| 2715 |
setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
|
|
@@ -2723,6 +2854,46 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2723 |
})
|
| 2724 |
}
|
| 2725 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2726 |
async function onCarregarModeloRepositorio(modeloIdOverride = '') {
|
| 2727 |
const overrideNormalizado = (
|
| 2728 |
modeloIdOverride
|
|
@@ -2749,6 +2920,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2749 |
setSelectedSheet('')
|
| 2750 |
setRequiresSheet(false)
|
| 2751 |
setSheetOptions([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2752 |
setTipoFonteDados('dai')
|
| 2753 |
const resp = await api.elaboracaoRepositorioCarregar(sessionId, modeloId)
|
| 2754 |
aplicarRespostaCarregamento(resp, 'dai', { source: 'repo' })
|
|
@@ -2825,27 +3001,79 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2825 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2826 |
setGeoAuto200(true)
|
| 2827 |
const resp = await api.confirmSheet(sessionId, selectedSheet)
|
| 2828 |
-
|
| 2829 |
-
|
| 2830 |
-
setGeoProcessError('')
|
| 2831 |
-
setGeoStatusHtml('')
|
| 2832 |
-
setGeoFalhasHtml('')
|
| 2833 |
-
setGeoCorrecoes([])
|
| 2834 |
-
setElaborador(resp.elaborador || null)
|
| 2835 |
-
setObservacaoModelo(String(resp?.observacao_modelo || '').trim())
|
| 2836 |
-
setModeloCarregadoInfo(null)
|
| 2837 |
-
setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
|
| 2838 |
-
setNomeArquivoExport('')
|
| 2839 |
-
setRequiresSheet(false)
|
| 2840 |
-
setArquivoCarregadoInfo(buildArquivoCarregadoInfo(resp, {
|
| 2841 |
fileName: String(uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || ''),
|
| 2842 |
sheetName: selectedSheet,
|
| 2843 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2844 |
applyBaseResponse(resp, { resetXSelection: true })
|
| 2845 |
setSectionsMountKey((prev) => prev + 1)
|
| 2846 |
setModeloLoadSource('')
|
| 2847 |
}, {
|
| 2848 |
-
onError: (err) => setImportacaoErro(err?.message || 'Falha ao
|
| 2849 |
})
|
| 2850 |
}
|
| 2851 |
|
|
@@ -4188,7 +4416,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4188 |
</aside>
|
| 4189 |
|
| 4190 |
<div key={`sections-${sectionsMountKey}`} className="workflow-sections-stack elaboracao-sections-stack">
|
| 4191 |
-
<SectionBlock step="1" title="Importar Dados" subtitle="
|
| 4192 |
<div className="section1-groups">
|
| 4193 |
<div className="subpanel section1-group">
|
| 4194 |
{!modeloLoadSource ? (
|
|
@@ -4217,6 +4445,18 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4217 |
>
|
| 4218 |
Fazer upload de Excel ou modelo
|
| 4219 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4220 |
</div>
|
| 4221 |
) : (
|
| 4222 |
<div className="model-source-flow">
|
|
@@ -4307,6 +4547,44 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4307 |
) : null}
|
| 4308 |
</>
|
| 4309 |
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4310 |
</div>
|
| 4311 |
)}
|
| 4312 |
|
|
@@ -4353,6 +4631,115 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4353 |
</>
|
| 4354 |
) : null}
|
| 4355 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4356 |
{modeloCarregadoInfo ? (
|
| 4357 |
<>
|
| 4358 |
<h4>Informações do modelo</h4>
|
|
@@ -4433,7 +4820,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4433 |
</div>
|
| 4434 |
</SectionBlock>
|
| 4435 |
|
| 4436 |
-
{baseCarregada ? (
|
| 4437 |
<>
|
| 4438 |
<SectionBlock step="2" title="Resolver Coordenadas" subtitle="Mapeie lat/lon ou execute geocodificação automática.">
|
| 4439 |
<div className="coords-section-groups">
|
|
|
|
| 908 |
const isDai = tipo === 'dai' || fileNameLower.endsWith('.dai')
|
| 909 |
if (isDai) return null
|
| 910 |
|
| 911 |
+
const importPreview = resp?.import_preview || null
|
| 912 |
const extMatch = fileName.match(/\.([^.]+)$/)
|
| 913 |
const tipoArquivo = extMatch?.[1] ? String(extMatch[1]).toUpperCase() : '-'
|
| 914 |
const sheetName = String(options.sheetName || resp?.sheet_selected || '').trim()
|
|
|
|
| 916 |
? Number(resp.dados.total_rows)
|
| 917 |
: Array.isArray(resp?.dados?.rows)
|
| 918 |
? resp.dados.rows.length
|
| 919 |
+
: Number.isFinite(Number(importPreview?.total_rows))
|
| 920 |
+
? Number(importPreview.total_rows)
|
| 921 |
+
: null
|
| 922 |
+
const totalColunas = Array.isArray(resp?.dados?.columns)
|
| 923 |
+
? resp.dados.columns.length
|
| 924 |
+
: Number.isFinite(Number(importPreview?.total_columns))
|
| 925 |
+
? Number(importPreview.total_columns)
|
| 926 |
+
: Array.isArray(importPreview?.columns)
|
| 927 |
+
? importPreview.columns.length
|
| 928 |
+
: null
|
| 929 |
const totalAbas = Array.isArray(resp?.sheets) ? resp.sheets.length : 0
|
| 930 |
|
| 931 |
if (!fileName && totalLinhas === null && totalColunas === null && !sheetName) return null
|
|
|
|
| 1035 |
const [requiresSheet, setRequiresSheet] = useState(false)
|
| 1036 |
const [sheetOptions, setSheetOptions] = useState([])
|
| 1037 |
const [selectedSheet, setSelectedSheet] = useState('')
|
| 1038 |
+
const [pastedTableText, setPastedTableText] = useState('')
|
| 1039 |
+
const [importPreview, setImportPreview] = useState(null)
|
| 1040 |
+
const [importSelectedColumns, setImportSelectedColumns] = useState([])
|
| 1041 |
+
const [requiresImportColumnSelection, setRequiresImportColumnSelection] = useState(false)
|
| 1042 |
+
const [firstColumnsCount, setFirstColumnsCount] = useState('')
|
| 1043 |
+
const [firstColumnsChecked, setFirstColumnsChecked] = useState(false)
|
| 1044 |
const [elaborador, setElaborador] = useState(null)
|
| 1045 |
const [modeloCarregadoInfo, setModeloCarregadoInfo] = useState(null)
|
| 1046 |
const [repoModelos, setRepoModelos] = useState([])
|
|
|
|
| 1207 |
|
| 1208 |
const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
|
| 1209 |
const mapaModoDisponivel = mapaVariavel !== MAPA_VARIAVEL_PADRAO
|
| 1210 |
+
const importPreviewColumns = useMemo(
|
| 1211 |
+
() => (Array.isArray(importPreview?.columns) ? importPreview.columns.map((item) => String(item)) : []),
|
| 1212 |
+
[importPreview],
|
| 1213 |
+
)
|
| 1214 |
+
const importPreviewRows = useMemo(
|
| 1215 |
+
() => (Array.isArray(importPreview?.rows) ? importPreview.rows : []),
|
| 1216 |
+
[importPreview],
|
| 1217 |
+
)
|
| 1218 |
+
const importSelectedColumnsSet = useMemo(
|
| 1219 |
+
() => new Set(importSelectedColumns),
|
| 1220 |
+
[importSelectedColumns],
|
| 1221 |
+
)
|
| 1222 |
+
const importSelectionReady = useMemo(
|
| 1223 |
+
() => importSelectedColumns.length >= 2,
|
| 1224 |
+
[importSelectedColumns],
|
| 1225 |
+
)
|
| 1226 |
const colunasXDisponiveis = useMemo(
|
| 1227 |
() => (colunaY ? colunasNumericas.filter((coluna) => coluna !== colunaY) : []),
|
| 1228 |
[colunasNumericas, colunaY],
|
|
|
|
| 1963 |
}
|
| 1964 |
observer.disconnect()
|
| 1965 |
}
|
| 1966 |
+
}, [sectionsMountKey, baseCarregada, hasSelection, hasFit, requiresImportColumnSelection])
|
| 1967 |
|
| 1968 |
useEffect(() => {
|
| 1969 |
if (typeof window === 'undefined') return undefined
|
|
|
|
| 2362 |
}
|
| 2363 |
}
|
| 2364 |
|
| 2365 |
+
function limparBaseCarregadaParaSelecaoImportacao() {
|
| 2366 |
+
setDados(null)
|
| 2367 |
+
clearMapaResponse()
|
| 2368 |
+
setMapaGerado(false)
|
| 2369 |
+
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2370 |
+
setMapaModo(MAPA_MODO_PONTOS)
|
| 2371 |
+
setMapaResiduosGerado(false)
|
| 2372 |
+
clearMapaResiduosResponse()
|
| 2373 |
+
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2374 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2375 |
+
setColunasNumericas([])
|
| 2376 |
+
setColunaY('')
|
| 2377 |
+
setColunaYDraft('')
|
| 2378 |
+
setTipoY('')
|
| 2379 |
+
setTipoYDraft('')
|
| 2380 |
+
setColunasAreaCandidatas([])
|
| 2381 |
+
setColunaArea('')
|
| 2382 |
+
setColunasX([])
|
| 2383 |
+
setDicotomicas([])
|
| 2384 |
+
setCodigoAlocado([])
|
| 2385 |
+
setPercentuais([])
|
| 2386 |
+
setColunasDataMercado([])
|
| 2387 |
+
setColunaDataMercadoSugerida('')
|
| 2388 |
+
setColunaDataMercado('')
|
| 2389 |
+
setColunaDataMercadoAplicada('')
|
| 2390 |
+
setPeriodoDadosMercado(null)
|
| 2391 |
+
setPeriodoDadosMercadoPreview(null)
|
| 2392 |
+
setDataMercadoError('')
|
| 2393 |
+
setSelection(null)
|
| 2394 |
+
setGrauCoef(0)
|
| 2395 |
+
setGrauF(0)
|
| 2396 |
+
setFit(null)
|
| 2397 |
+
setSecao10InterativoFigura(null)
|
| 2398 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 2399 |
+
setSecao10InterativoSelecionado('none')
|
| 2400 |
+
setSecao13InterativoFigura(null)
|
| 2401 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 2402 |
+
setSecao13InterativoSelecionado('none')
|
| 2403 |
+
setSelectionAppliedSnapshot(buildSelectionSnapshot())
|
| 2404 |
+
setTransformacaoY('(x)')
|
| 2405 |
+
setTransformacoesX({})
|
| 2406 |
+
setTransformacaoYFixaBusca('Livre')
|
| 2407 |
+
setTransformacoesFixasBusca({})
|
| 2408 |
+
setManualTransformPreview(null)
|
| 2409 |
+
setManualTransformPreviewLoading(false)
|
| 2410 |
+
setTransformacoesAplicadas(null)
|
| 2411 |
+
setOrigemTransformacoes(null)
|
| 2412 |
+
setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
|
| 2413 |
+
setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
|
| 2414 |
+
setFiltros(defaultFiltros())
|
| 2415 |
+
setOutliersTexto('')
|
| 2416 |
+
setReincluirTexto('')
|
| 2417 |
+
setResumoOutliers('Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0')
|
| 2418 |
+
setOutliersHtml('')
|
| 2419 |
+
setOutliersAnteriores([])
|
| 2420 |
+
setIteracao(1)
|
| 2421 |
+
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 2422 |
+
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 2423 |
+
setCoordsInfo(null)
|
| 2424 |
+
setManualLat('')
|
| 2425 |
+
setManualLon('')
|
| 2426 |
+
setGeoCdlog('')
|
| 2427 |
+
setGeoNum('')
|
| 2428 |
+
setCoordsMode('menu')
|
| 2429 |
+
setConfirmarExclusaoCoords(false)
|
| 2430 |
+
setCamposAvaliacao([])
|
| 2431 |
+
valoresAvaliacaoRef.current = {}
|
| 2432 |
+
setAvaliacaoFormVersion((prev) => prev + 1)
|
| 2433 |
+
setAvaliacaoPendente(false)
|
| 2434 |
+
setConfirmarLimpezaAvaliacoes(false)
|
| 2435 |
+
setResultadoAvaliacaoHtml('')
|
| 2436 |
+
setBaseChoices([])
|
| 2437 |
+
setBaseValue('')
|
| 2438 |
+
}
|
| 2439 |
+
|
| 2440 |
function applyBaseResponse(resp, options = {}) {
|
| 2441 |
const resetXSelection = Boolean(options.resetXSelection)
|
| 2442 |
const colunaYPadrao = String(resp.coluna_y_padrao || '')
|
|
|
|
| 2708 |
setImportacaoErro('')
|
| 2709 |
setRequiresSheet(Boolean(resp.requires_sheet))
|
| 2710 |
setSheetOptions(resp.sheets || [])
|
| 2711 |
+
const proximaImportPreview = resp?.import_preview || null
|
| 2712 |
+
const proximaSelecaoColunasPendente = Boolean(resp?.requires_column_selection)
|
| 2713 |
+
setImportPreview(proximaImportPreview)
|
| 2714 |
+
setRequiresImportColumnSelection(proximaSelecaoColunasPendente)
|
| 2715 |
+
if (proximaImportPreview && proximaSelecaoColunasPendente) {
|
| 2716 |
+
setImportSelectedColumns([])
|
| 2717 |
+
setFirstColumnsCount('')
|
| 2718 |
+
setFirstColumnsChecked(false)
|
| 2719 |
+
} else if (!proximaImportPreview) {
|
| 2720 |
+
setImportSelectedColumns([])
|
| 2721 |
+
setFirstColumnsCount('')
|
| 2722 |
+
setFirstColumnsChecked(false)
|
| 2723 |
+
}
|
| 2724 |
const nomeModeloCarregado = String(resp?.nome_modelo || '').trim()
|
| 2725 |
if (Boolean(resp.requires_sheet)) {
|
| 2726 |
setSelectedSheet('')
|
|
|
|
| 2736 |
? 'dai'
|
| 2737 |
: 'tabular'
|
| 2738 |
setTipoFonteDados(tipoFonte)
|
| 2739 |
+
if (proximaSelecaoColunasPendente) {
|
| 2740 |
+
setNomeArquivoExport('')
|
| 2741 |
+
setModeloLoadSource('')
|
| 2742 |
+
limparBaseCarregadaParaSelecaoImportacao()
|
| 2743 |
+
return
|
| 2744 |
+
}
|
| 2745 |
if (tipoFonte === 'dai' && nomeModeloCarregado) {
|
| 2746 |
setNomeArquivoExport(nomeModeloCarregado)
|
| 2747 |
} else if (tipoFonte !== 'dai') {
|
|
|
|
| 2755 |
|
| 2756 |
setTipoFonteDados('tabular')
|
| 2757 |
setNomeArquivoExport('')
|
| 2758 |
+
setRequiresImportColumnSelection(false)
|
| 2759 |
setDados(null)
|
| 2760 |
setColunasNumericas([])
|
| 2761 |
setColunaY('')
|
|
|
|
| 2836 |
setSelectedSheet('')
|
| 2837 |
setRequiresSheet(false)
|
| 2838 |
setSheetOptions([])
|
| 2839 |
+
setImportPreview(null)
|
| 2840 |
+
setImportSelectedColumns([])
|
| 2841 |
+
setRequiresImportColumnSelection(false)
|
| 2842 |
+
setFirstColumnsCount('')
|
| 2843 |
+
setFirstColumnsChecked(false)
|
| 2844 |
const nomeArquivo = String(arquivoUpload?.name || '').toLowerCase()
|
| 2845 |
const uploadEhDai = nomeArquivo.endsWith('.dai')
|
| 2846 |
setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
|
|
|
|
| 2854 |
})
|
| 2855 |
}
|
| 2856 |
|
| 2857 |
+
async function onLoadPastedTable() {
|
| 2858 |
+
const textoColado = String(pastedTableText || '')
|
| 2859 |
+
if (!sessionId) return
|
| 2860 |
+
if (!textoColado.trim()) {
|
| 2861 |
+
setImportacaoErro('Cole os dados copiados do Excel antes de carregar.')
|
| 2862 |
+
return
|
| 2863 |
+
}
|
| 2864 |
+
setModeloLoadSource('paste')
|
| 2865 |
+
setImportacaoErro('')
|
| 2866 |
+
await withBusy(async () => {
|
| 2867 |
+
setMapaGerado(false)
|
| 2868 |
+
clearMapaResponse()
|
| 2869 |
+
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2870 |
+
setMapaModo(MAPA_MODO_PONTOS)
|
| 2871 |
+
setMapaResiduosGerado(false)
|
| 2872 |
+
clearMapaResiduosResponse()
|
| 2873 |
+
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2874 |
+
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2875 |
+
setGeoAuto200(true)
|
| 2876 |
+
setSelectedSheet('')
|
| 2877 |
+
setRequiresSheet(false)
|
| 2878 |
+
setSheetOptions([])
|
| 2879 |
+
setImportPreview(null)
|
| 2880 |
+
setImportSelectedColumns([])
|
| 2881 |
+
setRequiresImportColumnSelection(false)
|
| 2882 |
+
setFirstColumnsCount('')
|
| 2883 |
+
setFirstColumnsChecked(false)
|
| 2884 |
+
setUploadedFile(null)
|
| 2885 |
+
setTipoFonteDados('tabular')
|
| 2886 |
+
const resp = await api.pasteElaboracaoTable(sessionId, textoColado)
|
| 2887 |
+
aplicarRespostaCarregamento(resp, 'tabular', {
|
| 2888 |
+
source: 'paste',
|
| 2889 |
+
fileName: String(resp?.uploaded_filename || 'dados_colados.csv'),
|
| 2890 |
+
sheetName: 'Dados colados',
|
| 2891 |
+
})
|
| 2892 |
+
}, {
|
| 2893 |
+
onError: (err) => setImportacaoErro(err?.message || 'Falha ao carregar dados colados.'),
|
| 2894 |
+
})
|
| 2895 |
+
}
|
| 2896 |
+
|
| 2897 |
async function onCarregarModeloRepositorio(modeloIdOverride = '') {
|
| 2898 |
const overrideNormalizado = (
|
| 2899 |
modeloIdOverride
|
|
|
|
| 2920 |
setSelectedSheet('')
|
| 2921 |
setRequiresSheet(false)
|
| 2922 |
setSheetOptions([])
|
| 2923 |
+
setImportPreview(null)
|
| 2924 |
+
setImportSelectedColumns([])
|
| 2925 |
+
setRequiresImportColumnSelection(false)
|
| 2926 |
+
setFirstColumnsCount('')
|
| 2927 |
+
setFirstColumnsChecked(false)
|
| 2928 |
setTipoFonteDados('dai')
|
| 2929 |
const resp = await api.elaboracaoRepositorioCarregar(sessionId, modeloId)
|
| 2930 |
aplicarRespostaCarregamento(resp, 'dai', { source: 'repo' })
|
|
|
|
| 3001 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 3002 |
setGeoAuto200(true)
|
| 3003 |
const resp = await api.confirmSheet(sessionId, selectedSheet)
|
| 3004 |
+
aplicarRespostaCarregamento(resp, 'tabular', {
|
| 3005 |
+
source: 'upload',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3006 |
fileName: String(uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || ''),
|
| 3007 |
sheetName: selectedSheet,
|
| 3008 |
+
})
|
| 3009 |
+
}, {
|
| 3010 |
+
onError: (err) => setImportacaoErro(err?.message || 'Falha ao confirmar aba do arquivo.'),
|
| 3011 |
+
})
|
| 3012 |
+
}
|
| 3013 |
+
|
| 3014 |
+
function onToggleImportColumn(coluna) {
|
| 3015 |
+
const valor = String(coluna || '').trim()
|
| 3016 |
+
if (!valor) return
|
| 3017 |
+
setFirstColumnsChecked(false)
|
| 3018 |
+
setImportSelectedColumns((prev) => {
|
| 3019 |
+
const next = prev.includes(valor)
|
| 3020 |
+
? prev.filter((item) => item !== valor)
|
| 3021 |
+
: importPreviewColumns.filter((col) => col === valor || prev.includes(col))
|
| 3022 |
+
return next
|
| 3023 |
+
})
|
| 3024 |
+
setRequiresImportColumnSelection(true)
|
| 3025 |
+
}
|
| 3026 |
+
|
| 3027 |
+
function getFirstImportColumnsQuantity(value = firstColumnsCount) {
|
| 3028 |
+
const limite = importPreviewColumns.length
|
| 3029 |
+
const parsed = Number.parseInt(String(value || '0'), 10)
|
| 3030 |
+
return Number.isFinite(parsed) ? Math.max(0, Math.min(limite, parsed)) : 0
|
| 3031 |
+
}
|
| 3032 |
+
|
| 3033 |
+
function onFirstColumnsCountChange(value) {
|
| 3034 |
+
setFirstColumnsCount(value)
|
| 3035 |
+
if (!firstColumnsChecked) return
|
| 3036 |
+
const quantidade = getFirstImportColumnsQuantity(value)
|
| 3037 |
+
setImportSelectedColumns(importPreviewColumns.slice(0, quantidade))
|
| 3038 |
+
setRequiresImportColumnSelection(true)
|
| 3039 |
+
setImportacaoErro('')
|
| 3040 |
+
}
|
| 3041 |
+
|
| 3042 |
+
function onToggleFirstImportColumns(event) {
|
| 3043 |
+
const checked = event.target.checked
|
| 3044 |
+
setFirstColumnsChecked(checked)
|
| 3045 |
+
const quantidade = getFirstImportColumnsQuantity()
|
| 3046 |
+
const primeiras = importPreviewColumns.slice(0, quantidade)
|
| 3047 |
+
if (checked) {
|
| 3048 |
+
setFirstColumnsCount(String(quantidade))
|
| 3049 |
+
setImportSelectedColumns(primeiras)
|
| 3050 |
+
} else {
|
| 3051 |
+
const primeirasSet = new Set(primeiras)
|
| 3052 |
+
setImportSelectedColumns((prev) => prev.filter((coluna) => !primeirasSet.has(coluna)))
|
| 3053 |
+
}
|
| 3054 |
+
setRequiresImportColumnSelection(true)
|
| 3055 |
+
setImportacaoErro('')
|
| 3056 |
+
}
|
| 3057 |
+
|
| 3058 |
+
function onClearImportColumns() {
|
| 3059 |
+
setImportSelectedColumns([])
|
| 3060 |
+
setFirstColumnsChecked(false)
|
| 3061 |
+
setRequiresImportColumnSelection(true)
|
| 3062 |
+
setImportacaoErro('')
|
| 3063 |
+
}
|
| 3064 |
+
|
| 3065 |
+
async function onApplyImportColumns() {
|
| 3066 |
+
if (!sessionId || importSelectedColumns.length < 2) return
|
| 3067 |
+
setImportacaoErro('')
|
| 3068 |
+
await withBusy(async () => {
|
| 3069 |
+
const resp = await api.applyImportColumns(sessionId, importSelectedColumns)
|
| 3070 |
+
setRequiresImportColumnSelection(false)
|
| 3071 |
+
setImportSelectedColumns(Array.isArray(resp.import_columns_selected) ? resp.import_columns_selected.map((item) => String(item)) : importSelectedColumns)
|
| 3072 |
applyBaseResponse(resp, { resetXSelection: true })
|
| 3073 |
setSectionsMountKey((prev) => prev + 1)
|
| 3074 |
setModeloLoadSource('')
|
| 3075 |
}, {
|
| 3076 |
+
onError: (err) => setImportacaoErro(err?.message || 'Falha ao aplicar seleção de colunas.'),
|
| 3077 |
})
|
| 3078 |
}
|
| 3079 |
|
|
|
|
| 4416 |
</aside>
|
| 4417 |
|
| 4418 |
<div key={`sections-${sectionsMountKey}`} className="workflow-sections-stack elaboracao-sections-stack">
|
| 4419 |
+
<SectionBlock step="1" title="Importar Dados" subtitle="Modelo, planilha ou dados colados com recuperação do fluxo.">
|
| 4420 |
<div className="section1-groups">
|
| 4421 |
<div className="subpanel section1-group">
|
| 4422 |
{!modeloLoadSource ? (
|
|
|
|
| 4445 |
>
|
| 4446 |
Fazer upload de Excel ou modelo
|
| 4447 |
</button>
|
| 4448 |
+
<button
|
| 4449 |
+
type="button"
|
| 4450 |
+
className="model-source-choice-btn model-source-choice-btn-tertiary"
|
| 4451 |
+
onClick={() => {
|
| 4452 |
+
setModeloLoadSource('paste')
|
| 4453 |
+
setRepoModeloDropdownOpen(false)
|
| 4454 |
+
setImportacaoErro('')
|
| 4455 |
+
}}
|
| 4456 |
+
disabled={loading}
|
| 4457 |
+
>
|
| 4458 |
+
Colar dados do Excel
|
| 4459 |
+
</button>
|
| 4460 |
</div>
|
| 4461 |
) : (
|
| 4462 |
<div className="model-source-flow">
|
|
|
|
| 4547 |
) : null}
|
| 4548 |
</>
|
| 4549 |
) : null}
|
| 4550 |
+
|
| 4551 |
+
{modeloLoadSource === 'paste' ? (
|
| 4552 |
+
<div className="paste-excel-panel">
|
| 4553 |
+
<label className="paste-excel-field">
|
| 4554 |
+
Dados do Excel
|
| 4555 |
+
<textarea
|
| 4556 |
+
value={pastedTableText}
|
| 4557 |
+
onChange={(event) => {
|
| 4558 |
+
setPastedTableText(event.target.value)
|
| 4559 |
+
setImportacaoErro('')
|
| 4560 |
+
}}
|
| 4561 |
+
placeholder="Cole a tabela copiada do Excel"
|
| 4562 |
+
disabled={loading}
|
| 4563 |
+
rows={8}
|
| 4564 |
+
/>
|
| 4565 |
+
</label>
|
| 4566 |
+
<div className="row compact paste-excel-actions">
|
| 4567 |
+
<button
|
| 4568 |
+
type="button"
|
| 4569 |
+
onClick={onLoadPastedTable}
|
| 4570 |
+
disabled={loading || !pastedTableText.trim()}
|
| 4571 |
+
>
|
| 4572 |
+
Carregar dados colados
|
| 4573 |
+
</button>
|
| 4574 |
+
<button
|
| 4575 |
+
type="button"
|
| 4576 |
+
className="paste-excel-clear-btn"
|
| 4577 |
+
onClick={() => {
|
| 4578 |
+
setPastedTableText('')
|
| 4579 |
+
setImportacaoErro('')
|
| 4580 |
+
}}
|
| 4581 |
+
disabled={loading || !pastedTableText}
|
| 4582 |
+
>
|
| 4583 |
+
Limpar
|
| 4584 |
+
</button>
|
| 4585 |
+
</div>
|
| 4586 |
+
</div>
|
| 4587 |
+
) : null}
|
| 4588 |
</div>
|
| 4589 |
)}
|
| 4590 |
|
|
|
|
| 4631 |
</>
|
| 4632 |
) : null}
|
| 4633 |
|
| 4634 |
+
{importPreview ? (
|
| 4635 |
+
<div className="import-preview-card">
|
| 4636 |
+
<div className="import-preview-head">
|
| 4637 |
+
<div>
|
| 4638 |
+
<h4>Prévia da planilha</h4>
|
| 4639 |
+
<div className="section1-empty-hint">
|
| 4640 |
+
Cabeçalhos e {Number(importPreview?.returned_rows || importPreviewRows.length).toLocaleString('pt-BR')} linhas aleatórias.
|
| 4641 |
+
</div>
|
| 4642 |
+
</div>
|
| 4643 |
+
<div className="import-preview-counter">
|
| 4644 |
+
{importSelectedColumns.length}/{importPreviewColumns.length} selecionadas
|
| 4645 |
+
</div>
|
| 4646 |
+
</div>
|
| 4647 |
+
|
| 4648 |
+
<div className="import-preview-disclaimer">
|
| 4649 |
+
Inclua as colunas referentes ao Bairro e à Finalidade Cadastral do imóvel para seguir as diretrizes da DAI.
|
| 4650 |
+
</div>
|
| 4651 |
+
|
| 4652 |
+
<div className="import-preview-tools">
|
| 4653 |
+
<label className="import-preview-first-columns-toggle">
|
| 4654 |
+
<input
|
| 4655 |
+
type="checkbox"
|
| 4656 |
+
checked={firstColumnsChecked}
|
| 4657 |
+
onChange={onToggleFirstImportColumns}
|
| 4658 |
+
disabled={loading || importPreviewColumns.length === 0}
|
| 4659 |
+
/>
|
| 4660 |
+
Selecionar primeiras
|
| 4661 |
+
</label>
|
| 4662 |
+
<label className="import-preview-first-columns-field">
|
| 4663 |
+
<input
|
| 4664 |
+
type="number"
|
| 4665 |
+
min="0"
|
| 4666 |
+
max={importPreviewColumns.length}
|
| 4667 |
+
value={firstColumnsCount}
|
| 4668 |
+
onChange={(event) => onFirstColumnsCountChange(event.target.value)}
|
| 4669 |
+
disabled={loading || importPreviewColumns.length === 0}
|
| 4670 |
+
/>
|
| 4671 |
+
colunas
|
| 4672 |
+
</label>
|
| 4673 |
+
<button
|
| 4674 |
+
type="button"
|
| 4675 |
+
className="import-preview-clear-btn"
|
| 4676 |
+
onClick={onClearImportColumns}
|
| 4677 |
+
disabled={loading || importSelectedColumns.length === 0}
|
| 4678 |
+
>
|
| 4679 |
+
Limpar selecionadas
|
| 4680 |
+
</button>
|
| 4681 |
+
</div>
|
| 4682 |
+
|
| 4683 |
+
{!importSelectionReady ? (
|
| 4684 |
+
<div className="section1-empty-hint">
|
| 4685 |
+
Selecione pelo menos 2 colunas para liberar as próximas etapas.
|
| 4686 |
+
</div>
|
| 4687 |
+
) : null}
|
| 4688 |
+
|
| 4689 |
+
<div className="import-preview-table-wrap">
|
| 4690 |
+
<table className="import-preview-table">
|
| 4691 |
+
<thead>
|
| 4692 |
+
<tr>
|
| 4693 |
+
<th className="import-preview-index-cell">#</th>
|
| 4694 |
+
{importPreviewColumns.map((coluna) => {
|
| 4695 |
+
const selected = importSelectedColumnsSet.has(coluna)
|
| 4696 |
+
return (
|
| 4697 |
+
<th key={`import-preview-head-${coluna}`} className={selected ? 'is-selected' : ''}>
|
| 4698 |
+
<button
|
| 4699 |
+
type="button"
|
| 4700 |
+
className="import-preview-header-btn"
|
| 4701 |
+
onClick={() => onToggleImportColumn(coluna)}
|
| 4702 |
+
aria-pressed={selected}
|
| 4703 |
+
disabled={loading}
|
| 4704 |
+
>
|
| 4705 |
+
{coluna}
|
| 4706 |
+
</button>
|
| 4707 |
+
</th>
|
| 4708 |
+
)
|
| 4709 |
+
})}
|
| 4710 |
+
</tr>
|
| 4711 |
+
</thead>
|
| 4712 |
+
<tbody>
|
| 4713 |
+
{importPreviewRows.map((row, rowIndex) => (
|
| 4714 |
+
<tr key={`import-preview-row-${row?._index ?? rowIndex}`}>
|
| 4715 |
+
<td className="import-preview-index-cell">{row?._index ?? rowIndex + 1}</td>
|
| 4716 |
+
{importPreviewColumns.map((coluna) => {
|
| 4717 |
+
const selected = importSelectedColumnsSet.has(coluna)
|
| 4718 |
+
return (
|
| 4719 |
+
<td key={`import-preview-cell-${rowIndex}-${coluna}`} className={selected ? 'is-selected' : ''}>
|
| 4720 |
+
{String(row?.[coluna] ?? '')}
|
| 4721 |
+
</td>
|
| 4722 |
+
)
|
| 4723 |
+
})}
|
| 4724 |
+
</tr>
|
| 4725 |
+
))}
|
| 4726 |
+
</tbody>
|
| 4727 |
+
</table>
|
| 4728 |
+
</div>
|
| 4729 |
+
|
| 4730 |
+
<div className="import-preview-apply-row">
|
| 4731 |
+
<button
|
| 4732 |
+
type="button"
|
| 4733 |
+
className="import-preview-apply-btn"
|
| 4734 |
+
onClick={onApplyImportColumns}
|
| 4735 |
+
disabled={loading || !importSelectionReady || !requiresImportColumnSelection}
|
| 4736 |
+
>
|
| 4737 |
+
{requiresImportColumnSelection ? 'Aplicar colunas selecionadas' : 'Seleção aplicada'}
|
| 4738 |
+
</button>
|
| 4739 |
+
</div>
|
| 4740 |
+
</div>
|
| 4741 |
+
) : null}
|
| 4742 |
+
|
| 4743 |
{modeloCarregadoInfo ? (
|
| 4744 |
<>
|
| 4745 |
<h4>Informações do modelo</h4>
|
|
|
|
| 4820 |
</div>
|
| 4821 |
</SectionBlock>
|
| 4822 |
|
| 4823 |
+
{baseCarregada && !requiresImportColumnSelection ? (
|
| 4824 |
<>
|
| 4825 |
<SectionBlock step="2" title="Resolver Coordenadas" subtitle="Mapeie lat/lon ou execute geocodificação automática.">
|
| 4826 |
<div className="coords-section-groups">
|
frontend/src/components/TrabalhosTecnicosTab.jsx
CHANGED
|
@@ -92,6 +92,28 @@ function nomeModeloExibicao(modelo) {
|
|
| 92 |
return String(modelo?.mesa_modelo_nome || modelo?.nome || '').trim()
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
function buildEditState(trabalho) {
|
| 96 |
const modelos = dedupeTextList(
|
| 97 |
(Array.isArray(trabalho?.modelos) ? trabalho.modelos : []).map((item) => nomeModeloExibicao(item))
|
|
@@ -106,14 +128,7 @@ function buildEditState(trabalho) {
|
|
| 106 |
coord_y: item?.coord_y ?? '',
|
| 107 |
}))
|
| 108 |
: [
|
| 109 |
-
|
| 110 |
-
id: `${trabalho?.id || 'trabalho'}-imovel-1`,
|
| 111 |
-
label: '',
|
| 112 |
-
endereco: '',
|
| 113 |
-
numero: '',
|
| 114 |
-
coord_x: '',
|
| 115 |
-
coord_y: '',
|
| 116 |
-
},
|
| 117 |
]
|
| 118 |
|
| 119 |
return {
|
|
@@ -126,6 +141,23 @@ function buildEditState(trabalho) {
|
|
| 126 |
}
|
| 127 |
}
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
function toListItemFromDetail(trabalho) {
|
| 130 |
const modelos = Array.isArray(trabalho?.modelos) ? trabalho.modelos : []
|
| 131 |
const modelosMesa = modelos.filter((item) => item?.disponivel_mesa)
|
|
@@ -182,6 +214,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 182 |
const [trabalhoLoading, setTrabalhoLoading] = useState(false)
|
| 183 |
const [trabalhoError, setTrabalhoError] = useState('')
|
| 184 |
const [editando, setEditando] = useState(false)
|
|
|
|
| 185 |
const [salvando, setSalvando] = useState(false)
|
| 186 |
const [edicao, setEdicao] = useState(null)
|
| 187 |
const [edicaoErro, setEdicaoErro] = useState('')
|
|
@@ -420,6 +453,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 420 |
setTrabalhoError('')
|
| 421 |
setEdicaoErro('')
|
| 422 |
setEditando(false)
|
|
|
|
| 423 |
setEdicao(null)
|
| 424 |
setOrigemAbertura(String(options?.origem || 'lista').trim() || 'lista')
|
| 425 |
try {
|
|
@@ -450,6 +484,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 450 |
setTrabalhoError('')
|
| 451 |
setEdicaoErro('')
|
| 452 |
setEditando(false)
|
|
|
|
| 453 |
setEdicao(null)
|
| 454 |
setModeloDraft('')
|
| 455 |
if (options.syncRoute !== false && typeof onRouteChange === 'function') {
|
|
@@ -461,6 +496,10 @@ export default function TrabalhosTecnicosTab({
|
|
| 461 |
}
|
| 462 |
|
| 463 |
function onVoltarDoDetalhe() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
if (origemAbertura === 'pesquisa_mapa' && typeof onVoltarAoMapaPesquisa === 'function') {
|
| 465 |
onVoltarLista({ syncRoute: false })
|
| 466 |
onVoltarAoMapaPesquisa()
|
|
@@ -475,6 +514,41 @@ export default function TrabalhosTecnicosTab({
|
|
| 475 |
onVoltarLista({ subtab: activeInnerTab })
|
| 476 |
}
|
| 477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
function onAbrirModeloAssociado(modelo) {
|
| 479 |
if (!modelo?.disponivel_mesa || typeof onAbrirModeloNoRepositorio !== 'function') return
|
| 480 |
onAbrirModeloNoRepositorio({
|
|
@@ -500,6 +574,10 @@ export default function TrabalhosTecnicosTab({
|
|
| 500 |
|
| 501 |
function onCancelarEdicao() {
|
| 502 |
if (salvando) return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
setEditando(false)
|
| 504 |
setEdicao(null)
|
| 505 |
setEdicaoErro('')
|
|
@@ -575,40 +653,41 @@ export default function TrabalhosTecnicosTab({
|
|
| 575 |
}
|
| 576 |
|
| 577 |
async function onSalvarEdicao() {
|
| 578 |
-
if (!
|
|
|
|
| 579 |
setSalvando(true)
|
| 580 |
setEdicaoErro('')
|
| 581 |
try {
|
| 582 |
-
const payload =
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
ano: parseNullableInt(edicao.ano),
|
| 587 |
-
processos_sei: splitTextareaLines(edicao.processos_sei_text),
|
| 588 |
-
modelos: dedupeTextList(edicao.modelos),
|
| 589 |
-
imoveis: edicao.imoveis.map((item) => ({
|
| 590 |
-
label: item.label,
|
| 591 |
-
endereco: item.endereco,
|
| 592 |
-
numero: item.numero,
|
| 593 |
-
coord_x: parseNullableNumber(item.coord_x),
|
| 594 |
-
coord_y: parseNullableNumber(item.coord_y),
|
| 595 |
-
})),
|
| 596 |
-
}
|
| 597 |
-
const resp = await api.trabalhosTecnicosSalvar(payload)
|
| 598 |
const trabalhoAtualizado = resp?.trabalho || null
|
| 599 |
setTrabalhoAberto(trabalhoAtualizado)
|
| 600 |
setEditando(false)
|
|
|
|
| 601 |
setEdicao(null)
|
| 602 |
setModeloDraft('')
|
| 603 |
if (trabalhoAtualizado?.id) {
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
}
|
| 610 |
} catch (err) {
|
| 611 |
-
setEdicaoErro(err.message || 'Falha ao salvar alterações do trabalho técnico.')
|
| 612 |
} finally {
|
| 613 |
setSalvando(false)
|
| 614 |
}
|
|
@@ -686,23 +765,24 @@ export default function TrabalhosTecnicosTab({
|
|
| 686 |
}, [activeInnerTab, trabalhoAberto])
|
| 687 |
|
| 688 |
if (trabalhoAberto) {
|
|
|
|
| 689 |
const modelos = Array.isArray(trabalhoAberto?.modelos) ? trabalhoAberto.modelos : []
|
| 690 |
const modelosMesa = modelos.filter((item) => item?.disponivel_mesa)
|
| 691 |
const imoveis = Array.isArray(trabalhoAberto?.imoveis) ? trabalhoAberto.imoveis : []
|
| 692 |
const processosSei = listarProcessosSei(trabalhoAberto)
|
| 693 |
-
const shareHref = buildTrabalhoTecnicoLink(trabalhoAberto
|
| 694 |
|
| 695 |
return (
|
| 696 |
<div className="tab-content">
|
| 697 |
<div className="pesquisa-opened-model-view">
|
| 698 |
<div className="pesquisa-opened-model-head">
|
| 699 |
<div className="pesquisa-opened-model-title-wrap">
|
| 700 |
-
<h3>{trabalhoAberto?.nome || 'Trabalho técnico'}</h3>
|
| 701 |
-
<p>{trabalhoAberto?.tipo_label || 'Tipo não identificado'}</p>
|
| 702 |
</div>
|
| 703 |
<div className="trabalho-detail-actions">
|
| 704 |
-
<ShareLinkButton href={shareHref} />
|
| 705 |
-
{!editando ? (
|
| 706 |
<button
|
| 707 |
type="button"
|
| 708 |
className="model-source-back-btn"
|
|
@@ -719,7 +799,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 719 |
onClick={() => void onSalvarEdicao()}
|
| 720 |
disabled={salvando}
|
| 721 |
>
|
| 722 |
-
{salvando ? 'Salvando...' : 'Salvar alterações'}
|
| 723 |
</button>
|
| 724 |
<button
|
| 725 |
type="button"
|
|
@@ -737,7 +817,9 @@ export default function TrabalhosTecnicosTab({
|
|
| 737 |
onClick={onVoltarDoDetalhe}
|
| 738 |
disabled={trabalhoLoading || salvando}
|
| 739 |
>
|
| 740 |
-
{
|
|
|
|
|
|
|
| 741 |
? 'Voltar ao mapa'
|
| 742 |
: 'Voltar à lista'}
|
| 743 |
</button>
|
|
@@ -1022,7 +1104,9 @@ export default function TrabalhosTecnicosTab({
|
|
| 1022 |
</div>
|
| 1023 |
<LoadingOverlay
|
| 1024 |
show={trabalhoLoading || salvando}
|
| 1025 |
-
label={salvando
|
|
|
|
|
|
|
| 1026 |
/>
|
| 1027 |
</div>
|
| 1028 |
)
|
|
@@ -1061,6 +1145,9 @@ export default function TrabalhosTecnicosTab({
|
|
| 1061 |
<div><strong>Total:</strong> {trabalhos.length}</div>
|
| 1062 |
</div>
|
| 1063 |
<div className="repo-actions">
|
|
|
|
|
|
|
|
|
|
| 1064 |
<button type="button" className="repo-refresh-btn" onClick={() => void carregarTrabalhos()} disabled={loading}>
|
| 1065 |
Atualizar lista
|
| 1066 |
</button>
|
|
|
|
| 92 |
return String(modelo?.mesa_modelo_nome || modelo?.nome || '').trim()
|
| 93 |
}
|
| 94 |
|
| 95 |
+
function buildEmptyImovel(idPrefix = 'novo-trabalho', index = 1) {
|
| 96 |
+
return {
|
| 97 |
+
id: `${idPrefix}-imovel-${index}`,
|
| 98 |
+
label: '',
|
| 99 |
+
endereco: '',
|
| 100 |
+
numero: '',
|
| 101 |
+
coord_x: '',
|
| 102 |
+
coord_y: '',
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function buildCadastroState() {
|
| 107 |
+
return {
|
| 108 |
+
nome: '',
|
| 109 |
+
tipo_codigo: '',
|
| 110 |
+
ano: '',
|
| 111 |
+
processos_sei_text: '',
|
| 112 |
+
modelos: [],
|
| 113 |
+
imoveis: [buildEmptyImovel('novo-trabalho', 1)],
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
function buildEditState(trabalho) {
|
| 118 |
const modelos = dedupeTextList(
|
| 119 |
(Array.isArray(trabalho?.modelos) ? trabalho.modelos : []).map((item) => nomeModeloExibicao(item))
|
|
|
|
| 128 |
coord_y: item?.coord_y ?? '',
|
| 129 |
}))
|
| 130 |
: [
|
| 131 |
+
buildEmptyImovel(trabalho?.id || 'trabalho', 1),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
]
|
| 133 |
|
| 134 |
return {
|
|
|
|
| 141 |
}
|
| 142 |
}
|
| 143 |
|
| 144 |
+
function buildTrabalhoPayload(edicao) {
|
| 145 |
+
return {
|
| 146 |
+
nome: edicao.nome,
|
| 147 |
+
tipo_codigo: edicao.tipo_codigo,
|
| 148 |
+
ano: parseNullableInt(edicao.ano),
|
| 149 |
+
processos_sei: splitTextareaLines(edicao.processos_sei_text),
|
| 150 |
+
modelos: dedupeTextList(edicao.modelos),
|
| 151 |
+
imoveis: (Array.isArray(edicao.imoveis) ? edicao.imoveis : []).map((item) => ({
|
| 152 |
+
label: item.label,
|
| 153 |
+
endereco: item.endereco,
|
| 154 |
+
numero: item.numero,
|
| 155 |
+
coord_x: parseNullableNumber(item.coord_x),
|
| 156 |
+
coord_y: parseNullableNumber(item.coord_y),
|
| 157 |
+
})),
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
function toListItemFromDetail(trabalho) {
|
| 162 |
const modelos = Array.isArray(trabalho?.modelos) ? trabalho.modelos : []
|
| 163 |
const modelosMesa = modelos.filter((item) => item?.disponivel_mesa)
|
|
|
|
| 214 |
const [trabalhoLoading, setTrabalhoLoading] = useState(false)
|
| 215 |
const [trabalhoError, setTrabalhoError] = useState('')
|
| 216 |
const [editando, setEditando] = useState(false)
|
| 217 |
+
const [cadastrando, setCadastrando] = useState(false)
|
| 218 |
const [salvando, setSalvando] = useState(false)
|
| 219 |
const [edicao, setEdicao] = useState(null)
|
| 220 |
const [edicaoErro, setEdicaoErro] = useState('')
|
|
|
|
| 453 |
setTrabalhoError('')
|
| 454 |
setEdicaoErro('')
|
| 455 |
setEditando(false)
|
| 456 |
+
setCadastrando(false)
|
| 457 |
setEdicao(null)
|
| 458 |
setOrigemAbertura(String(options?.origem || 'lista').trim() || 'lista')
|
| 459 |
try {
|
|
|
|
| 484 |
setTrabalhoError('')
|
| 485 |
setEdicaoErro('')
|
| 486 |
setEditando(false)
|
| 487 |
+
setCadastrando(false)
|
| 488 |
setEdicao(null)
|
| 489 |
setModeloDraft('')
|
| 490 |
if (options.syncRoute !== false && typeof onRouteChange === 'function') {
|
|
|
|
| 496 |
}
|
| 497 |
|
| 498 |
function onVoltarDoDetalhe() {
|
| 499 |
+
if (cadastrando) {
|
| 500 |
+
onVoltarLista({ subtab: 'repositorio' })
|
| 501 |
+
return
|
| 502 |
+
}
|
| 503 |
if (origemAbertura === 'pesquisa_mapa' && typeof onVoltarAoMapaPesquisa === 'function') {
|
| 504 |
onVoltarLista({ syncRoute: false })
|
| 505 |
onVoltarAoMapaPesquisa()
|
|
|
|
| 514 |
onVoltarLista({ subtab: activeInnerTab })
|
| 515 |
}
|
| 516 |
|
| 517 |
+
function onIniciarCadastro() {
|
| 518 |
+
const draft = buildCadastroState()
|
| 519 |
+
setActiveInnerTab('repositorio')
|
| 520 |
+
setTrabalhoAberto({
|
| 521 |
+
id: '',
|
| 522 |
+
nome: '',
|
| 523 |
+
tipo_codigo: '',
|
| 524 |
+
tipo_label: 'Novo trabalho técnico',
|
| 525 |
+
ano: '',
|
| 526 |
+
endereco_resumo: '',
|
| 527 |
+
modelo_resumo: '',
|
| 528 |
+
total_registros_planilha: 0,
|
| 529 |
+
total_imoveis: 0,
|
| 530 |
+
total_modelos: 0,
|
| 531 |
+
tem_coordenadas: false,
|
| 532 |
+
processos_sei: [],
|
| 533 |
+
modelos: [],
|
| 534 |
+
imoveis: [],
|
| 535 |
+
})
|
| 536 |
+
setEdicao(draft)
|
| 537 |
+
setModeloDraft('')
|
| 538 |
+
setEdicaoErro('')
|
| 539 |
+
setTrabalhoError('')
|
| 540 |
+
setOrigemAbertura('lista')
|
| 541 |
+
setCadastrando(true)
|
| 542 |
+
setEditando(true)
|
| 543 |
+
void carregarSugestoesModelosMesa()
|
| 544 |
+
if (typeof onRouteChange === 'function') {
|
| 545 |
+
onRouteChange({
|
| 546 |
+
tab: 'trabalhos',
|
| 547 |
+
subtab: 'repositorio',
|
| 548 |
+
})
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
function onAbrirModeloAssociado(modelo) {
|
| 553 |
if (!modelo?.disponivel_mesa || typeof onAbrirModeloNoRepositorio !== 'function') return
|
| 554 |
onAbrirModeloNoRepositorio({
|
|
|
|
| 574 |
|
| 575 |
function onCancelarEdicao() {
|
| 576 |
if (salvando) return
|
| 577 |
+
if (cadastrando) {
|
| 578 |
+
onVoltarLista({ subtab: 'repositorio' })
|
| 579 |
+
return
|
| 580 |
+
}
|
| 581 |
setEditando(false)
|
| 582 |
setEdicao(null)
|
| 583 |
setEdicaoErro('')
|
|
|
|
| 653 |
}
|
| 654 |
|
| 655 |
async function onSalvarEdicao() {
|
| 656 |
+
if (!edicao) return
|
| 657 |
+
if (!cadastrando && !trabalhoAberto?.id) return
|
| 658 |
setSalvando(true)
|
| 659 |
setEdicaoErro('')
|
| 660 |
try {
|
| 661 |
+
const payload = buildTrabalhoPayload(edicao)
|
| 662 |
+
const resp = cadastrando
|
| 663 |
+
? await api.trabalhosTecnicosCadastrar(payload)
|
| 664 |
+
: await api.trabalhosTecnicosSalvar({ trabalho_id: trabalhoAberto.id, ...payload })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
const trabalhoAtualizado = resp?.trabalho || null
|
| 666 |
setTrabalhoAberto(trabalhoAtualizado)
|
| 667 |
setEditando(false)
|
| 668 |
+
setCadastrando(false)
|
| 669 |
setEdicao(null)
|
| 670 |
setModeloDraft('')
|
| 671 |
if (trabalhoAtualizado?.id) {
|
| 672 |
+
const itemAtualizado = toListItemFromDetail(trabalhoAtualizado)
|
| 673 |
+
setTrabalhos((prev) => {
|
| 674 |
+
const existe = prev.some((item) => String(item?.id || '') === String(trabalhoAtualizado.id))
|
| 675 |
+
if (!existe) return [itemAtualizado, ...prev]
|
| 676 |
+
return prev.map((item) => (
|
| 677 |
+
String(item?.id || '') === String(trabalhoAtualizado.id)
|
| 678 |
+
? itemAtualizado
|
| 679 |
+
: item
|
| 680 |
+
))
|
| 681 |
+
})
|
| 682 |
+
if (typeof onRouteChange === 'function') {
|
| 683 |
+
onRouteChange({
|
| 684 |
+
tab: 'trabalhos',
|
| 685 |
+
trabalhoId: String(trabalhoAtualizado.id),
|
| 686 |
+
})
|
| 687 |
+
}
|
| 688 |
}
|
| 689 |
} catch (err) {
|
| 690 |
+
setEdicaoErro(err.message || (cadastrando ? 'Falha ao cadastrar trabalho técnico.' : 'Falha ao salvar alterações do trabalho técnico.'))
|
| 691 |
} finally {
|
| 692 |
setSalvando(false)
|
| 693 |
}
|
|
|
|
| 765 |
}, [activeInnerTab, trabalhoAberto])
|
| 766 |
|
| 767 |
if (trabalhoAberto) {
|
| 768 |
+
const isCadastro = Boolean(cadastrando)
|
| 769 |
const modelos = Array.isArray(trabalhoAberto?.modelos) ? trabalhoAberto.modelos : []
|
| 770 |
const modelosMesa = modelos.filter((item) => item?.disponivel_mesa)
|
| 771 |
const imoveis = Array.isArray(trabalhoAberto?.imoveis) ? trabalhoAberto.imoveis : []
|
| 772 |
const processosSei = listarProcessosSei(trabalhoAberto)
|
| 773 |
+
const shareHref = !isCadastro && trabalhoAberto?.id ? buildTrabalhoTecnicoLink(trabalhoAberto.id) : ''
|
| 774 |
|
| 775 |
return (
|
| 776 |
<div className="tab-content">
|
| 777 |
<div className="pesquisa-opened-model-view">
|
| 778 |
<div className="pesquisa-opened-model-head">
|
| 779 |
<div className="pesquisa-opened-model-title-wrap">
|
| 780 |
+
<h3>{isCadastro ? (edicao?.nome || 'Novo trabalho técnico') : (trabalhoAberto?.nome || 'Trabalho técnico')}</h3>
|
| 781 |
+
<p>{isCadastro ? 'Cadastro manual' : (trabalhoAberto?.tipo_label || 'Tipo não identificado')}</p>
|
| 782 |
</div>
|
| 783 |
<div className="trabalho-detail-actions">
|
| 784 |
+
{shareHref ? <ShareLinkButton href={shareHref} /> : null}
|
| 785 |
+
{!editando && !isCadastro ? (
|
| 786 |
<button
|
| 787 |
type="button"
|
| 788 |
className="model-source-back-btn"
|
|
|
|
| 799 |
onClick={() => void onSalvarEdicao()}
|
| 800 |
disabled={salvando}
|
| 801 |
>
|
| 802 |
+
{salvando ? 'Salvando...' : (isCadastro ? 'Cadastrar trabalho' : 'Salvar alterações')}
|
| 803 |
</button>
|
| 804 |
<button
|
| 805 |
type="button"
|
|
|
|
| 817 |
onClick={onVoltarDoDetalhe}
|
| 818 |
disabled={trabalhoLoading || salvando}
|
| 819 |
>
|
| 820 |
+
{isCadastro
|
| 821 |
+
? 'Voltar à lista'
|
| 822 |
+
: origemAbertura === 'pesquisa_mapa' || origemAbertura === 'trabalhos_tecnicos_mapa'
|
| 823 |
? 'Voltar ao mapa'
|
| 824 |
: 'Voltar à lista'}
|
| 825 |
</button>
|
|
|
|
| 1104 |
</div>
|
| 1105 |
<LoadingOverlay
|
| 1106 |
show={trabalhoLoading || salvando}
|
| 1107 |
+
label={salvando
|
| 1108 |
+
? (cadastrando ? 'Cadastrando trabalho técnico...' : 'Salvando trabalho técnico...')
|
| 1109 |
+
: 'Abrindo trabalho técnico...'}
|
| 1110 |
/>
|
| 1111 |
</div>
|
| 1112 |
)
|
|
|
|
| 1145 |
<div><strong>Total:</strong> {trabalhos.length}</div>
|
| 1146 |
</div>
|
| 1147 |
<div className="repo-actions">
|
| 1148 |
+
<button type="button" className="repo-refresh-btn trabalhos-create-btn" onClick={onIniciarCadastro} disabled={loading || trabalhoLoading}>
|
| 1149 |
+
Cadastrar trabalho
|
| 1150 |
+
</button>
|
| 1151 |
<button type="button" className="repo-refresh-btn" onClick={() => void carregarTrabalhos()} disabled={loading}>
|
| 1152 |
Atualizar lista
|
| 1153 |
</button>
|
frontend/src/styles.css
CHANGED
|
@@ -460,6 +460,14 @@ textarea {
|
|
| 460 |
color: #ffffff;
|
| 461 |
}
|
| 462 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
.repo-upload-row,
|
| 464 |
.repo-delete-row {
|
| 465 |
display: flex;
|
|
@@ -4641,6 +4649,15 @@ button.model-source-choice-btn-secondary {
|
|
| 4641 |
color: #35506a;
|
| 4642 |
}
|
| 4643 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4644 |
.model-source-flow {
|
| 4645 |
display: grid;
|
| 4646 |
gap: 12px;
|
|
@@ -4731,6 +4748,46 @@ button.btn-upload-select {
|
|
| 4731 |
word-break: break-word;
|
| 4732 |
}
|
| 4733 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4734 |
.upload-file-info-card {
|
| 4735 |
margin-top: 0;
|
| 4736 |
border: 1px solid #d1deea;
|
|
@@ -4770,6 +4827,228 @@ button.btn-upload-select {
|
|
| 4770 |
word-break: break-word;
|
| 4771 |
}
|
| 4772 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4773 |
.import-feedback-line {
|
| 4774 |
margin-top: 10px;
|
| 4775 |
}
|
|
|
|
| 460 |
color: #ffffff;
|
| 461 |
}
|
| 462 |
|
| 463 |
+
.trabalhos-create-btn {
|
| 464 |
+
--btn-bg-start: #2f8f5b;
|
| 465 |
+
--btn-bg-end: #247a4b;
|
| 466 |
+
--btn-border: #1f6b42;
|
| 467 |
+
--btn-shadow-soft: rgba(47, 143, 91, 0.2);
|
| 468 |
+
--btn-shadow-strong: rgba(47, 143, 91, 0.28);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
.repo-upload-row,
|
| 472 |
.repo-delete-row {
|
| 473 |
display: flex;
|
|
|
|
| 4649 |
color: #35506a;
|
| 4650 |
}
|
| 4651 |
|
| 4652 |
+
button.model-source-choice-btn-tertiary {
|
| 4653 |
+
--btn-bg-start: #fff6e8;
|
| 4654 |
+
--btn-bg-end: #ffe6bf;
|
| 4655 |
+
--btn-border: #e2a94d;
|
| 4656 |
+
--btn-shadow-soft: rgba(226, 169, 77, 0.16);
|
| 4657 |
+
--btn-shadow-strong: rgba(226, 169, 77, 0.24);
|
| 4658 |
+
color: #704b12;
|
| 4659 |
+
}
|
| 4660 |
+
|
| 4661 |
.model-source-flow {
|
| 4662 |
display: grid;
|
| 4663 |
gap: 12px;
|
|
|
|
| 4748 |
word-break: break-word;
|
| 4749 |
}
|
| 4750 |
|
| 4751 |
+
.paste-excel-panel {
|
| 4752 |
+
display: grid;
|
| 4753 |
+
gap: 10px;
|
| 4754 |
+
border: 1px solid #d1deea;
|
| 4755 |
+
border-radius: 8px;
|
| 4756 |
+
background: #fbfdff;
|
| 4757 |
+
padding: 11px 12px;
|
| 4758 |
+
}
|
| 4759 |
+
|
| 4760 |
+
.paste-excel-field {
|
| 4761 |
+
display: grid;
|
| 4762 |
+
gap: 7px;
|
| 4763 |
+
color: #33485d;
|
| 4764 |
+
font-size: 0.9rem;
|
| 4765 |
+
font-weight: 800;
|
| 4766 |
+
}
|
| 4767 |
+
|
| 4768 |
+
.paste-excel-field textarea {
|
| 4769 |
+
min-height: 168px;
|
| 4770 |
+
resize: vertical;
|
| 4771 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
| 4772 |
+
font-size: 0.84rem;
|
| 4773 |
+
font-weight: 500;
|
| 4774 |
+
line-height: 1.45;
|
| 4775 |
+
white-space: pre;
|
| 4776 |
+
}
|
| 4777 |
+
|
| 4778 |
+
.paste-excel-actions {
|
| 4779 |
+
justify-content: flex-start;
|
| 4780 |
+
}
|
| 4781 |
+
|
| 4782 |
+
button.paste-excel-clear-btn {
|
| 4783 |
+
--btn-bg-start: #f5f8fb;
|
| 4784 |
+
--btn-bg-end: #edf2f7;
|
| 4785 |
+
--btn-border: #c9d7e4;
|
| 4786 |
+
--btn-shadow-soft: rgba(53, 74, 95, 0.08);
|
| 4787 |
+
--btn-shadow-strong: rgba(53, 74, 95, 0.12);
|
| 4788 |
+
color: #40576f;
|
| 4789 |
+
}
|
| 4790 |
+
|
| 4791 |
.upload-file-info-card {
|
| 4792 |
margin-top: 0;
|
| 4793 |
border: 1px solid #d1deea;
|
|
|
|
| 4827 |
word-break: break-word;
|
| 4828 |
}
|
| 4829 |
|
| 4830 |
+
.import-preview-card {
|
| 4831 |
+
display: grid;
|
| 4832 |
+
gap: 12px;
|
| 4833 |
+
border: 1px solid #cbd9e7;
|
| 4834 |
+
border-radius: 8px;
|
| 4835 |
+
background: #fbfdff;
|
| 4836 |
+
padding: 12px;
|
| 4837 |
+
}
|
| 4838 |
+
|
| 4839 |
+
.import-preview-head {
|
| 4840 |
+
display: flex;
|
| 4841 |
+
justify-content: space-between;
|
| 4842 |
+
align-items: flex-start;
|
| 4843 |
+
gap: 12px;
|
| 4844 |
+
}
|
| 4845 |
+
|
| 4846 |
+
.import-preview-head h4 {
|
| 4847 |
+
margin: 0 0 4px;
|
| 4848 |
+
}
|
| 4849 |
+
|
| 4850 |
+
.import-preview-counter {
|
| 4851 |
+
flex: 0 0 auto;
|
| 4852 |
+
border: 1px solid #a9c9e7;
|
| 4853 |
+
border-radius: 999px;
|
| 4854 |
+
background: #eef7ff;
|
| 4855 |
+
color: #245f91;
|
| 4856 |
+
font-size: 0.78rem;
|
| 4857 |
+
font-weight: 800;
|
| 4858 |
+
padding: 5px 9px;
|
| 4859 |
+
}
|
| 4860 |
+
|
| 4861 |
+
.import-preview-disclaimer {
|
| 4862 |
+
border-left: 3px solid #e28a29;
|
| 4863 |
+
background: #fff8ed;
|
| 4864 |
+
color: #624622;
|
| 4865 |
+
font-size: 0.86rem;
|
| 4866 |
+
font-weight: 700;
|
| 4867 |
+
line-height: 1.4;
|
| 4868 |
+
padding: 8px 10px;
|
| 4869 |
+
}
|
| 4870 |
+
|
| 4871 |
+
.import-preview-tools {
|
| 4872 |
+
display: flex;
|
| 4873 |
+
flex-wrap: wrap;
|
| 4874 |
+
align-items: center;
|
| 4875 |
+
gap: 8px;
|
| 4876 |
+
}
|
| 4877 |
+
|
| 4878 |
+
.import-preview-first-columns-field {
|
| 4879 |
+
display: inline-flex;
|
| 4880 |
+
align-items: center;
|
| 4881 |
+
gap: 7px;
|
| 4882 |
+
color: #33485d;
|
| 4883 |
+
font-size: 0.86rem;
|
| 4884 |
+
font-weight: 700;
|
| 4885 |
+
}
|
| 4886 |
+
|
| 4887 |
+
.import-preview-first-columns-toggle {
|
| 4888 |
+
display: inline-flex;
|
| 4889 |
+
align-items: center;
|
| 4890 |
+
gap: 7px;
|
| 4891 |
+
min-height: 34px;
|
| 4892 |
+
border: 1px solid #c8d8e8;
|
| 4893 |
+
border-radius: 8px;
|
| 4894 |
+
background: #f6f9fc;
|
| 4895 |
+
color: #33485d;
|
| 4896 |
+
font-size: 0.86rem;
|
| 4897 |
+
font-weight: 800;
|
| 4898 |
+
padding: 6px 9px;
|
| 4899 |
+
}
|
| 4900 |
+
|
| 4901 |
+
.import-preview-first-columns-toggle input {
|
| 4902 |
+
width: auto;
|
| 4903 |
+
margin: 0;
|
| 4904 |
+
}
|
| 4905 |
+
|
| 4906 |
+
.import-preview-first-columns-field input {
|
| 4907 |
+
width: 78px;
|
| 4908 |
+
}
|
| 4909 |
+
|
| 4910 |
+
button.import-preview-apply-btn {
|
| 4911 |
+
--btn-bg-start: #ff8c00;
|
| 4912 |
+
--btn-bg-end: #e67900;
|
| 4913 |
+
--btn-border: #cf6f00;
|
| 4914 |
+
--btn-shadow-soft: rgba(255, 140, 0, 0.2);
|
| 4915 |
+
--btn-shadow-strong: rgba(255, 140, 0, 0.28);
|
| 4916 |
+
}
|
| 4917 |
+
|
| 4918 |
+
button.import-preview-clear-btn {
|
| 4919 |
+
--btn-bg-start: #f5f8fb;
|
| 4920 |
+
--btn-bg-end: #edf2f7;
|
| 4921 |
+
--btn-border: #c9d7e4;
|
| 4922 |
+
--btn-shadow-soft: rgba(53, 74, 95, 0.08);
|
| 4923 |
+
--btn-shadow-strong: rgba(53, 74, 95, 0.12);
|
| 4924 |
+
color: #40576f;
|
| 4925 |
+
}
|
| 4926 |
+
|
| 4927 |
+
.import-preview-apply-row {
|
| 4928 |
+
display: flex;
|
| 4929 |
+
justify-content: flex-start;
|
| 4930 |
+
padding-top: 2px;
|
| 4931 |
+
}
|
| 4932 |
+
|
| 4933 |
+
.import-preview-table-wrap {
|
| 4934 |
+
overflow: auto;
|
| 4935 |
+
max-height: 360px;
|
| 4936 |
+
border: 1px solid #d4e0eb;
|
| 4937 |
+
border-radius: 8px;
|
| 4938 |
+
background: #ffffff;
|
| 4939 |
+
}
|
| 4940 |
+
|
| 4941 |
+
.import-preview-table {
|
| 4942 |
+
width: max-content;
|
| 4943 |
+
min-width: 100%;
|
| 4944 |
+
border-collapse: separate;
|
| 4945 |
+
border-spacing: 0;
|
| 4946 |
+
font-size: 0.82rem;
|
| 4947 |
+
}
|
| 4948 |
+
|
| 4949 |
+
.import-preview-table th,
|
| 4950 |
+
.import-preview-table td {
|
| 4951 |
+
max-width: 220px;
|
| 4952 |
+
min-width: 120px;
|
| 4953 |
+
border-right: 1px solid #e0e8f0;
|
| 4954 |
+
border-bottom: 1px solid #e8eef4;
|
| 4955 |
+
padding: 0;
|
| 4956 |
+
text-align: left;
|
| 4957 |
+
vertical-align: top;
|
| 4958 |
+
}
|
| 4959 |
+
|
| 4960 |
+
.import-preview-table td {
|
| 4961 |
+
color: #2f4358;
|
| 4962 |
+
padding: 7px 9px;
|
| 4963 |
+
white-space: nowrap;
|
| 4964 |
+
overflow: hidden;
|
| 4965 |
+
text-overflow: ellipsis;
|
| 4966 |
+
}
|
| 4967 |
+
|
| 4968 |
+
.import-preview-table th {
|
| 4969 |
+
position: sticky;
|
| 4970 |
+
top: 0;
|
| 4971 |
+
z-index: 1;
|
| 4972 |
+
background: #eef3f8;
|
| 4973 |
+
padding: 6px;
|
| 4974 |
+
}
|
| 4975 |
+
|
| 4976 |
+
.import-preview-table th.is-selected {
|
| 4977 |
+
background: #d9edff;
|
| 4978 |
+
box-shadow: inset 0 -3px 0 #2f80cf;
|
| 4979 |
+
}
|
| 4980 |
+
|
| 4981 |
+
.import-preview-table td.is-selected {
|
| 4982 |
+
background: #f0f8ff;
|
| 4983 |
+
}
|
| 4984 |
+
|
| 4985 |
+
.import-preview-table tr:hover td {
|
| 4986 |
+
background: #f8fbfe;
|
| 4987 |
+
}
|
| 4988 |
+
|
| 4989 |
+
.import-preview-table tr:hover td.is-selected {
|
| 4990 |
+
background: #e6f3ff;
|
| 4991 |
+
}
|
| 4992 |
+
|
| 4993 |
+
.import-preview-header-btn {
|
| 4994 |
+
display: flex;
|
| 4995 |
+
align-items: center;
|
| 4996 |
+
gap: 8px;
|
| 4997 |
+
width: 100%;
|
| 4998 |
+
min-height: 42px;
|
| 4999 |
+
border: 1px solid #9fbfdd;
|
| 5000 |
+
border-radius: 7px;
|
| 5001 |
+
background: linear-gradient(180deg, #ffffff 0%, #edf6ff 100%);
|
| 5002 |
+
box-shadow: 0 2px 5px rgba(49, 86, 122, 0.12);
|
| 5003 |
+
color: #24506f;
|
| 5004 |
+
font: inherit;
|
| 5005 |
+
font-weight: 800;
|
| 5006 |
+
line-height: 1.25;
|
| 5007 |
+
text-align: left;
|
| 5008 |
+
white-space: normal;
|
| 5009 |
+
overflow-wrap: anywhere;
|
| 5010 |
+
padding: 8px 10px;
|
| 5011 |
+
}
|
| 5012 |
+
|
| 5013 |
+
.import-preview-header-btn::before {
|
| 5014 |
+
content: "";
|
| 5015 |
+
flex: 0 0 14px;
|
| 5016 |
+
width: 14px;
|
| 5017 |
+
height: 14px;
|
| 5018 |
+
border: 2px solid #6d9dcc;
|
| 5019 |
+
border-radius: 4px;
|
| 5020 |
+
background: #ffffff;
|
| 5021 |
+
box-shadow: inset 0 0 0 2px #ffffff;
|
| 5022 |
+
}
|
| 5023 |
+
|
| 5024 |
+
.import-preview-header-btn:hover,
|
| 5025 |
+
.import-preview-header-btn:focus-visible {
|
| 5026 |
+
border-color: #2f80cf;
|
| 5027 |
+
background: linear-gradient(180deg, #f7fbff 0%, #dceeff 100%);
|
| 5028 |
+
box-shadow: 0 4px 10px rgba(47, 128, 207, 0.18);
|
| 5029 |
+
transform: none;
|
| 5030 |
+
}
|
| 5031 |
+
|
| 5032 |
+
.import-preview-table th.is-selected .import-preview-header-btn {
|
| 5033 |
+
border-color: #2f80cf;
|
| 5034 |
+
background: linear-gradient(180deg, #e6f4ff 0%, #cde8ff 100%);
|
| 5035 |
+
color: #155e99;
|
| 5036 |
+
}
|
| 5037 |
+
|
| 5038 |
+
.import-preview-table th.is-selected .import-preview-header-btn::before {
|
| 5039 |
+
border-color: #2f80cf;
|
| 5040 |
+
background: #2f80cf;
|
| 5041 |
+
}
|
| 5042 |
+
|
| 5043 |
+
.import-preview-index-cell {
|
| 5044 |
+
min-width: 58px !important;
|
| 5045 |
+
max-width: 72px !important;
|
| 5046 |
+
width: 64px;
|
| 5047 |
+
color: #66788a;
|
| 5048 |
+
font-weight: 800;
|
| 5049 |
+
background: #f7f9fb;
|
| 5050 |
+
}
|
| 5051 |
+
|
| 5052 |
.import-feedback-line {
|
| 5053 |
margin-top: 10px;
|
| 5054 |
}
|