github-actions commited on
Commit
7ad8558
·
0 Parent(s):

Sync from GitHub de3ca9b6f57913ea7bbc4e8a3b73a8d5f7844d2d

Browse files
.dockerignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ frontend/node_modules
4
+ frontend/dist
5
+ backend/.venv
6
+ backend/data/results
7
+ backend/data/app.db
8
+ backend/data/base/cadastro_base.db
9
+ backend-dev.log
10
+ backend-dev.err.log
11
+ frontend-dev.log
12
+ frontend-dev.err.log
13
+ cloudflared.log
14
+ cloudflared.err.log
15
+ tmp_mesa_react.html
16
+ tmp_mesa_react.css
17
+ **/__pycache__/
.gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
4
+ *.webp filter=lfs diff=lfs merge=lfs -text
5
+ *.db filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ *.sqlite3
7
+ *.db
8
+ !backend/data/base/cadastro_base_space_filtrada.db
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ node_modules/
12
+ dist/
13
+ .env
14
+ backend/uploads/
15
+ backend/data/app.db
16
+ backend/data/results/
17
+ backend/data/base/AUXILIAR_INSCRICOES.txt
18
+ backend/data/base/cadastro_base.db
19
+ cloudflared.log
20
+ cloudflared.err.log
21
+ tmp_mesa_react.html
22
+ tmp_mesa_react.css
23
+ backend-dev.log
24
+ backend-dev.err.log
25
+ frontend-dev.log
26
+ frontend-dev.err.log
27
+ **/__pycache__/
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-bookworm-slim AS frontend-builder
2
+ WORKDIR /app/frontend
3
+
4
+ COPY frontend/package*.json ./
5
+ RUN npm ci
6
+
7
+ COPY frontend/ ./
8
+ RUN npm run build
9
+
10
+
11
+ FROM python:3.13-slim AS runtime
12
+ WORKDIR /app
13
+
14
+ ENV PYTHONDONTWRITEBYTECODE=1
15
+ ENV PYTHONUNBUFFERED=1
16
+ ENV PORT=7860
17
+
18
+ COPY backend/requirements.txt /app/backend/requirements.txt
19
+ RUN pip install --no-cache-dir -r /app/backend/requirements.txt
20
+
21
+ COPY backend/ /app/backend/
22
+ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
23
+
24
+ EXPOSE 7860
25
+
26
+ CMD ["sh", "-c", "uvicorn backend.app.main:app --host 0.0.0.0 --port ${PORT}"]
README.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: cda
3
+ emoji: 🏢
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+
10
+ # Cadastro Imobiliario
11
+
12
+ Estrutura inicial para um sistema de cadastro de dados imobiliarios com:
13
+
14
+ - Backend em FastAPI
15
+ - Frontend em React + Vite
16
+ - Banco local SQLite para desenvolvimento
17
+ - Upload inicial de planilha para preview e importacao futura
18
+
19
+ ## Estrutura
20
+
21
+ - `backend/`: API e persistencia
22
+ - `frontend/`: interface web
23
+ - `run-dev.ps1`: sobe backend e frontend em desenvolvimento
24
+
25
+ ## Bases de dados locais
26
+
27
+ - `backend/data/base/AUXILIAR_INSCRICOES.txt`: base cadastral bruta, somente leitura
28
+ - `backend/data/base/cadastro_base.db`: base otimizada em SQLite, gerada automaticamente a partir do TXT
29
+ - `backend/data/results/`: area reservada para planilhas e arquivos gerados pelo sistema
30
+
31
+ ## Atualizacao da base cadastral
32
+
33
+ Quando o `AUXILIAR_INSCRICOES.txt` for atualizado, o sistema recria automaticamente o `cadastro_base.db` na proxima inicializacao da API.
34
+
35
+ Se quiser forcar a reconstrucao manualmente:
36
+
37
+ ```powershell
38
+ cd backend
39
+ .\.venv\Scripts\python.exe scripts\rebuild_cadastro_base.py
40
+ ```
41
+
42
+ ## Campos iniciais
43
+
44
+ - `titulo`
45
+ - `finalidade`
46
+ - `area_total`
47
+ - `area_privativa`
48
+ - `valor`
49
+ - `anuncio`
50
+ - `origem`
51
+ - `observacoes`
52
+
53
+ ## Como rodar
54
+
55
+ No Windows PowerShell:
56
+
57
+ ```powershell
58
+ Set-ExecutionPolicy -Scope Process Bypass
59
+ .\run-dev.ps1
60
+ ```
61
+
62
+ Se quiser instalar dependencias automaticamente:
63
+
64
+ ```powershell
65
+ Set-ExecutionPolicy -Scope Process Bypass
66
+ .\run-dev.ps1 -Install
67
+ ```
68
+
69
+ ## URLs padrao
70
+
71
+ - Frontend: `http://localhost:5173`
72
+ - Backend: `http://localhost:8000`
73
+ - Docs da API: `http://localhost:8000/docs`
74
+
75
+ ## Deploy no Hugging Face Space
76
+
77
+ O projeto esta preparado para rodar em `Docker Space`, com o frontend buildado e servido pelo backend em porta unica.
78
+
79
+ - Space: `https://huggingface.co/spaces/ESJL/cda`
80
+ - Porta exposta no container: `7860`
81
+ - Arquivo principal de deploy: `Dockerfile`
82
+
83
+ ## Proximos passos
84
+
85
+ - Ajustar o layout da planilha que voce vai enviar
86
+ - Adicionar mais campos do cadastro
87
+ - Implementar validacoes de negocio
88
+ - Criar autenticacao e perfis, se necessario
backend/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
backend/app/cadastro_base.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import sqlite3
3
+ from pathlib import Path
4
+
5
+ from .schemas import CadastroBaseRecord
6
+
7
+
8
+ BASE_FILE = Path(__file__).resolve().parent.parent / "data" / "base" / "AUXILIAR_INSCRICOES.txt"
9
+ SQLITE_FILE = Path(__file__).resolve().parent.parent / "data" / "base" / "cadastro_base.db"
10
+ SQLITE_SPACE_FILE = (
11
+ Path(__file__).resolve().parent.parent / "data" / "base" / "cadastro_base_space_filtrada.db"
12
+ )
13
+ BASE_COLUMNS = [
14
+ "NUM_BLOCO",
15
+ "NUM_INSCRICAO",
16
+ "COD_ENDLOC_LOGRADOURO",
17
+ "NME_ENDLOC_LOGRADOURO",
18
+ "NUM_ENDLOC_ENDERECO",
19
+ "NUM_ENDLOC_UNIDADE",
20
+ "NME_ENDLOC_BAIRRO_CDL",
21
+ "DES_FINALIDADE",
22
+ "RH_NOME",
23
+ "RH_VALOR",
24
+ "COORD_X",
25
+ "COORD_Y",
26
+ "ANO_EXERCICIO",
27
+ "NUM_VERSAO",
28
+ "IDF_REG_REGIAO_HOMOGENEA",
29
+ "AREA_TERRITORIAL",
30
+ "AREA_CONSTRUIDA",
31
+ "LATITUDE",
32
+ "LONGITUDE",
33
+ ]
34
+
35
+ BASE_REQUIRED_COLUMNS = {
36
+ "num_bloco",
37
+ "num_inscricao",
38
+ "cod_endloc_logradouro",
39
+ "nme_endloc_logradouro",
40
+ "num_endloc_endereco",
41
+ "num_endloc_unidade",
42
+ "nme_endloc_bairro_cdl",
43
+ "des_finalidade",
44
+ "rh_nome",
45
+ "rh_valor",
46
+ "coord_x",
47
+ "coord_y",
48
+ "ano_exercicio",
49
+ "num_versao",
50
+ "idf_reg_regiao_homogenea",
51
+ "area_territorial",
52
+ "area_construida",
53
+ "latitude",
54
+ "longitude",
55
+ "search_inscricao",
56
+ "search_address",
57
+ }
58
+
59
+ SPACE_REQUIRED_COLUMNS = {
60
+ "num_bloco",
61
+ "num_inscricao",
62
+ "cod_endloc_logradouro",
63
+ "nme_endloc_logradouro",
64
+ "num_endloc_endereco",
65
+ "num_endloc_unidade",
66
+ "nme_endloc_bairro_cdl",
67
+ "des_finalidade",
68
+ "rh_nome",
69
+ "rh_valor",
70
+ "area_territorial",
71
+ "area_construida",
72
+ "latitude",
73
+ "longitude",
74
+ "search_inscricao",
75
+ "search_address",
76
+ }
77
+
78
+
79
+ def _parse_decimal(value: str | None) -> float | None:
80
+ if value is None or value == "":
81
+ return None
82
+ try:
83
+ return float(str(value).replace(".", "").replace(",", "."))
84
+ except ValueError:
85
+ try:
86
+ return float(str(value).replace(",", "."))
87
+ except ValueError:
88
+ return None
89
+
90
+
91
+ def _normalize_text(value: str | None) -> str:
92
+ return (value or "").strip().upper()
93
+
94
+
95
+ def _to_record(row: dict[str, str | None]) -> CadastroBaseRecord:
96
+ logradouro = row.get("nme_endloc_logradouro") or None
97
+ numero = row.get("num_endloc_endereco") or None
98
+ bairro = row.get("nme_endloc_bairro_cdl") or None
99
+ finalidade = row.get("des_finalidade") or None
100
+ unidade = row.get("num_endloc_unidade") or None
101
+ num_inscricao = row.get("num_inscricao") or ""
102
+
103
+ endereco_base = " ".join(part for part in [logradouro, numero] if part)
104
+ titulo_sugerido = endereco_base or f"Inscricao {num_inscricao}"
105
+ label_parts = [f"Inscricao {num_inscricao}"]
106
+ if endereco_base:
107
+ label_parts.append(endereco_base)
108
+ if unidade:
109
+ label_parts.append(f"Unidade {unidade}")
110
+ if bairro:
111
+ label_parts.append(bairro)
112
+
113
+ return CadastroBaseRecord(
114
+ num_bloco=row.get("num_bloco") or None,
115
+ num_inscricao=num_inscricao,
116
+ cod_endloc_logradouro=row.get("cod_endloc_logradouro") or None,
117
+ nme_endloc_logradouro=logradouro,
118
+ num_endloc_endereco=numero,
119
+ num_endloc_unidade=unidade,
120
+ nme_endloc_bairro_cdl=bairro,
121
+ des_finalidade=finalidade,
122
+ rh_nome=row.get("rh_nome") or None,
123
+ rh_valor=_parse_decimal(row.get("rh_valor")),
124
+ coord_x=_parse_decimal(row.get("coord_x")),
125
+ coord_y=_parse_decimal(row.get("coord_y")),
126
+ ano_exercicio=_parse_decimal(row.get("ano_exercicio")),
127
+ num_versao=_parse_decimal(row.get("num_versao")),
128
+ idf_reg_regiao_homogenea=_parse_decimal(row.get("idf_reg_regiao_homogenea")),
129
+ area_territorial=_parse_decimal(row.get("area_territorial")),
130
+ area_construida=_parse_decimal(row.get("area_construida")),
131
+ latitude=_parse_decimal(row.get("latitude")),
132
+ longitude=_parse_decimal(row.get("longitude")),
133
+ titulo_sugerido=titulo_sugerido,
134
+ display_label=" | ".join(label_parts),
135
+ )
136
+
137
+
138
+ def _connect_sqlite() -> sqlite3.Connection:
139
+ connection = sqlite3.connect(_get_active_sqlite_file())
140
+ connection.row_factory = sqlite3.Row
141
+ return connection
142
+
143
+
144
+ def _get_sqlite_columns(path: Path) -> set[str]:
145
+ connection = sqlite3.connect(path)
146
+ connection.row_factory = sqlite3.Row
147
+ try:
148
+ return {row["name"] for row in connection.execute("PRAGMA table_info(cadastro_base)").fetchall()}
149
+ finally:
150
+ connection.close()
151
+
152
+
153
+ def _get_active_sqlite_file() -> Path:
154
+ if SQLITE_SPACE_FILE.exists():
155
+ try:
156
+ existing_columns = _get_sqlite_columns(SQLITE_SPACE_FILE)
157
+ if SPACE_REQUIRED_COLUMNS.issubset(existing_columns):
158
+ return SQLITE_SPACE_FILE
159
+ except sqlite3.DatabaseError:
160
+ pass
161
+ return SQLITE_FILE
162
+
163
+
164
+ def _needs_rebuild() -> bool:
165
+ if SQLITE_SPACE_FILE.exists():
166
+ try:
167
+ existing_columns = _get_sqlite_columns(SQLITE_SPACE_FILE)
168
+ if SPACE_REQUIRED_COLUMNS.issubset(existing_columns):
169
+ return False
170
+ except sqlite3.DatabaseError:
171
+ pass
172
+
173
+ if not BASE_FILE.exists():
174
+ raise FileNotFoundError(f"Base auxiliar nao encontrada em {BASE_FILE}")
175
+ if not SQLITE_FILE.exists():
176
+ return True
177
+ try:
178
+ existing_columns = _get_sqlite_columns(SQLITE_FILE)
179
+ except sqlite3.DatabaseError:
180
+ return True
181
+
182
+ if not BASE_REQUIRED_COLUMNS.issubset(existing_columns):
183
+ return True
184
+ return SQLITE_FILE.stat().st_mtime < BASE_FILE.stat().st_mtime
185
+
186
+
187
+ def ensure_cadastro_base_sqlite(force_rebuild: bool = False) -> tuple[Path, int]:
188
+ if not force_rebuild and not _needs_rebuild():
189
+ active_file = _get_active_sqlite_file()
190
+ connection = sqlite3.connect(active_file)
191
+ connection.row_factory = sqlite3.Row
192
+ with connection:
193
+ row = connection.execute("SELECT COUNT(*) AS total FROM cadastro_base").fetchone()
194
+ return active_file, int(row["total"])
195
+
196
+ SQLITE_FILE.parent.mkdir(parents=True, exist_ok=True)
197
+ temp_db = SQLITE_FILE.with_suffix(".tmp")
198
+ if temp_db.exists():
199
+ temp_db.unlink()
200
+
201
+ connection = sqlite3.connect(temp_db)
202
+ try:
203
+ connection.execute(
204
+ """
205
+ CREATE TABLE cadastro_base (
206
+ num_bloco TEXT,
207
+ num_inscricao TEXT,
208
+ cod_endloc_logradouro TEXT,
209
+ nme_endloc_logradouro TEXT,
210
+ num_endloc_endereco TEXT,
211
+ num_endloc_unidade TEXT,
212
+ nme_endloc_bairro_cdl TEXT,
213
+ des_finalidade TEXT,
214
+ rh_nome TEXT,
215
+ rh_valor TEXT,
216
+ coord_x TEXT,
217
+ coord_y TEXT,
218
+ ano_exercicio TEXT,
219
+ num_versao TEXT,
220
+ idf_reg_regiao_homogenea TEXT,
221
+ area_territorial TEXT,
222
+ area_construida TEXT,
223
+ latitude TEXT,
224
+ longitude TEXT,
225
+ search_inscricao TEXT,
226
+ search_address TEXT
227
+ )
228
+ """
229
+ )
230
+
231
+ insert_sql = """
232
+ INSERT INTO cadastro_base (
233
+ num_bloco,
234
+ num_inscricao,
235
+ cod_endloc_logradouro,
236
+ nme_endloc_logradouro,
237
+ num_endloc_endereco,
238
+ num_endloc_unidade,
239
+ nme_endloc_bairro_cdl,
240
+ des_finalidade,
241
+ rh_nome,
242
+ rh_valor,
243
+ coord_x,
244
+ coord_y,
245
+ ano_exercicio,
246
+ num_versao,
247
+ idf_reg_regiao_homogenea,
248
+ area_territorial,
249
+ area_construida,
250
+ latitude,
251
+ longitude,
252
+ search_inscricao,
253
+ search_address
254
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
255
+ """
256
+
257
+ batch: list[tuple[str, ...]] = []
258
+ total = 0
259
+ with BASE_FILE.open("r", encoding="utf-8", newline="") as handle:
260
+ reader = csv.DictReader(handle, delimiter="|")
261
+ for source_row in reader:
262
+ row = {column: source_row.get(column, "") for column in BASE_COLUMNS}
263
+ search_inscricao = _normalize_text(row["NUM_INSCRICAO"])
264
+ search_address = " ".join(
265
+ part
266
+ for part in [
267
+ _normalize_text(row["NME_ENDLOC_LOGRADOURO"]),
268
+ _normalize_text(row["NUM_ENDLOC_ENDERECO"]),
269
+ _normalize_text(row["NUM_ENDLOC_UNIDADE"]),
270
+ ]
271
+ if part
272
+ )
273
+ batch.append(
274
+ (
275
+ row["NUM_BLOCO"],
276
+ row["NUM_INSCRICAO"],
277
+ row["COD_ENDLOC_LOGRADOURO"],
278
+ row["NME_ENDLOC_LOGRADOURO"],
279
+ row["NUM_ENDLOC_ENDERECO"],
280
+ row["NUM_ENDLOC_UNIDADE"],
281
+ row["NME_ENDLOC_BAIRRO_CDL"],
282
+ row["DES_FINALIDADE"],
283
+ row["RH_NOME"],
284
+ row["RH_VALOR"],
285
+ row["COORD_X"],
286
+ row["COORD_Y"],
287
+ row["ANO_EXERCICIO"],
288
+ row["NUM_VERSAO"],
289
+ row["IDF_REG_REGIAO_HOMOGENEA"],
290
+ row["AREA_TERRITORIAL"],
291
+ row["AREA_CONSTRUIDA"],
292
+ row["LATITUDE"],
293
+ row["LONGITUDE"],
294
+ search_inscricao,
295
+ search_address,
296
+ )
297
+ )
298
+ if len(batch) >= 10000:
299
+ connection.executemany(insert_sql, batch)
300
+ total += len(batch)
301
+ batch.clear()
302
+
303
+ if batch:
304
+ connection.executemany(insert_sql, batch)
305
+ total += len(batch)
306
+
307
+ connection.execute(
308
+ "CREATE INDEX idx_cadastro_base_inscricao ON cadastro_base(search_inscricao)"
309
+ )
310
+ connection.execute(
311
+ "CREATE INDEX idx_cadastro_base_endereco ON cadastro_base(search_address)"
312
+ )
313
+ connection.commit()
314
+ finally:
315
+ connection.close()
316
+
317
+ try:
318
+ temp_db.replace(SQLITE_FILE)
319
+ except PermissionError as exc:
320
+ if temp_db.exists():
321
+ temp_db.unlink(missing_ok=True)
322
+ raise PermissionError(
323
+ "Nao foi possivel substituir cadastro_base.db. Feche a API antes de rodar o rebuild manual."
324
+ ) from exc
325
+ return SQLITE_FILE, total
326
+
327
+
328
+ def search_cadastro_base(mode: str, query: str, limit: int = 20) -> list[CadastroBaseRecord]:
329
+ if mode not in {"inscricao", "endereco"}:
330
+ raise ValueError("Modo de busca invalido.")
331
+
332
+ cleaned_query = _normalize_text(query)
333
+ if len(cleaned_query) < 2:
334
+ return []
335
+
336
+ ensure_cadastro_base_sqlite()
337
+
338
+ with _connect_sqlite() as connection:
339
+ available_columns = _get_sqlite_columns(_get_active_sqlite_file())
340
+ select_candidates = [
341
+ "num_bloco",
342
+ "num_inscricao",
343
+ "cod_endloc_logradouro",
344
+ "nme_endloc_logradouro",
345
+ "num_endloc_endereco",
346
+ "num_endloc_unidade",
347
+ "nme_endloc_bairro_cdl",
348
+ "des_finalidade",
349
+ "rh_nome",
350
+ "rh_valor",
351
+ "coord_x",
352
+ "coord_y",
353
+ "ano_exercicio",
354
+ "num_versao",
355
+ "idf_reg_regiao_homogenea",
356
+ "area_territorial",
357
+ "area_construida",
358
+ "latitude",
359
+ "longitude",
360
+ ]
361
+ selected_columns = ",\n ".join(
362
+ column for column in select_candidates if column in available_columns
363
+ )
364
+ if mode == "inscricao":
365
+ rows = connection.execute(
366
+ """
367
+ SELECT
368
+ """
369
+ + selected_columns
370
+ + """
371
+ FROM cadastro_base
372
+ WHERE search_inscricao LIKE ?
373
+ LIMIT ?
374
+ """,
375
+ (f"%{cleaned_query}%", limit),
376
+ ).fetchall()
377
+ else:
378
+ terms = [term for term in cleaned_query.split() if term]
379
+ where_clause = " AND ".join(["search_address LIKE ?"] * len(terms))
380
+ params = [f"%{term}%" for term in terms]
381
+ params.append(limit)
382
+ rows = connection.execute(
383
+ f"""
384
+ SELECT
385
+ {selected_columns}
386
+ FROM cadastro_base
387
+ WHERE {where_clause}
388
+ LIMIT ?
389
+ """,
390
+ params,
391
+ ).fetchall()
392
+
393
+ return [_to_record(dict(row)) for row in rows]
backend/app/crud.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import select
2
+ from sqlalchemy.orm import Session
3
+
4
+ from . import models, schemas
5
+
6
+
7
+ def list_properties(db: Session) -> list[models.Property]:
8
+ return list(db.scalars(select(models.Property).order_by(models.Property.id.desc())))
9
+
10
+
11
+ def create_property(db: Session, payload: schemas.PropertyCreate) -> models.Property:
12
+ data = payload.model_dump()
13
+ if not data.get("titulo"):
14
+ endereco = " ".join(
15
+ part
16
+ for part in [data.get("nme_endloc_logradouro"), data.get("num_endloc_endereco")]
17
+ if part
18
+ ).strip()
19
+ data["titulo"] = endereco or data.get("num_inscricao") or "Novo registro"
20
+ item = models.Property(**data)
21
+ db.add(item)
22
+ db.commit()
23
+ db.refresh(item)
24
+ return item
25
+
26
+
27
+ def delete_property(db: Session, property_id: int) -> bool:
28
+ item = db.get(models.Property, property_id)
29
+ if item is None:
30
+ return False
31
+ db.delete(item)
32
+ db.commit()
33
+ return True
34
+
35
+
36
+ def export_properties(db: Session) -> list[dict]:
37
+ items = list_properties(db)
38
+ return [
39
+ {
40
+ "id": item.id,
41
+ "num_bloco": item.num_bloco,
42
+ "num_inscricao": item.num_inscricao,
43
+ "cod_endloc_logradouro": item.cod_endloc_logradouro,
44
+ "logradouro": item.nme_endloc_logradouro,
45
+ "numero": item.num_endloc_endereco,
46
+ "unidade": item.num_endloc_unidade,
47
+ "bairro": item.nme_endloc_bairro_cdl,
48
+ "finalidade": item.finalidade,
49
+ "rh_nome": item.rh_nome,
50
+ "rh_valor": item.rh_valor,
51
+ "area_total_separada": item.area_total_detalhe,
52
+ "area_total_soma": item.area_total,
53
+ "area_privativa_separada": item.area_privativa_detalhe,
54
+ "area_privativa_soma": item.area_privativa,
55
+ "finalidade_oferta": item.finalidade_oferta,
56
+ "area_total_oferta": item.area_total_oferta,
57
+ "area_privativa_oferta": item.area_privativa_oferta,
58
+ "valor_oferta": item.valor_oferta,
59
+ "latitude": item.latitude,
60
+ "longitude": item.longitude,
61
+ "descricao_oferta": item.descricao_oferta,
62
+ "observacao": item.observacao,
63
+ "url": item.url,
64
+ "imobiliaria": item.imobiliaria,
65
+ "codigo": item.codigo,
66
+ "infra": item.infra,
67
+ "padrao": item.padrao,
68
+ "conservacao": item.conservacao,
69
+ "vaga": item.vaga,
70
+ }
71
+ for item in items
72
+ ]
backend/app/database.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from sqlalchemy import create_engine, inspect, text
4
+ from sqlalchemy.orm import DeclarativeBase, sessionmaker
5
+
6
+
7
+ BASE_DIR = Path(__file__).resolve().parent.parent
8
+ DATA_DIR = BASE_DIR / "data"
9
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
10
+ DATABASE_URL = f"sqlite:///{DATA_DIR / 'app.db'}"
11
+
12
+
13
+ class Base(DeclarativeBase):
14
+ pass
15
+
16
+
17
+ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
18
+ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
19
+
20
+
21
+ def get_db():
22
+ db = SessionLocal()
23
+ try:
24
+ yield db
25
+ finally:
26
+ db.close()
27
+
28
+
29
+ def ensure_property_schema() -> None:
30
+ inspector = inspect(engine)
31
+ if "properties" not in inspector.get_table_names():
32
+ return
33
+
34
+ existing_columns = {column["name"] for column in inspector.get_columns("properties")}
35
+ migrations = {
36
+ "num_bloco": "ALTER TABLE properties ADD COLUMN num_bloco VARCHAR(30)",
37
+ "num_inscricao": "ALTER TABLE properties ADD COLUMN num_inscricao VARCHAR(30)",
38
+ "cod_endloc_logradouro": "ALTER TABLE properties ADD COLUMN cod_endloc_logradouro VARCHAR(30)",
39
+ "nme_endloc_logradouro": "ALTER TABLE properties ADD COLUMN nme_endloc_logradouro VARCHAR(150)",
40
+ "num_endloc_endereco": "ALTER TABLE properties ADD COLUMN num_endloc_endereco VARCHAR(30)",
41
+ "num_endloc_unidade": "ALTER TABLE properties ADD COLUMN num_endloc_unidade VARCHAR(30)",
42
+ "nme_endloc_bairro_cdl": "ALTER TABLE properties ADD COLUMN nme_endloc_bairro_cdl VARCHAR(120)",
43
+ "rh_nome": "ALTER TABLE properties ADD COLUMN rh_nome VARCHAR(80)",
44
+ "rh_valor": "ALTER TABLE properties ADD COLUMN rh_valor FLOAT",
45
+ "coord_x": "ALTER TABLE properties ADD COLUMN coord_x FLOAT",
46
+ "coord_y": "ALTER TABLE properties ADD COLUMN coord_y FLOAT",
47
+ "ano_exercicio": "ALTER TABLE properties ADD COLUMN ano_exercicio FLOAT",
48
+ "num_versao": "ALTER TABLE properties ADD COLUMN num_versao FLOAT",
49
+ "idf_reg_regiao_homogenea": "ALTER TABLE properties ADD COLUMN idf_reg_regiao_homogenea FLOAT",
50
+ "area_total_detalhe": "ALTER TABLE properties ADD COLUMN area_total_detalhe VARCHAR(1000)",
51
+ "area_privativa_detalhe": "ALTER TABLE properties ADD COLUMN area_privativa_detalhe VARCHAR(1000)",
52
+ "latitude": "ALTER TABLE properties ADD COLUMN latitude FLOAT",
53
+ "longitude": "ALTER TABLE properties ADD COLUMN longitude FLOAT",
54
+ "finalidade_oferta": "ALTER TABLE properties ADD COLUMN finalidade_oferta VARCHAR(50)",
55
+ "area_total_oferta": "ALTER TABLE properties ADD COLUMN area_total_oferta FLOAT",
56
+ "area_privativa_oferta": "ALTER TABLE properties ADD COLUMN area_privativa_oferta FLOAT",
57
+ "valor_oferta": "ALTER TABLE properties ADD COLUMN valor_oferta FLOAT",
58
+ "descricao_oferta": "ALTER TABLE properties ADD COLUMN descricao_oferta TEXT",
59
+ "observacao": "ALTER TABLE properties ADD COLUMN observacao TEXT",
60
+ "url": "ALTER TABLE properties ADD COLUMN url TEXT",
61
+ "imobiliaria": "ALTER TABLE properties ADD COLUMN imobiliaria VARCHAR(150)",
62
+ "codigo": "ALTER TABLE properties ADD COLUMN codigo VARCHAR(80)",
63
+ "infra": "ALTER TABLE properties ADD COLUMN infra TEXT",
64
+ "padrao": "ALTER TABLE properties ADD COLUMN padrao VARCHAR(80)",
65
+ "conservacao": "ALTER TABLE properties ADD COLUMN conservacao VARCHAR(80)",
66
+ "vaga": "ALTER TABLE properties ADD COLUMN vaga VARCHAR(80)",
67
+ }
68
+
69
+ with engine.begin() as connection:
70
+ for column_name, statement in migrations.items():
71
+ if column_name not in existing_columns:
72
+ connection.execute(text(statement))
backend/app/main.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import FileResponse, HTMLResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from sqlalchemy.orm import Session
12
+
13
+ from . import crud, models, schemas
14
+ from .cadastro_base import ensure_cadastro_base_sqlite, search_cadastro_base
15
+ from .database import BASE_DIR, Base, engine, ensure_property_schema, get_db
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ Base.metadata.create_all(bind=engine)
20
+ ensure_property_schema()
21
+ RESULTS_DIR = BASE_DIR / "data" / "results"
22
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
23
+ FRONTEND_DIST_DIR = BASE_DIR.parent / "frontend" / "dist"
24
+ FRONTEND_ASSETS_DIR = FRONTEND_DIST_DIR / "assets"
25
+
26
+ app = FastAPI(
27
+ title="Cadastro Imobiliario API",
28
+ version="0.1.0",
29
+ description="API inicial para cadastro e importacao de dados imobiliarios.",
30
+ )
31
+
32
+ app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+
41
+ @app.on_event("startup")
42
+ def prepare_cadastro_base():
43
+ try:
44
+ ensure_cadastro_base_sqlite()
45
+ except FileNotFoundError:
46
+ logger.warning("Base auxiliar nao encontrada. A busca cadastral ficara indisponivel.")
47
+
48
+
49
+ @app.get("/health")
50
+ def healthcheck():
51
+ return {"status": "ok"}
52
+
53
+
54
+ @app.get("/properties", response_model=list[schemas.PropertyRead])
55
+ def get_properties(db: Session = Depends(get_db)):
56
+ return crud.list_properties(db)
57
+
58
+
59
+ @app.post("/properties", response_model=schemas.PropertyRead, status_code=201)
60
+ def post_property(payload: schemas.PropertyCreate, db: Session = Depends(get_db)):
61
+ return crud.create_property(db, payload)
62
+
63
+
64
+ @app.delete("/properties/{property_id}", status_code=204)
65
+ def delete_property(property_id: int, db: Session = Depends(get_db)):
66
+ deleted = crud.delete_property(db, property_id)
67
+ if not deleted:
68
+ raise HTTPException(status_code=404, detail="Registro nao encontrado.")
69
+
70
+
71
+ @app.get("/properties/export")
72
+ def export_properties(db: Session = Depends(get_db)):
73
+ rows = crud.export_properties(db)
74
+ file_name = f"cadastro_imobiliario_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
75
+ file_path = RESULTS_DIR / file_name
76
+
77
+ df = pd.DataFrame(rows)
78
+ if df.empty:
79
+ df = pd.DataFrame(
80
+ columns=[
81
+ "id",
82
+ "num_bloco",
83
+ "num_inscricao",
84
+ "cod_endloc_logradouro",
85
+ "logradouro",
86
+ "numero",
87
+ "unidade",
88
+ "bairro",
89
+ "finalidade",
90
+ "rh_nome",
91
+ "rh_valor",
92
+ "area_total_separada",
93
+ "area_total_soma",
94
+ "area_privativa_separada",
95
+ "area_privativa_soma",
96
+ "finalidade_oferta",
97
+ "area_total_oferta",
98
+ "area_privativa_oferta",
99
+ "valor_oferta",
100
+ "latitude",
101
+ "longitude",
102
+ "descricao_oferta",
103
+ "observacao",
104
+ "url",
105
+ "imobiliaria",
106
+ "codigo",
107
+ "infra",
108
+ "padrao",
109
+ "conservacao",
110
+ "vaga",
111
+ ]
112
+ )
113
+ df.to_excel(file_path, index=False)
114
+
115
+ return FileResponse(
116
+ path=Path(file_path),
117
+ filename=file_name,
118
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
119
+ )
120
+
121
+
122
+ @app.get("/cadastro-base/search", response_model=schemas.CadastroBaseSearchResponse)
123
+ def get_cadastro_base_search(mode: str, q: str, limit: int = 20):
124
+ try:
125
+ items = search_cadastro_base(mode=mode, query=q, limit=limit)
126
+ except FileNotFoundError as exc:
127
+ raise HTTPException(status_code=500, detail=str(exc)) from exc
128
+ except ValueError as exc:
129
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
130
+
131
+ return schemas.CadastroBaseSearchResponse(mode=mode, total=len(items), items=items)
132
+
133
+
134
+ @app.post("/properties/import-preview", response_model=schemas.SpreadsheetPreview)
135
+ async def import_preview(file: UploadFile = File(...)):
136
+ if not file.filename:
137
+ raise HTTPException(status_code=400, detail="Arquivo invalido.")
138
+
139
+ content = await file.read()
140
+ suffix = file.filename.lower()
141
+
142
+ try:
143
+ if suffix.endswith(".csv"):
144
+ df = pd.read_csv(BytesIO(content))
145
+ elif suffix.endswith(".xlsx") or suffix.endswith(".xls"):
146
+ df = pd.read_excel(BytesIO(content))
147
+ else:
148
+ raise HTTPException(status_code=400, detail="Formato nao suportado. Use CSV ou XLSX.")
149
+ except HTTPException:
150
+ raise
151
+ except Exception as exc:
152
+ raise HTTPException(status_code=400, detail=f"Falha ao ler planilha: {exc}") from exc
153
+
154
+ df = df.where(pd.notnull(df), None)
155
+ preview_rows = df.head(10).to_dict(orient="records")
156
+
157
+ return schemas.SpreadsheetPreview(
158
+ file_name=file.filename,
159
+ columns=[str(column) for column in df.columns.tolist()],
160
+ rows=preview_rows,
161
+ total_rows=len(df),
162
+ )
163
+
164
+
165
+ if FRONTEND_ASSETS_DIR.exists():
166
+ app.mount("/assets", StaticFiles(directory=FRONTEND_ASSETS_DIR), name="assets")
167
+
168
+
169
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
170
+ def serve_frontend_index():
171
+ if FRONTEND_DIST_DIR.exists():
172
+ return FileResponse(FRONTEND_DIST_DIR / "index.html")
173
+ raise HTTPException(status_code=404, detail="Frontend build nao encontrado.")
174
+
175
+
176
+ @app.get("/{full_path:path}", response_class=HTMLResponse, include_in_schema=False)
177
+ def serve_frontend_app(full_path: str):
178
+ if full_path.startswith(("properties", "cadastro-base", "health", "docs", "openapi.json")):
179
+ raise HTTPException(status_code=404, detail="Recurso nao encontrado.")
180
+
181
+ requested_path = FRONTEND_DIST_DIR / full_path
182
+ if FRONTEND_DIST_DIR.exists() and requested_path.is_file():
183
+ return FileResponse(requested_path)
184
+ if FRONTEND_DIST_DIR.exists():
185
+ return FileResponse(FRONTEND_DIST_DIR / "index.html")
186
+ raise HTTPException(status_code=404, detail="Frontend build nao encontrado.")
backend/app/models.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Float, String, Text
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
+
4
+ from .database import Base
5
+
6
+
7
+ class Property(Base):
8
+ __tablename__ = "properties"
9
+
10
+ id: Mapped[int] = mapped_column(primary_key=True, index=True)
11
+ titulo: Mapped[str] = mapped_column(String(1000), nullable=False)
12
+ finalidade: Mapped[str] = mapped_column(String(1000), nullable=False)
13
+ num_bloco: Mapped[str | None] = mapped_column(String(1000), nullable=True)
14
+ num_inscricao: Mapped[str | None] = mapped_column(String(1000), nullable=True, index=True)
15
+ cod_endloc_logradouro: Mapped[str | None] = mapped_column(String(1000), nullable=True)
16
+ nme_endloc_logradouro: Mapped[str | None] = mapped_column(String(1000), nullable=True, index=True)
17
+ num_endloc_endereco: Mapped[str | None] = mapped_column(String(1000), nullable=True)
18
+ num_endloc_unidade: Mapped[str | None] = mapped_column(String(1000), nullable=True)
19
+ nme_endloc_bairro_cdl: Mapped[str | None] = mapped_column(String(1000), nullable=True)
20
+ rh_nome: Mapped[str | None] = mapped_column(String(1000), nullable=True)
21
+ rh_valor: Mapped[float | None] = mapped_column(Float, nullable=True)
22
+ coord_x: Mapped[float | None] = mapped_column(Float, nullable=True)
23
+ coord_y: Mapped[float | None] = mapped_column(Float, nullable=True)
24
+ ano_exercicio: Mapped[float | None] = mapped_column(Float, nullable=True)
25
+ num_versao: Mapped[float | None] = mapped_column(Float, nullable=True)
26
+ idf_reg_regiao_homogenea: Mapped[float | None] = mapped_column(Float, nullable=True)
27
+ area_total_detalhe: Mapped[str | None] = mapped_column(String(1000), nullable=True)
28
+ area_total: Mapped[float | None] = mapped_column(Float, nullable=True)
29
+ area_privativa_detalhe: Mapped[str | None] = mapped_column(String(1000), nullable=True)
30
+ area_privativa: Mapped[float | None] = mapped_column(Float, nullable=True)
31
+ finalidade_oferta: Mapped[str | None] = mapped_column(String(50), nullable=True)
32
+ area_total_oferta: Mapped[float | None] = mapped_column(Float, nullable=True)
33
+ area_privativa_oferta: Mapped[float | None] = mapped_column(Float, nullable=True)
34
+ valor_oferta: Mapped[float | None] = mapped_column(Float, nullable=True)
35
+ latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
36
+ longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
37
+ descricao_oferta: Mapped[str | None] = mapped_column(Text, nullable=True)
38
+ observacao: Mapped[str | None] = mapped_column(Text, nullable=True)
39
+ url: Mapped[str | None] = mapped_column(Text, nullable=True)
40
+ imobiliaria: Mapped[str | None] = mapped_column(String(150), nullable=True)
41
+ codigo: Mapped[str | None] = mapped_column(String(80), nullable=True)
42
+ infra: Mapped[str | None] = mapped_column(Text, nullable=True)
43
+ padrao: Mapped[str | None] = mapped_column(String(80), nullable=True)
44
+ conservacao: Mapped[str | None] = mapped_column(String(80), nullable=True)
45
+ vaga: Mapped[str | None] = mapped_column(String(80), nullable=True)
46
+ origem: Mapped[str] = mapped_column(String(30), default="manual", nullable=False)
backend/app/schemas.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+
4
+ class PropertyBase(BaseModel):
5
+ titulo: str | None = Field(default=None, min_length=2, max_length=1000)
6
+ finalidade: str = Field(min_length=2, max_length=1000)
7
+ num_bloco: str | None = Field(default=None, max_length=1000)
8
+ num_inscricao: str | None = Field(default=None, max_length=1000)
9
+ cod_endloc_logradouro: str | None = Field(default=None, max_length=1000)
10
+ nme_endloc_logradouro: str | None = Field(default=None, max_length=1000)
11
+ num_endloc_endereco: str | None = Field(default=None, max_length=1000)
12
+ num_endloc_unidade: str | None = Field(default=None, max_length=1000)
13
+ nme_endloc_bairro_cdl: str | None = Field(default=None, max_length=1000)
14
+ rh_nome: str | None = Field(default=None, max_length=1000)
15
+ rh_valor: float | None = Field(default=None, ge=0)
16
+ coord_x: float | None = None
17
+ coord_y: float | None = None
18
+ ano_exercicio: float | None = Field(default=None, ge=0)
19
+ num_versao: float | None = Field(default=None, ge=0)
20
+ idf_reg_regiao_homogenea: float | None = Field(default=None, ge=0)
21
+ area_total_detalhe: str | None = Field(default=None, max_length=1000)
22
+ area_total: float | None = Field(default=None, ge=0)
23
+ area_privativa_detalhe: str | None = Field(default=None, max_length=1000)
24
+ area_privativa: float | None = Field(default=None, ge=0)
25
+ finalidade_oferta: str | None = Field(default=None, max_length=50)
26
+ area_total_oferta: float | None = Field(default=None, ge=0)
27
+ area_privativa_oferta: float | None = Field(default=None, ge=0)
28
+ valor_oferta: float | None = Field(default=None, ge=0)
29
+ latitude: float | None = None
30
+ longitude: float | None = None
31
+ descricao_oferta: str | None = None
32
+ observacao: str | None = None
33
+ url: str | None = None
34
+ imobiliaria: str | None = Field(default=None, max_length=150)
35
+ codigo: str | None = Field(default=None, max_length=80)
36
+ infra: str | None = None
37
+ padrao: str | None = Field(default=None, max_length=80)
38
+ conservacao: str | None = Field(default=None, max_length=80)
39
+ vaga: str | None = Field(default=None, max_length=80)
40
+ origem: str = "manual"
41
+
42
+
43
+ class PropertyCreate(PropertyBase):
44
+ pass
45
+
46
+
47
+ class PropertyRead(PropertyBase):
48
+ id: int
49
+
50
+ model_config = ConfigDict(from_attributes=True)
51
+
52
+
53
+ class SpreadsheetPreview(BaseModel):
54
+ file_name: str
55
+ columns: list[str]
56
+ rows: list[dict]
57
+ total_rows: int
58
+
59
+
60
+ class CadastroBaseRecord(BaseModel):
61
+ num_bloco: str | None = None
62
+ num_inscricao: str
63
+ cod_endloc_logradouro: str | None = None
64
+ nme_endloc_logradouro: str | None = None
65
+ num_endloc_endereco: str | None = None
66
+ num_endloc_unidade: str | None = None
67
+ nme_endloc_bairro_cdl: str | None = None
68
+ des_finalidade: str | None = None
69
+ rh_nome: str | None = None
70
+ rh_valor: float | None = None
71
+ coord_x: float | None = None
72
+ coord_y: float | None = None
73
+ ano_exercicio: float | None = None
74
+ num_versao: float | None = None
75
+ idf_reg_regiao_homogenea: float | None = None
76
+ area_territorial: float | None = None
77
+ area_construida: float | None = None
78
+ latitude: float | None = None
79
+ longitude: float | None = None
80
+ titulo_sugerido: str
81
+ display_label: str
82
+
83
+
84
+ class CadastroBaseSearchResponse(BaseModel):
85
+ mode: str
86
+ total: int
87
+ items: list[CadastroBaseRecord]
backend/data/base/cadastro_base_space_filtrada.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ecdb05ddc8f2bfcf468a35d5456cd33802b5721ad6298103a7431b2b2358c3c
3
+ size 54325248
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.118.0
2
+ uvicorn[standard]==0.37.0
3
+ sqlalchemy==2.0.43
4
+ pydantic==2.11.7
5
+ pandas==2.3.3
6
+ openpyxl==3.1.5
7
+ python-multipart==0.0.20
8
+
backend/scripts/rebuild_cadastro_base.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ BASE_DIR = Path(__file__).resolve().parents[1]
5
+ if str(BASE_DIR) not in sys.path:
6
+ sys.path.insert(0, str(BASE_DIR))
7
+
8
+ from app.cadastro_base import ensure_cadastro_base_sqlite
9
+
10
+
11
+ if __name__ == "__main__":
12
+ db_path, row_count = ensure_cadastro_base_sqlite(force_rebuild=True)
13
+ print(f"Base SQLite atualizada: {db_path} ({row_count} registros)")
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Cadastro Imobiliario</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
13
+
frontend/package-lock.json ADDED
@@ -0,0 +1,1729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cadastro-imobiliario-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "cadastro-imobiliario-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1"
13
+ },
14
+ "devDependencies": {
15
+ "@types/react": "^18.3.12",
16
+ "@types/react-dom": "^18.3.1",
17
+ "@vitejs/plugin-react": "^4.3.4",
18
+ "typescript": "^5.6.3",
19
+ "vite": "^5.4.10"
20
+ }
21
+ },
22
+ "node_modules/@babel/code-frame": {
23
+ "version": "7.29.0",
24
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
25
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
26
+ "dev": true,
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@babel/helper-validator-identifier": "^7.28.5",
30
+ "js-tokens": "^4.0.0",
31
+ "picocolors": "^1.1.1"
32
+ },
33
+ "engines": {
34
+ "node": ">=6.9.0"
35
+ }
36
+ },
37
+ "node_modules/@babel/compat-data": {
38
+ "version": "7.29.0",
39
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
40
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
41
+ "dev": true,
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=6.9.0"
45
+ }
46
+ },
47
+ "node_modules/@babel/core": {
48
+ "version": "7.29.0",
49
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
50
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
51
+ "dev": true,
52
+ "license": "MIT",
53
+ "dependencies": {
54
+ "@babel/code-frame": "^7.29.0",
55
+ "@babel/generator": "^7.29.0",
56
+ "@babel/helper-compilation-targets": "^7.28.6",
57
+ "@babel/helper-module-transforms": "^7.28.6",
58
+ "@babel/helpers": "^7.28.6",
59
+ "@babel/parser": "^7.29.0",
60
+ "@babel/template": "^7.28.6",
61
+ "@babel/traverse": "^7.29.0",
62
+ "@babel/types": "^7.29.0",
63
+ "@jridgewell/remapping": "^2.3.5",
64
+ "convert-source-map": "^2.0.0",
65
+ "debug": "^4.1.0",
66
+ "gensync": "^1.0.0-beta.2",
67
+ "json5": "^2.2.3",
68
+ "semver": "^6.3.1"
69
+ },
70
+ "engines": {
71
+ "node": ">=6.9.0"
72
+ },
73
+ "funding": {
74
+ "type": "opencollective",
75
+ "url": "https://opencollective.com/babel"
76
+ }
77
+ },
78
+ "node_modules/@babel/generator": {
79
+ "version": "7.29.1",
80
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
81
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
82
+ "dev": true,
83
+ "license": "MIT",
84
+ "dependencies": {
85
+ "@babel/parser": "^7.29.0",
86
+ "@babel/types": "^7.29.0",
87
+ "@jridgewell/gen-mapping": "^0.3.12",
88
+ "@jridgewell/trace-mapping": "^0.3.28",
89
+ "jsesc": "^3.0.2"
90
+ },
91
+ "engines": {
92
+ "node": ">=6.9.0"
93
+ }
94
+ },
95
+ "node_modules/@babel/helper-compilation-targets": {
96
+ "version": "7.28.6",
97
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
98
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
99
+ "dev": true,
100
+ "license": "MIT",
101
+ "dependencies": {
102
+ "@babel/compat-data": "^7.28.6",
103
+ "@babel/helper-validator-option": "^7.27.1",
104
+ "browserslist": "^4.24.0",
105
+ "lru-cache": "^5.1.1",
106
+ "semver": "^6.3.1"
107
+ },
108
+ "engines": {
109
+ "node": ">=6.9.0"
110
+ }
111
+ },
112
+ "node_modules/@babel/helper-globals": {
113
+ "version": "7.28.0",
114
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
115
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
116
+ "dev": true,
117
+ "license": "MIT",
118
+ "engines": {
119
+ "node": ">=6.9.0"
120
+ }
121
+ },
122
+ "node_modules/@babel/helper-module-imports": {
123
+ "version": "7.28.6",
124
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
125
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
126
+ "dev": true,
127
+ "license": "MIT",
128
+ "dependencies": {
129
+ "@babel/traverse": "^7.28.6",
130
+ "@babel/types": "^7.28.6"
131
+ },
132
+ "engines": {
133
+ "node": ">=6.9.0"
134
+ }
135
+ },
136
+ "node_modules/@babel/helper-module-transforms": {
137
+ "version": "7.28.6",
138
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
139
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
140
+ "dev": true,
141
+ "license": "MIT",
142
+ "dependencies": {
143
+ "@babel/helper-module-imports": "^7.28.6",
144
+ "@babel/helper-validator-identifier": "^7.28.5",
145
+ "@babel/traverse": "^7.28.6"
146
+ },
147
+ "engines": {
148
+ "node": ">=6.9.0"
149
+ },
150
+ "peerDependencies": {
151
+ "@babel/core": "^7.0.0"
152
+ }
153
+ },
154
+ "node_modules/@babel/helper-plugin-utils": {
155
+ "version": "7.28.6",
156
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
157
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
158
+ "dev": true,
159
+ "license": "MIT",
160
+ "engines": {
161
+ "node": ">=6.9.0"
162
+ }
163
+ },
164
+ "node_modules/@babel/helper-string-parser": {
165
+ "version": "7.27.1",
166
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
167
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
168
+ "dev": true,
169
+ "license": "MIT",
170
+ "engines": {
171
+ "node": ">=6.9.0"
172
+ }
173
+ },
174
+ "node_modules/@babel/helper-validator-identifier": {
175
+ "version": "7.28.5",
176
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
177
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
178
+ "dev": true,
179
+ "license": "MIT",
180
+ "engines": {
181
+ "node": ">=6.9.0"
182
+ }
183
+ },
184
+ "node_modules/@babel/helper-validator-option": {
185
+ "version": "7.27.1",
186
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
187
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
188
+ "dev": true,
189
+ "license": "MIT",
190
+ "engines": {
191
+ "node": ">=6.9.0"
192
+ }
193
+ },
194
+ "node_modules/@babel/helpers": {
195
+ "version": "7.29.2",
196
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
197
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
198
+ "dev": true,
199
+ "license": "MIT",
200
+ "dependencies": {
201
+ "@babel/template": "^7.28.6",
202
+ "@babel/types": "^7.29.0"
203
+ },
204
+ "engines": {
205
+ "node": ">=6.9.0"
206
+ }
207
+ },
208
+ "node_modules/@babel/parser": {
209
+ "version": "7.29.2",
210
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
211
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
212
+ "dev": true,
213
+ "license": "MIT",
214
+ "dependencies": {
215
+ "@babel/types": "^7.29.0"
216
+ },
217
+ "bin": {
218
+ "parser": "bin/babel-parser.js"
219
+ },
220
+ "engines": {
221
+ "node": ">=6.0.0"
222
+ }
223
+ },
224
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
225
+ "version": "7.27.1",
226
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
227
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
228
+ "dev": true,
229
+ "license": "MIT",
230
+ "dependencies": {
231
+ "@babel/helper-plugin-utils": "^7.27.1"
232
+ },
233
+ "engines": {
234
+ "node": ">=6.9.0"
235
+ },
236
+ "peerDependencies": {
237
+ "@babel/core": "^7.0.0-0"
238
+ }
239
+ },
240
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
241
+ "version": "7.27.1",
242
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
243
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
244
+ "dev": true,
245
+ "license": "MIT",
246
+ "dependencies": {
247
+ "@babel/helper-plugin-utils": "^7.27.1"
248
+ },
249
+ "engines": {
250
+ "node": ">=6.9.0"
251
+ },
252
+ "peerDependencies": {
253
+ "@babel/core": "^7.0.0-0"
254
+ }
255
+ },
256
+ "node_modules/@babel/template": {
257
+ "version": "7.28.6",
258
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
259
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
260
+ "dev": true,
261
+ "license": "MIT",
262
+ "dependencies": {
263
+ "@babel/code-frame": "^7.28.6",
264
+ "@babel/parser": "^7.28.6",
265
+ "@babel/types": "^7.28.6"
266
+ },
267
+ "engines": {
268
+ "node": ">=6.9.0"
269
+ }
270
+ },
271
+ "node_modules/@babel/traverse": {
272
+ "version": "7.29.0",
273
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
274
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
275
+ "dev": true,
276
+ "license": "MIT",
277
+ "dependencies": {
278
+ "@babel/code-frame": "^7.29.0",
279
+ "@babel/generator": "^7.29.0",
280
+ "@babel/helper-globals": "^7.28.0",
281
+ "@babel/parser": "^7.29.0",
282
+ "@babel/template": "^7.28.6",
283
+ "@babel/types": "^7.29.0",
284
+ "debug": "^4.3.1"
285
+ },
286
+ "engines": {
287
+ "node": ">=6.9.0"
288
+ }
289
+ },
290
+ "node_modules/@babel/types": {
291
+ "version": "7.29.0",
292
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
293
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
294
+ "dev": true,
295
+ "license": "MIT",
296
+ "dependencies": {
297
+ "@babel/helper-string-parser": "^7.27.1",
298
+ "@babel/helper-validator-identifier": "^7.28.5"
299
+ },
300
+ "engines": {
301
+ "node": ">=6.9.0"
302
+ }
303
+ },
304
+ "node_modules/@esbuild/aix-ppc64": {
305
+ "version": "0.21.5",
306
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
307
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
308
+ "cpu": [
309
+ "ppc64"
310
+ ],
311
+ "dev": true,
312
+ "license": "MIT",
313
+ "optional": true,
314
+ "os": [
315
+ "aix"
316
+ ],
317
+ "engines": {
318
+ "node": ">=12"
319
+ }
320
+ },
321
+ "node_modules/@esbuild/android-arm": {
322
+ "version": "0.21.5",
323
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
324
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
325
+ "cpu": [
326
+ "arm"
327
+ ],
328
+ "dev": true,
329
+ "license": "MIT",
330
+ "optional": true,
331
+ "os": [
332
+ "android"
333
+ ],
334
+ "engines": {
335
+ "node": ">=12"
336
+ }
337
+ },
338
+ "node_modules/@esbuild/android-arm64": {
339
+ "version": "0.21.5",
340
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
341
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
342
+ "cpu": [
343
+ "arm64"
344
+ ],
345
+ "dev": true,
346
+ "license": "MIT",
347
+ "optional": true,
348
+ "os": [
349
+ "android"
350
+ ],
351
+ "engines": {
352
+ "node": ">=12"
353
+ }
354
+ },
355
+ "node_modules/@esbuild/android-x64": {
356
+ "version": "0.21.5",
357
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
358
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
359
+ "cpu": [
360
+ "x64"
361
+ ],
362
+ "dev": true,
363
+ "license": "MIT",
364
+ "optional": true,
365
+ "os": [
366
+ "android"
367
+ ],
368
+ "engines": {
369
+ "node": ">=12"
370
+ }
371
+ },
372
+ "node_modules/@esbuild/darwin-arm64": {
373
+ "version": "0.21.5",
374
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
375
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
376
+ "cpu": [
377
+ "arm64"
378
+ ],
379
+ "dev": true,
380
+ "license": "MIT",
381
+ "optional": true,
382
+ "os": [
383
+ "darwin"
384
+ ],
385
+ "engines": {
386
+ "node": ">=12"
387
+ }
388
+ },
389
+ "node_modules/@esbuild/darwin-x64": {
390
+ "version": "0.21.5",
391
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
392
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
393
+ "cpu": [
394
+ "x64"
395
+ ],
396
+ "dev": true,
397
+ "license": "MIT",
398
+ "optional": true,
399
+ "os": [
400
+ "darwin"
401
+ ],
402
+ "engines": {
403
+ "node": ">=12"
404
+ }
405
+ },
406
+ "node_modules/@esbuild/freebsd-arm64": {
407
+ "version": "0.21.5",
408
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
409
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
410
+ "cpu": [
411
+ "arm64"
412
+ ],
413
+ "dev": true,
414
+ "license": "MIT",
415
+ "optional": true,
416
+ "os": [
417
+ "freebsd"
418
+ ],
419
+ "engines": {
420
+ "node": ">=12"
421
+ }
422
+ },
423
+ "node_modules/@esbuild/freebsd-x64": {
424
+ "version": "0.21.5",
425
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
426
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
427
+ "cpu": [
428
+ "x64"
429
+ ],
430
+ "dev": true,
431
+ "license": "MIT",
432
+ "optional": true,
433
+ "os": [
434
+ "freebsd"
435
+ ],
436
+ "engines": {
437
+ "node": ">=12"
438
+ }
439
+ },
440
+ "node_modules/@esbuild/linux-arm": {
441
+ "version": "0.21.5",
442
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
443
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
444
+ "cpu": [
445
+ "arm"
446
+ ],
447
+ "dev": true,
448
+ "license": "MIT",
449
+ "optional": true,
450
+ "os": [
451
+ "linux"
452
+ ],
453
+ "engines": {
454
+ "node": ">=12"
455
+ }
456
+ },
457
+ "node_modules/@esbuild/linux-arm64": {
458
+ "version": "0.21.5",
459
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
460
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
461
+ "cpu": [
462
+ "arm64"
463
+ ],
464
+ "dev": true,
465
+ "license": "MIT",
466
+ "optional": true,
467
+ "os": [
468
+ "linux"
469
+ ],
470
+ "engines": {
471
+ "node": ">=12"
472
+ }
473
+ },
474
+ "node_modules/@esbuild/linux-ia32": {
475
+ "version": "0.21.5",
476
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
477
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
478
+ "cpu": [
479
+ "ia32"
480
+ ],
481
+ "dev": true,
482
+ "license": "MIT",
483
+ "optional": true,
484
+ "os": [
485
+ "linux"
486
+ ],
487
+ "engines": {
488
+ "node": ">=12"
489
+ }
490
+ },
491
+ "node_modules/@esbuild/linux-loong64": {
492
+ "version": "0.21.5",
493
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
494
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
495
+ "cpu": [
496
+ "loong64"
497
+ ],
498
+ "dev": true,
499
+ "license": "MIT",
500
+ "optional": true,
501
+ "os": [
502
+ "linux"
503
+ ],
504
+ "engines": {
505
+ "node": ">=12"
506
+ }
507
+ },
508
+ "node_modules/@esbuild/linux-mips64el": {
509
+ "version": "0.21.5",
510
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
511
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
512
+ "cpu": [
513
+ "mips64el"
514
+ ],
515
+ "dev": true,
516
+ "license": "MIT",
517
+ "optional": true,
518
+ "os": [
519
+ "linux"
520
+ ],
521
+ "engines": {
522
+ "node": ">=12"
523
+ }
524
+ },
525
+ "node_modules/@esbuild/linux-ppc64": {
526
+ "version": "0.21.5",
527
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
528
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
529
+ "cpu": [
530
+ "ppc64"
531
+ ],
532
+ "dev": true,
533
+ "license": "MIT",
534
+ "optional": true,
535
+ "os": [
536
+ "linux"
537
+ ],
538
+ "engines": {
539
+ "node": ">=12"
540
+ }
541
+ },
542
+ "node_modules/@esbuild/linux-riscv64": {
543
+ "version": "0.21.5",
544
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
545
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
546
+ "cpu": [
547
+ "riscv64"
548
+ ],
549
+ "dev": true,
550
+ "license": "MIT",
551
+ "optional": true,
552
+ "os": [
553
+ "linux"
554
+ ],
555
+ "engines": {
556
+ "node": ">=12"
557
+ }
558
+ },
559
+ "node_modules/@esbuild/linux-s390x": {
560
+ "version": "0.21.5",
561
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
562
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
563
+ "cpu": [
564
+ "s390x"
565
+ ],
566
+ "dev": true,
567
+ "license": "MIT",
568
+ "optional": true,
569
+ "os": [
570
+ "linux"
571
+ ],
572
+ "engines": {
573
+ "node": ">=12"
574
+ }
575
+ },
576
+ "node_modules/@esbuild/linux-x64": {
577
+ "version": "0.21.5",
578
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
579
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
580
+ "cpu": [
581
+ "x64"
582
+ ],
583
+ "dev": true,
584
+ "license": "MIT",
585
+ "optional": true,
586
+ "os": [
587
+ "linux"
588
+ ],
589
+ "engines": {
590
+ "node": ">=12"
591
+ }
592
+ },
593
+ "node_modules/@esbuild/netbsd-x64": {
594
+ "version": "0.21.5",
595
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
596
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
597
+ "cpu": [
598
+ "x64"
599
+ ],
600
+ "dev": true,
601
+ "license": "MIT",
602
+ "optional": true,
603
+ "os": [
604
+ "netbsd"
605
+ ],
606
+ "engines": {
607
+ "node": ">=12"
608
+ }
609
+ },
610
+ "node_modules/@esbuild/openbsd-x64": {
611
+ "version": "0.21.5",
612
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
613
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
614
+ "cpu": [
615
+ "x64"
616
+ ],
617
+ "dev": true,
618
+ "license": "MIT",
619
+ "optional": true,
620
+ "os": [
621
+ "openbsd"
622
+ ],
623
+ "engines": {
624
+ "node": ">=12"
625
+ }
626
+ },
627
+ "node_modules/@esbuild/sunos-x64": {
628
+ "version": "0.21.5",
629
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
630
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
631
+ "cpu": [
632
+ "x64"
633
+ ],
634
+ "dev": true,
635
+ "license": "MIT",
636
+ "optional": true,
637
+ "os": [
638
+ "sunos"
639
+ ],
640
+ "engines": {
641
+ "node": ">=12"
642
+ }
643
+ },
644
+ "node_modules/@esbuild/win32-arm64": {
645
+ "version": "0.21.5",
646
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
647
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
648
+ "cpu": [
649
+ "arm64"
650
+ ],
651
+ "dev": true,
652
+ "license": "MIT",
653
+ "optional": true,
654
+ "os": [
655
+ "win32"
656
+ ],
657
+ "engines": {
658
+ "node": ">=12"
659
+ }
660
+ },
661
+ "node_modules/@esbuild/win32-ia32": {
662
+ "version": "0.21.5",
663
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
664
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
665
+ "cpu": [
666
+ "ia32"
667
+ ],
668
+ "dev": true,
669
+ "license": "MIT",
670
+ "optional": true,
671
+ "os": [
672
+ "win32"
673
+ ],
674
+ "engines": {
675
+ "node": ">=12"
676
+ }
677
+ },
678
+ "node_modules/@esbuild/win32-x64": {
679
+ "version": "0.21.5",
680
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
681
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
682
+ "cpu": [
683
+ "x64"
684
+ ],
685
+ "dev": true,
686
+ "license": "MIT",
687
+ "optional": true,
688
+ "os": [
689
+ "win32"
690
+ ],
691
+ "engines": {
692
+ "node": ">=12"
693
+ }
694
+ },
695
+ "node_modules/@jridgewell/gen-mapping": {
696
+ "version": "0.3.13",
697
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
698
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "dependencies": {
702
+ "@jridgewell/sourcemap-codec": "^1.5.0",
703
+ "@jridgewell/trace-mapping": "^0.3.24"
704
+ }
705
+ },
706
+ "node_modules/@jridgewell/remapping": {
707
+ "version": "2.3.5",
708
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
709
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
710
+ "dev": true,
711
+ "license": "MIT",
712
+ "dependencies": {
713
+ "@jridgewell/gen-mapping": "^0.3.5",
714
+ "@jridgewell/trace-mapping": "^0.3.24"
715
+ }
716
+ },
717
+ "node_modules/@jridgewell/resolve-uri": {
718
+ "version": "3.1.2",
719
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
720
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
721
+ "dev": true,
722
+ "license": "MIT",
723
+ "engines": {
724
+ "node": ">=6.0.0"
725
+ }
726
+ },
727
+ "node_modules/@jridgewell/sourcemap-codec": {
728
+ "version": "1.5.5",
729
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
730
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
731
+ "dev": true,
732
+ "license": "MIT"
733
+ },
734
+ "node_modules/@jridgewell/trace-mapping": {
735
+ "version": "0.3.31",
736
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
737
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
738
+ "dev": true,
739
+ "license": "MIT",
740
+ "dependencies": {
741
+ "@jridgewell/resolve-uri": "^3.1.0",
742
+ "@jridgewell/sourcemap-codec": "^1.4.14"
743
+ }
744
+ },
745
+ "node_modules/@rolldown/pluginutils": {
746
+ "version": "1.0.0-beta.27",
747
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
748
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
749
+ "dev": true,
750
+ "license": "MIT"
751
+ },
752
+ "node_modules/@rollup/rollup-android-arm-eabi": {
753
+ "version": "4.60.1",
754
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
755
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
756
+ "cpu": [
757
+ "arm"
758
+ ],
759
+ "dev": true,
760
+ "license": "MIT",
761
+ "optional": true,
762
+ "os": [
763
+ "android"
764
+ ]
765
+ },
766
+ "node_modules/@rollup/rollup-android-arm64": {
767
+ "version": "4.60.1",
768
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
769
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
770
+ "cpu": [
771
+ "arm64"
772
+ ],
773
+ "dev": true,
774
+ "license": "MIT",
775
+ "optional": true,
776
+ "os": [
777
+ "android"
778
+ ]
779
+ },
780
+ "node_modules/@rollup/rollup-darwin-arm64": {
781
+ "version": "4.60.1",
782
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
783
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
784
+ "cpu": [
785
+ "arm64"
786
+ ],
787
+ "dev": true,
788
+ "license": "MIT",
789
+ "optional": true,
790
+ "os": [
791
+ "darwin"
792
+ ]
793
+ },
794
+ "node_modules/@rollup/rollup-darwin-x64": {
795
+ "version": "4.60.1",
796
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
797
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
798
+ "cpu": [
799
+ "x64"
800
+ ],
801
+ "dev": true,
802
+ "license": "MIT",
803
+ "optional": true,
804
+ "os": [
805
+ "darwin"
806
+ ]
807
+ },
808
+ "node_modules/@rollup/rollup-freebsd-arm64": {
809
+ "version": "4.60.1",
810
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
811
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
812
+ "cpu": [
813
+ "arm64"
814
+ ],
815
+ "dev": true,
816
+ "license": "MIT",
817
+ "optional": true,
818
+ "os": [
819
+ "freebsd"
820
+ ]
821
+ },
822
+ "node_modules/@rollup/rollup-freebsd-x64": {
823
+ "version": "4.60.1",
824
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
825
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
826
+ "cpu": [
827
+ "x64"
828
+ ],
829
+ "dev": true,
830
+ "license": "MIT",
831
+ "optional": true,
832
+ "os": [
833
+ "freebsd"
834
+ ]
835
+ },
836
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
837
+ "version": "4.60.1",
838
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
839
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
840
+ "cpu": [
841
+ "arm"
842
+ ],
843
+ "dev": true,
844
+ "license": "MIT",
845
+ "optional": true,
846
+ "os": [
847
+ "linux"
848
+ ]
849
+ },
850
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
851
+ "version": "4.60.1",
852
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
853
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
854
+ "cpu": [
855
+ "arm"
856
+ ],
857
+ "dev": true,
858
+ "license": "MIT",
859
+ "optional": true,
860
+ "os": [
861
+ "linux"
862
+ ]
863
+ },
864
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
865
+ "version": "4.60.1",
866
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
867
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
868
+ "cpu": [
869
+ "arm64"
870
+ ],
871
+ "dev": true,
872
+ "license": "MIT",
873
+ "optional": true,
874
+ "os": [
875
+ "linux"
876
+ ]
877
+ },
878
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
879
+ "version": "4.60.1",
880
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
881
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
882
+ "cpu": [
883
+ "arm64"
884
+ ],
885
+ "dev": true,
886
+ "license": "MIT",
887
+ "optional": true,
888
+ "os": [
889
+ "linux"
890
+ ]
891
+ },
892
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
893
+ "version": "4.60.1",
894
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
895
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
896
+ "cpu": [
897
+ "loong64"
898
+ ],
899
+ "dev": true,
900
+ "license": "MIT",
901
+ "optional": true,
902
+ "os": [
903
+ "linux"
904
+ ]
905
+ },
906
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
907
+ "version": "4.60.1",
908
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
909
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
910
+ "cpu": [
911
+ "loong64"
912
+ ],
913
+ "dev": true,
914
+ "license": "MIT",
915
+ "optional": true,
916
+ "os": [
917
+ "linux"
918
+ ]
919
+ },
920
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
921
+ "version": "4.60.1",
922
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
923
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
924
+ "cpu": [
925
+ "ppc64"
926
+ ],
927
+ "dev": true,
928
+ "license": "MIT",
929
+ "optional": true,
930
+ "os": [
931
+ "linux"
932
+ ]
933
+ },
934
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
935
+ "version": "4.60.1",
936
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
937
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
938
+ "cpu": [
939
+ "ppc64"
940
+ ],
941
+ "dev": true,
942
+ "license": "MIT",
943
+ "optional": true,
944
+ "os": [
945
+ "linux"
946
+ ]
947
+ },
948
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
949
+ "version": "4.60.1",
950
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
951
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
952
+ "cpu": [
953
+ "riscv64"
954
+ ],
955
+ "dev": true,
956
+ "license": "MIT",
957
+ "optional": true,
958
+ "os": [
959
+ "linux"
960
+ ]
961
+ },
962
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
963
+ "version": "4.60.1",
964
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
965
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
966
+ "cpu": [
967
+ "riscv64"
968
+ ],
969
+ "dev": true,
970
+ "license": "MIT",
971
+ "optional": true,
972
+ "os": [
973
+ "linux"
974
+ ]
975
+ },
976
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
977
+ "version": "4.60.1",
978
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
979
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
980
+ "cpu": [
981
+ "s390x"
982
+ ],
983
+ "dev": true,
984
+ "license": "MIT",
985
+ "optional": true,
986
+ "os": [
987
+ "linux"
988
+ ]
989
+ },
990
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
991
+ "version": "4.60.1",
992
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
993
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
994
+ "cpu": [
995
+ "x64"
996
+ ],
997
+ "dev": true,
998
+ "license": "MIT",
999
+ "optional": true,
1000
+ "os": [
1001
+ "linux"
1002
+ ]
1003
+ },
1004
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1005
+ "version": "4.60.1",
1006
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
1007
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
1008
+ "cpu": [
1009
+ "x64"
1010
+ ],
1011
+ "dev": true,
1012
+ "license": "MIT",
1013
+ "optional": true,
1014
+ "os": [
1015
+ "linux"
1016
+ ]
1017
+ },
1018
+ "node_modules/@rollup/rollup-openbsd-x64": {
1019
+ "version": "4.60.1",
1020
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
1021
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
1022
+ "cpu": [
1023
+ "x64"
1024
+ ],
1025
+ "dev": true,
1026
+ "license": "MIT",
1027
+ "optional": true,
1028
+ "os": [
1029
+ "openbsd"
1030
+ ]
1031
+ },
1032
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1033
+ "version": "4.60.1",
1034
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
1035
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
1036
+ "cpu": [
1037
+ "arm64"
1038
+ ],
1039
+ "dev": true,
1040
+ "license": "MIT",
1041
+ "optional": true,
1042
+ "os": [
1043
+ "openharmony"
1044
+ ]
1045
+ },
1046
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1047
+ "version": "4.60.1",
1048
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
1049
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
1050
+ "cpu": [
1051
+ "arm64"
1052
+ ],
1053
+ "dev": true,
1054
+ "license": "MIT",
1055
+ "optional": true,
1056
+ "os": [
1057
+ "win32"
1058
+ ]
1059
+ },
1060
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1061
+ "version": "4.60.1",
1062
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
1063
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
1064
+ "cpu": [
1065
+ "ia32"
1066
+ ],
1067
+ "dev": true,
1068
+ "license": "MIT",
1069
+ "optional": true,
1070
+ "os": [
1071
+ "win32"
1072
+ ]
1073
+ },
1074
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1075
+ "version": "4.60.1",
1076
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
1077
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
1078
+ "cpu": [
1079
+ "x64"
1080
+ ],
1081
+ "dev": true,
1082
+ "license": "MIT",
1083
+ "optional": true,
1084
+ "os": [
1085
+ "win32"
1086
+ ]
1087
+ },
1088
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1089
+ "version": "4.60.1",
1090
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
1091
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
1092
+ "cpu": [
1093
+ "x64"
1094
+ ],
1095
+ "dev": true,
1096
+ "license": "MIT",
1097
+ "optional": true,
1098
+ "os": [
1099
+ "win32"
1100
+ ]
1101
+ },
1102
+ "node_modules/@types/babel__core": {
1103
+ "version": "7.20.5",
1104
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1105
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1106
+ "dev": true,
1107
+ "license": "MIT",
1108
+ "dependencies": {
1109
+ "@babel/parser": "^7.20.7",
1110
+ "@babel/types": "^7.20.7",
1111
+ "@types/babel__generator": "*",
1112
+ "@types/babel__template": "*",
1113
+ "@types/babel__traverse": "*"
1114
+ }
1115
+ },
1116
+ "node_modules/@types/babel__generator": {
1117
+ "version": "7.27.0",
1118
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1119
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1120
+ "dev": true,
1121
+ "license": "MIT",
1122
+ "dependencies": {
1123
+ "@babel/types": "^7.0.0"
1124
+ }
1125
+ },
1126
+ "node_modules/@types/babel__template": {
1127
+ "version": "7.4.4",
1128
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1129
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1130
+ "dev": true,
1131
+ "license": "MIT",
1132
+ "dependencies": {
1133
+ "@babel/parser": "^7.1.0",
1134
+ "@babel/types": "^7.0.0"
1135
+ }
1136
+ },
1137
+ "node_modules/@types/babel__traverse": {
1138
+ "version": "7.28.0",
1139
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1140
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1141
+ "dev": true,
1142
+ "license": "MIT",
1143
+ "dependencies": {
1144
+ "@babel/types": "^7.28.2"
1145
+ }
1146
+ },
1147
+ "node_modules/@types/estree": {
1148
+ "version": "1.0.8",
1149
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1150
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1151
+ "dev": true,
1152
+ "license": "MIT"
1153
+ },
1154
+ "node_modules/@types/prop-types": {
1155
+ "version": "15.7.15",
1156
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1157
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1158
+ "dev": true,
1159
+ "license": "MIT"
1160
+ },
1161
+ "node_modules/@types/react": {
1162
+ "version": "18.3.28",
1163
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
1164
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
1165
+ "dev": true,
1166
+ "license": "MIT",
1167
+ "dependencies": {
1168
+ "@types/prop-types": "*",
1169
+ "csstype": "^3.2.2"
1170
+ }
1171
+ },
1172
+ "node_modules/@types/react-dom": {
1173
+ "version": "18.3.7",
1174
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1175
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1176
+ "dev": true,
1177
+ "license": "MIT",
1178
+ "peerDependencies": {
1179
+ "@types/react": "^18.0.0"
1180
+ }
1181
+ },
1182
+ "node_modules/@vitejs/plugin-react": {
1183
+ "version": "4.7.0",
1184
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1185
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1186
+ "dev": true,
1187
+ "license": "MIT",
1188
+ "dependencies": {
1189
+ "@babel/core": "^7.28.0",
1190
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1191
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1192
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1193
+ "@types/babel__core": "^7.20.5",
1194
+ "react-refresh": "^0.17.0"
1195
+ },
1196
+ "engines": {
1197
+ "node": "^14.18.0 || >=16.0.0"
1198
+ },
1199
+ "peerDependencies": {
1200
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1201
+ }
1202
+ },
1203
+ "node_modules/baseline-browser-mapping": {
1204
+ "version": "2.10.12",
1205
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz",
1206
+ "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==",
1207
+ "dev": true,
1208
+ "license": "Apache-2.0",
1209
+ "bin": {
1210
+ "baseline-browser-mapping": "dist/cli.cjs"
1211
+ },
1212
+ "engines": {
1213
+ "node": ">=6.0.0"
1214
+ }
1215
+ },
1216
+ "node_modules/browserslist": {
1217
+ "version": "4.28.1",
1218
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
1219
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
1220
+ "dev": true,
1221
+ "funding": [
1222
+ {
1223
+ "type": "opencollective",
1224
+ "url": "https://opencollective.com/browserslist"
1225
+ },
1226
+ {
1227
+ "type": "tidelift",
1228
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1229
+ },
1230
+ {
1231
+ "type": "github",
1232
+ "url": "https://github.com/sponsors/ai"
1233
+ }
1234
+ ],
1235
+ "license": "MIT",
1236
+ "dependencies": {
1237
+ "baseline-browser-mapping": "^2.9.0",
1238
+ "caniuse-lite": "^1.0.30001759",
1239
+ "electron-to-chromium": "^1.5.263",
1240
+ "node-releases": "^2.0.27",
1241
+ "update-browserslist-db": "^1.2.0"
1242
+ },
1243
+ "bin": {
1244
+ "browserslist": "cli.js"
1245
+ },
1246
+ "engines": {
1247
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1248
+ }
1249
+ },
1250
+ "node_modules/caniuse-lite": {
1251
+ "version": "1.0.30001782",
1252
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz",
1253
+ "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==",
1254
+ "dev": true,
1255
+ "funding": [
1256
+ {
1257
+ "type": "opencollective",
1258
+ "url": "https://opencollective.com/browserslist"
1259
+ },
1260
+ {
1261
+ "type": "tidelift",
1262
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1263
+ },
1264
+ {
1265
+ "type": "github",
1266
+ "url": "https://github.com/sponsors/ai"
1267
+ }
1268
+ ],
1269
+ "license": "CC-BY-4.0"
1270
+ },
1271
+ "node_modules/convert-source-map": {
1272
+ "version": "2.0.0",
1273
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1274
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1275
+ "dev": true,
1276
+ "license": "MIT"
1277
+ },
1278
+ "node_modules/csstype": {
1279
+ "version": "3.2.3",
1280
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1281
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1282
+ "dev": true,
1283
+ "license": "MIT"
1284
+ },
1285
+ "node_modules/debug": {
1286
+ "version": "4.4.3",
1287
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1288
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1289
+ "dev": true,
1290
+ "license": "MIT",
1291
+ "dependencies": {
1292
+ "ms": "^2.1.3"
1293
+ },
1294
+ "engines": {
1295
+ "node": ">=6.0"
1296
+ },
1297
+ "peerDependenciesMeta": {
1298
+ "supports-color": {
1299
+ "optional": true
1300
+ }
1301
+ }
1302
+ },
1303
+ "node_modules/electron-to-chromium": {
1304
+ "version": "1.5.328",
1305
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz",
1306
+ "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==",
1307
+ "dev": true,
1308
+ "license": "ISC"
1309
+ },
1310
+ "node_modules/esbuild": {
1311
+ "version": "0.21.5",
1312
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1313
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1314
+ "dev": true,
1315
+ "hasInstallScript": true,
1316
+ "license": "MIT",
1317
+ "bin": {
1318
+ "esbuild": "bin/esbuild"
1319
+ },
1320
+ "engines": {
1321
+ "node": ">=12"
1322
+ },
1323
+ "optionalDependencies": {
1324
+ "@esbuild/aix-ppc64": "0.21.5",
1325
+ "@esbuild/android-arm": "0.21.5",
1326
+ "@esbuild/android-arm64": "0.21.5",
1327
+ "@esbuild/android-x64": "0.21.5",
1328
+ "@esbuild/darwin-arm64": "0.21.5",
1329
+ "@esbuild/darwin-x64": "0.21.5",
1330
+ "@esbuild/freebsd-arm64": "0.21.5",
1331
+ "@esbuild/freebsd-x64": "0.21.5",
1332
+ "@esbuild/linux-arm": "0.21.5",
1333
+ "@esbuild/linux-arm64": "0.21.5",
1334
+ "@esbuild/linux-ia32": "0.21.5",
1335
+ "@esbuild/linux-loong64": "0.21.5",
1336
+ "@esbuild/linux-mips64el": "0.21.5",
1337
+ "@esbuild/linux-ppc64": "0.21.5",
1338
+ "@esbuild/linux-riscv64": "0.21.5",
1339
+ "@esbuild/linux-s390x": "0.21.5",
1340
+ "@esbuild/linux-x64": "0.21.5",
1341
+ "@esbuild/netbsd-x64": "0.21.5",
1342
+ "@esbuild/openbsd-x64": "0.21.5",
1343
+ "@esbuild/sunos-x64": "0.21.5",
1344
+ "@esbuild/win32-arm64": "0.21.5",
1345
+ "@esbuild/win32-ia32": "0.21.5",
1346
+ "@esbuild/win32-x64": "0.21.5"
1347
+ }
1348
+ },
1349
+ "node_modules/escalade": {
1350
+ "version": "3.2.0",
1351
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1352
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1353
+ "dev": true,
1354
+ "license": "MIT",
1355
+ "engines": {
1356
+ "node": ">=6"
1357
+ }
1358
+ },
1359
+ "node_modules/fsevents": {
1360
+ "version": "2.3.3",
1361
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1362
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1363
+ "dev": true,
1364
+ "hasInstallScript": true,
1365
+ "license": "MIT",
1366
+ "optional": true,
1367
+ "os": [
1368
+ "darwin"
1369
+ ],
1370
+ "engines": {
1371
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1372
+ }
1373
+ },
1374
+ "node_modules/gensync": {
1375
+ "version": "1.0.0-beta.2",
1376
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1377
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1378
+ "dev": true,
1379
+ "license": "MIT",
1380
+ "engines": {
1381
+ "node": ">=6.9.0"
1382
+ }
1383
+ },
1384
+ "node_modules/js-tokens": {
1385
+ "version": "4.0.0",
1386
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1387
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1388
+ "license": "MIT"
1389
+ },
1390
+ "node_modules/jsesc": {
1391
+ "version": "3.1.0",
1392
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1393
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1394
+ "dev": true,
1395
+ "license": "MIT",
1396
+ "bin": {
1397
+ "jsesc": "bin/jsesc"
1398
+ },
1399
+ "engines": {
1400
+ "node": ">=6"
1401
+ }
1402
+ },
1403
+ "node_modules/json5": {
1404
+ "version": "2.2.3",
1405
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1406
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1407
+ "dev": true,
1408
+ "license": "MIT",
1409
+ "bin": {
1410
+ "json5": "lib/cli.js"
1411
+ },
1412
+ "engines": {
1413
+ "node": ">=6"
1414
+ }
1415
+ },
1416
+ "node_modules/loose-envify": {
1417
+ "version": "1.4.0",
1418
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1419
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1420
+ "license": "MIT",
1421
+ "dependencies": {
1422
+ "js-tokens": "^3.0.0 || ^4.0.0"
1423
+ },
1424
+ "bin": {
1425
+ "loose-envify": "cli.js"
1426
+ }
1427
+ },
1428
+ "node_modules/lru-cache": {
1429
+ "version": "5.1.1",
1430
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1431
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1432
+ "dev": true,
1433
+ "license": "ISC",
1434
+ "dependencies": {
1435
+ "yallist": "^3.0.2"
1436
+ }
1437
+ },
1438
+ "node_modules/ms": {
1439
+ "version": "2.1.3",
1440
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1441
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1442
+ "dev": true,
1443
+ "license": "MIT"
1444
+ },
1445
+ "node_modules/nanoid": {
1446
+ "version": "3.3.11",
1447
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1448
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1449
+ "dev": true,
1450
+ "funding": [
1451
+ {
1452
+ "type": "github",
1453
+ "url": "https://github.com/sponsors/ai"
1454
+ }
1455
+ ],
1456
+ "license": "MIT",
1457
+ "bin": {
1458
+ "nanoid": "bin/nanoid.cjs"
1459
+ },
1460
+ "engines": {
1461
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1462
+ }
1463
+ },
1464
+ "node_modules/node-releases": {
1465
+ "version": "2.0.36",
1466
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
1467
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
1468
+ "dev": true,
1469
+ "license": "MIT"
1470
+ },
1471
+ "node_modules/picocolors": {
1472
+ "version": "1.1.1",
1473
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1474
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1475
+ "dev": true,
1476
+ "license": "ISC"
1477
+ },
1478
+ "node_modules/postcss": {
1479
+ "version": "8.5.8",
1480
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
1481
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
1482
+ "dev": true,
1483
+ "funding": [
1484
+ {
1485
+ "type": "opencollective",
1486
+ "url": "https://opencollective.com/postcss/"
1487
+ },
1488
+ {
1489
+ "type": "tidelift",
1490
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1491
+ },
1492
+ {
1493
+ "type": "github",
1494
+ "url": "https://github.com/sponsors/ai"
1495
+ }
1496
+ ],
1497
+ "license": "MIT",
1498
+ "dependencies": {
1499
+ "nanoid": "^3.3.11",
1500
+ "picocolors": "^1.1.1",
1501
+ "source-map-js": "^1.2.1"
1502
+ },
1503
+ "engines": {
1504
+ "node": "^10 || ^12 || >=14"
1505
+ }
1506
+ },
1507
+ "node_modules/react": {
1508
+ "version": "18.3.1",
1509
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1510
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1511
+ "license": "MIT",
1512
+ "dependencies": {
1513
+ "loose-envify": "^1.1.0"
1514
+ },
1515
+ "engines": {
1516
+ "node": ">=0.10.0"
1517
+ }
1518
+ },
1519
+ "node_modules/react-dom": {
1520
+ "version": "18.3.1",
1521
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1522
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1523
+ "license": "MIT",
1524
+ "dependencies": {
1525
+ "loose-envify": "^1.1.0",
1526
+ "scheduler": "^0.23.2"
1527
+ },
1528
+ "peerDependencies": {
1529
+ "react": "^18.3.1"
1530
+ }
1531
+ },
1532
+ "node_modules/react-refresh": {
1533
+ "version": "0.17.0",
1534
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1535
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1536
+ "dev": true,
1537
+ "license": "MIT",
1538
+ "engines": {
1539
+ "node": ">=0.10.0"
1540
+ }
1541
+ },
1542
+ "node_modules/rollup": {
1543
+ "version": "4.60.1",
1544
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
1545
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
1546
+ "dev": true,
1547
+ "license": "MIT",
1548
+ "dependencies": {
1549
+ "@types/estree": "1.0.8"
1550
+ },
1551
+ "bin": {
1552
+ "rollup": "dist/bin/rollup"
1553
+ },
1554
+ "engines": {
1555
+ "node": ">=18.0.0",
1556
+ "npm": ">=8.0.0"
1557
+ },
1558
+ "optionalDependencies": {
1559
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
1560
+ "@rollup/rollup-android-arm64": "4.60.1",
1561
+ "@rollup/rollup-darwin-arm64": "4.60.1",
1562
+ "@rollup/rollup-darwin-x64": "4.60.1",
1563
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
1564
+ "@rollup/rollup-freebsd-x64": "4.60.1",
1565
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
1566
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
1567
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
1568
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
1569
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
1570
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
1571
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
1572
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
1573
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
1574
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
1575
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
1576
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
1577
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
1578
+ "@rollup/rollup-openbsd-x64": "4.60.1",
1579
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
1580
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
1581
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
1582
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
1583
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
1584
+ "fsevents": "~2.3.2"
1585
+ }
1586
+ },
1587
+ "node_modules/scheduler": {
1588
+ "version": "0.23.2",
1589
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1590
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1591
+ "license": "MIT",
1592
+ "dependencies": {
1593
+ "loose-envify": "^1.1.0"
1594
+ }
1595
+ },
1596
+ "node_modules/semver": {
1597
+ "version": "6.3.1",
1598
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1599
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1600
+ "dev": true,
1601
+ "license": "ISC",
1602
+ "bin": {
1603
+ "semver": "bin/semver.js"
1604
+ }
1605
+ },
1606
+ "node_modules/source-map-js": {
1607
+ "version": "1.2.1",
1608
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1609
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1610
+ "dev": true,
1611
+ "license": "BSD-3-Clause",
1612
+ "engines": {
1613
+ "node": ">=0.10.0"
1614
+ }
1615
+ },
1616
+ "node_modules/typescript": {
1617
+ "version": "5.9.3",
1618
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1619
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1620
+ "dev": true,
1621
+ "license": "Apache-2.0",
1622
+ "bin": {
1623
+ "tsc": "bin/tsc",
1624
+ "tsserver": "bin/tsserver"
1625
+ },
1626
+ "engines": {
1627
+ "node": ">=14.17"
1628
+ }
1629
+ },
1630
+ "node_modules/update-browserslist-db": {
1631
+ "version": "1.2.3",
1632
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1633
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1634
+ "dev": true,
1635
+ "funding": [
1636
+ {
1637
+ "type": "opencollective",
1638
+ "url": "https://opencollective.com/browserslist"
1639
+ },
1640
+ {
1641
+ "type": "tidelift",
1642
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1643
+ },
1644
+ {
1645
+ "type": "github",
1646
+ "url": "https://github.com/sponsors/ai"
1647
+ }
1648
+ ],
1649
+ "license": "MIT",
1650
+ "dependencies": {
1651
+ "escalade": "^3.2.0",
1652
+ "picocolors": "^1.1.1"
1653
+ },
1654
+ "bin": {
1655
+ "update-browserslist-db": "cli.js"
1656
+ },
1657
+ "peerDependencies": {
1658
+ "browserslist": ">= 4.21.0"
1659
+ }
1660
+ },
1661
+ "node_modules/vite": {
1662
+ "version": "5.4.21",
1663
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1664
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1665
+ "dev": true,
1666
+ "license": "MIT",
1667
+ "dependencies": {
1668
+ "esbuild": "^0.21.3",
1669
+ "postcss": "^8.4.43",
1670
+ "rollup": "^4.20.0"
1671
+ },
1672
+ "bin": {
1673
+ "vite": "bin/vite.js"
1674
+ },
1675
+ "engines": {
1676
+ "node": "^18.0.0 || >=20.0.0"
1677
+ },
1678
+ "funding": {
1679
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1680
+ },
1681
+ "optionalDependencies": {
1682
+ "fsevents": "~2.3.3"
1683
+ },
1684
+ "peerDependencies": {
1685
+ "@types/node": "^18.0.0 || >=20.0.0",
1686
+ "less": "*",
1687
+ "lightningcss": "^1.21.0",
1688
+ "sass": "*",
1689
+ "sass-embedded": "*",
1690
+ "stylus": "*",
1691
+ "sugarss": "*",
1692
+ "terser": "^5.4.0"
1693
+ },
1694
+ "peerDependenciesMeta": {
1695
+ "@types/node": {
1696
+ "optional": true
1697
+ },
1698
+ "less": {
1699
+ "optional": true
1700
+ },
1701
+ "lightningcss": {
1702
+ "optional": true
1703
+ },
1704
+ "sass": {
1705
+ "optional": true
1706
+ },
1707
+ "sass-embedded": {
1708
+ "optional": true
1709
+ },
1710
+ "stylus": {
1711
+ "optional": true
1712
+ },
1713
+ "sugarss": {
1714
+ "optional": true
1715
+ },
1716
+ "terser": {
1717
+ "optional": true
1718
+ }
1719
+ }
1720
+ },
1721
+ "node_modules/yallist": {
1722
+ "version": "3.1.1",
1723
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1724
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1725
+ "dev": true,
1726
+ "license": "ISC"
1727
+ }
1728
+ }
1729
+ }
frontend/package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "cadastro-imobiliario-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^18.3.12",
17
+ "@types/react-dom": "^18.3.1",
18
+ "@vitejs/plugin-react": "^4.3.4",
19
+ "typescript": "^5.6.3",
20
+ "vite": "^5.4.10"
21
+ }
22
+ }
23
+
frontend/src/App.tsx ADDED
@@ -0,0 +1,1172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FormEvent, useEffect, useState } from "react";
2
+ import {
3
+ createProperty,
4
+ deleteProperty,
5
+ fetchProperties,
6
+ getExportUrl,
7
+ searchCadastroBase
8
+ } from "./api";
9
+ import type { CadastroBaseRecord, Property, PropertyDraft } from "./types";
10
+ import logoCdA from "./assets/cdA_logo_transparent.png";
11
+
12
+ const CONSERVACAO_OPTIONS = [
13
+ {
14
+ categoria: "Otimo",
15
+ classes: ["A", "B"],
16
+ explicacao: [
17
+ "Imovel novo ou quase novo",
18
+ "Sem danos aparentes",
19
+ "Nao precisa de reparos",
20
+ "Tudo funcionando normalmente",
21
+ "Pode exigir no maximo pintura leve"
22
+ ]
23
+ },
24
+ {
25
+ categoria: "Bom",
26
+ classes: ["C", "D"],
27
+ explicacao: [
28
+ "Imovel bem conservado",
29
+ "Apresenta sinais normais de uso",
30
+ "Pode precisar de pequenos reparos pontuais",
31
+ "Pode ter leves fissuras ou desgaste superficial",
32
+ "Nao compromete o uso do imovel"
33
+ ]
34
+ },
35
+ {
36
+ categoria: "Regular",
37
+ classes: ["E"],
38
+ explicacao: [
39
+ "Precisa de manutencao simples mais ampla",
40
+ "Pode necessitar pintura interna e externa",
41
+ "Pode ter fissuras superficiais mais visiveis",
42
+ "Pode exigir revisao hidraulica ou eletrica",
43
+ "Continua funcional para uso"
44
+ ]
45
+ },
46
+ {
47
+ categoria: "Ruim",
48
+ classes: ["F", "G", "H"],
49
+ explicacao: [
50
+ "Precisa de reparos importantes ou generalizados",
51
+ "Pode comprometer estetica, funcionalidade ou seguranca",
52
+ "Pode exigir troca de revestimentos, telhado ou instalacoes",
53
+ "Pode envolver problemas estruturais",
54
+ "Nos casos mais graves, requer reforma pesada ou quase total"
55
+ ]
56
+ }
57
+ ] as const;
58
+
59
+ const CLASSIFICATION_GUIDES = {
60
+ infra: {
61
+ titulo: "Infra",
62
+ descricao: "Espaco reservado para os criterios de infraestrutura.",
63
+ status: "Em breve",
64
+ blocos: [
65
+ {
66
+ titulo: "Sem guia ainda",
67
+ itens: [
68
+ "As explicacoes de classificacao de Infra serao adicionadas aqui futuramente.",
69
+ "O campo continua disponivel para selecao manual."
70
+ ]
71
+ }
72
+ ]
73
+ },
74
+ padrao: {
75
+ titulo: "Padrao",
76
+ descricao: "Espaco reservado para os criterios de padrao construtivo.",
77
+ status: "Em breve",
78
+ blocos: [
79
+ {
80
+ titulo: "Sem guia ainda",
81
+ itens: [
82
+ "As explicacoes de classificacao de Padrao serao adicionadas aqui futuramente.",
83
+ "O campo continua disponivel para selecao manual."
84
+ ]
85
+ }
86
+ ]
87
+ },
88
+ conservacao: {
89
+ titulo: "Conservacao",
90
+ descricao: "Leia os textos e escolha a categoria mais adequada ao estado atual do imovel.",
91
+ status: "Disponivel",
92
+ blocos: CONSERVACAO_OPTIONS.map((option) => ({
93
+ titulo: `${option.categoria} | Classes ${option.classes.join(", ")}`,
94
+ valor: option.categoria,
95
+ itens: option.explicacao
96
+ }))
97
+ },
98
+ vaga: {
99
+ titulo: "Vaga",
100
+ descricao: "Considere a existencia de vaga e a facilidade de estacionamento no entorno.",
101
+ status: "Disponivel",
102
+ blocos: [
103
+ {
104
+ titulo: "Sim",
105
+ valor: "Sim",
106
+ itens: [
107
+ "O imovel possui vaga propria ou vaga disponivel no empreendimento."
108
+ ]
109
+ },
110
+ {
111
+ titulo: "Nao | estacionamento facilitado",
112
+ valor: "Nao - facilidade de estacionamento",
113
+ itens: [
114
+ "O imovel nao possui vaga.",
115
+ "Mesmo assim, a regiao oferece facilidade para estacionar."
116
+ ]
117
+ },
118
+ {
119
+ titulo: "Nao | estacionamento dificil",
120
+ valor: "Nao - dificuldade de estacionamento",
121
+ itens: [
122
+ "O imovel nao possui vaga.",
123
+ "Ha dificuldade de encontrar estacionamento na regiao."
124
+ ]
125
+ }
126
+ ]
127
+ }
128
+ } as const;
129
+
130
+ const initialForm: PropertyDraft = {
131
+ titulo: null,
132
+ finalidade: "RESIDENCIAL",
133
+ num_bloco: null,
134
+ num_inscricao: null,
135
+ cod_endloc_logradouro: null,
136
+ nme_endloc_logradouro: null,
137
+ num_endloc_endereco: null,
138
+ num_endloc_unidade: null,
139
+ nme_endloc_bairro_cdl: null,
140
+ rh_nome: null,
141
+ rh_valor: null,
142
+ coord_x: null,
143
+ coord_y: null,
144
+ ano_exercicio: null,
145
+ num_versao: null,
146
+ idf_reg_regiao_homogenea: null,
147
+ area_total_detalhe: null,
148
+ area_total: null,
149
+ area_privativa_detalhe: null,
150
+ area_privativa: null,
151
+ finalidade_oferta: null,
152
+ area_total_oferta: null,
153
+ area_privativa_oferta: null,
154
+ valor_oferta: null,
155
+ latitude: null,
156
+ longitude: null,
157
+ descricao_oferta: "",
158
+ observacao: "",
159
+ url: "",
160
+ imobiliaria: "",
161
+ codigo: "",
162
+ infra: "",
163
+ padrao: "",
164
+ conservacao: "",
165
+ vaga: "",
166
+ origem: "manual",
167
+ };
168
+
169
+ function joinUnique(values: Array<string | null | undefined>): string | null {
170
+ const uniqueValues = Array.from(
171
+ new Set(values.map((value) => value?.trim()).filter((value): value is string => Boolean(value)))
172
+ );
173
+ return uniqueValues.length > 0 ? uniqueValues.join(" | ") : null;
174
+ }
175
+
176
+ function sumValues(values: Array<number | null | undefined>): number | null {
177
+ const validValues = values.filter((value): value is number => typeof value === "number");
178
+ return validValues.length > 0
179
+ ? Number(validValues.reduce((total, value) => total + value, 0).toFixed(2))
180
+ : null;
181
+ }
182
+
183
+ function joinNumbers(values: Array<number | null | undefined>): string | null {
184
+ const formattedValues = values
185
+ .filter((value): value is number => typeof value === "number")
186
+ .map((value) => String(value));
187
+ return formattedValues.length > 0 ? formattedValues.join(" | ") : null;
188
+ }
189
+
190
+ function firstValue<T>(values: Array<T | null | undefined>): T | null {
191
+ const found = values.find((value): value is T => value !== null && value !== undefined);
192
+ return found ?? null;
193
+ }
194
+
195
+ function buildCadastroFields(records: CadastroBaseRecord[]): Pick<
196
+ PropertyDraft,
197
+ | "finalidade"
198
+ | "num_bloco"
199
+ | "num_inscricao"
200
+ | "cod_endloc_logradouro"
201
+ | "nme_endloc_logradouro"
202
+ | "num_endloc_endereco"
203
+ | "num_endloc_unidade"
204
+ | "nme_endloc_bairro_cdl"
205
+ | "rh_nome"
206
+ | "rh_valor"
207
+ | "coord_x"
208
+ | "coord_y"
209
+ | "ano_exercicio"
210
+ | "num_versao"
211
+ | "idf_reg_regiao_homogenea"
212
+ | "area_total_detalhe"
213
+ | "area_total"
214
+ | "area_privativa_detalhe"
215
+ | "area_privativa"
216
+ | "latitude"
217
+ | "longitude"
218
+ | "origem"
219
+ > {
220
+ return {
221
+ finalidade: joinUnique(records.map((record) => record.des_finalidade)) ?? "RESIDENCIAL",
222
+ num_bloco: joinUnique(records.map((record) => record.num_bloco)),
223
+ num_inscricao: joinUnique(records.map((record) => record.num_inscricao)),
224
+ cod_endloc_logradouro: joinUnique(records.map((record) => record.cod_endloc_logradouro)),
225
+ nme_endloc_logradouro: joinUnique(records.map((record) => record.nme_endloc_logradouro)),
226
+ num_endloc_endereco: joinUnique(records.map((record) => record.num_endloc_endereco)),
227
+ num_endloc_unidade: joinUnique(records.map((record) => record.num_endloc_unidade)),
228
+ nme_endloc_bairro_cdl: joinUnique(records.map((record) => record.nme_endloc_bairro_cdl)),
229
+ rh_nome: firstValue(records.map((record) => record.rh_nome)),
230
+ rh_valor: firstValue(records.map((record) => record.rh_valor)),
231
+ coord_x: firstValue(records.map((record) => record.coord_x)),
232
+ coord_y: firstValue(records.map((record) => record.coord_y)),
233
+ ano_exercicio: firstValue(records.map((record) => record.ano_exercicio)),
234
+ num_versao: firstValue(records.map((record) => record.num_versao)),
235
+ idf_reg_regiao_homogenea: firstValue(records.map((record) => record.idf_reg_regiao_homogenea)),
236
+ area_total_detalhe: joinNumbers(records.map((record) => record.area_territorial)),
237
+ area_total: sumValues(records.map((record) => record.area_territorial)),
238
+ area_privativa_detalhe: joinNumbers(records.map((record) => record.area_construida)),
239
+ area_privativa: sumValues(records.map((record) => record.area_construida)),
240
+ latitude: firstValue(records.map((record) => record.latitude)),
241
+ longitude: firstValue(records.map((record) => record.longitude)),
242
+ origem: "cadastro_base"
243
+ };
244
+ }
245
+
246
+ function App() {
247
+ const [properties, setProperties] = useState<Property[]>([]);
248
+ const [form, setForm] = useState<PropertyDraft>(initialForm);
249
+ const [cadastroRecords, setCadastroRecords] = useState<CadastroBaseRecord[]>([]);
250
+ const [searchMode, setSearchMode] = useState<"inscricao" | "endereco">("inscricao");
251
+ const [searchQuery, setSearchQuery] = useState("");
252
+ const [searchResults, setSearchResults] = useState<CadastroBaseRecord[]>([]);
253
+ const [searching, setSearching] = useState(false);
254
+ const [loading, setLoading] = useState(false);
255
+ const [message, setMessage] = useState<string>("");
256
+ const [error, setError] = useState<string>("");
257
+ const [deleteTarget, setDeleteTarget] = useState<number | null>(null);
258
+ const [activeClassifier, setActiveClassifier] =
259
+ useState<keyof typeof CLASSIFICATION_GUIDES>("conservacao");
260
+ const [openSections, setOpenSections] = useState({
261
+ busca: true,
262
+ cadastro: true,
263
+ oferta: true,
264
+ outras: true,
265
+ tabela: true
266
+ });
267
+
268
+ async function loadProperties() {
269
+ setLoading(true);
270
+ setError("");
271
+ try {
272
+ const data = await fetchProperties();
273
+ setProperties(data);
274
+ } catch (err) {
275
+ setError(err instanceof Error ? err.message : "Erro ao carregar dados.");
276
+ } finally {
277
+ setLoading(false);
278
+ }
279
+ }
280
+
281
+ useEffect(() => {
282
+ void loadProperties();
283
+ }, []);
284
+
285
+ function updateField<K extends keyof PropertyDraft>(field: K, value: PropertyDraft[K]) {
286
+ setForm((current) => ({ ...current, [field]: value }));
287
+ }
288
+
289
+ function applyCadastroBase(record: CadastroBaseRecord) {
290
+ setCadastroRecords((current) => {
291
+ const alreadyAdded = current.some((item) => item.num_inscricao === record.num_inscricao);
292
+ const nextRecords = alreadyAdded ? current : [...current, record];
293
+ setForm((formState) => ({
294
+ ...formState,
295
+ ...buildCadastroFields(nextRecords)
296
+ }));
297
+ return nextRecords;
298
+ });
299
+ setMessage("Imovel adicionado ao cadastro da oferta. Os campos continuam editaveis.");
300
+ setError("");
301
+ }
302
+
303
+ function removeCadastroRecord(numInscricao: string) {
304
+ setCadastroRecords((current) => {
305
+ const nextRecords = current.filter((record) => record.num_inscricao !== numInscricao);
306
+ setForm((formState) =>
307
+ nextRecords.length > 0
308
+ ? {
309
+ ...formState,
310
+ ...buildCadastroFields(nextRecords)
311
+ }
312
+ : {
313
+ ...formState,
314
+ finalidade: initialForm.finalidade,
315
+ num_bloco: null,
316
+ num_inscricao: null,
317
+ cod_endloc_logradouro: null,
318
+ nme_endloc_logradouro: null,
319
+ num_endloc_endereco: null,
320
+ num_endloc_unidade: null,
321
+ nme_endloc_bairro_cdl: null,
322
+ rh_nome: null,
323
+ rh_valor: null,
324
+ coord_x: null,
325
+ coord_y: null,
326
+ ano_exercicio: null,
327
+ num_versao: null,
328
+ idf_reg_regiao_homogenea: null,
329
+ area_total_detalhe: null,
330
+ area_total: null,
331
+ area_privativa_detalhe: null,
332
+ area_privativa: null,
333
+ latitude: null,
334
+ longitude: null,
335
+ origem: "manual"
336
+ }
337
+ );
338
+ return nextRecords;
339
+ });
340
+ }
341
+
342
+ async function handleSubmit(event: FormEvent) {
343
+ event.preventDefault();
344
+ setError("");
345
+ setMessage("");
346
+
347
+ try {
348
+ const created = await createProperty(form);
349
+ setProperties((current) => [created, ...current]);
350
+ setForm(initialForm);
351
+ setCadastroRecords([]);
352
+ setMessage("Imovel cadastrado com sucesso.");
353
+ } catch (err) {
354
+ setError(err instanceof Error ? err.message : "Erro ao salvar cadastro.");
355
+ }
356
+ }
357
+
358
+ async function handleCadastroSearch() {
359
+ setSearching(true);
360
+ setError("");
361
+ setMessage("");
362
+
363
+ try {
364
+ const result = await searchCadastroBase(searchMode, searchQuery);
365
+ setSearchResults(result.items);
366
+ if (result.items.length === 0) {
367
+ setMessage("Nenhum registro encontrado na base cadastral.");
368
+ }
369
+ } catch (err) {
370
+ setError(err instanceof Error ? err.message : "Erro ao consultar base cadastral.");
371
+ } finally {
372
+ setSearching(false);
373
+ }
374
+ }
375
+
376
+ function handleExport() {
377
+ window.open(getExportUrl(), "_blank", "noopener,noreferrer");
378
+ }
379
+
380
+ async function handleDelete(propertyId: number) {
381
+ setError("");
382
+ setMessage("");
383
+ try {
384
+ await deleteProperty(propertyId);
385
+ setProperties((current) => current.filter((item) => item.id !== propertyId));
386
+ setDeleteTarget(null);
387
+ setMessage("Registro excluido com sucesso.");
388
+ } catch (err) {
389
+ setError(err instanceof Error ? err.message : "Erro ao excluir registro.");
390
+ }
391
+ }
392
+
393
+ function toggleSection(section: keyof typeof openSections) {
394
+ setOpenSections((current) => ({ ...current, [section]: !current[section] }));
395
+ }
396
+
397
+ const activeGuide = CLASSIFICATION_GUIDES[activeClassifier];
398
+
399
+ return (
400
+ <div className="page">
401
+ <header className="hero hero-logo-only">
402
+ <img src={logoCdA} alt="cdA" className="hero-logo" />
403
+ <div className="hero-search">
404
+ <div className="hero-search-head">
405
+ <p className="sequence-kicker">Busca cadastral</p>
406
+ <h2>Inscricao ou endereco</h2>
407
+ </div>
408
+ <div className="search-bar">
409
+ <select
410
+ value={searchMode}
411
+ onChange={(event) =>
412
+ setSearchMode(event.target.value as "inscricao" | "endereco")
413
+ }
414
+ >
415
+ <option value="endereco">Endereco</option>
416
+ <option value="inscricao">Inscricao</option>
417
+ </select>
418
+ <input
419
+ value={searchQuery}
420
+ onChange={(event) => setSearchQuery(event.target.value)}
421
+ placeholder={
422
+ searchMode === "inscricao"
423
+ ? "Ex.: 2720"
424
+ : "Ex.: RUA AFONSO ARINOS 115 ou RUA AFONSO ARINOS 115 2"
425
+ }
426
+ />
427
+ <button type="button" onClick={() => void handleCadastroSearch()}>
428
+ {searching ? "Buscando..." : "Buscar"}
429
+ </button>
430
+ </div>
431
+ <p className="hero-search-help">
432
+ Em endereco, informe logradouro e numero. Se existir unidade, ela pode ser
433
+ adicionada ao final da busca.
434
+ </p>
435
+ </div>
436
+ </header>
437
+
438
+ <main className="sequence">
439
+ <section className="panel sequence-panel">
440
+ <div className="sequence-head">
441
+ <div className="sequence-index">01</div>
442
+ <div>
443
+ <p className="sequence-kicker">Etapa inicial</p>
444
+ <h2>Resultados da busca</h2>
445
+ </div>
446
+ <button
447
+ type="button"
448
+ className="collapse-btn secondary"
449
+ onClick={() => toggleSection("busca")}
450
+ >
451
+ {openSections.busca ? "Recolher" : "Expandir"}
452
+ </button>
453
+ </div>
454
+ {openSections.busca ? (
455
+ <div className="section-body">
456
+ <p className="muted">
457
+ Selecione um dos registros retornados para preencher automaticamente o formulario
458
+ abaixo. A edicao continua livre depois disso.
459
+ </p>
460
+
461
+ <div className="result-list">
462
+ {searchResults.length === 0 ? (
463
+ <div className="empty-state">Nenhum resultado carregado ainda.</div>
464
+ ) : (
465
+ searchResults.map((item) => (
466
+ <button
467
+ type="button"
468
+ key={`${item.num_inscricao}-${item.num_endloc_endereco ?? ""}`}
469
+ className="result-card"
470
+ onClick={() => applyCadastroBase(item)}
471
+ >
472
+ <strong>{item.display_label}</strong>
473
+ <span className="result-line">
474
+ Finalidade: {item.des_finalidade ?? "-"}
475
+ </span>
476
+ <span className="result-line">
477
+ Area territorial: {item.area_territorial ?? "-"} | Area construida:{" "}
478
+ {item.area_construida ?? "-"}
479
+ </span>
480
+ </button>
481
+ ))
482
+ )}
483
+ </div>
484
+ </div>
485
+ ) : null}
486
+ </section>
487
+
488
+ <form onSubmit={handleSubmit} className="sequence-form">
489
+ <section className="panel sequence-panel">
490
+ <div className="sequence-head">
491
+ <div className="sequence-index">02</div>
492
+ <div>
493
+ <p className="sequence-kicker">Novo registro</p>
494
+ <h2>Cadastro</h2>
495
+ </div>
496
+ <button
497
+ type="button"
498
+ className="collapse-btn secondary"
499
+ onClick={() => toggleSection("cadastro")}
500
+ >
501
+ {openSections.cadastro ? "Recolher" : "Expandir"}
502
+ </button>
503
+ </div>
504
+ {openSections.cadastro ? (
505
+ <div className="section-body">
506
+ <div className="form-banner">
507
+ Adicione uma ou mais inscricoes para a mesma oferta. A planilha final mantem uma
508
+ linha por oferta, com os dados cadastrais consolidados.
509
+ </div>
510
+ <div className="selected-cadastros">
511
+ <div className="selected-cadastros-head">
512
+ <strong>Imoveis desta oferta</strong>
513
+ <span>{cadastroRecords.length} selecionado(s)</span>
514
+ </div>
515
+ {cadastroRecords.length === 0 ? (
516
+ <p className="muted">Nenhuma inscricao adicionada ainda.</p>
517
+ ) : (
518
+ <div className="selected-cadastro-list">
519
+ {cadastroRecords.map((record) => (
520
+ <div className="selected-cadastro-item" key={record.num_inscricao}>
521
+ <div>
522
+ <strong>{record.num_inscricao}</strong>
523
+ <span>
524
+ {[record.nme_endloc_logradouro, record.num_endloc_endereco]
525
+ .filter(Boolean)
526
+ .join(", ") || "-"}
527
+ {record.num_endloc_unidade ? ` / ${record.num_endloc_unidade}` : ""}
528
+ </span>
529
+ </div>
530
+ <button
531
+ type="button"
532
+ className="secondary"
533
+ onClick={() => removeCadastroRecord(record.num_inscricao)}
534
+ >
535
+ Remover
536
+ </button>
537
+ </div>
538
+ ))}
539
+ </div>
540
+ )}
541
+ </div>
542
+ <div className="form-grid cadastro-grid">
543
+ <label>
544
+ NUM_BLOCO
545
+ <input
546
+ value={form.num_bloco ?? ""}
547
+ onChange={(event) => updateField("num_bloco", event.target.value || null)}
548
+ />
549
+ </label>
550
+
551
+ <label>
552
+ NUM_INSCRICAO
553
+ <input
554
+ value={form.num_inscricao ?? ""}
555
+ onChange={(event) => updateField("num_inscricao", event.target.value || null)}
556
+ />
557
+ </label>
558
+
559
+ <label>
560
+ COD_ENDLOC_LOGRADOURO
561
+ <input
562
+ value={form.cod_endloc_logradouro ?? ""}
563
+ onChange={(event) =>
564
+ updateField("cod_endloc_logradouro", event.target.value || null)
565
+ }
566
+ />
567
+ </label>
568
+
569
+ <label>
570
+ NME_ENDLOC_LOGRADOURO
571
+ <input
572
+ value={form.nme_endloc_logradouro ?? ""}
573
+ onChange={(event) =>
574
+ updateField("nme_endloc_logradouro", event.target.value || null)
575
+ }
576
+ />
577
+ </label>
578
+
579
+ <label>
580
+ NUM_ENDLOC_ENDERECO
581
+ <input
582
+ value={form.num_endloc_endereco ?? ""}
583
+ onChange={(event) =>
584
+ updateField("num_endloc_endereco", event.target.value || null)
585
+ }
586
+ />
587
+ </label>
588
+
589
+ <label>
590
+ NUM_ENDLOC_UNIDADE
591
+ <input
592
+ value={form.num_endloc_unidade ?? ""}
593
+ onChange={(event) =>
594
+ updateField("num_endloc_unidade", event.target.value || null)
595
+ }
596
+ />
597
+ </label>
598
+
599
+ <label>
600
+ NME_ENDLOC_BAIRRO_CDL
601
+ <input
602
+ value={form.nme_endloc_bairro_cdl ?? ""}
603
+ onChange={(event) =>
604
+ updateField("nme_endloc_bairro_cdl", event.target.value || null)
605
+ }
606
+ />
607
+ </label>
608
+
609
+ <label>
610
+ DES_FINALIDADE
611
+ <input
612
+ value={form.finalidade}
613
+ onChange={(event) => updateField("finalidade", event.target.value)}
614
+ />
615
+ </label>
616
+
617
+ <label>
618
+ AREA_TERRITORIAL_SEPARADA
619
+ <input
620
+ value={form.area_total_detalhe ?? ""}
621
+ onChange={(event) =>
622
+ updateField("area_total_detalhe", event.target.value || null)
623
+ }
624
+ />
625
+ </label>
626
+
627
+ <label>
628
+ AREA_TERRITORIAL_SOMA
629
+ <input
630
+ type="number"
631
+ step="0.01"
632
+ value={form.area_total ?? ""}
633
+ onChange={(event) =>
634
+ updateField(
635
+ "area_total",
636
+ event.target.value ? Number(event.target.value) : null
637
+ )
638
+ }
639
+ />
640
+ </label>
641
+
642
+ <label>
643
+ AREA_CONSTRUIDA_SEPARADA
644
+ <input
645
+ value={form.area_privativa_detalhe ?? ""}
646
+ onChange={(event) =>
647
+ updateField("area_privativa_detalhe", event.target.value || null)
648
+ }
649
+ />
650
+ </label>
651
+
652
+ <label>
653
+ AREA_CONSTRUIDA_SOMA
654
+ <input
655
+ type="number"
656
+ step="0.01"
657
+ value={form.area_privativa ?? ""}
658
+ onChange={(event) =>
659
+ updateField(
660
+ "area_privativa",
661
+ event.target.value ? Number(event.target.value) : null
662
+ )
663
+ }
664
+ />
665
+ </label>
666
+
667
+ <label>
668
+ RH_NOME
669
+ <input
670
+ value={form.rh_nome ?? ""}
671
+ onChange={(event) => updateField("rh_nome", event.target.value || null)}
672
+ />
673
+ </label>
674
+
675
+ <label>
676
+ RH_VALOR
677
+ <input
678
+ type="number"
679
+ step="0.01"
680
+ value={form.rh_valor ?? ""}
681
+ onChange={(event) =>
682
+ updateField(
683
+ "rh_valor",
684
+ event.target.value ? Number(event.target.value) : null
685
+ )
686
+ }
687
+ />
688
+ </label>
689
+
690
+ <label>
691
+ LATITUDE
692
+ <input
693
+ type="number"
694
+ step="0.000001"
695
+ value={form.latitude ?? ""}
696
+ onChange={(event) =>
697
+ updateField(
698
+ "latitude",
699
+ event.target.value ? Number(event.target.value) : null
700
+ )
701
+ }
702
+ />
703
+ </label>
704
+
705
+ <label>
706
+ LONGITUDE
707
+ <input
708
+ type="number"
709
+ step="0.000001"
710
+ value={form.longitude ?? ""}
711
+ onChange={(event) =>
712
+ updateField(
713
+ "longitude",
714
+ event.target.value ? Number(event.target.value) : null
715
+ )
716
+ }
717
+ />
718
+ </label>
719
+ </div>
720
+ </div>
721
+ ) : null}
722
+ </section>
723
+
724
+ <section className="panel sequence-panel">
725
+ <div className="sequence-head">
726
+ <div className="sequence-index">03</div>
727
+ <div>
728
+ <p className="sequence-kicker">Novo registro</p>
729
+ <h2>Oferta</h2>
730
+ </div>
731
+ <button
732
+ type="button"
733
+ className="collapse-btn secondary"
734
+ onClick={() => toggleSection("oferta")}
735
+ >
736
+ {openSections.oferta ? "Recolher" : "Expandir"}
737
+ </button>
738
+ </div>
739
+ {openSections.oferta ? (
740
+ <div className="section-body">
741
+ <div className="form-grid offer-grid">
742
+ <label>
743
+ Finalidade oferta
744
+ <select
745
+ value={form.finalidade_oferta ?? ""}
746
+ onChange={(event) =>
747
+ updateField("finalidade_oferta", event.target.value || null)
748
+ }
749
+ >
750
+ <option value="">Selecione</option>
751
+ <option value="Andar comercial">Andar comercial</option>
752
+ <option value="Casa comercial">Casa comercial</option>
753
+ <option value="Edificio isolado">Edificio isolado</option>
754
+ <option value="Estacionamento">Estacionamento</option>
755
+ <option value="Imovel especial">Imovel especial</option>
756
+ <option value="Loja em conjunto comercial">Loja em conjunto comercial</option>
757
+ <option value="Loja em galeria fechada">Loja em galeria fechada</option>
758
+ <option value="Loja em shopping fechado">Loja em shopping fechado</option>
759
+ <option value="Loja isolada">Loja isolada</option>
760
+ <option value="Loja nao isolada">Loja nao isolada</option>
761
+ <option value="Loja terrea em edificio">Loja terrea em edificio</option>
762
+ <option value="Sala comercial">Sala comercial</option>
763
+ <option value="Terreno">Terreno</option>
764
+ </select>
765
+ </label>
766
+
767
+ <label>
768
+ Area total oferta
769
+ <input
770
+ type="number"
771
+ step="0.01"
772
+ value={form.area_total_oferta ?? ""}
773
+ onChange={(event) =>
774
+ updateField(
775
+ "area_total_oferta",
776
+ event.target.value ? Number(event.target.value) : null
777
+ )
778
+ }
779
+ />
780
+ </label>
781
+
782
+ <label>
783
+ Area privativa oferta
784
+ <input
785
+ type="number"
786
+ step="0.01"
787
+ value={form.area_privativa_oferta ?? ""}
788
+ onChange={(event) =>
789
+ updateField(
790
+ "area_privativa_oferta",
791
+ event.target.value ? Number(event.target.value) : null
792
+ )
793
+ }
794
+ />
795
+ </label>
796
+
797
+ <label>
798
+ Valor oferta
799
+ <input
800
+ type="number"
801
+ step="0.01"
802
+ value={form.valor_oferta ?? ""}
803
+ onChange={(event) =>
804
+ updateField(
805
+ "valor_oferta",
806
+ event.target.value ? Number(event.target.value) : null
807
+ )
808
+ }
809
+ />
810
+ </label>
811
+
812
+ <label className="offer-wide">
813
+ Descricao oferta
814
+ <textarea
815
+ value={form.descricao_oferta ?? ""}
816
+ onChange={(event) =>
817
+ updateField("descricao_oferta", event.target.value || null)
818
+ }
819
+ rows={4}
820
+ />
821
+ </label>
822
+
823
+ <label className="offer-wide">
824
+ Observacao
825
+ <textarea
826
+ value={form.observacao ?? ""}
827
+ onChange={(event) => updateField("observacao", event.target.value || null)}
828
+ rows={3}
829
+ />
830
+ </label>
831
+
832
+ <label>
833
+ URL
834
+ <input
835
+ value={form.url ?? ""}
836
+ onChange={(event) => updateField("url", event.target.value || null)}
837
+ />
838
+ </label>
839
+
840
+ <label>
841
+ Imobiliaria
842
+ <input
843
+ value={form.imobiliaria ?? ""}
844
+ onChange={(event) => updateField("imobiliaria", event.target.value || null)}
845
+ />
846
+ </label>
847
+
848
+ <label>
849
+ Codigo
850
+ <input
851
+ value={form.codigo ?? ""}
852
+ onChange={(event) => updateField("codigo", event.target.value || null)}
853
+ />
854
+ </label>
855
+
856
+ </div>
857
+ </div>
858
+ ) : null}
859
+ </section>
860
+
861
+ <section className="panel sequence-panel">
862
+ <div className="sequence-head">
863
+ <div className="sequence-index">04</div>
864
+ <div>
865
+ <p className="sequence-kicker">Novo registro</p>
866
+ <h2>Outras Caracteristicas</h2>
867
+ </div>
868
+ <button
869
+ type="button"
870
+ className="collapse-btn secondary"
871
+ onClick={() => toggleSection("outras")}
872
+ >
873
+ {openSections.outras ? "Recolher" : "Expandir"}
874
+ </button>
875
+ </div>
876
+ {openSections.outras ? (
877
+ <div className="section-body">
878
+ <div className="classification-layout">
879
+ <div className="classification-controls">
880
+ <div className="classification-row">
881
+ <label>
882
+ Infra
883
+ <select
884
+ value={form.infra ?? ""}
885
+ onChange={(event) => updateField("infra", event.target.value || null)}
886
+ >
887
+ <option value="">Selecione</option>
888
+ <option value="Minima">Minima</option>
889
+ <option value="Basica">Basica</option>
890
+ <option value="Intermediaria">Intermediaria</option>
891
+ <option value="Completa">Completa</option>
892
+ </select>
893
+ </label>
894
+ <button
895
+ type="button"
896
+ className="secondary classify-btn"
897
+ onClick={() => setActiveClassifier("infra")}
898
+ >
899
+ Classificar
900
+ </button>
901
+ </div>
902
+
903
+ <div className="classification-row">
904
+ <label>
905
+ Padrao
906
+ <select
907
+ value={form.padrao ?? ""}
908
+ onChange={(event) => updateField("padrao", event.target.value || null)}
909
+ >
910
+ <option value="">Selecione</option>
911
+ <option value="Baixo">Baixo</option>
912
+ <option value="Normal">Normal</option>
913
+ <option value="Normal/Alto">Normal/Alto</option>
914
+ <option value="Alto">Alto</option>
915
+ </select>
916
+ </label>
917
+ <button
918
+ type="button"
919
+ className="secondary classify-btn"
920
+ onClick={() => setActiveClassifier("padrao")}
921
+ >
922
+ Classificar
923
+ </button>
924
+ </div>
925
+
926
+ <div className="classification-row">
927
+ <label>
928
+ Conservacao
929
+ <select
930
+ value={form.conservacao ?? ""}
931
+ onChange={(event) =>
932
+ updateField("conservacao", event.target.value || null)
933
+ }
934
+ >
935
+ <option value="">Selecione</option>
936
+ {CONSERVACAO_OPTIONS.map((option) => (
937
+ <option key={option.categoria} value={option.categoria}>
938
+ {option.categoria}
939
+ </option>
940
+ ))}
941
+ </select>
942
+ </label>
943
+ <button
944
+ type="button"
945
+ className="secondary classify-btn"
946
+ onClick={() => setActiveClassifier("conservacao")}
947
+ >
948
+ Classificar
949
+ </button>
950
+ </div>
951
+
952
+ <div className="classification-row">
953
+ <label>
954
+ Vaga
955
+ <select
956
+ value={form.vaga ?? ""}
957
+ onChange={(event) => updateField("vaga", event.target.value || null)}
958
+ >
959
+ <option value="">Selecione</option>
960
+ <option value="Sim">Sim</option>
961
+ <option value="Nao - facilidade de estacionamento">
962
+ Nao - facilidade de estacionamento
963
+ </option>
964
+ <option value="Nao - dificuldade de estacionamento">
965
+ Nao - dificuldade de estacionamento
966
+ </option>
967
+ </select>
968
+ </label>
969
+ <button
970
+ type="button"
971
+ className="secondary classify-btn"
972
+ onClick={() => setActiveClassifier("vaga")}
973
+ >
974
+ Classificar
975
+ </button>
976
+ </div>
977
+ </div>
978
+
979
+ <div className="criteria-panel">
980
+ <div className="criteria-head">
981
+ <strong>{activeGuide.titulo}</strong>
982
+ <span>{activeGuide.descricao}</span>
983
+ <span className="criteria-status">{activeGuide.status}</span>
984
+ </div>
985
+ <div className="criteria-grid">
986
+ {activeGuide.blocos.map((bloco) => (
987
+ (() => {
988
+ const blocoValor = "valor" in bloco ? bloco.valor : undefined;
989
+ return (
990
+ <button
991
+ key={bloco.titulo}
992
+ type="button"
993
+ className={`criteria-card${
994
+ blocoValor &&
995
+ ((activeClassifier === "conservacao" &&
996
+ form.conservacao === blocoValor) ||
997
+ (activeClassifier === "vaga" && form.vaga === blocoValor))
998
+ ? " is-selected"
999
+ : ""
1000
+ }`}
1001
+ onClick={() => {
1002
+ if (!blocoValor) {
1003
+ return;
1004
+ }
1005
+ if (activeClassifier === "conservacao") {
1006
+ updateField("conservacao", blocoValor);
1007
+ }
1008
+ if (activeClassifier === "vaga") {
1009
+ updateField("vaga", blocoValor);
1010
+ }
1011
+ }}
1012
+ >
1013
+ <strong>{bloco.titulo}</strong>
1014
+ <ul>
1015
+ {bloco.itens.map((item) => (
1016
+ <li key={item}>{item}</li>
1017
+ ))}
1018
+ </ul>
1019
+ </button>
1020
+ );
1021
+ })()
1022
+ ))}
1023
+ </div>
1024
+ </div>
1025
+ </div>
1026
+ </div>
1027
+ ) : null}
1028
+ </section>
1029
+
1030
+ <div className="form-actions form-actions-outside">
1031
+ <button type="submit">Salvar registro</button>
1032
+ </div>
1033
+ </form>
1034
+
1035
+ <section className="panel sequence-panel full-width">
1036
+ <div className="sequence-head">
1037
+ <div className="sequence-index">05</div>
1038
+ <div>
1039
+ <p className="sequence-kicker">Base consolidada</p>
1040
+ <h2>Banco de dados</h2>
1041
+ </div>
1042
+ <button
1043
+ type="button"
1044
+ className="collapse-btn secondary"
1045
+ onClick={() => toggleSection("tabela")}
1046
+ >
1047
+ {openSections.tabela ? "Recolher" : "Expandir"}
1048
+ </button>
1049
+ </div>
1050
+ {openSections.tabela ? (
1051
+ <div className="section-body">
1052
+ <div className="section-head">
1053
+ <h2>Tabela final</h2>
1054
+ <div className="actions">
1055
+ <button type="button" className="secondary" onClick={() => void loadProperties()}>
1056
+ Atualizar
1057
+ </button>
1058
+ <button type="button" className="secondary" onClick={handleExport}>
1059
+ Exportar planilha
1060
+ </button>
1061
+ </div>
1062
+ </div>
1063
+
1064
+ {loading ? <p className="muted">Carregando...</p> : null}
1065
+ {message ? <p className="success">{message}</p> : null}
1066
+ {error ? <p className="error">{error}</p> : null}
1067
+
1068
+ <div className="table-wrap">
1069
+ <table>
1070
+ <thead>
1071
+ <tr>
1072
+ <th>ID</th>
1073
+ <th>Inscricao(s)</th>
1074
+ <th>Endereco</th>
1075
+ <th>Cadastro</th>
1076
+ <th>Oferta</th>
1077
+ <th>Outras caracteristicas</th>
1078
+ <th>Acao</th>
1079
+ </tr>
1080
+ </thead>
1081
+ <tbody>
1082
+ {properties.length === 0 ? (
1083
+ <tr>
1084
+ <td colSpan={7} className="empty-cell">
1085
+ Nenhum registro salvo.
1086
+ </td>
1087
+ </tr>
1088
+ ) : (
1089
+ properties.map((property) => (
1090
+ <tr key={property.id}>
1091
+ <td>{property.id}</td>
1092
+ <td>{property.num_inscricao ?? "-"}</td>
1093
+ <td>
1094
+ {[property.nme_endloc_logradouro, property.num_endloc_endereco]
1095
+ .filter(Boolean)
1096
+ .join(", ")}
1097
+ {property.num_endloc_unidade ? ` / ${property.num_endloc_unidade}` : ""}
1098
+ {!property.nme_endloc_logradouro && !property.num_endloc_endereco
1099
+ ? "-"
1100
+ : ""}
1101
+ </td>
1102
+ <td>
1103
+ {[
1104
+ `Finalidade: ${property.finalidade ?? "-"}`,
1105
+ `Area total: ${property.area_total_detalhe ?? "-"} | Soma: ${
1106
+ property.area_total ?? "-"
1107
+ }`,
1108
+ `Area privativa: ${property.area_privativa_detalhe ?? "-"} | Soma: ${
1109
+ property.area_privativa ?? "-"
1110
+ }`,
1111
+ `Bairro: ${property.nme_endloc_bairro_cdl ?? "-"}`,
1112
+ ].join(" | ")}
1113
+ </td>
1114
+ <td>
1115
+ {[
1116
+ `Finalidade: ${property.finalidade_oferta ?? "-"}`,
1117
+ `Valor: ${property.valor_oferta ?? "-"}`,
1118
+ `Imobiliaria: ${property.imobiliaria ?? "-"}`,
1119
+ `Codigo: ${property.codigo ?? "-"}`,
1120
+ ].join(" | ")}
1121
+ </td>
1122
+ <td>
1123
+ {[
1124
+ `Infra: ${property.infra ?? "-"}`,
1125
+ `Padrao: ${property.padrao ?? "-"}`,
1126
+ `Conservacao: ${property.conservacao ?? "-"}`,
1127
+ `Vaga: ${property.vaga ?? "-"}`,
1128
+ ].join(" | ")}
1129
+ </td>
1130
+ <td className="action-cell">
1131
+ {deleteTarget === property.id ? (
1132
+ <div className="delete-confirm">
1133
+ <button
1134
+ type="button"
1135
+ className="secondary danger-soft"
1136
+ onClick={() => void handleDelete(property.id)}
1137
+ >
1138
+ Confirmar
1139
+ </button>
1140
+ <button
1141
+ type="button"
1142
+ className="secondary"
1143
+ onClick={() => setDeleteTarget(null)}
1144
+ >
1145
+ Cancelar
1146
+ </button>
1147
+ </div>
1148
+ ) : (
1149
+ <button
1150
+ type="button"
1151
+ className="secondary danger-soft"
1152
+ onClick={() => setDeleteTarget(property.id)}
1153
+ >
1154
+ Excluir
1155
+ </button>
1156
+ )}
1157
+ </td>
1158
+ </tr>
1159
+ ))
1160
+ )}
1161
+ </tbody>
1162
+ </table>
1163
+ </div>
1164
+ </div>
1165
+ ) : null}
1166
+ </section>
1167
+ </main>
1168
+ </div>
1169
+ );
1170
+ }
1171
+
1172
+ export default App;
frontend/src/api.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ CadastroBaseSearchResponse,
3
+ Property,
4
+ PropertyDraft,
5
+ SpreadsheetPreview
6
+ } from "./types";
7
+
8
+ const API_URL = window.location.port === "5173" ? "http://localhost:8001" : "";
9
+
10
+ export async function fetchProperties(): Promise<Property[]> {
11
+ const response = await fetch(`${API_URL}/properties`);
12
+ if (!response.ok) {
13
+ throw new Error("Falha ao carregar imoveis.");
14
+ }
15
+ return response.json();
16
+ }
17
+
18
+ export async function createProperty(payload: PropertyDraft): Promise<Property> {
19
+ const response = await fetch(`${API_URL}/properties`, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json"
23
+ },
24
+ body: JSON.stringify(payload)
25
+ });
26
+
27
+ if (!response.ok) {
28
+ const errorText = await response.text();
29
+ throw new Error(errorText || "Falha ao salvar imovel.");
30
+ }
31
+
32
+ return response.json();
33
+ }
34
+
35
+ export async function deleteProperty(propertyId: number): Promise<void> {
36
+ const response = await fetch(`${API_URL}/properties/${propertyId}`, {
37
+ method: "DELETE"
38
+ });
39
+
40
+ if (!response.ok) {
41
+ throw new Error("Falha ao excluir registro.");
42
+ }
43
+ }
44
+
45
+ export async function previewSpreadsheet(file: File): Promise<SpreadsheetPreview> {
46
+ const formData = new FormData();
47
+ formData.append("file", file);
48
+
49
+ const response = await fetch(`${API_URL}/properties/import-preview`, {
50
+ method: "POST",
51
+ body: formData
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const errorText = await response.text();
56
+ throw new Error(errorText || "Falha ao ler planilha.");
57
+ }
58
+
59
+ return response.json();
60
+ }
61
+
62
+ export async function searchCadastroBase(
63
+ mode: "inscricao" | "endereco",
64
+ query: string
65
+ ): Promise<CadastroBaseSearchResponse> {
66
+ const params = new URLSearchParams({ mode, q: query, limit: "20" });
67
+ const response = await fetch(`${API_URL}/cadastro-base/search?${params.toString()}`);
68
+
69
+ if (!response.ok) {
70
+ const errorText = await response.text();
71
+ throw new Error(errorText || "Falha ao consultar base cadastral.");
72
+ }
73
+
74
+ return response.json();
75
+ }
76
+
77
+ export function getExportUrl(): string {
78
+ return `${API_URL}/properties/export`;
79
+ }
frontend/src/assets/cdA_logo.png ADDED

Git LFS Details

  • SHA256: ffb54006944be6bb617441c07e1953fe84c2b9cd99fc8a5f164e64e31ba2876a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.09 MB
frontend/src/assets/cdA_logo_transparent.png ADDED

Git LFS Details

  • SHA256: 87d7940a2be5ae1d802699ffc1df20b3ad82aba3e96bc6fe53f89e097cb65978
  • Pointer size: 131 Bytes
  • Size of remote file: 847 kB
frontend/src/main.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+ import "./styles.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
11
+
frontend/src/styles.css ADDED
@@ -0,0 +1,789 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700;800&family=Nunito+Sans:wght@400;600;700&family=JetBrains+Mono:wght@500;700&display=swap");
2
+
3
+ :root {
4
+ color-scheme: light;
5
+ font-family: "Nunito Sans", sans-serif;
6
+ --bg-0: #f4f6f8;
7
+ --bg-1: #edf2f6;
8
+ --bg-2: #ffffff;
9
+ --ink-0: #161c24;
10
+ --ink-1: #2f3b4a;
11
+ --ink-2: #5a6b7c;
12
+ --accent: #ff8c00;
13
+ --accent-strong: #e67900;
14
+ --accent-soft: #fff2df;
15
+ --support: #2f80cf;
16
+ --support-soft: #edf5ff;
17
+ --ok: #1e8e49;
18
+ --danger: #b42318;
19
+ --border: #d4dde6;
20
+ --border-soft: #e6edf3;
21
+ --shadow-sm: 0 3px 10px rgba(20, 28, 36, 0.06);
22
+ --shadow-md: 0 10px 26px rgba(20, 28, 36, 0.1);
23
+ --radius-lg: 16px;
24
+ --radius-md: 12px;
25
+ --radius-sm: 9px;
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ html,
33
+ body,
34
+ #root {
35
+ min-height: 100%;
36
+ }
37
+
38
+ body {
39
+ margin: 0;
40
+ min-width: 320px;
41
+ color: var(--ink-0);
42
+ background:
43
+ radial-gradient(1000px 430px at -8% -12%, #ffe8cf 0%, transparent 62%),
44
+ radial-gradient(860px 360px at 105% 10%, #dbeaf6 0%, transparent 60%),
45
+ linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 35%, #f8fafc 100%);
46
+ }
47
+
48
+ button,
49
+ input,
50
+ select,
51
+ textarea {
52
+ font: inherit;
53
+ }
54
+
55
+ .page {
56
+ width: min(1500px, 95vw);
57
+ margin: 18px auto 40px;
58
+ padding: 0;
59
+ }
60
+
61
+ .hero {
62
+ display: grid;
63
+ grid-template-columns: 1.65fr 0.95fr;
64
+ gap: 24px;
65
+ align-items: stretch;
66
+ margin-bottom: 24px;
67
+ border: 1px solid var(--border);
68
+ border-radius: var(--radius-lg);
69
+ background: linear-gradient(130deg, #fffdf9, #fff 55%, #f6fbff);
70
+ box-shadow: var(--shadow-md);
71
+ padding: 18px 22px;
72
+ }
73
+
74
+ .hero.hero-logo-only {
75
+ display: grid;
76
+ grid-template-columns: minmax(260px, 360px) 1fr;
77
+ justify-content: flex-start;
78
+ align-items: center;
79
+ gap: 22px;
80
+ padding: 8px 18px;
81
+ }
82
+
83
+ .hero-logo {
84
+ width: min(340px, 64vw);
85
+ height: auto;
86
+ object-fit: contain;
87
+ }
88
+
89
+ .hero-search {
90
+ display: grid;
91
+ gap: 10px;
92
+ }
93
+
94
+ .hero-search-head {
95
+ display: grid;
96
+ gap: 4px;
97
+ }
98
+
99
+ .hero-search-head h2 {
100
+ margin: 0;
101
+ color: #2b4258;
102
+ font-family: "Sora", sans-serif;
103
+ font-size: 1rem;
104
+ }
105
+
106
+ .hero-search-help {
107
+ margin: -2px 2px 0;
108
+ color: #5f758a;
109
+ font-size: 0.82rem;
110
+ line-height: 1.35;
111
+ }
112
+
113
+ .eyebrow {
114
+ margin: 0 0 12px;
115
+ color: #7d4b12;
116
+ font-size: 12px;
117
+ font-weight: 800;
118
+ letter-spacing: 0.12em;
119
+ text-transform: uppercase;
120
+ }
121
+
122
+ .hero h1 {
123
+ margin: 0 0 14px;
124
+ color: var(--ink-0);
125
+ font-family: "Sora", sans-serif;
126
+ font-size: clamp(2rem, 3vw, 3.55rem);
127
+ line-height: 1.02;
128
+ letter-spacing: 0.01em;
129
+ }
130
+
131
+ .lead {
132
+ max-width: 72ch;
133
+ margin: 0;
134
+ color: var(--ink-2);
135
+ font-size: 1.02rem;
136
+ }
137
+
138
+ .hero-notes {
139
+ display: flex;
140
+ gap: 10px;
141
+ flex-wrap: wrap;
142
+ margin-top: 18px;
143
+ }
144
+
145
+ .hero-chip {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ min-height: 32px;
149
+ padding: 6px 11px;
150
+ border: 1px solid #f2cb8f;
151
+ border-radius: 999px;
152
+ background: #fff6e8;
153
+ color: #8a5a15;
154
+ font-size: 0.78rem;
155
+ font-weight: 700;
156
+ }
157
+
158
+ .status-card,
159
+ .panel {
160
+ border: 1px solid var(--border);
161
+ border-radius: var(--radius-lg);
162
+ background: rgba(255, 255, 255, 0.92);
163
+ box-shadow: var(--shadow-sm);
164
+ }
165
+
166
+ .status-card {
167
+ display: grid;
168
+ gap: 8px;
169
+ align-content: center;
170
+ padding: 24px;
171
+ background: linear-gradient(180deg, #fff, #f8fbff);
172
+ }
173
+
174
+ .status-card span {
175
+ color: var(--ink-2);
176
+ font-size: 0.78rem;
177
+ font-weight: 800;
178
+ letter-spacing: 0.08em;
179
+ text-transform: uppercase;
180
+ }
181
+
182
+ .status-card strong {
183
+ color: var(--ink-1);
184
+ font-family: "Sora", sans-serif;
185
+ font-size: 1.2rem;
186
+ }
187
+
188
+ .grid {
189
+ display: grid;
190
+ grid-template-columns: repeat(2, minmax(0, 1fr));
191
+ gap: 24px;
192
+ }
193
+
194
+ .sequence {
195
+ display: grid;
196
+ gap: 24px;
197
+ }
198
+
199
+ .sequence-form {
200
+ display: grid;
201
+ gap: 24px;
202
+ }
203
+
204
+ .panel {
205
+ padding: 24px;
206
+ }
207
+
208
+ .panel h2 {
209
+ margin: 0 0 10px;
210
+ color: #2b4258;
211
+ font-family: "Sora", sans-serif;
212
+ font-size: 1.05rem;
213
+ }
214
+
215
+ .sequence-panel {
216
+ position: relative;
217
+ overflow: hidden;
218
+ }
219
+
220
+ .sequence-panel::before {
221
+ content: "";
222
+ position: absolute;
223
+ inset: 0 auto 0 0;
224
+ display: block;
225
+ width: 4px;
226
+ background: linear-gradient(180deg, var(--support), var(--accent));
227
+ }
228
+
229
+ .sequence-panel > * {
230
+ position: relative;
231
+ z-index: 1;
232
+ }
233
+
234
+ .sequence-head {
235
+ position: relative;
236
+ z-index: 1;
237
+ display: grid;
238
+ grid-template-columns: 58px 1fr auto;
239
+ gap: 14px;
240
+ align-items: start;
241
+ margin-bottom: 18px;
242
+ }
243
+
244
+ .sequence-head h2 {
245
+ margin: 2px 0 0;
246
+ }
247
+
248
+ .sequence-index {
249
+ display: grid;
250
+ place-items: center;
251
+ width: 42px;
252
+ height: 42px;
253
+ border-radius: 999px;
254
+ border: 1px solid #f2cb8f;
255
+ background: linear-gradient(180deg, #ffb14d, #e67900);
256
+ color: #fff;
257
+ font-family: "Sora", sans-serif;
258
+ font-size: 0.9rem;
259
+ font-weight: 800;
260
+ box-shadow: 0 10px 22px rgba(230, 121, 0, 0.22);
261
+ }
262
+
263
+ .sequence-kicker {
264
+ margin: 0;
265
+ color: #7d4b12;
266
+ font-size: 0.76rem;
267
+ font-weight: 800;
268
+ letter-spacing: 0.08em;
269
+ text-transform: uppercase;
270
+ }
271
+
272
+ .section-body {
273
+ position: relative;
274
+ z-index: 1;
275
+ }
276
+
277
+ .full-width,
278
+ .full {
279
+ grid-column: 1 / -1;
280
+ }
281
+
282
+ .form-grid {
283
+ display: grid;
284
+ grid-template-columns: repeat(3, minmax(0, 1fr));
285
+ gap: 16px;
286
+ }
287
+
288
+ .cadastro-grid {
289
+ grid-template-columns: repeat(4, minmax(0, 1fr));
290
+ }
291
+
292
+ .text-half {
293
+ grid-column: span 1;
294
+ }
295
+
296
+ .offer-text-pair {
297
+ grid-column: span 1;
298
+ }
299
+
300
+ .offer-grid {
301
+ grid-template-columns: repeat(4, minmax(0, 1fr));
302
+ }
303
+
304
+ .offer-wide {
305
+ grid-column: span 2;
306
+ }
307
+
308
+ .search-bar {
309
+ display: grid;
310
+ grid-template-columns: 280px 1fr 140px;
311
+ gap: 12px;
312
+ align-items: center;
313
+ padding: 12px;
314
+ border: 1px solid #dbe6f1;
315
+ border-radius: 14px;
316
+ background: linear-gradient(180deg, #fff, #f9fbfe);
317
+ }
318
+
319
+ .result-list {
320
+ display: grid;
321
+ gap: 12px;
322
+ margin-top: 18px;
323
+ max-height: 360px;
324
+ overflow: auto;
325
+ padding-right: 2px;
326
+ }
327
+
328
+ .result-card {
329
+ border: 1px solid #d2deea;
330
+ border-radius: 10px;
331
+ background: linear-gradient(180deg, #f7fbff, #edf3fa);
332
+ color: #32475d;
333
+ text-align: left;
334
+ box-shadow: none;
335
+ }
336
+
337
+ .result-card strong {
338
+ font-family: "Sora", sans-serif;
339
+ font-size: 0.92rem;
340
+ }
341
+
342
+ .result-card span {
343
+ display: block;
344
+ margin-top: 6px;
345
+ color: #5f758a;
346
+ font-weight: 400;
347
+ }
348
+
349
+ .result-line {
350
+ font-size: 0.88rem;
351
+ }
352
+
353
+ .selected-cadastros {
354
+ margin-bottom: 18px;
355
+ padding: 14px;
356
+ border: 1px solid #dbe6f1;
357
+ border-radius: 10px;
358
+ background: #f8fbfe;
359
+ }
360
+
361
+ .selected-cadastros-head,
362
+ .selected-cadastro-item {
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: space-between;
366
+ gap: 12px;
367
+ }
368
+
369
+ .selected-cadastros-head {
370
+ margin-bottom: 10px;
371
+ color: #39536d;
372
+ }
373
+
374
+ .selected-cadastros-head span {
375
+ color: #66788a;
376
+ font-size: 0.86rem;
377
+ font-weight: 700;
378
+ }
379
+
380
+ .selected-cadastro-list {
381
+ display: grid;
382
+ gap: 10px;
383
+ }
384
+
385
+ .selected-cadastro-item {
386
+ padding: 10px 12px;
387
+ border: 1px solid #d6e2ec;
388
+ border-radius: 8px;
389
+ background: #fff;
390
+ }
391
+
392
+ .selected-cadastro-item div {
393
+ display: grid;
394
+ gap: 4px;
395
+ min-width: 0;
396
+ }
397
+
398
+ .selected-cadastro-item span {
399
+ color: #66788a;
400
+ font-size: 0.86rem;
401
+ overflow-wrap: anywhere;
402
+ }
403
+
404
+ .selected-cadastro-item button {
405
+ max-width: 120px;
406
+ flex: 0 0 120px;
407
+ }
408
+
409
+ label {
410
+ display: grid;
411
+ gap: 8px;
412
+ color: #39536d;
413
+ font-size: 0.9rem;
414
+ font-weight: 700;
415
+ }
416
+
417
+ input,
418
+ select,
419
+ textarea,
420
+ button {
421
+ width: 100%;
422
+ border-radius: 10px;
423
+ border: 1px solid #d6e2ec;
424
+ padding: 12px 14px;
425
+ background: #fff;
426
+ }
427
+
428
+ input,
429
+ select,
430
+ textarea {
431
+ color: #314b64;
432
+ }
433
+
434
+ input:focus,
435
+ select:focus,
436
+ textarea:focus {
437
+ outline: 2px solid rgba(255, 140, 0, 0.2);
438
+ border-color: #ffba66;
439
+ }
440
+
441
+ textarea {
442
+ resize: vertical;
443
+ min-height: 106px;
444
+ }
445
+
446
+ button {
447
+ cursor: pointer;
448
+ border: 1px solid var(--accent-strong);
449
+ background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%);
450
+ color: #fff;
451
+ font-weight: 700;
452
+ box-shadow:
453
+ 0 8px 18px rgba(230, 121, 0, 0.22),
454
+ inset 0 0 0 1px rgba(255, 255, 255, 0.15);
455
+ transition:
456
+ transform 0.18s ease,
457
+ box-shadow 0.18s ease,
458
+ border-color 0.18s ease;
459
+ }
460
+
461
+ button:hover {
462
+ transform: translateY(-1px);
463
+ box-shadow:
464
+ 0 10px 22px rgba(230, 121, 0, 0.28),
465
+ inset 0 0 0 1px rgba(255, 255, 255, 0.18);
466
+ }
467
+
468
+ button.secondary {
469
+ width: auto;
470
+ border-color: #c8d8e8;
471
+ background: linear-gradient(180deg, #f7fbff, #edf3fa);
472
+ color: #355370;
473
+ box-shadow:
474
+ 0 5px 14px rgba(53, 83, 114, 0.1),
475
+ inset 0 0 0 1px rgba(255, 255, 255, 0.22);
476
+ }
477
+
478
+ .section-head {
479
+ display: flex;
480
+ align-items: center;
481
+ justify-content: space-between;
482
+ gap: 16px;
483
+ }
484
+
485
+ .search-help {
486
+ margin: 12px 2px 0;
487
+ color: #5f758a;
488
+ font-size: 0.83rem;
489
+ }
490
+
491
+ .collapse-btn {
492
+ min-width: 110px;
493
+ align-self: center;
494
+ justify-self: end;
495
+ }
496
+
497
+ .form-actions {
498
+ position: relative;
499
+ z-index: 1;
500
+ display: flex;
501
+ justify-content: flex-end;
502
+ }
503
+
504
+ .form-actions-outside {
505
+ margin-top: -6px;
506
+ }
507
+
508
+ .actions {
509
+ display: flex;
510
+ gap: 12px;
511
+ flex-wrap: wrap;
512
+ }
513
+
514
+ .muted {
515
+ color: var(--ink-2);
516
+ }
517
+
518
+ .success {
519
+ color: var(--ok);
520
+ font-weight: 700;
521
+ padding: 8px 10px;
522
+ border-radius: 10px;
523
+ background: #eefaf1;
524
+ border: 1px solid #cbead5;
525
+ }
526
+
527
+ .error {
528
+ color: var(--danger);
529
+ font-weight: 700;
530
+ padding: 8px 10px;
531
+ border-radius: 10px;
532
+ background: #fef0ef;
533
+ border: 1px solid #f7c0bc;
534
+ }
535
+
536
+ .empty-state,
537
+ .empty-cell {
538
+ color: var(--ink-2);
539
+ text-align: center;
540
+ padding: 24px;
541
+ border: 1px dashed var(--border);
542
+ border-radius: 10px;
543
+ background: linear-gradient(180deg, #fbfdff, #f5f9fd);
544
+ }
545
+
546
+ .form-banner {
547
+ margin-bottom: 16px;
548
+ padding: 10px 12px;
549
+ border: 1px solid #dbe6f0;
550
+ border-radius: 12px;
551
+ background: linear-gradient(180deg, #fff, #f8fbff);
552
+ color: #4d6379;
553
+ font-size: 0.88rem;
554
+ line-height: 1.45;
555
+ }
556
+
557
+ .criteria-panel {
558
+ padding: 14px;
559
+ border: 1px solid #dbe6f0;
560
+ border-radius: 14px;
561
+ background: linear-gradient(180deg, #fcfdff, #f5f9fd);
562
+ }
563
+
564
+ .classification-layout {
565
+ display: grid;
566
+ grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.45fr);
567
+ gap: 18px;
568
+ align-items: start;
569
+ }
570
+
571
+ .classification-controls {
572
+ display: grid;
573
+ gap: 12px;
574
+ }
575
+
576
+ .classification-row {
577
+ display: grid;
578
+ grid-template-columns: minmax(0, 1fr) 120px;
579
+ gap: 10px;
580
+ align-items: end;
581
+ }
582
+
583
+ .classify-btn {
584
+ min-height: 46px;
585
+ }
586
+
587
+ .criteria-head {
588
+ display: grid;
589
+ gap: 4px;
590
+ margin-bottom: 12px;
591
+ }
592
+
593
+ .criteria-head strong {
594
+ color: #2f4a63;
595
+ font-family: "Sora", sans-serif;
596
+ font-size: 0.95rem;
597
+ }
598
+
599
+ .criteria-head span {
600
+ color: #5f758a;
601
+ font-size: 0.84rem;
602
+ }
603
+
604
+ .criteria-status {
605
+ display: inline-flex;
606
+ width: fit-content;
607
+ padding: 4px 8px;
608
+ border-radius: 999px;
609
+ background: #eef5ff;
610
+ color: #45627f !important;
611
+ font-size: 0.74rem !important;
612
+ font-weight: 800;
613
+ text-transform: uppercase;
614
+ letter-spacing: 0.04em;
615
+ }
616
+
617
+ .criteria-grid {
618
+ display: grid;
619
+ grid-template-columns: repeat(2, minmax(0, 1fr));
620
+ gap: 12px;
621
+ }
622
+
623
+ .criteria-card {
624
+ border: 1px solid #d2deea;
625
+ border-radius: 12px;
626
+ background: linear-gradient(180deg, #f8fbff, #edf3fa);
627
+ color: #32475d;
628
+ text-align: left;
629
+ box-shadow: none;
630
+ }
631
+
632
+ .criteria-card strong {
633
+ display: flex;
634
+ gap: 12px;
635
+ align-items: baseline;
636
+ font-family: "Sora", sans-serif;
637
+ font-size: 0.92rem;
638
+ }
639
+
640
+ .criteria-card strong span {
641
+ color: #6a8096;
642
+ font-family: "Nunito Sans", sans-serif;
643
+ font-size: 0.78rem;
644
+ font-weight: 700;
645
+ }
646
+
647
+ .criteria-card ul {
648
+ margin: 10px 0 0;
649
+ padding-left: 18px;
650
+ color: #52687d;
651
+ }
652
+
653
+ .criteria-card li + li {
654
+ margin-top: 6px;
655
+ }
656
+
657
+ .criteria-card.is-selected {
658
+ border-color: #ffb14d;
659
+ background: linear-gradient(180deg, #fff6e8, #ffefda);
660
+ box-shadow: 0 8px 20px rgba(230, 121, 0, 0.14);
661
+ }
662
+
663
+ .table-wrap {
664
+ overflow: auto;
665
+ margin-top: 16px;
666
+ border: 1px solid var(--border-soft);
667
+ border-radius: 16px;
668
+ background: #fff;
669
+ }
670
+
671
+ table {
672
+ width: 100%;
673
+ min-width: 720px;
674
+ border-collapse: collapse;
675
+ }
676
+
677
+ th,
678
+ td {
679
+ padding: 12px 14px;
680
+ border-bottom: 1px solid #e1e9f1;
681
+ text-align: left;
682
+ }
683
+
684
+ th {
685
+ background: #f7fbff;
686
+ color: #48627a;
687
+ font-family: "Sora", sans-serif;
688
+ font-size: 0.78rem;
689
+ letter-spacing: 0.04em;
690
+ text-transform: uppercase;
691
+ }
692
+
693
+ td {
694
+ color: #30475e;
695
+ vertical-align: top;
696
+ }
697
+
698
+ .action-cell {
699
+ min-width: 170px;
700
+ }
701
+
702
+ .delete-confirm {
703
+ display: flex;
704
+ gap: 8px;
705
+ flex-wrap: wrap;
706
+ }
707
+
708
+ .danger-soft {
709
+ border-color: #e3adb8 !important;
710
+ background: linear-gradient(180deg, #fff4f6, #fee9ed) !important;
711
+ color: #a63446 !important;
712
+ box-shadow:
713
+ 0 5px 14px rgba(178, 47, 64, 0.1),
714
+ inset 0 0 0 1px rgba(255, 255, 255, 0.22) !important;
715
+ }
716
+
717
+ td:nth-child(1),
718
+ td:nth-child(3),
719
+ td:nth-child(4) {
720
+ font-family: "JetBrains Mono", monospace;
721
+ font-size: 0.82rem;
722
+ }
723
+
724
+ ::-webkit-scrollbar {
725
+ width: 8px;
726
+ height: 8px;
727
+ }
728
+
729
+ ::-webkit-scrollbar-thumb {
730
+ background: #c7d4e2;
731
+ border-radius: 999px;
732
+ }
733
+
734
+ ::-webkit-scrollbar-track {
735
+ background: #eef3f8;
736
+ }
737
+
738
+ @media (max-width: 900px) {
739
+ .page {
740
+ width: 97vw;
741
+ margin-top: 10px;
742
+ }
743
+
744
+ .hero,
745
+ .grid,
746
+ .form-grid,
747
+ .search-bar {
748
+ grid-template-columns: 1fr;
749
+ }
750
+
751
+ .hero {
752
+ padding: 10px 14px;
753
+ }
754
+
755
+ .hero.hero-logo-only {
756
+ grid-template-columns: 1fr;
757
+ }
758
+
759
+ .section-head {
760
+ align-items: flex-start;
761
+ flex-wrap: wrap;
762
+ }
763
+
764
+ .sequence-head {
765
+ grid-template-columns: 42px 1fr;
766
+ }
767
+
768
+ .form-grid {
769
+ grid-template-columns: 1fr;
770
+ }
771
+
772
+ .offer-grid {
773
+ grid-template-columns: 1fr;
774
+ }
775
+
776
+ .offer-wide {
777
+ grid-column: span 1;
778
+ }
779
+
780
+ .classification-layout,
781
+ .classification-row,
782
+ .criteria-grid {
783
+ grid-template-columns: 1fr;
784
+ }
785
+
786
+ .classify-btn {
787
+ width: 100%;
788
+ }
789
+ }
frontend/src/types.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Property = {
2
+ id: number;
3
+ titulo: string | null;
4
+ finalidade: string;
5
+ num_bloco: string | null;
6
+ num_inscricao: string | null;
7
+ cod_endloc_logradouro: string | null;
8
+ nme_endloc_logradouro: string | null;
9
+ num_endloc_endereco: string | null;
10
+ num_endloc_unidade: string | null;
11
+ nme_endloc_bairro_cdl: string | null;
12
+ rh_nome: string | null;
13
+ rh_valor: number | null;
14
+ coord_x: number | null;
15
+ coord_y: number | null;
16
+ ano_exercicio: number | null;
17
+ num_versao: number | null;
18
+ idf_reg_regiao_homogenea: number | null;
19
+ area_total_detalhe: string | null;
20
+ area_total: number | null;
21
+ area_privativa_detalhe: string | null;
22
+ area_privativa: number | null;
23
+ finalidade_oferta: string | null;
24
+ area_total_oferta: number | null;
25
+ area_privativa_oferta: number | null;
26
+ valor_oferta: number | null;
27
+ latitude: number | null;
28
+ longitude: number | null;
29
+ descricao_oferta: string | null;
30
+ observacao: string | null;
31
+ url: string | null;
32
+ imobiliaria: string | null;
33
+ codigo: string | null;
34
+ infra: string | null;
35
+ padrao: string | null;
36
+ conservacao: string | null;
37
+ vaga: string | null;
38
+ origem: string;
39
+ };
40
+
41
+ export type PropertyDraft = Omit<Property, "id">;
42
+
43
+ export type SpreadsheetPreview = {
44
+ file_name: string;
45
+ columns: string[];
46
+ rows: Record<string, unknown>[];
47
+ total_rows: number;
48
+ };
49
+
50
+ export type CadastroBaseRecord = {
51
+ num_bloco: string | null;
52
+ num_inscricao: string;
53
+ cod_endloc_logradouro: string | null;
54
+ nme_endloc_logradouro: string | null;
55
+ num_endloc_endereco: string | null;
56
+ num_endloc_unidade: string | null;
57
+ nme_endloc_bairro_cdl: string | null;
58
+ des_finalidade: string | null;
59
+ rh_nome: string | null;
60
+ rh_valor: number | null;
61
+ coord_x: number | null;
62
+ coord_y: number | null;
63
+ ano_exercicio: number | null;
64
+ num_versao: number | null;
65
+ idf_reg_regiao_homogenea: number | null;
66
+ area_territorial: number | null;
67
+ area_construida: number | null;
68
+ latitude: number | null;
69
+ longitude: number | null;
70
+ titulo_sugerido: string;
71
+ display_label: string;
72
+ };
73
+
74
+ export type CadastroBaseSearchResponse = {
75
+ mode: string;
76
+ total: number;
77
+ items: CadastroBaseRecord[];
78
+ };
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
frontend/tsconfig.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"],
20
+ "references": []
21
+ }
22
+
frontend/vite.config.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ host: "0.0.0.0",
8
+ port: 5173
9
+ }
10
+ });
11
+
run-dev.ps1 ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [switch]$Install
3
+ )
4
+
5
+ $ErrorActionPreference = "Stop"
6
+
7
+ $Root = Split-Path -Parent $MyInvocation.MyCommand.Path
8
+ $BackendPath = Join-Path $Root "backend"
9
+ $FrontendPath = Join-Path $Root "frontend"
10
+ $VenvPath = Join-Path $BackendPath ".venv"
11
+ $PythonExe = Join-Path $VenvPath "Scripts\python.exe"
12
+ $PipExe = Join-Path $VenvPath "Scripts\pip.exe"
13
+ $PythonLauncher = "py -3.13"
14
+
15
+ function Ensure-Command($Name) {
16
+ if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
17
+ throw "Comando obrigatorio nao encontrado: $Name"
18
+ }
19
+ }
20
+
21
+ Ensure-Command py
22
+ Ensure-Command npm
23
+
24
+ if ($Install -and -not (Test-Path $PythonExe)) {
25
+ Write-Host "Criando ambiente virtual do backend..."
26
+ & py -3.13 -m venv $VenvPath
27
+ }
28
+
29
+ if ($Install) {
30
+ Write-Host "Instalando dependencias do backend..."
31
+ & $PythonExe -m pip install --upgrade pip
32
+ & $PipExe install -r (Join-Path $BackendPath "requirements.txt")
33
+
34
+ Write-Host "Instalando dependencias do frontend..."
35
+ Push-Location $FrontendPath
36
+ npm install
37
+ Pop-Location
38
+ }
39
+
40
+ if (-not (Test-Path $PythonExe)) {
41
+ throw "Ambiente virtual nao encontrado em backend\.venv. Rode .\run-dev.ps1 -Install"
42
+ }
43
+
44
+ if (-not (Test-Path (Join-Path $FrontendPath "node_modules"))) {
45
+ throw "Dependencias do frontend nao encontradas. Rode .\run-dev.ps1 -Install"
46
+ }
47
+
48
+ Write-Host "Subindo backend em http://localhost:8000 ..."
49
+ Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$BackendPath'; & '$PythonExe' -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"
50
+
51
+ Write-Host "Subindo frontend em http://localhost:5173 ..."
52
+ Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$FrontendPath'; npm run dev -- --host 0.0.0.0 --port 5173"
53
+
54
+ Write-Host "Ambiente iniciado."
55
+ Write-Host "Frontend: http://localhost:5173"
56
+ Write-Host "Backend: http://localhost:8000"
57
+ Write-Host "Docs: http://localhost:8000/docs"