Guilherme Silberfarb Costa commited on
Commit
a577d9a
·
1 Parent(s): b0eaf10

alteracoes generalizadas

Browse files
backend/app/api/elaboracao.py CHANGED
@@ -63,6 +63,8 @@ class ClassificarXPayload(SessionPayload):
63
  class SearchTransformPayload(SessionPayload):
64
  grau_min_coef: int = 0
65
  grau_min_f: int = 0
 
 
66
 
67
 
68
  class AdoptSuggestionPayload(SessionPayload):
@@ -250,6 +252,8 @@ def search_transformations(payload: SearchTransformPayload) -> dict[str, Any]:
250
  session,
251
  grau_min_coef=payload.grau_min_coef,
252
  grau_min_f=payload.grau_min_f,
 
 
253
  )
254
 
255
 
 
63
  class SearchTransformPayload(SessionPayload):
64
  grau_min_coef: int = 0
65
  grau_min_f: int = 0
66
+ transformacoes_fixas: dict[str, str] = Field(default_factory=dict)
67
+ transformacao_y_fixa: str | None = None
68
 
69
 
70
  class AdoptSuggestionPayload(SessionPayload):
 
252
  session,
253
  grau_min_coef=payload.grau_min_coef,
254
  grau_min_f=payload.grau_min_f,
255
+ transformacoes_fixas_usuario=payload.transformacoes_fixas,
256
+ transformacao_y_fixa_usuario=payload.transformacao_y_fixa,
257
  )
258
 
259
 
backend/app/api/pesquisa.py CHANGED
@@ -65,6 +65,7 @@ def pesquisar_modelos(
65
  rh_max: float | None = Query(None),
66
  aval_finalidade: str | None = Query(None),
67
  aval_finalidade_colunas: str | None = Query(None),
 
68
  aval_bairro: str | None = Query(None),
69
  aval_bairro_colunas: str | None = Query(None),
70
  aval_endereco: str | None = Query(None),
@@ -108,6 +109,7 @@ def pesquisar_modelos(
108
  rh_max=rh_max,
109
  aval_finalidade=aval_finalidade,
110
  aval_finalidade_colunas=_split_csv(aval_finalidade_colunas),
 
111
  aval_bairro=aval_bairro,
112
  aval_bairro_colunas=_split_csv(aval_bairro_colunas),
113
  aval_endereco=aval_endereco,
 
65
  rh_max: float | None = Query(None),
66
  aval_finalidade: str | None = Query(None),
67
  aval_finalidade_colunas: str | None = Query(None),
68
+ aval_zona: str | None = Query(None),
69
  aval_bairro: str | None = Query(None),
70
  aval_bairro_colunas: str | None = Query(None),
71
  aval_endereco: str | None = Query(None),
 
109
  rh_max=rh_max,
110
  aval_finalidade=aval_finalidade,
111
  aval_finalidade_colunas=_split_csv(aval_finalidade_colunas),
112
+ aval_zona=aval_zona,
113
  aval_bairro=aval_bairro,
114
  aval_bairro_colunas=_split_csv(aval_bairro_colunas),
115
  aval_endereco=aval_endereco,
backend/app/core/auth/usuarios.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "usuarios": [
3
  { "usuario": "adriana", "matricula": "188661", "nome": "ADRIANA KIRSCH BISSIGO", "perfil": "viewer" },
4
- { "usuario": "carla", "matricula": "458846", "nome": "CARLA ALEXANDRA GODOY FELICE", "perfil": "viewer" },
5
  { "usuario": "charles", "matricula": "1650645", "nome": "CHARLES ELIS SANDER PINTO DIAS", "perfil": "viewer" },
6
  { "usuario": "clara", "matricula": "1515977", "nome": "CLARA FRANCISCA MARQUES", "perfil": "viewer" },
7
  { "usuario": "daniela", "matricula": "1335618", "nome": "DANIELA CRISTINA JAHNEL", "perfil": "viewer" },
@@ -9,13 +9,13 @@
9
  { "usuario": "debora", "matricula": "1507850", "nome": "DEBORA FONSECA ALVES", "perfil": "viewer" },
10
  { "usuario": "edgar", "matricula": "1507559", "nome": "EDGAR ALEJANDRO VARGAS", "perfil": "viewer" },
11
  { "usuario": "fernanda_lazzari", "matricula": "1298640", "nome": "FERNANDA LAZZARI COSTI", "perfil": "viewer" },
12
- { "usuario": "fernanda_pontel", "matricula": "1036130", "nome": "FERNANDA PONTEL", "perfil": "viewer" },
13
  { "usuario": "fernando", "matricula": "1279670", "nome": "FERNANDO ROBERTO SCHWARTZER", "perfil": "viewer" },
14
  { "usuario": "francisco", "matricula": "1507583", "nome": "FRANCISCO TESTON TISBIEREK", "perfil": "viewer" },
15
  { "usuario": "gilmara", "matricula": "326310", "nome": "GILMARA MULLER", "perfil": "viewer" },
16
  { "usuario": "guilherme", "matricula": "1177028", "nome": "GUILHERME SILBERFARB COSTA", "perfil": "admin" },
17
  { "usuario": "gustavo", "matricula": "1043714", "nome": "GUSTAVO NATANIEL BARCELOS BASTOS", "perfil": "viewer" },
18
- { "usuario": "jessica", "matricula": "1279688", "nome": "JESSICA LANGE", "perfil": "viewer" },
19
  { "usuario": "joao", "matricula": "1226916", "nome": "JOAO MARCIO LOPES MORALLES", "perfil": "viewer" },
20
  { "usuario": "jones", "matricula": "1512170", "nome": "JONES RITTA RODRIGUES", "perfil": "viewer" },
21
  { "usuario": "julio", "matricula": "221287", "nome": "JULIO CESAR PEREIRA DIVAN", "perfil": "viewer" },
@@ -24,6 +24,6 @@
24
  { "usuario": "roberta", "matricula": "370440", "nome": "ROBERTA BRENNER AYUB", "perfil": "viewer" },
25
  { "usuario": "sabrina", "matricula": "1510428", "nome": "SABRINA VILLANOVA IBEIRO", "perfil": "viewer" },
26
  { "usuario": "vanessa_rocha", "matricula": "1052560", "nome": "VANESSA COSTA DA ROCHA", "perfil": "viewer" },
27
- { "usuario": "vanessa_staats", "matricula": "1507540", "nome": "VANESSA STAATS", "perfil": "viewer" }
28
  ]
29
  }
 
1
  {
2
  "usuarios": [
3
  { "usuario": "adriana", "matricula": "188661", "nome": "ADRIANA KIRSCH BISSIGO", "perfil": "viewer" },
4
+ { "usuario": "carla", "matricula": "458846", "nome": "CARLA ALEXANDRA GODOY FELICE", "perfil": "admin" },
5
  { "usuario": "charles", "matricula": "1650645", "nome": "CHARLES ELIS SANDER PINTO DIAS", "perfil": "viewer" },
6
  { "usuario": "clara", "matricula": "1515977", "nome": "CLARA FRANCISCA MARQUES", "perfil": "viewer" },
7
  { "usuario": "daniela", "matricula": "1335618", "nome": "DANIELA CRISTINA JAHNEL", "perfil": "viewer" },
 
9
  { "usuario": "debora", "matricula": "1507850", "nome": "DEBORA FONSECA ALVES", "perfil": "viewer" },
10
  { "usuario": "edgar", "matricula": "1507559", "nome": "EDGAR ALEJANDRO VARGAS", "perfil": "viewer" },
11
  { "usuario": "fernanda_lazzari", "matricula": "1298640", "nome": "FERNANDA LAZZARI COSTI", "perfil": "viewer" },
12
+ { "usuario": "fernanda_pontel", "matricula": "1036130", "nome": "FERNANDA PONTEL", "perfil": "admin" },
13
  { "usuario": "fernando", "matricula": "1279670", "nome": "FERNANDO ROBERTO SCHWARTZER", "perfil": "viewer" },
14
  { "usuario": "francisco", "matricula": "1507583", "nome": "FRANCISCO TESTON TISBIEREK", "perfil": "viewer" },
15
  { "usuario": "gilmara", "matricula": "326310", "nome": "GILMARA MULLER", "perfil": "viewer" },
16
  { "usuario": "guilherme", "matricula": "1177028", "nome": "GUILHERME SILBERFARB COSTA", "perfil": "admin" },
17
  { "usuario": "gustavo", "matricula": "1043714", "nome": "GUSTAVO NATANIEL BARCELOS BASTOS", "perfil": "viewer" },
18
+ { "usuario": "jessica", "matricula": "1279688", "nome": "JESSICA LANGE", "perfil": "admin" },
19
  { "usuario": "joao", "matricula": "1226916", "nome": "JOAO MARCIO LOPES MORALLES", "perfil": "viewer" },
20
  { "usuario": "jones", "matricula": "1512170", "nome": "JONES RITTA RODRIGUES", "perfil": "viewer" },
21
  { "usuario": "julio", "matricula": "221287", "nome": "JULIO CESAR PEREIRA DIVAN", "perfil": "viewer" },
 
24
  { "usuario": "roberta", "matricula": "370440", "nome": "ROBERTA BRENNER AYUB", "perfil": "viewer" },
25
  { "usuario": "sabrina", "matricula": "1510428", "nome": "SABRINA VILLANOVA IBEIRO", "perfil": "viewer" },
26
  { "usuario": "vanessa_rocha", "matricula": "1052560", "nome": "VANESSA COSTA DA ROCHA", "perfil": "viewer" },
27
+ { "usuario": "vanessa_staats", "matricula": "1507540", "nome": "VANESSA STAATS", "perfil": "admin" }
28
  ]
29
  }
backend/app/core/dados/Bairros_LC12112_16.cpg ADDED
@@ -0,0 +1 @@
 
 
1
+ UTF-8
backend/app/core/dados/Bairros_LC12112_16.dbf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:044858d12fd1dc12b82c9012aad1f506e27c2f2406a1330e6bb117c58592e347
3
+ size 15234
backend/app/core/dados/Bairros_LC12112_16.prj ADDED
@@ -0,0 +1 @@
 
 
1
+ PROJCS["TM-POA",GEOGCS["GCS_SIRGAS_2000",DATUM["D_SIRGAS_2000",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",300000.0],PARAMETER["False_Northing",5000000.0],PARAMETER["Central_Meridian",-51.0],PARAMETER["Scale_Factor",0.999995],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
backend/app/core/dados/Bairros_LC12112_16.sbn ADDED
Binary file (1.48 kB). View file
 
backend/app/core/dados/Bairros_LC12112_16.sbx ADDED
Binary file (244 Bytes). View file
 
backend/app/core/dados/Bairros_LC12112_16.shp ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1446d8cc7fdff70154b792fd1cf78b251d064ce38a58c8a81128702d2352967a
3
+ size 5562340
backend/app/core/dados/Bairros_LC12112_16.shp.xml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <metadata xml:lang="pt"><Esri><CreaDate>20170123</CreaDate><CreaTime>13572700</CreaTime><ArcGISFormat>1.0</ArcGISFormat><ArcGISstyle>ISO 19139 Metadata Implementation Specification GML3.2</ArcGISstyle><SyncOnce>FALSE</SyncOnce><DataProperties><itemProps><itemName Sync="TRUE">Bairros_LC12112_16</itemName><itemSize Sync="TRUE">5.305</itemSize><itemLocation><linkage Sync="FALSE">withheld</linkage><protocol Sync="TRUE">Local Area Network</protocol></itemLocation><nativeExtBox><westBL Sync="TRUE">271062.949100</westBL><eastBL Sync="TRUE">298900.245600</eastBL><southBL Sync="TRUE">1650035.887400</southBL><northBL Sync="TRUE">1687546.462300</northBL><exTypeCode Sync="TRUE">1</exTypeCode></nativeExtBox><imsContentType Sync="TRUE" export="False">002</imsContentType></itemProps><coordRef><type Sync="TRUE">Projected</type><geogcsn Sync="TRUE">GCS_SIRGAS_2000</geogcsn><csUnits Sync="TRUE">Linear Unit: Meter (1.000000)</csUnits><projcsn Sync="TRUE">TM-POA</projcsn><peXml Sync="TRUE">&lt;ProjectedCoordinateSystem xsi:type='typens:ProjectedCoordinateSystem' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:typens='http://www.esri.com/schemas/ArcGIS/10.6'&gt;&lt;WKT&gt;PROJCS[&amp;quot;TM-POA&amp;quot;,GEOGCS[&amp;quot;GCS_SIRGAS_2000&amp;quot;,DATUM[&amp;quot;D_SIRGAS_2000&amp;quot;,SPHEROID[&amp;quot;GRS_1980&amp;quot;,6378137.0,298.257222101]],PRIMEM[&amp;quot;Greenwich&amp;quot;,0.0],UNIT[&amp;quot;Degree&amp;quot;,0.0174532925199433]],PROJECTION[&amp;quot;Transverse_Mercator&amp;quot;],PARAMETER[&amp;quot;False_Easting&amp;quot;,300000.0],PARAMETER[&amp;quot;False_Northing&amp;quot;,5000000.0],PARAMETER[&amp;quot;Central_Meridian&amp;quot;,-51.0],PARAMETER[&amp;quot;Scale_Factor&amp;quot;,0.999995],PARAMETER[&amp;quot;Latitude_Of_Origin&amp;quot;,0.0],UNIT[&amp;quot;Meter&amp;quot;,1.0]]&lt;/WKT&gt;&lt;XOrigin&gt;-5323100&lt;/XOrigin&gt;&lt;YOrigin&gt;-5002100&lt;/YOrigin&gt;&lt;XYScale&gt;450265407.00157917&lt;/XYScale&gt;&lt;ZOrigin&gt;-100000&lt;/ZOrigin&gt;&lt;ZScale&gt;10000&lt;/ZScale&gt;&lt;MOrigin&gt;-100000&lt;/MOrigin&gt;&lt;MScale&gt;10000&lt;/MScale&gt;&lt;XYTolerance&gt;0.001&lt;/XYTolerance&gt;&lt;ZTolerance&gt;0.001&lt;/ZTolerance&gt;&lt;MTolerance&gt;0.001&lt;/MTolerance&gt;&lt;HighPrecision&gt;true&lt;/HighPrecision&gt;&lt;/ProjectedCoordinateSystem&gt;</peXml></coordRef></DataProperties><SyncDate>20180322</SyncDate><SyncTime>15220700</SyncTime><ModDate>20180322</ModDate><ModTime>15220700</ModTime><locales><locale language="por" country="BR"/></locales><scaleRange><minScale>50000</minScale><maxScale>5000</maxScale></scaleRange><ArcGISProfile>NAP</ArcGISProfile></Esri><mdChar><CharSetCd value="004"/></mdChar><dataIdInfo><idCitation><resTitle Sync="FALSE">Limites dos Bairros do Município de Porto Alegre</resTitle><presForm><fgdcGeoform>vector digital data</fgdcGeoform><PresFormCd value="005" Sync="TRUE"/></presForm><date><pubDate>2017-01-30T00:00:00</pubDate><reviseDate>2017-01-30T00:00:00</reviseDate></date><citRespParty><rpIndName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="003"/></role></citRespParty></idCitation><idPurp>Representação vetorial dos limites de bairros de Porto Alegre.</idPurp><idAbs>&lt;DIV STYLE="text-align:Left;"&gt;&lt;DIV&gt;&lt;DIV&gt;&lt;P&gt;&lt;SPAN&gt;Representação vetorial dos limites de bairros de Porto Alegre, definidos pela Lei Nº 12.112, de 22 de agosto de 2016. De acordo com o Art. 2º desta lei, o limite individual de cada bairro é representado através de redação descritiva padronizada e espacialização gráfica georreferenciada, sendo considerados os seguintes referenciais:&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;I - eixo central da via (eixo de logradouros), quando se tratar de sistema viário (avenida, rua, estrada, travessa, beco e outros);&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;II - eixo central do leito ou talvegue, quando se tratar do Rio Gravataí e de arroios;&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;III - margem ou orla, quando se tratar do Lago Guaíba;&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;IV - coordenadas georreferenciadas, quando se tratar de torre de alta tensão e sempre que necessária a sua utilização; &lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;V - limite de propriedade, preferencialmente de uso institucional, na ausência dos elementos físicos citados nos incs. I a IV;&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN&gt;VI - linha reta e imaginária, com uma ou ambas as extremidades definidas por coordenadas georreferenciadas, na ausência das situações dos incs. I a V.&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN /&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN STYLE="font-style:italic;"&gt;Escala &lt;/SPAN&gt;&lt;SPAN STYLE="font-style:italic;"&gt;do aerolevantamento base&lt;/SPAN&gt;&lt;SPAN STYLE="font-style:italic;"&gt;: 1:1000&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN STYLE="font-style:italic;"&gt;Observação quanto ao Sistema Geodésico de Referência e à Proje��ão Cartográfica utilizada:&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN STYLE="font-style:italic;"&gt;O Decreto Municipal n° 18.315/13 estabelece que os trabalhos de delimitação e demarcação de terras, de topografia e cartografia realizadas e/ou contratados por Órgãos da Administração Municipal Direta ou Indireta, bem como os projetos relacionados a essas atividades, que dependam de exame, aprovação ou controle desses Órgãos, devam estar referenciados ao Sistema Geodésico de Referência SIRGAS2000 e suas coordenadas representadas na Projeção Transversa de Mercator para Porto Alegre (TM-POA).&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN STYLE="font-style:italic;"&gt;Para maiores informações sobre características e parâmetros de transformação, acessar: &lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;A href="http://www2.portoalegre.rs.gov.br/spm/default.php?p_secao=345"&gt;&lt;SPAN&gt;http://www2.portoalegre.rs.gov.br/spm/default.php?p_secao=345&lt;/SPAN&gt;&lt;/A&gt;&lt;/P&gt;&lt;P&gt;&lt;SPAN /&gt;&lt;/P&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;</idAbs><idCredit>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)
2
+ Secretaria Municipal de Urbanismo (SMUrb)
3
+ Porto Alegre/RS - Brasil</idCredit><searchKeys><keyword>Bairros</keyword><keyword>Bairros Vigentes</keyword><keyword>Limites de Bairros</keyword><keyword>Urbanismo</keyword><keyword>Porto Alegre.</keyword></searchKeys><dataChar><CharSetCd value="004"/></dataChar><idPoC><rpIndName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="010"/></role><displayName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</displayName></idPoC><resMaint><maintFreq><MaintFreqCd value="009"/></maintFreq><maintCont><rpIndName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="010"/></role><displayName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</displayName></maintCont><maintCont><rpIndName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="003"/></role><editorSave>False</editorSave><displayName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</displayName></maintCont></resMaint><resConst><SecConsts><class><ClasscationCd value="001"/></class></SecConsts></resConst><resConst><Consts><useLimit>&lt;DIV STYLE="text-align:Left;"&gt;&lt;DIV&gt;&lt;DIV&gt;&lt;P&gt;&lt;SPAN&gt;Não recomendado o uso para projetos arquitetônicos executivos, ou outros que exijam alta precisão geométrica.&lt;/SPAN&gt;&lt;/P&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;/DIV&gt;</useLimit></Consts></resConst><envirDesc Sync="TRUE">Microsoft Windows 7 Version 6.1 (Build 7601) Service Pack 1; Esri ArcGIS 10.6.0.8321</envirDesc><dataLang><languageCode value="por" Sync="TRUE"/><countryCode value="BRA" Sync="TRUE"/></dataLang><spatRpType><SpatRepTypCd value="001" Sync="TRUE"/></spatRpType><dataExt><geoEle><GeoBndBox esriExtentType="search"><exTypeCode Sync="TRUE">1</exTypeCode><westBL Sync="TRUE">-51.300724</westBL><eastBL Sync="TRUE">-51.011390</eastBL><northBL Sync="TRUE">-29.930709</northBL><southBL Sync="TRUE">-30.269428</southBL></GeoBndBox></geoEle></dataExt><idPoC><rpIndName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="003"/></role><displayName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</displayName></idPoC><tpCat><TopicCatCd value="010"/></tpCat><tpCat><TopicCatCd value="015"/></tpCat><tpCat><TopicCatCd value="003"/></tpCat><tpCat><TopicCatCd value="013"/></tpCat><tpCat><TopicCatCd value="016"/></tpCat></dataIdInfo><mdMaint><maintFreq><MaintFreqCd value="009"/></maintFreq><maintCont><rpIndName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="003"/></role><displayName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</displayName></maintCont></mdMaint><mdConst><SecConsts><class><ClasscationCd value="001"/></class></SecConsts></mdConst><dataSetFn><OnFunctCd value="001"/></dataSetFn><eainfo><detailed Name="Bairros_LC12112_16"><enttyp><enttypl Sync="TRUE">Bairros_LC12112_16</enttypl><enttypt Sync="TRUE">Feature Class</enttypt><enttypc Sync="TRUE">128</enttypc></enttyp><attr><attrlabl Sync="TRUE">FID</attrlabl><attalias Sync="TRUE">FID</attalias><attrtype Sync="TRUE">OID</attrtype><attwidth Sync="TRUE">4</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale><attrdef Sync="TRUE">Internal feature number.</attrdef><attrdefs Sync="TRUE">Esri</attrdefs><attrdomv><udom Sync="TRUE">Sequential unique whole numbers that are automatically generated.</udom></attrdomv></attr><attr><attrlabl Sync="TRUE">Shape</attrlabl><attalias Sync="TRUE">Shape</attalias><attrtype Sync="TRUE">Geometry</attrtype><attwidth Sync="TRUE">0</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale><attrdef Sync="TRUE">Feature geometry.</attrdef><attrdefs Sync="TRUE">Esri</attrdefs><attrdomv><udom Sync="TRUE">Coordinates defining the features.</udom></attrdomv></attr><attr><attrlabl Sync="TRUE">OBJECTID</attrlabl><attalias Sync="TRUE">OBJECTID</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">10</attwidth><atprecis Sync="TRUE">10</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">CODIGO</attrlabl><attalias Sync="TRUE">CODIGO</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">10</attwidth><atprecis Sync="TRUE">10</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NOME</attrlabl><attalias Sync="TRUE">NOME</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">20</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">EDITOR</attrlabl><attalias Sync="TRUE">EDITOR</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">30</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">DATA_EDICA</attrlabl><attalias Sync="TRUE">DATA_EDICA</attalias><attrtype Sync="TRUE">Date</attrtype><attwidth Sync="TRUE">8</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">GEOM_AREA</attrlabl><attalias Sync="TRUE">GEOM_AREA</attalias><attrtype Sync="TRUE">Double</attrtype><attwidth Sync="TRUE">19</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">GEOM_LEN</attrlabl><attalias Sync="TRUE">GEOM_LEN</attalias><attrtype Sync="TRUE">Double</attrtype><attwidth Sync="TRUE">19</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr></detailed></eainfo><mdLang><languageCode value="por" Sync="TRUE"/><countryCode value="BRA" Sync="TRUE"/></mdLang><distInfo><distFormat><formatName Sync="TRUE">Shapefile</formatName></distFormat><distTranOps><transSize Sync="TRUE">5.305</transSize></distTranOps></distInfo><mdHrLv><ScopeCd value="005" Sync="TRUE"/></mdHrLv><mdHrLvName Sync="TRUE">dataset</mdHrLvName><refSysInfo><RefSystem><refSysID><identCode code="0" Sync="TRUE"/></refSysID></RefSystem></refSysInfo><spatRepInfo><VectSpatRep><geometObjs Name="Bairros_LC12112_16"><geoObjTyp><GeoObjTypCd value="002" Sync="TRUE"></GeoObjTypCd></geoObjTyp><geoObjCnt Sync="TRUE">128</geoObjCnt></geometObjs><topLvl><TopoLevCd value="001" Sync="TRUE"></TopoLevCd></topLvl></VectSpatRep></spatRepInfo><spdoinfo><ptvctinf><esriterm Name="Bairros_LC12112_16"><efeatyp Sync="TRUE">Simple</efeatyp><efeageom code="4" Sync="TRUE"></efeageom><esritopo Sync="TRUE">FALSE</esritopo><efeacnt Sync="TRUE">128</efeacnt><spindex Sync="TRUE">TRUE</spindex><linrefer Sync="TRUE">FALSE</linrefer></esriterm></ptvctinf></spdoinfo><mdDateSt Sync="TRUE">20180322</mdDateSt><mdFileID>BAIRROS_VIG</mdFileID><Binary><Thumbnail><Data EsriPropertyType="PictureX">/9j/4AAQSkZJRgABAQEAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a
4
+ HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy
5
+ MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACFAMgDASIA
6
+ AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA
7
+ AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3
8
+ ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm
9
+ p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA
10
+ AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx
11
+ BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK
12
+ U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3
13
+ uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iii
14
+ gAooooAKKKKACimSyLDC8rZKopY4GTgVn3zm6iEDs9tGzDcxkCswHYYP5/1qJ1IwV5MaVy4l5A6b
15
+ yxjGSP3qGMn8GANTAhgCCCDyCK54XVhbtvtdRjkkLHKeYJN/fBxkjr17e9X9OuoZ3WW2P7mbcGX+
16
+ 6468dj1z9BXPTxXNLlta43E06KKK6yQooqGS6giKB5VXfnHPHBwf1IH40ATUUgIIyCCPaloAKKKK
17
+ ACiiigAooooAKKKKACiiigAooooAKKKKACiiigBGUOpVgCpGCD3FZkthBBeCdkEglIQmQ7ihxxgn
18
+ oD/M+9alNkjWWNo3GVYYIrOrTVSLixp2Mu5ubcK8EtrNKF/gFuzq3pg4xWZZqpa7NvIkLFwrmLI8
19
+ tum7aeeqrkHHGa0Hjm2SrJcyZibbtUY+XPDHHzE454IqlfXH2K0uhEzzEw5dpI8YySMnp03DjBJ9
20
+ +TXnUYOLdt1/mU2aAu7qGeNJH86WV3Qx7QqpjJDeuMYHfqKub7ntJF/37P8A8VWTbyAXxM6SF1AL
21
+ SkbgSFwMkcdGPOBn04rRW7tnGVuIiPUOKK2IqRdoPT/MaSKz22pTMfNvoHjzkRG2+X6H5sn/AOsK
22
+ VLW5jyUks1JAB22xGQOn8dTrdwuxCMz46lELD8wMUx3e5k8mMSRx4zJIVKk+y+/qe31ORzyq1Jv3
23
+ mOyK9nOkihY5pYDIThVj2hsZ6ErgnA5x/wDXrUs932VQ7FmUlSx74JFQNbQtEkezakf3AhK7eMcY
24
+ 6ccVLp6lLCGMtuaNfLLepXgn9K7cHUcpPX5EyRZooor0CAooooAKKKKACiiigAooooAKKKKACiii
25
+ gAooooAKKKbJvETmMAvtO0HpntQBh6m23UPOkeRYV/dHymKk/Lu7Hk8n6YNV3sootLbZAEikdBIZ
26
+ 8pkL8wJB+bO49upGQOcG2vmLdpZzSgzF0lUIMH7xZievHbPAPIq1EVe5lWRt1xGx+91VSTtx+Hp+
27
+ NcMpunzVGutv6/Iq19CPTbf7PZqCNpbkKRjaOwx2+nuadDBDvljaJWKuWBZQSQx3fzJH4Vaqrckx
28
+ TQSpkuWEZQD7wJGfyxn6Z9a81ycm33NC1RSEhVLE4AGSagFw8qgwwswPIZ/kH68/pUWAlm8wwSCI
29
+ gSbTsJ9ccVLaGM2yeUrKoyu1uoIODn1Oc81VN0sThLgLCTnaxYbW+h9f/r1JpU0M9grwyJIoZgXR
30
+ gQxDHJ49ev416OAum0RMu0UUV6RAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFV4JLh7i4SW
31
+ ELEjARSA/fGOeO2OnvViigCpZ2P2ZnmmlM9zJw0rKBwOgAHQVXa4kkvpVhiVgoCsWfABBPseeent
32
+ 7ir11I0VrK6Ebwp257t2H51Wt7dLaMomeWLMx6sxOST+NcONqKMOTuVFCW8ryhxIipJG21grbh0B
33
+ 4OB2I7VA06LfSSOH2xqEQhC3J5bGM/7P5UXDWaXBklkYMVAYKTggZxnH+8eD/SoLe4KEIu5TklIy
34
+ FVJBzjbx3615qXUssXEyyxiEK37xgh3oVBGfmGT7Zqe4Zkt5GU4IUnOM49x71Va333kbXBUNIDhV
35
+ A44Hy5PJ43enHbipRZlFCx3MyKP4QFI/IjgfTFGisMt/YLXGGgSQ9C0g3E/iasdKqaarJaBXbLKx
36
+ BAzhcdhkk4/xq3XvQacU0YhRRRVAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQBz2pFH
37
+ vvIuWm3u+I2V3CqMFhhRgE/Lx1557Yq3dQzXgHltJCFGVO7GWyCpIHbjofXpV6W3dpjLHIFYqFIZ
38
+ dw4J9x61DFNv2q6lJduSjdR649R7ivKxcJxnzdDSNjHWSETppl2FKoQUaON48nIXBGT69c4ORWz5
39
+ EX7rEajyv9XgY28Y4/CkuYRcW7xnqRlTnGCOh/A02zaV7ZTNzJkgnjnk88VySd1coWQ5uolAJwGJ
40
+ 9APX69vxNTVFLCshDFnBCkfIcHBx/gKbbXMc6BRJGZVA8xFblT3yO1T0AkgkEDmOTA8xyUbP3ie3
41
+ 1/wq5VC5/wBUDnAEiEn0AYZP5d6v17GDqOdPXpoZyVmFFFFdRIUUUUAFFFFABRRRQAUUUUAFFFFA
42
+ BRRRQAUUUUAFFFFABVW+UeVG+PnSVNp7jJAP6E1arP1BBKbd4pmWUPhdmCMZG7IPsCM9s+9Z1f4c
43
+ vQa3GXVygDRAvuyFOEY8dTg+uPxoW685SLRBIFJXe3yoCDjGe/4UfYh5nmCaTfv3k8cnbt9MdKrG
44
+ 2Yapl9+2RcExlkJwOCSuPcfiOleGlE1LYjumPzzxquOfLjwc/Ukj9KhmVUlt7eE7WB3buWIznr65
45
+ 5zz/AIiX7KIphLbqikgh1Hyhs45OByRj9TSpBKtz5xlTkbXVUxux07+9K4A1vJK6edIjxryU8vhu
46
+ O/P+f5WLNv3RjY/OpOV9AScY9sf5HSioXlW3uklfzNnlsDtQsM5XGcA47104Sry1LPZkyWhfopqO
47
+ siB0YMp5BU5Bp1ewZhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAIyh0ZW6MMHnFU4tN
48
+ jh+5LIvToqDgduFq7RUyhGXxK4FR7aeNd0c5kI6rIBz9CMYoRxJGrrnDAEZqe4hFxA8RZlDDGVOD
49
+ VRkktpYgZTIsjFdu0DZwTxjtxjnPUc1wYvDK3NBWtuXGXcmooorzCwooooAi+zrk/PJsJyYw2Fz3
50
+ /wD1dPalLtboXEvyKMkSHIx9ev8AOllk8tRwWduEUdWOM4/Sljt3klElwEwnKIrEjPqenTt6dfp2
51
+ YdVptOL0JdkWY2LxKzLtJAJU9qdRRXsGYUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB
52
+ VBbS7D+dJcJLKMqAVwoUnPGOnb16D61foPTgZqZQU1aQFEtLDLHHMY235CunGSOfunOOO+TSyTxw
53
+ 4DuAW+6vUt9B1P4Uz/TJpUne1VAqsuzzQW5I9sdAO/rT7AiR7iYoyyFwuHXBCgDA9xkk/jXnSwil
54
+ VslaJfNoQrfpLdG2hjkkkChj8uAuSR8xPQ8dOtTNI8f+thdFzjeMFf05A9yBUj2hfUYrsOF2IUIC
55
+ 8sD2J9M4P4e9WGVXUqyhlIwQRkEVt9Rp2sLmZVt133UsjH7mEUY6ZAJP48D8Kt1UtLSS2ldnnEil
56
+ VRfkwQATjJyc9at10UYckFFibuwooorUQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAB
57
+ RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf/2Q==</Data></Thumbnail></Binary><mdContact><rpIndName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="003"/></role><displayName>Unidade de Monitoramento do Desenvolvimento Urbano (UMDU)</displayName></mdContact><mdContact><rpIndName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</rpIndName><rpOrgName>Secretaria Municipal de Urbanismo (SMUrb) de Porto Alegre - RS</rpOrgName><role><RoleCd value="010"/></role><displayName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</displayName><displayName>Unidade de Geoprocessamento e Sensoriamento Remoto Aplicado</displayName></mdContact></metadata>
backend/app/core/dados/Bairros_LC12112_16.shx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d1f2aae715d854e1cb48e58d9561ab39985f9990aaeec8403cd95074654a320d
3
+ size 1124
backend/app/core/elaboracao/charts.py CHANGED
@@ -15,6 +15,7 @@ from folium import plugins
15
  import branca.colormap as cm
16
  from branca.element import Element
17
  from html import escape
 
18
 
19
  # ============================================================
20
  # CONSTANTES DE ESTILO
@@ -815,7 +816,7 @@ def _adicionar_superficie_continua(
815
  fill=True,
816
  fill_color=cor,
817
  fill_opacity=0.6,
818
- tooltip=folium.Tooltip(f"Resíduo interpolado: {valor_fmt}", sticky=False),
819
  ).add_to(camada_superficie)
820
 
821
  camada_superficie.add_to(m)
@@ -910,6 +911,7 @@ def criar_mapa(
910
  # Camadas base
911
  folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(m)
912
  folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(m)
 
913
 
914
  modo_normalizado = str(modo or "pontos").strip().lower()
915
  modo_calor = (
 
15
  import branca.colormap as cm
16
  from branca.element import Element
17
  from html import escape
18
+ from app.core.map_layers import add_bairros_layer
19
 
20
  # ============================================================
21
  # CONSTANTES DE ESTILO
 
816
  fill=True,
817
  fill_color=cor,
818
  fill_opacity=0.6,
819
+ tooltip=folium.Tooltip(f"{valor_col} interpolado: {valor_fmt}", sticky=False),
820
  ).add_to(camada_superficie)
821
 
822
  camada_superficie.add_to(m)
 
911
  # Camadas base
912
  folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(m)
913
  folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(m)
914
+ add_bairros_layer(m, show=True)
915
 
916
  modo_normalizado = str(modo or "pontos").strip().lower()
917
  modo_calor = (
backend/app/core/elaboracao/core.py CHANGED
@@ -2169,11 +2169,11 @@ def avaliar_imovel(modelo_sm, valores_x, colunas_x, transformacoes_x, transforma
2169
  min_val = float(limites.loc[col, "Mínimo"])
2170
  max_val = float(limites.loc[col, "Máximo"])
2171
 
2172
- # Dicotômica, categórica codificada ou percentual — validação já feita no callback
2173
  if col in (dicotomicas or []):
2174
  extrapolacoes[col] = {"status": "dicotomica", "percentual": 0.0, "direcao": "ok"}
2175
  continue
2176
- if col in (codigo_alocado or []):
2177
  extrapolacoes[col] = {"status": "codigo_alocado", "percentual": 0.0, "direcao": "ok"}
2178
  continue
2179
  if col in (percentuais or []):
 
2169
  min_val = float(limites.loc[col, "Mínimo"])
2170
  max_val = float(limites.loc[col, "Máximo"])
2171
 
2172
+ # Dicotômica, categórica codificada (exceto RH) ou percentual — validação já feita no callback
2173
  if col in (dicotomicas or []):
2174
  extrapolacoes[col] = {"status": "dicotomica", "percentual": 0.0, "direcao": "ok"}
2175
  continue
2176
+ if col in (codigo_alocado or []) and str(col).strip().upper() != "RH":
2177
  extrapolacoes[col] = {"status": "codigo_alocado", "percentual": 0.0, "direcao": "ok"}
2178
  continue
2179
  if col in (percentuais or []):
backend/app/core/map_layers.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from threading import Lock
6
+ from typing import Any
7
+
8
+ import folium
9
+
10
+ _BASE_DIR = Path(__file__).resolve().parent
11
+ _BAIRROS_SHP_PATH = _BASE_DIR / "dados" / "Bairros_LC12112_16.shp"
12
+ _TOOLTIP_FIELDS = ("NOME", "BAIRRO", "NME_BAI", "NOME_BAIRRO")
13
+ _SIMPLIFY_TOLERANCE = 0.00005
14
+
15
+ _BAIRROS_CACHE_LOCK = Lock()
16
+ _BAIRROS_GEOJSON_CACHE: dict[str, Any] | None = None
17
+ _BAIRROS_GEOJSON_CARREGADO = False
18
+
19
+
20
+ def _carregar_bairros_geojson() -> dict[str, Any] | None:
21
+ global _BAIRROS_GEOJSON_CACHE, _BAIRROS_GEOJSON_CARREGADO
22
+ with _BAIRROS_CACHE_LOCK:
23
+ if _BAIRROS_GEOJSON_CARREGADO:
24
+ return _BAIRROS_GEOJSON_CACHE
25
+ _BAIRROS_GEOJSON_CARREGADO = True
26
+
27
+ if not _BAIRROS_SHP_PATH.exists():
28
+ return None
29
+
30
+ try:
31
+ import geopandas as gpd
32
+ except Exception:
33
+ return None
34
+
35
+ try:
36
+ gdf = gpd.read_file(_BAIRROS_SHP_PATH, engine="fiona")
37
+ if gdf is None or gdf.empty:
38
+ geojson = None
39
+ else:
40
+ if gdf.crs is not None:
41
+ gdf = gdf.to_crs("EPSG:4326")
42
+ campos = ["geometry"]
43
+ for campo in _TOOLTIP_FIELDS:
44
+ if campo in gdf.columns:
45
+ campos.insert(0, campo)
46
+ break
47
+ gdf = gdf.loc[:, campos].copy()
48
+ if _SIMPLIFY_TOLERANCE > 0:
49
+ gdf["geometry"] = gdf.geometry.simplify(_SIMPLIFY_TOLERANCE, preserve_topology=True)
50
+ geojson = json.loads(gdf.to_json(drop_id=True))
51
+ except Exception:
52
+ geojson = None
53
+
54
+ with _BAIRROS_CACHE_LOCK:
55
+ _BAIRROS_GEOJSON_CACHE = geojson
56
+ return geojson
57
+
58
+
59
+ def add_bairros_layer(
60
+ mapa: folium.Map,
61
+ *,
62
+ show: bool = True,
63
+ layer_name: str = "Bairros",
64
+ ) -> bool:
65
+ geojson = _carregar_bairros_geojson()
66
+ if not geojson:
67
+ return False
68
+
69
+ tooltip = None
70
+ features = geojson.get("features") if isinstance(geojson, dict) else None
71
+ if isinstance(features, list) and features:
72
+ props = features[0].get("properties") if isinstance(features[0], dict) else None
73
+ if isinstance(props, dict):
74
+ for candidate in _TOOLTIP_FIELDS:
75
+ if candidate in props:
76
+ tooltip = folium.GeoJsonTooltip(
77
+ fields=[candidate],
78
+ aliases=["Bairro:"],
79
+ localize=False,
80
+ sticky=False,
81
+ labels=True,
82
+ )
83
+ break
84
+
85
+ folium.GeoJson(
86
+ data=geojson,
87
+ name=layer_name,
88
+ show=show,
89
+ control=True,
90
+ overlay=True,
91
+ smooth_factor=0.6,
92
+ style_function=lambda _: {
93
+ "color": "#4c6882",
94
+ "weight": 1.0,
95
+ "fillColor": "#f39c12",
96
+ "fillOpacity": 0.04,
97
+ },
98
+ highlight_function=lambda _: {
99
+ "color": "#e67e22",
100
+ "weight": 1.6,
101
+ "fillOpacity": 0.12,
102
+ },
103
+ tooltip=tooltip,
104
+ ).add_to(mapa)
105
+ return True
backend/app/core/visualizacao/app.py CHANGED
@@ -22,6 +22,7 @@ import math
22
 
23
  from app.core.elaboracao.core import avaliar_imovel, _migrar_pacote_v1_para_v2, exportar_avaliacoes_excel
24
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
 
25
 
26
  # ============================================================
27
  # CONSTANTES
@@ -741,6 +742,7 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
741
  # Camadas base
742
  folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(m)
743
  folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(m)
 
744
 
745
  # Se tamanho_col fornecido mas cor_col não, usa mesma variável para cor
746
  if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
 
22
 
23
  from app.core.elaboracao.core import avaliar_imovel, _migrar_pacote_v1_para_v2, exportar_avaliacoes_excel
24
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
25
+ from app.core.map_layers import add_bairros_layer
26
 
27
  # ============================================================
28
  # CONSTANTES
 
742
  # Camadas base
743
  folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(m)
744
  folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(m)
745
+ add_bairros_layer(m, show=True)
746
 
747
  # Se tamanho_col fornecido mas cor_col não, usa mesma variável para cor
748
  if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
backend/app/services/elaboracao_service.py CHANGED
@@ -50,6 +50,10 @@ _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboraca
50
  _AVALIADORES_CACHE: list[dict[str, Any]] | None = None
51
 
52
 
 
 
 
 
53
  def _parse_data_iso_segura(value: Any) -> str | None:
54
  if value is None:
55
  return None
@@ -846,20 +850,44 @@ def apply_selection(
846
  }
847
 
848
 
849
- def search_transformacoes(session: SessionState, grau_min_coef: int = 0, grau_min_f: int = 0) -> dict[str, Any]:
 
 
 
 
 
 
850
  df = session.df_filtrado if session.df_filtrado is not None else session.df_original
851
  if df is None or not session.coluna_y or not session.colunas_x:
852
  raise HTTPException(status_code=400, detail="Selecione variaveis antes de buscar transformacoes")
853
 
854
  transformacoes_fixas: dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
 
 
855
  for col in (session.dicotomicas or []) + (session.percentuais or []):
856
  transformacoes_fixas[col] = "(x)"
857
 
 
 
 
 
 
858
  resultados = buscar_melhores_transformacoes(
859
  df,
860
  session.coluna_y,
861
  session.colunas_x,
862
  transformacoes_fixas=transformacoes_fixas,
 
863
  top_n=5,
864
  grau_min_coef=int(grau_min_coef),
865
  grau_min_f=int(grau_min_f),
@@ -1549,7 +1577,10 @@ def calcular_avaliacao_elaboracao(
1549
  if col in session.codigo_alocado and col in est_idx.index:
1550
  min_v = float(est_idx.loc[col, "Mínimo"])
1551
  max_v = float(est_idx.loc[col, "Máximo"])
1552
- if float(valor) != int(float(valor)) or valor < min_v or valor > max_v:
 
 
 
1553
  raise HTTPException(status_code=400, detail=f"{col} aceita inteiros de {int(min_v)} a {int(max_v)}")
1554
  if col in session.percentuais and (valor < 0 or valor > 1):
1555
  raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
 
50
  _AVALIADORES_CACHE: list[dict[str, Any]] | None = None
51
 
52
 
53
+ def _is_rh_col(coluna: str) -> bool:
54
+ return str(coluna or "").strip().upper() == "RH"
55
+
56
+
57
  def _parse_data_iso_segura(value: Any) -> str | None:
58
  if value is None:
59
  return None
 
850
  }
851
 
852
 
853
+ def search_transformacoes(
854
+ session: SessionState,
855
+ grau_min_coef: int = 0,
856
+ grau_min_f: int = 0,
857
+ transformacoes_fixas_usuario: dict[str, str] | None = None,
858
+ transformacao_y_fixa_usuario: str | None = None,
859
+ ) -> dict[str, Any]:
860
  df = session.df_filtrado if session.df_filtrado is not None else session.df_original
861
  if df is None or not session.coluna_y or not session.colunas_x:
862
  raise HTTPException(status_code=400, detail="Selecione variaveis antes de buscar transformacoes")
863
 
864
  transformacoes_fixas: dict[str, str] = {}
865
+ for coluna, transformacao in (transformacoes_fixas_usuario or {}).items():
866
+ col = str(coluna or "").strip()
867
+ transf = str(transformacao or "").strip()
868
+ if not col or col not in session.colunas_x:
869
+ continue
870
+ if not transf or transf.lower() == "livre":
871
+ continue
872
+ if transf not in TRANSFORMACOES:
873
+ continue
874
+ transformacoes_fixas[col] = transf
875
+
876
+ # Dicotomicas e percentuais permanecem fixas em (x), independente da selecao manual.
877
  for col in (session.dicotomicas or []) + (session.percentuais or []):
878
  transformacoes_fixas[col] = "(x)"
879
 
880
+ transformacao_y_fixa: str | None = None
881
+ transformacao_y_lida = str(transformacao_y_fixa_usuario or "").strip()
882
+ if transformacao_y_lida and transformacao_y_lida.lower() != "livre" and transformacao_y_lida in TRANSFORMACOES:
883
+ transformacao_y_fixa = transformacao_y_lida
884
+
885
  resultados = buscar_melhores_transformacoes(
886
  df,
887
  session.coluna_y,
888
  session.colunas_x,
889
  transformacoes_fixas=transformacoes_fixas,
890
+ transformacao_y_fixa=transformacao_y_fixa,
891
  top_n=5,
892
  grau_min_coef=int(grau_min_coef),
893
  grau_min_f=int(grau_min_f),
 
1577
  if col in session.codigo_alocado and col in est_idx.index:
1578
  min_v = float(est_idx.loc[col, "Mínimo"])
1579
  max_v = float(est_idx.loc[col, "Máximo"])
1580
+ if _is_rh_col(col):
1581
+ if float(valor) != int(float(valor)):
1582
+ raise HTTPException(status_code=400, detail=f"{col} aceita apenas valores inteiros")
1583
+ elif float(valor) != int(float(valor)) or valor < min_v or valor > max_v:
1584
  raise HTTPException(status_code=400, detail=f"{col} aceita inteiros de {int(min_v)} a {int(max_v)}")
1585
  if col in session.percentuais and (valor < 0 or valor > 1):
1586
  raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
backend/app/services/pesquisa_service.py CHANGED
@@ -16,6 +16,7 @@ from fastapi import HTTPException
16
  from joblib import load
17
 
18
  from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
 
19
  from app.services import model_repository
20
  from app.services.serializers import sanitize_value
21
 
@@ -168,6 +169,7 @@ class PesquisaFiltros:
168
  rh_max: float | None = None
169
  aval_finalidade: str | None = None
170
  aval_finalidade_colunas: list[str] | None = None
 
171
  aval_bairro: str | None = None
172
  aval_bairro_colunas: list[str] | None = None
173
  aval_endereco: str | None = None
@@ -292,6 +294,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
292
  "otica": otica,
293
  "aval_finalidade": filtros.aval_finalidade,
294
  "aval_finalidade_colunas": filtros.aval_finalidade_colunas or [],
 
295
  "aval_bairro": filtros.aval_bairro,
296
  "aval_bairro_colunas": filtros.aval_bairro_colunas or [],
297
  "aval_endereco": filtros.aval_endereco,
@@ -358,6 +361,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
358
  "otica": otica,
359
  "aval_finalidade": filtros.aval_finalidade,
360
  "aval_finalidade_colunas": filtros.aval_finalidade_colunas or [],
 
361
  "aval_bairro": filtros.aval_bairro,
362
  "aval_bairro_colunas": filtros.aval_bairro_colunas or [],
363
  "aval_endereco": filtros.aval_endereco,
@@ -446,6 +450,7 @@ def gerar_mapa_modelos(modelos_ids: list[str], limite_pontos_por_modelo: int = 4
446
  )
447
  folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(mapa)
448
  folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(mapa)
 
449
 
450
  total_pontos = 0
451
  for modelo in modelos_plotados:
@@ -1144,6 +1149,19 @@ def _anexar_avaliando_info(
1144
  detalhe = "nenhum dos bairros informados esta na cobertura do modelo" if len(termos_bairro) > 1 else "bairro fora da cobertura do modelo"
1145
  registrar("bairro", bairro_info, aceito, detalhe)
1146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1147
  endereco_info = filtros.aval_endereco
1148
  if _is_provided(endereco_info):
1149
  candidatos = [item.get("endereco_referencia"), ", ".join(item.get("bairros") or [])]
@@ -1232,6 +1250,7 @@ def _extrair_sugestoes(
1232
  bairros: list[str] = []
1233
  enderecos: list[str] = []
1234
  tipos_modelo: list[str] = []
 
1235
 
1236
  fontes_finalidade = _dedupe_strings((fontes_admin.get("finalidade") or []) + (fontes_admin.get("aval_finalidade") or []))
1237
  fontes_bairro = _dedupe_strings((fontes_admin.get("bairros") or []) + (fontes_admin.get("aval_bairro") or []))
@@ -1251,6 +1270,7 @@ def _extrair_sugestoes(
1251
  bairros.extend([str(item) for item in (modelo.get("bairros") or [])])
1252
  enderecos.append(str(modelo.get("endereco_referencia") or ""))
1253
  tipos_modelo.append(str(_tipo_modelo_modelo(modelo) or ""))
 
1254
 
1255
  return {
1256
  "nomes_modelo": _lista_textos_unicos(nomes, limite),
@@ -1259,6 +1279,7 @@ def _extrair_sugestoes(
1259
  "bairros": _lista_textos_unicos(bairros, limite),
1260
  "enderecos": _lista_textos_unicos(enderecos, limite),
1261
  "tipos_modelo": _lista_textos_unicos(tipos_modelo, limite),
 
1262
  }
1263
 
1264
 
@@ -1770,6 +1791,72 @@ def _negociacao_modelo_modelo(modelo: dict[str, Any]) -> str | None:
1770
  return None
1771
 
1772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1773
  def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
1774
  termos: list[str] = []
1775
  if filtros.bairro:
 
16
  from joblib import load
17
 
18
  from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
19
+ from app.core.map_layers import add_bairros_layer
20
  from app.services import model_repository
21
  from app.services.serializers import sanitize_value
22
 
 
169
  rh_max: float | None = None
170
  aval_finalidade: str | None = None
171
  aval_finalidade_colunas: list[str] | None = None
172
+ aval_zona: str | None = None
173
  aval_bairro: str | None = None
174
  aval_bairro_colunas: list[str] | None = None
175
  aval_endereco: str | None = None
 
294
  "otica": otica,
295
  "aval_finalidade": filtros.aval_finalidade,
296
  "aval_finalidade_colunas": filtros.aval_finalidade_colunas or [],
297
+ "aval_zona": filtros.aval_zona,
298
  "aval_bairro": filtros.aval_bairro,
299
  "aval_bairro_colunas": filtros.aval_bairro_colunas or [],
300
  "aval_endereco": filtros.aval_endereco,
 
361
  "otica": otica,
362
  "aval_finalidade": filtros.aval_finalidade,
363
  "aval_finalidade_colunas": filtros.aval_finalidade_colunas or [],
364
+ "aval_zona": filtros.aval_zona,
365
  "aval_bairro": filtros.aval_bairro,
366
  "aval_bairro_colunas": filtros.aval_bairro_colunas or [],
367
  "aval_endereco": filtros.aval_endereco,
 
450
  )
451
  folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(mapa)
452
  folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(mapa)
453
+ add_bairros_layer(mapa, show=True)
454
 
455
  total_pontos = 0
456
  for modelo in modelos_plotados:
 
1149
  detalhe = "nenhum dos bairros informados esta na cobertura do modelo" if len(termos_bairro) > 1 else "bairro fora da cobertura do modelo"
1150
  registrar("bairro", bairro_info, aceito, detalhe)
1151
 
1152
+ zona_info = filtros.aval_zona
1153
+ if _is_provided(zona_info):
1154
+ zonas_informadas = _extrair_termos_zona(str(zona_info))
1155
+ zonas_modelo = _zonas_avaliacao_modelo(item)
1156
+ zonas_modelo_norm = {_normalize(zona) for zona in zonas_modelo}
1157
+ aceito = any(_normalize(zona) in zonas_modelo_norm for zona in zonas_informadas) if zonas_informadas else False
1158
+ detalhe = (
1159
+ "nenhuma das zonas informadas foi encontrada no modelo"
1160
+ if len(zonas_informadas) > 1
1161
+ else "zona fora da cobertura do modelo"
1162
+ )
1163
+ registrar("zona_avaliacao", zona_info, aceito, detalhe)
1164
+
1165
  endereco_info = filtros.aval_endereco
1166
  if _is_provided(endereco_info):
1167
  candidatos = [item.get("endereco_referencia"), ", ".join(item.get("bairros") or [])]
 
1250
  bairros: list[str] = []
1251
  enderecos: list[str] = []
1252
  tipos_modelo: list[str] = []
1253
+ zonas_avaliacao: list[str] = []
1254
 
1255
  fontes_finalidade = _dedupe_strings((fontes_admin.get("finalidade") or []) + (fontes_admin.get("aval_finalidade") or []))
1256
  fontes_bairro = _dedupe_strings((fontes_admin.get("bairros") or []) + (fontes_admin.get("aval_bairro") or []))
 
1270
  bairros.extend([str(item) for item in (modelo.get("bairros") or [])])
1271
  enderecos.append(str(modelo.get("endereco_referencia") or ""))
1272
  tipos_modelo.append(str(_tipo_modelo_modelo(modelo) or ""))
1273
+ zonas_avaliacao.extend(_zonas_avaliacao_modelo(modelo))
1274
 
1275
  return {
1276
  "nomes_modelo": _lista_textos_unicos(nomes, limite),
 
1279
  "bairros": _lista_textos_unicos(bairros, limite),
1280
  "enderecos": _lista_textos_unicos(enderecos, limite),
1281
  "tipos_modelo": _lista_textos_unicos(tipos_modelo, limite),
1282
+ "zonas_avaliacao": _lista_zonas_unicas(zonas_avaliacao, limite),
1283
  }
1284
 
1285
 
 
1791
  return None
1792
 
1793
 
1794
+ def _normalizar_token_zona(value: Any) -> str | None:
1795
+ texto = _str_or_none(value)
1796
+ if not texto:
1797
+ return None
1798
+ match = re.search(r"Z?\s*0*(\d+)", texto.upper())
1799
+ if not match:
1800
+ return None
1801
+ numero = int(match.group(1))
1802
+ if numero <= 0:
1803
+ return None
1804
+ return f"Z{numero}"
1805
+
1806
+
1807
+ def _extrair_termos_zona(texto: str) -> list[str]:
1808
+ zonas = []
1809
+ vistos = set()
1810
+ for termo in _split_terms(texto):
1811
+ zona = _normalizar_token_zona(termo)
1812
+ if not zona:
1813
+ continue
1814
+ chave = _normalize(zona)
1815
+ if chave in vistos:
1816
+ continue
1817
+ vistos.add(chave)
1818
+ zonas.append(zona)
1819
+ return zonas
1820
+
1821
+
1822
+ def _zonas_avaliacao_modelo(modelo: dict[str, Any]) -> list[str]:
1823
+ nomes_ref = [
1824
+ _str_or_none(modelo.get("arquivo")) or "",
1825
+ _str_or_none(modelo.get("nome_modelo")) or "",
1826
+ ]
1827
+ zonas = []
1828
+ vistos = set()
1829
+ for nome in nomes_ref:
1830
+ nome_upper = nome.upper()
1831
+ for match in re.finditer(r"(?:^|_)Z(\d+)(?=_|$)", nome_upper):
1832
+ zona = _normalizar_token_zona(f"Z{match.group(1)}")
1833
+ if not zona:
1834
+ continue
1835
+ chave = _normalize(zona)
1836
+ if chave in vistos:
1837
+ continue
1838
+ vistos.add(chave)
1839
+ zonas.append(zona)
1840
+ return sorted(zonas, key=lambda item: int(re.search(r"\d+", item).group(0)))
1841
+
1842
+
1843
+ def _lista_zonas_unicas(valores: list[str], limite: int = 200) -> list[str]:
1844
+ unicas = []
1845
+ vistos = set()
1846
+ for valor in valores:
1847
+ zona = _normalizar_token_zona(valor)
1848
+ if not zona:
1849
+ continue
1850
+ chave = _normalize(zona)
1851
+ if not chave or chave in vistos:
1852
+ continue
1853
+ vistos.add(chave)
1854
+ unicas.append(zona)
1855
+ if len(unicas) >= limite:
1856
+ break
1857
+ return sorted(unicas, key=lambda item: int(re.search(r"\d+", item).group(0)))
1858
+
1859
+
1860
  def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
1861
  termos: list[str] = []
1862
  if filtros.bairro:
backend/app/services/visualizacao_service.py CHANGED
@@ -20,6 +20,10 @@ from app.services.serializers import dataframe_to_payload, figure_to_payload, sa
20
  CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
21
 
22
 
 
 
 
 
23
  def listar_modelos_repositorio() -> dict[str, Any]:
24
  return sanitize_value(model_repository.list_repository_models())
25
 
@@ -294,7 +298,10 @@ def calcular_avaliacao(session: SessionState, valores_x: dict[str, Any], indice_
294
  if col in info["codigo_alocado"] and col in est_idx.index:
295
  min_v = float(est_idx.loc[col, "Mínimo"])
296
  max_v = float(est_idx.loc[col, "Máximo"])
297
- if float(valor) != int(float(valor)) or valor < min_v or valor > max_v:
 
 
 
298
  raise HTTPException(status_code=400, detail=f"{col} aceita inteiros de {int(min_v)} a {int(max_v)}")
299
  if col in info["percentuais"] and (valor < 0 or valor > 1):
300
  raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
 
20
  CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
21
 
22
 
23
+ def _is_rh_col(coluna: str) -> bool:
24
+ return str(coluna or "").strip().upper() == "RH"
25
+
26
+
27
  def listar_modelos_repositorio() -> dict[str, Any]:
28
  return sanitize_value(model_repository.list_repository_models())
29
 
 
298
  if col in info["codigo_alocado"] and col in est_idx.index:
299
  min_v = float(est_idx.loc[col, "Mínimo"])
300
  max_v = float(est_idx.loc[col, "Máximo"])
301
+ if _is_rh_col(col):
302
+ if float(valor) != int(float(valor)):
303
+ raise HTTPException(status_code=400, detail=f"{col} aceita apenas valores inteiros")
304
+ elif float(valor) != int(float(valor)) or valor < min_v or valor > max_v:
305
  raise HTTPException(status_code=400, detail=f"{col} aceita inteiros de {int(min_v)} a {int(max_v)}")
306
  if col in info["percentuais"] and (valor < 0 or valor > 1):
307
  raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
frontend/src/api.js CHANGED
@@ -143,10 +143,12 @@ export const api = {
143
  },
144
  classifyElaboracaoX: (sessionId, colunasX) => postJson('/api/elaboracao/classify-x', { session_id: sessionId, colunas_x: colunasX }),
145
 
146
- searchTransformations: (sessionId, grauCoef, grauF) => postJson('/api/elaboracao/search-transformations', {
147
  session_id: sessionId,
148
  grau_min_coef: grauCoef,
149
  grau_min_f: grauF,
 
 
150
  }),
151
 
152
  adoptSuggestion: (sessionId, indice) => postJson('/api/elaboracao/adopt-suggestion', { session_id: sessionId, indice }),
 
143
  },
144
  classifyElaboracaoX: (sessionId, colunasX) => postJson('/api/elaboracao/classify-x', { session_id: sessionId, colunas_x: colunasX }),
145
 
146
+ searchTransformations: (sessionId, grauCoef, grauF, transformacoesFixas = {}, transformacaoYFixa = 'Livre') => postJson('/api/elaboracao/search-transformations', {
147
  session_id: sessionId,
148
  grau_min_coef: grauCoef,
149
  grau_min_f: grauF,
150
+ transformacoes_fixas: transformacoesFixas,
151
+ transformacao_y_fixa: transformacaoYFixa,
152
  }),
153
 
154
  adoptSuggestion: (sessionId, indice) => postJson('/api/elaboracao/adopt-suggestion', { session_id: sessionId, indice }),
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -7,6 +7,7 @@ import LoadingOverlay from './LoadingOverlay'
7
  import MapFrame from './MapFrame'
8
  import PlotFigure from './PlotFigure'
9
  import SectionBlock from './SectionBlock'
 
10
 
11
  const OPERADORES = ['<=', '>=', '<', '>', '=']
12
  const GRAUS_COEF = [
@@ -68,6 +69,27 @@ function buildSelectionSnapshot(payload = {}) {
68
  })
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  function buildGrauSnapshot(grauCoef, grauF) {
72
  return `${Number(grauCoef) || 0}|${Number(grauF) || 0}`
73
  }
@@ -683,10 +705,13 @@ export default function ElaboracaoTab({ sessionId }) {
683
 
684
  const [transformacaoY, setTransformacaoY] = useState('(x)')
685
  const [transformacoesX, setTransformacoesX] = useState({})
 
 
686
  const [manualTransformPreview, setManualTransformPreview] = useState(null)
687
  const [manualTransformPreviewLoading, setManualTransformPreviewLoading] = useState(false)
688
  const [transformacoesAplicadas, setTransformacoesAplicadas] = useState(null)
689
  const [origemTransformacoes, setOrigemTransformacoes] = useState(null)
 
690
  const [section10ManualOpen, setSection10ManualOpen] = useState(false)
691
  const [section6EditOpen, setSection6EditOpen] = useState(true)
692
 
@@ -711,7 +736,7 @@ export default function ElaboracaoTab({ sessionId }) {
711
  const [baseChoices, setBaseChoices] = useState([])
712
  const [baseValue, setBaseValue] = useState('')
713
 
714
- const [nomeArquivoExport, setNomeArquivoExport] = useState('modelo_mesa')
715
  const [avaliadores, setAvaliadores] = useState([])
716
  const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
717
  const [tipoFonteDados, setTipoFonteDados] = useState('')
@@ -760,6 +785,27 @@ export default function ElaboracaoTab({ sessionId }) {
760
  transformacao: formatTransformacaoBadge(transformacoes[coluna]),
761
  }))
762
  }, [modeloCarregadoInfo])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  const periodoModeloCarregadoTexto = useMemo(
764
  () => formatPeriodoDadosMercado(modeloCarregadoInfo?.periodo_dados_mercado),
765
  [modeloCarregadoInfo],
@@ -1343,10 +1389,13 @@ export default function ElaboracaoTab({ sessionId }) {
1343
  setSelectionAppliedSnapshot(buildSelectionSnapshot({ coluna_y: '' }))
1344
  setTransformacaoY('(x)')
1345
  setTransformacoesX({})
 
 
1346
  setManualTransformPreview(null)
1347
  setManualTransformPreviewLoading(false)
1348
  setTransformacoesAplicadas(null)
1349
  setOrigemTransformacoes(null)
 
1350
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
1351
  setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
1352
  setOutliersAnteriores([])
@@ -1396,6 +1445,7 @@ export default function ElaboracaoTab({ sessionId }) {
1396
  function applySelectionResponse(resp) {
1397
  setSelection(resp)
1398
  setSection6EditOpen(false)
 
1399
  setSection10ManualOpen(false)
1400
  setManualTransformPreview(null)
1401
  setManualTransformPreviewLoading(false)
@@ -1411,6 +1461,14 @@ export default function ElaboracaoTab({ sessionId }) {
1411
  map[field.coluna] = field.valor || '(x)'
1412
  })
1413
  setTransformacoesX(map)
 
 
 
 
 
 
 
 
1414
  setManualTransformAppliedSnapshot(buildTransformacoesSnapshot(transformacaoYAplicada, map))
1415
 
1416
  const grauCoefAplicado = resp.busca?.grau_coef ?? grauCoef
@@ -1493,12 +1551,8 @@ export default function ElaboracaoTab({ sessionId }) {
1493
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1494
  const fileName = String(meta.fileName || uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || '').trim()
1495
  const sheetName = String(meta.sheetName || resp?.sheet_selected || '').trim()
1496
- if (Boolean(resp.requires_sheet)) {
1497
- setArquivoCarregadoInfo(null)
1498
- } else {
1499
- const infoArquivo = buildArquivoCarregadoInfo(resp, { fileName, sheetName })
1500
- setArquivoCarregadoInfo(infoArquivo)
1501
- }
1502
  setImportacaoErro('')
1503
  setRequiresSheet(Boolean(resp.requires_sheet))
1504
  setSheetOptions(resp.sheets || [])
@@ -1523,9 +1577,13 @@ export default function ElaboracaoTab({ sessionId }) {
1523
  }
1524
 
1525
  setTipoFonteDados('tabular')
 
 
1526
  setColunaY('')
1527
  setColunaYDraft('')
1528
  setSelection(null)
 
 
1529
  setFit(null)
1530
  setSelectionAppliedSnapshot(buildSelectionSnapshot())
1531
  setColunasX([])
@@ -1534,6 +1592,8 @@ export default function ElaboracaoTab({ sessionId }) {
1534
  setPercentuais([])
1535
  setTransformacaoY('(x)')
1536
  setTransformacoesX({})
 
 
1537
  setTransformacoesAplicadas(null)
1538
  setOrigemTransformacoes(null)
1539
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
@@ -1548,8 +1608,19 @@ export default function ElaboracaoTab({ sessionId }) {
1548
  setFiltros(defaultFiltros())
1549
  setOutliersTexto('')
1550
  setReincluirTexto('')
 
 
 
 
1551
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
1552
  setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
 
 
 
 
 
 
 
1553
  setCamposAvaliacao([])
1554
  valoresAvaliacaoRef.current = {}
1555
  setAvaliacaoFormVersion((prev) => prev + 1)
@@ -1750,6 +1821,8 @@ export default function ElaboracaoTab({ sessionId }) {
1750
  setFit(null)
1751
  setTransformacaoY('(x)')
1752
  setTransformacoesX({})
 
 
1753
  setTransformacoesAplicadas(null)
1754
  setOrigemTransformacoes(null)
1755
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
@@ -1920,7 +1993,13 @@ export default function ElaboracaoTab({ sessionId }) {
1920
  async function onSearchTransform() {
1921
  if (!sessionId) return
1922
  await withBusy(async () => {
1923
- const busca = await api.searchTransformations(sessionId, grauCoef, grauF)
 
 
 
 
 
 
1924
  const grauCoefAplicado = busca.grau_coef ?? grauCoef
1925
  const grauFAplicado = busca.grau_f ?? grauF
1926
  setSelection((prev) => ({ ...prev, busca }))
@@ -1930,6 +2009,17 @@ export default function ElaboracaoTab({ sessionId }) {
1930
  })
1931
  }
1932
 
 
 
 
 
 
 
 
 
 
 
 
1933
  async function onAdoptSuggestion(idx) {
1934
  if (!sessionId) return
1935
  await withBusy(async () => {
@@ -2257,11 +2347,12 @@ export default function ElaboracaoTab({ sessionId }) {
2257
  }
2258
 
2259
  async function onExportModel() {
2260
- if (!sessionId || !nomeArquivoExport) return
 
2261
  await withBusy(async () => {
2262
  const avaliadorEscolhido = avaliadores.find((item) => item.nome_completo === avaliadorSelecionado) || null
2263
- const blob = await api.exportModel(sessionId, nomeArquivoExport, avaliadorEscolhido || elaborador || null)
2264
- downloadBlob(blob, `${nomeArquivoExport.replace(/\.dai$/i, '')}.dai`)
2265
  })
2266
  }
2267
 
@@ -2557,21 +2648,18 @@ export default function ElaboracaoTab({ sessionId }) {
2557
 
2558
  {modeloLoadSource === 'repo' ? (
2559
  <div className="row upload-repo-row">
2560
- <label>Modelo do repositório</label>
2561
- <select
2562
- value={repoModeloSelecionado}
2563
- onChange={(e) => setRepoModeloSelecionado(e.target.value)}
2564
- disabled={loading || repoModelosLoading || repoModelos.length === 0}
2565
- >
2566
- <option value="">
2567
- {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
2568
- </option>
2569
- {repoModelos.map((item) => (
2570
- <option key={`repo-elab-${item.id}`} value={item.id}>
2571
- {item.nome_modelo || item.arquivo}
2572
- </option>
2573
- ))}
2574
- </select>
2575
  <div className="row compact upload-repo-actions">
2576
  <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
2577
  Carregar do repositório
@@ -3283,15 +3371,15 @@ export default function ElaboracaoTab({ sessionId }) {
3283
  </div>
3284
  </>
3285
  ) : null}
3286
- {colunaY ? (
3287
  <div className="section6-selected-summary">
3288
  <div className="section6-applied-badge">
3289
  <div className="section6-applied-badge-title">Variáveis atualmente aplicadas</div>
3290
  <div className="section6-summary-group">
3291
  <div className="section6-summary-label">Independentes:</div>
3292
- {colunasX.length > 0 ? (
3293
  <div className="checkbox-inline-wrap">
3294
- {colunasX.map((coluna) => (
3295
  <span key={`selected-x-${coluna}`} className="compact-chip">{coluna}</span>
3296
  ))}
3297
  </div>
@@ -3301,9 +3389,9 @@ export default function ElaboracaoTab({ sessionId }) {
3301
  </div>
3302
  <div className="section6-summary-group">
3303
  <div className="section6-summary-label">Dicotômicas:</div>
3304
- {dicotomicas.length > 0 ? (
3305
  <div className="checkbox-inline-wrap">
3306
- {dicotomicas.map((coluna) => (
3307
  <span key={`selected-d-${coluna}`} className="compact-chip">{coluna}</span>
3308
  ))}
3309
  </div>
@@ -3313,9 +3401,9 @@ export default function ElaboracaoTab({ sessionId }) {
3313
  </div>
3314
  <div className="section6-summary-group">
3315
  <div className="section6-summary-label">Variáveis categóricas codificadas:</div>
3316
- {codigoAlocado.length > 0 ? (
3317
  <div className="checkbox-inline-wrap">
3318
- {codigoAlocado.map((coluna) => (
3319
  <span key={`selected-c-${coluna}`} className="compact-chip">{coluna}</span>
3320
  ))}
3321
  </div>
@@ -3325,9 +3413,9 @@ export default function ElaboracaoTab({ sessionId }) {
3325
  </div>
3326
  <div className="section6-summary-group">
3327
  <div className="section6-summary-label">Percentuais:</div>
3328
- {percentuais.length > 0 ? (
3329
  <div className="checkbox-inline-wrap">
3330
- {percentuais.map((coluna) => (
3331
  <span key={`selected-p-${coluna}`} className="compact-chip">{coluna}</span>
3332
  ))}
3333
  </div>
@@ -3423,20 +3511,75 @@ export default function ElaboracaoTab({ sessionId }) {
3423
  </SectionBlock>
3424
 
3425
  <SectionBlock step="11" title="Transformações Sugeridas" subtitle="Busca automática de combinações por R² e enquadramento.">
3426
- <div className="row">
3427
- <label>Grau mínimo dos coeficientes</label>
3428
- <select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
3429
- {GRAUS_COEF.map((item) => (
3430
- <option key={`coef-${item.value}`} value={item.value}>{item.label}</option>
3431
- ))}
3432
- </select>
3433
- <label>Grau mínimo do teste F</label>
3434
- <select value={grauF} onChange={(e) => setGrauF(Number(e.target.value))}>
3435
- {GRAUS_F.map((item) => (
3436
- <option key={`f-${item.value}`} value={item.value}>{item.label}</option>
3437
- ))}
3438
- </select>
3439
- <button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3440
  </div>
3441
  {(selection.busca?.resultados || []).length > 0 ? (
3442
  <div className="transform-suggestions-grid">
@@ -4098,7 +4241,12 @@ export default function ElaboracaoTab({ sessionId }) {
4098
  <SectionBlock step="19" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
4099
  <div className="row">
4100
  <label>Nome do arquivo (.dai)</label>
4101
- <input type="text" value={nomeArquivoExport} onChange={(e) => setNomeArquivoExport(e.target.value)} />
 
 
 
 
 
4102
  <label>Avaliador</label>
4103
  <select value={avaliadorSelecionado} onChange={(e) => setAvaliadorSelecionado(e.target.value)}>
4104
  <option value="">Manter elaborador do modelo (se houver)</option>
@@ -4108,7 +4256,7 @@ export default function ElaboracaoTab({ sessionId }) {
4108
  </option>
4109
  ))}
4110
  </select>
4111
- <button onClick={onExportModel} disabled={loading || !nomeArquivoExport}>Exportar modelo</button>
4112
  <button onClick={onExportBase} disabled={loading}>Exportar base CSV</button>
4113
  </div>
4114
  </SectionBlock>
 
7
  import MapFrame from './MapFrame'
8
  import PlotFigure from './PlotFigure'
9
  import SectionBlock from './SectionBlock'
10
+ import SinglePillAutocomplete from './SinglePillAutocomplete'
11
 
12
  const OPERADORES = ['<=', '>=', '<', '>', '=']
13
  const GRAUS_COEF = [
 
69
  })
70
  }
71
 
72
+ function parseSelectionSnapshot(snapshot) {
73
+ try {
74
+ const parsed = JSON.parse(String(snapshot || '{}'))
75
+ return {
76
+ coluna_y: String(parsed?.coluna_y || '').trim(),
77
+ colunas_x: normalizeSnapshotArray(parsed?.colunas_x),
78
+ dicotomicas: normalizeSnapshotArray(parsed?.dicotomicas),
79
+ codigo_alocado: normalizeSnapshotArray(parsed?.codigo_alocado),
80
+ percentuais: normalizeSnapshotArray(parsed?.percentuais),
81
+ }
82
+ } catch {
83
+ return {
84
+ coluna_y: '',
85
+ colunas_x: [],
86
+ dicotomicas: [],
87
+ codigo_alocado: [],
88
+ percentuais: [],
89
+ }
90
+ }
91
+ }
92
+
93
  function buildGrauSnapshot(grauCoef, grauF) {
94
  return `${Number(grauCoef) || 0}|${Number(grauF) || 0}`
95
  }
 
705
 
706
  const [transformacaoY, setTransformacaoY] = useState('(x)')
707
  const [transformacoesX, setTransformacoesX] = useState({})
708
+ const [transformacaoYFixaBusca, setTransformacaoYFixaBusca] = useState('Livre')
709
+ const [transformacoesFixasBusca, setTransformacoesFixasBusca] = useState({})
710
  const [manualTransformPreview, setManualTransformPreview] = useState(null)
711
  const [manualTransformPreviewLoading, setManualTransformPreviewLoading] = useState(false)
712
  const [transformacoesAplicadas, setTransformacoesAplicadas] = useState(null)
713
  const [origemTransformacoes, setOrigemTransformacoes] = useState(null)
714
+ const [section11LocksOpen, setSection11LocksOpen] = useState(false)
715
  const [section10ManualOpen, setSection10ManualOpen] = useState(false)
716
  const [section6EditOpen, setSection6EditOpen] = useState(true)
717
 
 
736
  const [baseChoices, setBaseChoices] = useState([])
737
  const [baseValue, setBaseValue] = useState('')
738
 
739
+ const [nomeArquivoExport, setNomeArquivoExport] = useState('')
740
  const [avaliadores, setAvaliadores] = useState([])
741
  const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
742
  const [tipoFonteDados, setTipoFonteDados] = useState('')
 
785
  transformacao: formatTransformacaoBadge(transformacoes[coluna]),
786
  }))
787
  }, [modeloCarregadoInfo])
788
+ const repoModeloOptions = useMemo(
789
+ () => (repoModelos || []).map((item) => ({
790
+ value: String(item?.id || ''),
791
+ label: String(item?.nome_modelo || item?.arquivo || item?.id || ''),
792
+ secondary: String(item?.arquivo || ''),
793
+ })).filter((item) => item.value && item.label),
794
+ [repoModelos],
795
+ )
796
+ const selecaoAplicada = useMemo(() => {
797
+ const contexto = selection?.contexto
798
+ if (contexto && typeof contexto === 'object') {
799
+ return {
800
+ coluna_y: String(contexto.coluna_y || '').trim(),
801
+ colunas_x: normalizeSnapshotArray(contexto.colunas_x),
802
+ dicotomicas: normalizeSnapshotArray(contexto.dicotomicas),
803
+ codigo_alocado: normalizeSnapshotArray(contexto.codigo_alocado),
804
+ percentuais: normalizeSnapshotArray(contexto.percentuais),
805
+ }
806
+ }
807
+ return parseSelectionSnapshot(selectionAppliedSnapshot)
808
+ }, [selection, selectionAppliedSnapshot])
809
  const periodoModeloCarregadoTexto = useMemo(
810
  () => formatPeriodoDadosMercado(modeloCarregadoInfo?.periodo_dados_mercado),
811
  [modeloCarregadoInfo],
 
1389
  setSelectionAppliedSnapshot(buildSelectionSnapshot({ coluna_y: '' }))
1390
  setTransformacaoY('(x)')
1391
  setTransformacoesX({})
1392
+ setTransformacaoYFixaBusca('Livre')
1393
+ setTransformacoesFixasBusca({})
1394
  setManualTransformPreview(null)
1395
  setManualTransformPreviewLoading(false)
1396
  setTransformacoesAplicadas(null)
1397
  setOrigemTransformacoes(null)
1398
+ setSection11LocksOpen(false)
1399
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
1400
  setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
1401
  setOutliersAnteriores([])
 
1445
  function applySelectionResponse(resp) {
1446
  setSelection(resp)
1447
  setSection6EditOpen(false)
1448
+ setSection11LocksOpen(false)
1449
  setSection10ManualOpen(false)
1450
  setManualTransformPreview(null)
1451
  setManualTransformPreviewLoading(false)
 
1461
  map[field.coluna] = field.valor || '(x)'
1462
  })
1463
  setTransformacoesX(map)
1464
+ setTransformacaoYFixaBusca('Livre')
1465
+ const travasBusca = {}
1466
+ ;(resp.transform_fields || []).forEach((field) => {
1467
+ const coluna = String(field?.coluna || '').trim()
1468
+ if (!coluna) return
1469
+ travasBusca[coluna] = 'Livre'
1470
+ })
1471
+ setTransformacoesFixasBusca(travasBusca)
1472
  setManualTransformAppliedSnapshot(buildTransformacoesSnapshot(transformacaoYAplicada, map))
1473
 
1474
  const grauCoefAplicado = resp.busca?.grau_coef ?? grauCoef
 
1551
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1552
  const fileName = String(meta.fileName || uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || '').trim()
1553
  const sheetName = String(meta.sheetName || resp?.sheet_selected || '').trim()
1554
+ const infoArquivo = buildArquivoCarregadoInfo(resp, { fileName, sheetName })
1555
+ setArquivoCarregadoInfo(infoArquivo)
 
 
 
 
1556
  setImportacaoErro('')
1557
  setRequiresSheet(Boolean(resp.requires_sheet))
1558
  setSheetOptions(resp.sheets || [])
 
1577
  }
1578
 
1579
  setTipoFonteDados('tabular')
1580
+ setDados(null)
1581
+ setColunasNumericas([])
1582
  setColunaY('')
1583
  setColunaYDraft('')
1584
  setSelection(null)
1585
+ setGrauCoef(0)
1586
+ setGrauF(0)
1587
  setFit(null)
1588
  setSelectionAppliedSnapshot(buildSelectionSnapshot())
1589
  setColunasX([])
 
1592
  setPercentuais([])
1593
  setTransformacaoY('(x)')
1594
  setTransformacoesX({})
1595
+ setTransformacaoYFixaBusca('Livre')
1596
+ setTransformacoesFixasBusca({})
1597
  setTransformacoesAplicadas(null)
1598
  setOrigemTransformacoes(null)
1599
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
 
1608
  setFiltros(defaultFiltros())
1609
  setOutliersTexto('')
1610
  setReincluirTexto('')
1611
+ setResumoOutliers('Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0')
1612
+ setOutliersHtml('')
1613
+ setOutliersAnteriores([])
1614
+ setIteracao(1)
1615
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
1616
  setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
1617
+ setCoordsInfo(null)
1618
+ setManualLat('')
1619
+ setManualLon('')
1620
+ setGeoCdlog('')
1621
+ setGeoNum('')
1622
+ setCoordsMode('menu')
1623
+ setConfirmarExclusaoCoords(false)
1624
  setCamposAvaliacao([])
1625
  valoresAvaliacaoRef.current = {}
1626
  setAvaliacaoFormVersion((prev) => prev + 1)
 
1821
  setFit(null)
1822
  setTransformacaoY('(x)')
1823
  setTransformacoesX({})
1824
+ setTransformacaoYFixaBusca('Livre')
1825
+ setTransformacoesFixasBusca({})
1826
  setTransformacoesAplicadas(null)
1827
  setOrigemTransformacoes(null)
1828
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
 
1993
  async function onSearchTransform() {
1994
  if (!sessionId) return
1995
  await withBusy(async () => {
1996
+ const busca = await api.searchTransformations(
1997
+ sessionId,
1998
+ grauCoef,
1999
+ grauF,
2000
+ transformacoesFixasBusca,
2001
+ transformacaoYFixaBusca,
2002
+ )
2003
  const grauCoefAplicado = busca.grau_coef ?? grauCoef
2004
  const grauFAplicado = busca.grau_f ?? grauF
2005
  setSelection((prev) => ({ ...prev, busca }))
 
2009
  })
2010
  }
2011
 
2012
+ function onLiberarTravasBusca() {
2013
+ setTransformacaoYFixaBusca('Livre')
2014
+ const travas = {}
2015
+ ;(selection?.transform_fields || []).forEach((field) => {
2016
+ const coluna = String(field?.coluna || '').trim()
2017
+ if (!coluna) return
2018
+ travas[coluna] = 'Livre'
2019
+ })
2020
+ setTransformacoesFixasBusca(travas)
2021
+ }
2022
+
2023
  async function onAdoptSuggestion(idx) {
2024
  if (!sessionId) return
2025
  await withBusy(async () => {
 
2347
  }
2348
 
2349
  async function onExportModel() {
2350
+ const nomeExport = String(nomeArquivoExport || '').trim()
2351
+ if (!sessionId || !nomeExport) return
2352
  await withBusy(async () => {
2353
  const avaliadorEscolhido = avaliadores.find((item) => item.nome_completo === avaliadorSelecionado) || null
2354
+ const blob = await api.exportModel(sessionId, nomeExport, avaliadorEscolhido || elaborador || null)
2355
+ downloadBlob(blob, `${nomeExport.replace(/\.dai$/i, '')}.dai`)
2356
  })
2357
  }
2358
 
 
2648
 
2649
  {modeloLoadSource === 'repo' ? (
2650
  <div className="row upload-repo-row">
2651
+ <label className="upload-repo-field">
2652
+ Modelo do repositório
2653
+ <SinglePillAutocomplete
2654
+ value={repoModeloSelecionado}
2655
+ onChange={setRepoModeloSelecionado}
2656
+ options={repoModeloOptions}
2657
+ placeholder={repoModelosLoading ? 'Carregando lista...' : repoModeloOptions.length > 0 ? 'Digite para buscar modelo' : 'Nenhum modelo disponível'}
2658
+ emptyMessage={repoModeloOptions.length > 0 ? 'Nenhum modelo encontrado.' : 'Nenhum modelo disponível.'}
2659
+ loading={repoModelosLoading}
2660
+ disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
2661
+ />
2662
+ </label>
 
 
 
2663
  <div className="row compact upload-repo-actions">
2664
  <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
2665
  Carregar do repositório
 
3371
  </div>
3372
  </>
3373
  ) : null}
3374
+ {selection ? (
3375
  <div className="section6-selected-summary">
3376
  <div className="section6-applied-badge">
3377
  <div className="section6-applied-badge-title">Variáveis atualmente aplicadas</div>
3378
  <div className="section6-summary-group">
3379
  <div className="section6-summary-label">Independentes:</div>
3380
+ {selecaoAplicada.colunas_x.length > 0 ? (
3381
  <div className="checkbox-inline-wrap">
3382
+ {selecaoAplicada.colunas_x.map((coluna) => (
3383
  <span key={`selected-x-${coluna}`} className="compact-chip">{coluna}</span>
3384
  ))}
3385
  </div>
 
3389
  </div>
3390
  <div className="section6-summary-group">
3391
  <div className="section6-summary-label">Dicotômicas:</div>
3392
+ {selecaoAplicada.dicotomicas.length > 0 ? (
3393
  <div className="checkbox-inline-wrap">
3394
+ {selecaoAplicada.dicotomicas.map((coluna) => (
3395
  <span key={`selected-d-${coluna}`} className="compact-chip">{coluna}</span>
3396
  ))}
3397
  </div>
 
3401
  </div>
3402
  <div className="section6-summary-group">
3403
  <div className="section6-summary-label">Variáveis categóricas codificadas:</div>
3404
+ {selecaoAplicada.codigo_alocado.length > 0 ? (
3405
  <div className="checkbox-inline-wrap">
3406
+ {selecaoAplicada.codigo_alocado.map((coluna) => (
3407
  <span key={`selected-c-${coluna}`} className="compact-chip">{coluna}</span>
3408
  ))}
3409
  </div>
 
3413
  </div>
3414
  <div className="section6-summary-group">
3415
  <div className="section6-summary-label">Percentuais:</div>
3416
+ {selecaoAplicada.percentuais.length > 0 ? (
3417
  <div className="checkbox-inline-wrap">
3418
+ {selecaoAplicada.percentuais.map((coluna) => (
3419
  <span key={`selected-p-${coluna}`} className="compact-chip">{coluna}</span>
3420
  ))}
3421
  </div>
 
3511
  </SectionBlock>
3512
 
3513
  <SectionBlock step="11" title="Transformações Sugeridas" subtitle="Busca automática de combinações por R² e enquadramento.">
3514
+ <div className="manual-transform-toggle section11-toggle-wrap">
3515
+ <button
3516
+ type="button"
3517
+ className={section11LocksOpen ? 'btn-manual-toggle active' : 'btn-manual-toggle'}
3518
+ onClick={() => setSection11LocksOpen((prev) => !prev)}
3519
+ >
3520
+ {section11LocksOpen ? 'Ocultar travas de transformação' : 'Trancar variáveis em transformações'}
3521
+ </button>
3522
+ {section11LocksOpen ? (
3523
+ <button type="button" className="btn-section11-unlock" onClick={onLiberarTravasBusca} disabled={loading}>
3524
+ Liberar todas variáveis
3525
+ </button>
3526
+ ) : null}
3527
+ </div>
3528
+ {section11LocksOpen ? (
3529
+ <>
3530
+ <div className="transform-grid">
3531
+ <div className="transform-card transform-card-y">
3532
+ <div className="transform-card-head">
3533
+ <span>{colunaY ? `${colunaY} (Y)` : 'Variável dependente (Y)'}</span>
3534
+ </div>
3535
+ <select
3536
+ value={transformacaoYFixaBusca}
3537
+ onChange={(e) => setTransformacaoYFixaBusca(e.target.value)}
3538
+ >
3539
+ <option value="Livre">Livre</option>
3540
+ {['(x)', '1/(x)', 'ln(x)', 'exp(x)', '(x)^2', 'raiz(x)', '1/raiz(x)'].map((choice) => (
3541
+ <option key={`y-lock-${choice}`} value={choice}>{choice}</option>
3542
+ ))}
3543
+ </select>
3544
+ </div>
3545
+ {(selection.transform_fields || [])
3546
+ .filter((field) => !field.locked)
3547
+ .map((field) => (
3548
+ <div key={`tf-lock-${field.coluna}`} className="transform-card">
3549
+ <div className="transform-card-head">
3550
+ <span>{field.coluna}</span>
3551
+ </div>
3552
+ <select
3553
+ value={transformacoesFixasBusca[field.coluna] || 'Livre'}
3554
+ onChange={(e) => setTransformacoesFixasBusca((prev) => ({ ...prev, [field.coluna]: e.target.value }))}
3555
+ >
3556
+ <option value="Livre">Livre</option>
3557
+ {(field.choices || []).map((choice) => (
3558
+ <option key={`${field.coluna}-lock-${choice}`} value={choice}>{choice}</option>
3559
+ ))}
3560
+ </select>
3561
+ </div>
3562
+ ))}
3563
+ </div>
3564
+ </>
3565
+ ) : null}
3566
+ <div className="section11-search-criteria">
3567
+ <div className="section11-search-criteria-title">Critérios da busca</div>
3568
+ <div className="row section11-search-row">
3569
+ <label>Grau mínimo dos coeficientes</label>
3570
+ <select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
3571
+ {GRAUS_COEF.map((item) => (
3572
+ <option key={`coef-${item.value}`} value={item.value}>{item.label}</option>
3573
+ ))}
3574
+ </select>
3575
+ <label>Grau mínimo do teste F</label>
3576
+ <select value={grauF} onChange={(e) => setGrauF(Number(e.target.value))}>
3577
+ {GRAUS_F.map((item) => (
3578
+ <option key={`f-${item.value}`} value={item.value}>{item.label}</option>
3579
+ ))}
3580
+ </select>
3581
+ <button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
3582
+ </div>
3583
  </div>
3584
  {(selection.busca?.resultados || []).length > 0 ? (
3585
  <div className="transform-suggestions-grid">
 
4241
  <SectionBlock step="19" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
4242
  <div className="row">
4243
  <label>Nome do arquivo (.dai)</label>
4244
+ <input
4245
+ type="text"
4246
+ value={nomeArquivoExport}
4247
+ onChange={(e) => setNomeArquivoExport(e.target.value)}
4248
+ placeholder="Digite o nome do arquivo"
4249
+ />
4250
  <label>Avaliador</label>
4251
  <select value={avaliadorSelecionado} onChange={(e) => setAvaliadorSelecionado(e.target.value)}>
4252
  <option value="">Manter elaborador do modelo (se houver)</option>
 
4256
  </option>
4257
  ))}
4258
  </select>
4259
+ <button onClick={onExportModel} disabled={loading || !String(nomeArquivoExport || '').trim()}>Exportar modelo</button>
4260
  <button onClick={onExportBase} disabled={loading}>Exportar base CSV</button>
4261
  </div>
4262
  </SectionBlock>
frontend/src/components/PesquisaTab.jsx CHANGED
@@ -11,6 +11,7 @@ const EMPTY_FILTERS = {
11
  dataMin: '',
12
  dataMax: '',
13
  avalFinalidade: '',
 
14
  avalBairro: '',
15
  avalArea: '',
16
  avalRh: '',
@@ -140,6 +141,7 @@ function buildApiFilters(filters) {
140
  tipo_modelo: filters.tipoModelo,
141
  negociacao_modelo: filters.negociacaoModelo,
142
  aval_finalidade: filters.avalFinalidade,
 
143
  aval_bairro: filters.avalBairro,
144
  data_min: filters.dataMin,
145
  data_max: filters.dataMax,
@@ -718,17 +720,30 @@ export default function PesquisaTab() {
718
  <NumberFieldInput field="avalRh" value={filters.avalRh} onChange={onFieldChange} placeholder="0" />
719
  </label>
720
  </div>
721
- <label className="pesquisa-field pesquisa-bairro-bottom-field">
722
- Bairro do imovel
723
- <ChipAutocompleteInput
724
- field="avalBairro"
725
- value={filters.avalBairro}
726
- onChange={onFieldChange}
727
- placeholder="Digite e pressione Enter"
728
- suggestions={sugestoes.bairros || []}
729
- panelTitle="Bairros sugeridos"
730
- />
731
- </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  </div>
733
  </div>
734
 
 
11
  dataMin: '',
12
  dataMax: '',
13
  avalFinalidade: '',
14
+ avalZona: '',
15
  avalBairro: '',
16
  avalArea: '',
17
  avalRh: '',
 
141
  tipo_modelo: filters.tipoModelo,
142
  negociacao_modelo: filters.negociacaoModelo,
143
  aval_finalidade: filters.avalFinalidade,
144
+ aval_zona: filters.avalZona,
145
  aval_bairro: filters.avalBairro,
146
  data_min: filters.dataMin,
147
  data_max: filters.dataMax,
 
720
  <NumberFieldInput field="avalRh" value={filters.avalRh} onChange={onFieldChange} placeholder="0" />
721
  </label>
722
  </div>
723
+ <div className="pesquisa-bairro-zona-grid">
724
+ <label className="pesquisa-field">
725
+ Zona de avaliacao
726
+ <ChipAutocompleteInput
727
+ field="avalZona"
728
+ value={filters.avalZona}
729
+ onChange={onFieldChange}
730
+ placeholder="Selecione uma ou mais zonas"
731
+ suggestions={sugestoes.zonas_avaliacao || []}
732
+ panelTitle="Zonas sugeridas"
733
+ />
734
+ </label>
735
+ <label className="pesquisa-field pesquisa-bairro-bottom-field">
736
+ Bairro do imovel
737
+ <ChipAutocompleteInput
738
+ field="avalBairro"
739
+ value={filters.avalBairro}
740
+ onChange={onFieldChange}
741
+ placeholder="Digite e pressione Enter"
742
+ suggestions={sugestoes.bairros || []}
743
+ panelTitle="Bairros sugeridos"
744
+ />
745
+ </label>
746
+ </div>
747
  </div>
748
  </div>
749
 
frontend/src/components/SinglePillAutocomplete.jsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+
3
+ function normalizeSearchText(value) {
4
+ return String(value || '')
5
+ .normalize('NFD')
6
+ .replace(/[\u0300-\u036f]/g, '')
7
+ .toLowerCase()
8
+ .trim()
9
+ }
10
+
11
+ function normalizeOption(option) {
12
+ if (typeof option === 'string') {
13
+ const text = String(option || '').trim()
14
+ return text ? { value: text, label: text, secondary: '' } : null
15
+ }
16
+ if (!option || typeof option !== 'object') return null
17
+ const rawValue = option.value ?? option.id ?? option.key ?? ''
18
+ const value = String(rawValue || '').trim()
19
+ if (!value) return null
20
+ const label = String(option.label ?? option.nome_modelo ?? option.arquivo ?? value).trim() || value
21
+ const secondary = String(option.secondary ?? option.arquivo ?? '').trim()
22
+ return { value, label, secondary }
23
+ }
24
+
25
+ export default function SinglePillAutocomplete({
26
+ value,
27
+ onChange,
28
+ options = [],
29
+ placeholder = 'Selecione um item',
30
+ panelTitle = '',
31
+ emptyMessage = 'Nenhuma sugestao encontrada.',
32
+ loading = false,
33
+ disabled = false,
34
+ }) {
35
+ const rootRef = useRef(null)
36
+ const inputRef = useRef(null)
37
+ const [query, setQuery] = useState('')
38
+ const [open, setOpen] = useState(false)
39
+ const [activeIndex, setActiveIndex] = useState(-1)
40
+
41
+ const selectedValue = String(value || '')
42
+ const normalizedOptions = useMemo(() => {
43
+ const unique = []
44
+ const seen = new Set()
45
+ ;(options || []).forEach((item) => {
46
+ const normalized = normalizeOption(item)
47
+ if (!normalized) return
48
+ if (seen.has(normalized.value)) return
49
+ seen.add(normalized.value)
50
+ unique.push(normalized)
51
+ })
52
+ return unique
53
+ }, [options])
54
+
55
+ const selectedOption = useMemo(
56
+ () => normalizedOptions.find((item) => item.value === selectedValue) || null,
57
+ [normalizedOptions, selectedValue],
58
+ )
59
+
60
+ const queryNormalized = normalizeSearchText(query)
61
+ const filteredOptions = useMemo(() => {
62
+ if (loading) return []
63
+ if (!queryNormalized) return normalizedOptions.slice(0, 160)
64
+ return normalizedOptions
65
+ .filter((item) => (
66
+ normalizeSearchText(item.label).includes(queryNormalized)
67
+ || normalizeSearchText(item.secondary).includes(queryNormalized)
68
+ ))
69
+ .slice(0, 160)
70
+ }, [loading, normalizedOptions, queryNormalized])
71
+
72
+ useEffect(() => {
73
+ if (!open) return undefined
74
+ function onDocumentMouseDown(event) {
75
+ if (!rootRef.current) return
76
+ if (!rootRef.current.contains(event.target)) setOpen(false)
77
+ }
78
+ document.addEventListener('mousedown', onDocumentMouseDown)
79
+ return () => document.removeEventListener('mousedown', onDocumentMouseDown)
80
+ }, [open])
81
+
82
+ useEffect(() => {
83
+ if (!open || filteredOptions.length === 0) {
84
+ setActiveIndex(-1)
85
+ return
86
+ }
87
+ if (activeIndex >= filteredOptions.length) setActiveIndex(filteredOptions.length - 1)
88
+ }, [activeIndex, filteredOptions, open])
89
+
90
+ function emitChange(nextValue) {
91
+ if (typeof onChange === 'function') onChange(String(nextValue || ''))
92
+ }
93
+
94
+ function selectOption(option) {
95
+ if (!option) return
96
+ emitChange(option.value)
97
+ setQuery('')
98
+ setOpen(false)
99
+ setActiveIndex(-1)
100
+ }
101
+
102
+ function clearSelection(event) {
103
+ event.preventDefault()
104
+ event.stopPropagation()
105
+ emitChange('')
106
+ setQuery('')
107
+ setOpen(true)
108
+ setActiveIndex(-1)
109
+ window.requestAnimationFrame(() => inputRef.current?.focus())
110
+ }
111
+
112
+ function onInputChange(event) {
113
+ if (disabled) return
114
+ setQuery(event.target.value)
115
+ setOpen(true)
116
+ setActiveIndex(-1)
117
+ }
118
+
119
+ function onInputFocus() {
120
+ if (disabled) return
121
+ setOpen(true)
122
+ setActiveIndex(-1)
123
+ }
124
+
125
+ function onInputKeyDown(event) {
126
+ if (disabled) return
127
+
128
+ if (event.key === 'Escape') {
129
+ setOpen(false)
130
+ return
131
+ }
132
+
133
+ if (event.key === 'Backspace' && !query && selectedOption) {
134
+ emitChange('')
135
+ setOpen(true)
136
+ return
137
+ }
138
+
139
+ if (!filteredOptions.length) return
140
+
141
+ if (event.key === 'ArrowDown') {
142
+ event.preventDefault()
143
+ setOpen(true)
144
+ setActiveIndex((prev) => (prev < 0 ? 0 : (prev + 1) % filteredOptions.length))
145
+ return
146
+ }
147
+
148
+ if (event.key === 'ArrowUp') {
149
+ event.preventDefault()
150
+ setOpen(true)
151
+ setActiveIndex((prev) => {
152
+ if (prev < 0) return filteredOptions.length - 1
153
+ return (prev - 1 + filteredOptions.length) % filteredOptions.length
154
+ })
155
+ return
156
+ }
157
+
158
+ if (event.key === 'Enter') {
159
+ event.preventDefault()
160
+ if (activeIndex >= 0 && activeIndex < filteredOptions.length) {
161
+ selectOption(filteredOptions[activeIndex])
162
+ return
163
+ }
164
+ if (filteredOptions.length === 1) {
165
+ selectOption(filteredOptions[0])
166
+ }
167
+ }
168
+ }
169
+
170
+ return (
171
+ <div className={`chip-autocomplete chip-autocomplete-single${open ? ' is-open' : ''}${disabled ? ' is-disabled' : ''}`} ref={rootRef}>
172
+ <div className={`chip-autocomplete-single-control${disabled ? ' is-disabled' : ''}`}>
173
+ {selectedOption ? (
174
+ <span className="chip-autocomplete-selected chip-autocomplete-selected-inline">
175
+ <span>{selectedOption.label}</span>
176
+ {!disabled ? (
177
+ <button
178
+ type="button"
179
+ className="chip-autocomplete-selected-remove"
180
+ onMouseDown={clearSelection}
181
+ onClick={(event) => {
182
+ event.preventDefault()
183
+ event.stopPropagation()
184
+ }}
185
+ aria-label={`Remover ${selectedOption.label}`}
186
+ >
187
+ ×
188
+ </button>
189
+ ) : null}
190
+ </span>
191
+ ) : null}
192
+ <input
193
+ ref={inputRef}
194
+ type="text"
195
+ className="chip-autocomplete-single-input"
196
+ value={query}
197
+ onChange={onInputChange}
198
+ onFocus={onInputFocus}
199
+ onKeyDown={onInputKeyDown}
200
+ placeholder={selectedOption ? '' : placeholder}
201
+ autoComplete="off"
202
+ autoCorrect="off"
203
+ autoCapitalize="none"
204
+ spellCheck={false}
205
+ disabled={disabled}
206
+ />
207
+ </div>
208
+
209
+ {open && !disabled ? (
210
+ <div className="chip-autocomplete-panel" role="listbox">
211
+ {panelTitle ? <div className="chip-autocomplete-panel-head">{panelTitle}</div> : null}
212
+ {loading ? (
213
+ <div className="chip-autocomplete-empty">Carregando lista...</div>
214
+ ) : filteredOptions.length ? (
215
+ <div className="chip-autocomplete-chip-wrap">
216
+ {filteredOptions.map((item, idx) => (
217
+ <button
218
+ type="button"
219
+ key={`single-chip-${item.value}-${idx}`}
220
+ className={`chip-autocomplete-chip${idx === activeIndex ? ' is-active' : ''}`}
221
+ onMouseDown={(event) => {
222
+ event.preventDefault()
223
+ event.stopPropagation()
224
+ selectOption(item)
225
+ }}
226
+ title={item.secondary ? `${item.label} | ${item.secondary}` : item.label}
227
+ >
228
+ {item.label}
229
+ </button>
230
+ ))}
231
+ </div>
232
+ ) : (
233
+ <div className="chip-autocomplete-empty">
234
+ {emptyMessage}
235
+ </div>
236
+ )}
237
+ </div>
238
+ ) : null}
239
+ </div>
240
+ )
241
+ }
frontend/src/components/VisualizacaoTab.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import DataTable from './DataTable'
4
  import EquationFormatsPanel from './EquationFormatsPanel'
@@ -6,6 +6,7 @@ import LoadingOverlay from './LoadingOverlay'
6
  import MapFrame from './MapFrame'
7
  import PlotFigure from './PlotFigure'
8
  import SectionBlock from './SectionBlock'
 
9
 
10
  const INNER_TABS = [
11
  { key: 'mapa', label: 'Mapa' },
@@ -65,6 +66,14 @@ export default function VisualizacaoTab({ sessionId }) {
65
  const deleteConfirmTimersRef = useRef({})
66
  const uploadInputRef = useRef(null)
67
  const temAvaliacoes = Array.isArray(baseChoices) && baseChoices.length > 0
 
 
 
 
 
 
 
 
68
 
69
  function resetConteudoVisualizacao() {
70
  setDados(null)
@@ -424,21 +433,18 @@ export default function VisualizacaoTab({ sessionId }) {
424
 
425
  {modeloLoadSource === 'repo' ? (
426
  <div className="row upload-repo-row">
427
- <label>Modelo do repositório</label>
428
- <select
429
- value={repoModeloSelecionado}
430
- onChange={(e) => setRepoModeloSelecionado(e.target.value)}
431
- disabled={loading || repoModelosLoading || repoModelos.length === 0}
432
- >
433
- <option value="">
434
- {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
435
- </option>
436
- {repoModelos.map((item) => (
437
- <option key={`repo-viz-${item.id}`} value={item.id}>
438
- {item.nome_modelo || item.arquivo}
439
- </option>
440
- ))}
441
- </select>
442
  <div className="row compact upload-repo-actions">
443
  <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
444
  Carregar do repositório
 
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import DataTable from './DataTable'
4
  import EquationFormatsPanel from './EquationFormatsPanel'
 
6
  import MapFrame from './MapFrame'
7
  import PlotFigure from './PlotFigure'
8
  import SectionBlock from './SectionBlock'
9
+ import SinglePillAutocomplete from './SinglePillAutocomplete'
10
 
11
  const INNER_TABS = [
12
  { key: 'mapa', label: 'Mapa' },
 
66
  const deleteConfirmTimersRef = useRef({})
67
  const uploadInputRef = useRef(null)
68
  const temAvaliacoes = Array.isArray(baseChoices) && baseChoices.length > 0
69
+ const repoModeloOptions = useMemo(
70
+ () => (repoModelos || []).map((item) => ({
71
+ value: String(item?.id || ''),
72
+ label: String(item?.nome_modelo || item?.arquivo || item?.id || ''),
73
+ secondary: String(item?.arquivo || ''),
74
+ })).filter((item) => item.value && item.label),
75
+ [repoModelos],
76
+ )
77
 
78
  function resetConteudoVisualizacao() {
79
  setDados(null)
 
433
 
434
  {modeloLoadSource === 'repo' ? (
435
  <div className="row upload-repo-row">
436
+ <label className="upload-repo-field">
437
+ Modelo do repositório
438
+ <SinglePillAutocomplete
439
+ value={repoModeloSelecionado}
440
+ onChange={setRepoModeloSelecionado}
441
+ options={repoModeloOptions}
442
+ placeholder={repoModelosLoading ? 'Carregando lista...' : repoModeloOptions.length > 0 ? 'Digite para buscar modelo' : 'Nenhum modelo disponível'}
443
+ emptyMessage={repoModeloOptions.length > 0 ? 'Nenhum modelo encontrado.' : 'Nenhum modelo disponível.'}
444
+ loading={repoModelosLoading}
445
+ disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
446
+ />
447
+ </label>
 
 
 
448
  <div className="row compact upload-repo-actions">
449
  <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
450
  Carregar do repositório
frontend/src/styles.css CHANGED
@@ -951,6 +951,51 @@ button.pesquisa-otica-btn.active:hover {
951
  position: relative;
952
  }
953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
954
  .chip-autocomplete-selected-wrap {
955
  display: flex;
956
  flex-wrap: wrap;
@@ -1016,6 +1061,15 @@ button.pesquisa-otica-btn.active:hover {
1016
  overflow: auto;
1017
  }
1018
 
 
 
 
 
 
 
 
 
 
1019
  .chip-autocomplete-panel-head {
1020
  font-size: 0.72rem;
1021
  font-weight: 700;
@@ -1155,14 +1209,23 @@ button.pesquisa-otica-btn.active:hover {
1155
 
1156
  .pesquisa-avaliando-periodo-pair {
1157
  margin: 0;
 
 
1158
  }
1159
 
1160
  .pesquisa-avaliando-bottom-stack {
1161
  gap: 12px;
1162
  }
1163
 
 
 
 
 
 
 
 
1164
  .pesquisa-bairro-bottom-field {
1165
- grid-column: 1 / -1;
1166
  }
1167
 
1168
  .pesquisa-avaliando-bottom-grid .pesquisa-field-pair {
@@ -1985,6 +2048,14 @@ button.model-source-back-btn {
1985
 
1986
  .upload-repo-row {
1987
  margin-bottom: 10px;
 
 
 
 
 
 
 
 
1988
  }
1989
 
1990
  .upload-repo-actions {
@@ -2622,6 +2693,43 @@ button.btn-upload-select {
2622
  margin-bottom: 18px;
2623
  }
2624
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2625
  .transform-preview-summary {
2626
  border: 1px solid #d8e5f2;
2627
  border-radius: 12px;
@@ -3998,6 +4106,10 @@ button.btn-download-subtle {
3998
  grid-template-columns: 1fr;
3999
  }
4000
 
 
 
 
 
4001
  .pesquisa-dynamic-filter-row,
4002
  .pesquisa-range-values-row,
4003
  .pesquisa-range-row,
 
951
  position: relative;
952
  }
953
 
954
+ .chip-autocomplete-single-control {
955
+ display: flex;
956
+ align-items: center;
957
+ gap: 6px;
958
+ flex-wrap: wrap;
959
+ min-height: 34px;
960
+ width: 100%;
961
+ border: 1px solid #c8d7e6;
962
+ border-radius: 10px;
963
+ background: #ffffff;
964
+ padding: 4px 8px;
965
+ }
966
+
967
+ .chip-autocomplete-single-control:focus-within {
968
+ border-color: #6aa1d4;
969
+ box-shadow: 0 0 0 2px rgba(106, 161, 212, 0.16);
970
+ }
971
+
972
+ .chip-autocomplete-single-control.is-disabled {
973
+ background: #f6f8fb;
974
+ color: #74889c;
975
+ cursor: not-allowed;
976
+ }
977
+
978
+ .chip-autocomplete-single-input {
979
+ flex: 1 1 120px;
980
+ min-width: 120px;
981
+ border: none;
982
+ background: transparent;
983
+ color: #2e4760;
984
+ font-size: 0.84rem;
985
+ line-height: 1.25;
986
+ padding: 4px 2px;
987
+ min-height: 22px;
988
+ }
989
+
990
+ .chip-autocomplete-single-input:focus {
991
+ outline: none;
992
+ box-shadow: none;
993
+ }
994
+
995
+ .chip-autocomplete-single .chip-autocomplete-selected-inline {
996
+ margin: 0;
997
+ }
998
+
999
  .chip-autocomplete-selected-wrap {
1000
  display: flex;
1001
  flex-wrap: wrap;
 
1061
  overflow: auto;
1062
  }
1063
 
1064
+ .workflow-section[data-section-step="1"] .chip-autocomplete.is-open .chip-autocomplete-panel {
1065
+ position: static;
1066
+ top: auto;
1067
+ left: auto;
1068
+ right: auto;
1069
+ margin-top: 6px;
1070
+ z-index: auto;
1071
+ }
1072
+
1073
  .chip-autocomplete-panel-head {
1074
  font-size: 0.72rem;
1075
  font-weight: 700;
 
1209
 
1210
  .pesquisa-avaliando-periodo-pair {
1211
  margin: 0;
1212
+ height: auto;
1213
+ align-self: start;
1214
  }
1215
 
1216
  .pesquisa-avaliando-bottom-stack {
1217
  gap: 12px;
1218
  }
1219
 
1220
+ .pesquisa-bairro-zona-grid {
1221
+ display: grid;
1222
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1223
+ gap: 12px 14px;
1224
+ align-items: start;
1225
+ }
1226
+
1227
  .pesquisa-bairro-bottom-field {
1228
+ min-width: 0;
1229
  }
1230
 
1231
  .pesquisa-avaliando-bottom-grid .pesquisa-field-pair {
 
2048
 
2049
  .upload-repo-row {
2050
  margin-bottom: 10px;
2051
+ align-items: flex-start;
2052
+ }
2053
+
2054
+ .upload-repo-field {
2055
+ display: grid;
2056
+ gap: 7px;
2057
+ flex: 1 1 420px;
2058
+ min-width: min(100%, 300px);
2059
  }
2060
 
2061
  .upload-repo-actions {
 
2693
  margin-bottom: 18px;
2694
  }
2695
 
2696
+ .section11-toggle-wrap {
2697
+ margin-bottom: 12px;
2698
+ display: flex;
2699
+ align-items: center;
2700
+ flex-wrap: wrap;
2701
+ gap: 8px;
2702
+ }
2703
+
2704
+ .btn-section11-unlock {
2705
+ --btn-bg-start: #f4f8fc;
2706
+ --btn-bg-end: #e8eff6;
2707
+ --btn-border: #c4d1df;
2708
+ --btn-shadow-soft: rgba(75, 102, 128, 0.12);
2709
+ --btn-shadow-strong: rgba(75, 102, 128, 0.2);
2710
+ color: #395470;
2711
+ }
2712
+
2713
+ .section11-search-criteria {
2714
+ margin-top: 8px;
2715
+ padding-top: 11px;
2716
+ border-top: 1px solid #d7e2ee;
2717
+ display: grid;
2718
+ gap: 8px;
2719
+ }
2720
+
2721
+ .section11-search-criteria-title {
2722
+ font-size: 0.76rem;
2723
+ font-weight: 800;
2724
+ letter-spacing: 0.03em;
2725
+ text-transform: uppercase;
2726
+ color: #4b6177;
2727
+ }
2728
+
2729
+ .section11-search-row {
2730
+ margin-bottom: 0;
2731
+ }
2732
+
2733
  .transform-preview-summary {
2734
  border: 1px solid #d8e5f2;
2735
  border-radius: 12px;
 
4106
  grid-template-columns: 1fr;
4107
  }
4108
 
4109
+ .pesquisa-bairro-zona-grid {
4110
+ grid-template-columns: 1fr;
4111
+ }
4112
+
4113
  .pesquisa-dynamic-filter-row,
4114
  .pesquisa-range-values-row,
4115
  .pesquisa-range-row,