From 9e6ba459a816da2935597977b70f5f35256dc692 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Tue, 9 Dec 2025 01:24:35 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Sistema=20de=20Ranking=20de=20Consultor?= =?UTF-8?q?es=20CAPES=20-=20vers=C3=A3o=20inicial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (FastAPI + DDD): - Arquitetura DDD com camadas Domain, Application, Infrastructure, Interface - Integração com Elasticsearch (ATUACAPES) para dados de consultores - Integração com Oracle (SUCUPIRA_PAINEL) para coordenações PPG - Cálculo dos 4 componentes de pontuação (A, B, C, D) - Cache em memória para otimização de performance - API REST com endpoints /ranking, /ranking/detalhado, /consultor/{id} Frontend (React + Vite): - Interface responsiva com cards expansíveis - Visualização detalhada de pontuação por componente - Filtro por quantidade de consultores (Top 10, 50, 100, etc) Docker: - docker-compose com shared_network externa - Backend com Oracle Instant Client - Frontend com Vite dev server --- .env.example | 8 + .gitignore | 30 + README.md | 267 +++ backend/.env.example | 16 + backend/Dockerfile | 31 + backend/poetry.lock | 6 + backend/pyproject.toml | 45 + backend/requirements.txt | 9 + backend/src/__init__.py | 0 backend/src/application/__init__.py | 0 backend/src/application/dtos/__init__.py | 0 backend/src/application/dtos/consultor_dto.py | 98 + backend/src/application/use_cases/__init__.py | 0 .../application/use_cases/obter_consultor.py | 23 + .../application/use_cases/obter_ranking.py | 145 ++ backend/src/domain/__init__.py | 0 backend/src/domain/entities/__init__.py | 0 backend/src/domain/entities/consultor.py | 77 + backend/src/domain/repositories/__init__.py | 0 .../repositories/consultor_repository.py | 26 + .../domain/services/calculador_pontuacao.py | 93 + backend/src/domain/value_objects/__init__.py | 0 backend/src/domain/value_objects/periodo.py | 23 + backend/src/domain/value_objects/pontuacao.py | 67 + backend/src/infrastructure/__init__.py | 0 .../infrastructure/elasticsearch/__init__.py | 0 .../infrastructure/elasticsearch/client.py | 104 + backend/src/infrastructure/mcp/__init__.py | 0 backend/src/infrastructure/oracle/__init__.py | 0 backend/src/infrastructure/oracle/client.py | 112 + .../repositories/consultor_repository_impl.py | 287 +++ backend/src/interface/__init__.py | 0 backend/src/interface/api/__init__.py | 0 backend/src/interface/api/app.py | 50 + backend/src/interface/api/config.py | 30 + backend/src/interface/api/dependencies.py | 25 + backend/src/interface/api/routes.py | 77 + backend/src/interface/schemas/__init__.py | 0 .../src/interface/schemas/consultor_schema.py | 98 + backend/src/main.py | 12 + docker-compose.yml | 46 + frontend/.gitignore | 24 + frontend/Dockerfile | 12 + frontend/README.md | 16 + frontend/eslint.config.js | 29 + frontend/frontend/index.html | 0 frontend/frontend/package.json | 0 frontend/frontend/src/App.css | 1 + frontend/frontend/src/App.jsx | 0 frontend/frontend/src/index.css | 0 frontend/frontend/src/main.jsx | 0 frontend/frontend/vite.config.js | 0 frontend/index.html | 15 + frontend/nginx.conf | 29 + frontend/package-lock.json | 1796 +++++++++++++++++ frontend/package.json | 22 + frontend/public/vite.svg | 1 + frontend/src/App.css | 97 + frontend/src/App.jsx | 84 + frontend/src/assets/react.svg | 1 + frontend/src/components/ConsultorCard.css | 322 +++ frontend/src/components/ConsultorCard.jsx | 216 ++ frontend/src/components/Header.css | 107 + frontend/src/components/Header.jsx | 69 + frontend/src/index.css | 49 + frontend/src/main.jsx | 10 + frontend/src/services/api.js | 29 + frontend/vite.config.js | 16 + index-teste.html | 152 ++ 69 files changed, 4902 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/poetry.lock create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements.txt create mode 100644 backend/src/__init__.py create mode 100644 backend/src/application/__init__.py create mode 100644 backend/src/application/dtos/__init__.py create mode 100644 backend/src/application/dtos/consultor_dto.py create mode 100644 backend/src/application/use_cases/__init__.py create mode 100644 backend/src/application/use_cases/obter_consultor.py create mode 100644 backend/src/application/use_cases/obter_ranking.py create mode 100644 backend/src/domain/__init__.py create mode 100644 backend/src/domain/entities/__init__.py create mode 100644 backend/src/domain/entities/consultor.py create mode 100644 backend/src/domain/repositories/__init__.py create mode 100644 backend/src/domain/repositories/consultor_repository.py create mode 100644 backend/src/domain/services/calculador_pontuacao.py create mode 100644 backend/src/domain/value_objects/__init__.py create mode 100644 backend/src/domain/value_objects/periodo.py create mode 100644 backend/src/domain/value_objects/pontuacao.py create mode 100644 backend/src/infrastructure/__init__.py create mode 100644 backend/src/infrastructure/elasticsearch/__init__.py create mode 100644 backend/src/infrastructure/elasticsearch/client.py create mode 100644 backend/src/infrastructure/mcp/__init__.py create mode 100644 backend/src/infrastructure/oracle/__init__.py create mode 100644 backend/src/infrastructure/oracle/client.py create mode 100644 backend/src/infrastructure/repositories/consultor_repository_impl.py create mode 100644 backend/src/interface/__init__.py create mode 100644 backend/src/interface/api/__init__.py create mode 100644 backend/src/interface/api/app.py create mode 100644 backend/src/interface/api/config.py create mode 100644 backend/src/interface/api/dependencies.py create mode 100644 backend/src/interface/api/routes.py create mode 100644 backend/src/interface/schemas/__init__.py create mode 100644 backend/src/interface/schemas/consultor_schema.py create mode 100644 backend/src/main.py create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/frontend/index.html create mode 100644 frontend/frontend/package.json create mode 100644 frontend/frontend/src/App.css create mode 100644 frontend/frontend/src/App.jsx create mode 100644 frontend/frontend/src/index.css create mode 100644 frontend/frontend/src/main.jsx create mode 100644 frontend/frontend/vite.config.js create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/ConsultorCard.css create mode 100644 frontend/src/components/ConsultorCard.jsx create mode 100644 frontend/src/components/Header.css create mode 100644 frontend/src/components/Header.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/services/api.js create mode 100644 frontend/vite.config.js create mode 100644 index-teste.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..702f824 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +ES_URL=http://seu-elasticsearch:9200 +ES_INDEX=atuacapes +ES_USER=seu_usuario_elastic +ES_PASSWORD=sua_senha_elastic + +ORACLE_USER=FREDERICOAC +ORACLE_PASSWORD=FREDEricoac +ORACLE_DSN=oracledhtsrv02.hom.capes.gov.br:1521/hom_dr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7587155 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +.env +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +node_modules/ +.DS_Store +.vscode/ +.idea/ +*.log +.cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e56a34 --- /dev/null +++ b/README.md @@ -0,0 +1,267 @@ +# Sistema de Ranking de Consultores CAPES + +Sistema completo de ranking de consultores CAPES baseado na Minuta Técnica, desenvolvido com arquitetura DDD (Domain-Driven Design). + +## Arquitetura + +### Backend (Python + FastAPI + DDD) +- **Domain Layer**: Entities, Value Objects, Domain Services +- **Application Layer**: Use Cases, DTOs +- **Infrastructure Layer**: Repositories, Elasticsearch Client, Oracle Client +- **Interface Layer**: FastAPI Routes, Schemas, Dependencies + +### Frontend (React + Vite) +- Interface moderna baseada no layout do HTML de referência +- Componentes reutilizáveis +- Integração com API via Axios +- Design responsivo + +## Estrutura do Projeto + +``` +ranking/ +├── backend/ +│ ├── src/ +│ │ ├── domain/ +│ │ │ ├── entities/ +│ │ │ │ └── consultor.py +│ │ │ ├── value_objects/ +│ │ │ │ ├── periodo.py +│ │ │ │ └── pontuacao.py +│ │ │ ├── repositories/ +│ │ │ │ └── consultor_repository.py +│ │ │ └── services/ +│ │ │ └── calculador_pontuacao.py +│ │ ├── application/ +│ │ │ ├── dtos/ +│ │ │ │ └── consultor_dto.py +│ │ │ └── use_cases/ +│ │ │ ├── obter_ranking.py +│ │ │ └── obter_consultor.py +│ │ ├── infrastructure/ +│ │ │ ├── elasticsearch/ +│ │ │ │ └── client.py +│ │ │ ├── oracle/ +│ │ │ │ └── client.py +│ │ │ └── repositories/ +│ │ │ └── consultor_repository_impl.py +│ │ └── interface/ +│ │ ├── api/ +│ │ │ ├── app.py +│ │ │ ├── routes.py +│ │ │ ├── config.py +│ │ │ └── dependencies.py +│ │ └── schemas/ +│ │ └── consultor_schema.py +│ ├── pyproject.toml +│ ├── Dockerfile +│ └── .env.example +│ +├── frontend/ +│ ├── src/ +│ │ ├── components/ +│ │ │ ├── Header.jsx +│ │ │ ├── Header.css +│ │ │ ├── ConsultorCard.jsx +│ │ │ └── ConsultorCard.css +│ │ ├── services/ +│ │ │ └── api.js +│ │ ├── App.jsx +│ │ ├── App.css +│ │ ├── main.jsx +│ │ └── index.css +│ ├── package.json +│ ├── vite.config.js +│ ├── Dockerfile +│ └── nginx.conf +│ +├── docker-compose.yml +├── .env.example +├── .gitignore +└── README.md +``` + +## Componentes de Pontuação + +### Componente A: Coordenação CAPES (máx 450 pts) +- **CA**: 200 base + 100 tempo + 100 áreas + 20 retorno + 30 bônus = 450 +- **CAJ**: 150 base + 80 tempo + 100 áreas + 20 retorno + 20 bônus = 370 +- **CAJ-MP**: 120 base + 60 tempo + 100 áreas + 20 retorno + 15 bônus = 315 +- **CAM**: 100 base + 50 tempo + 100 áreas + 20 retorno + 10 bônus = 280 + +### Componente B: Coordenação de Programa PPG (máx 180 pts) +- Base: 70 pts +- Tempo: 5 pts/ano (máx 50 pts) +- Programas extras: 20 pts/programa (máx 40 pts) +- Bônus ativo: 20 pts + +### Componente C: Consultoria (máx 230 pts) +- Ativo: 150 pts | Histórico: 100 pts +- Tempo: 5 pts/ano (máx 50 pts) +- Eventos: 2 pts/evento (máx 20 pts) +- Responsável: 5 pts/vez (máx 25 pts) +- Áreas extras: 10 pts/área (máx 30 pts) + +### Componente D: Premiações (máx 180 pts) +- Premiação: 60 pts +- Avaliação: 40 pts +- Inscrição: 20 pts + +## Requisitos + +- Python 3.11+ +- Node.js 18+ +- Docker e Docker Compose (opcional) +- Acesso ao Elasticsearch (ATUACAPES) +- Acesso ao Oracle (SUCUPIRA_PAINEL) + +## Setup Local + +### Backend + +1. Entre no diretório do backend: +```bash +cd backend +``` + +2. Crie ambiente virtual e instale dependências: +```bash +poetry install +``` + +3. Configure as variáveis de ambiente: +```bash +cp .env.example .env +``` + +4. Edite `.env` com suas credenciais: +``` +ES_URL=http://seu-elasticsearch:9200 +ES_INDEX=atuacapes +ES_USER=seu_usuario_elastic +ES_PASSWORD=sua_senha_elastic +ORACLE_USER=seu_usuario +ORACLE_PASSWORD=sua_senha +ORACLE_DSN=host:1521/service_name +``` + +5. Execute o backend: +```bash +poetry run python src/main.py +``` + +A API estará disponível em `http://localhost:8000` + +### Frontend + +1. Entre no diretório do frontend: +```bash +cd frontend +``` + +2. Instale dependências: +```bash +npm install +``` + +3. Execute em modo desenvolvimento: +```bash +npm run dev +``` + +O frontend estará disponível em `http://localhost:5173` + +## Setup com Docker + +1. Configure as variáveis de ambiente: +```bash +cp .env.example .env +``` + +2. Edite `.env` com suas credenciais + +3. Execute com Docker Compose: +```bash +docker-compose up -d +``` + +- Backend: `http://localhost:8000` +- Frontend: `http://localhost` + +## Endpoints da API + +### GET /api/v1/ranking +Retorna o ranking resumido de consultores. + +**Query Parameters:** +- `limite` (int, default=100): Limite de consultores +- `offset` (int, default=0): Offset para paginação +- `componente` (str, optional): Filtrar por componente (a, b, c, d) + +### GET /api/v1/ranking/detalhado +Retorna o ranking completo com todos os detalhes. + +**Query Parameters:** +- `limite` (int, default=100): Limite de consultores +- `componente` (str, optional): Filtrar por componente (a, b, c, d) + +### GET /api/v1/consultor/{id_pessoa} +Retorna detalhes completos de um consultor específico. + +### GET /api/v1/health +Health check da API. + +## Fontes de Dados + +### Elasticsearch (ATUACAPES) +- Índice: `atuacapes__1763197236` +- Campos: `id`, `dadosPessoais`, `atuacoes` +- Tipos de atuação: + - Coordenação de Área de Avaliação + - Consultor + - Premiação Prêmio + - Avaliação Prêmio + - Inscrição Prêmio + +### Oracle (SUCUPIRA_PAINEL) +- Tabela: `VM_COORDENADOR` +- JOIN com: `VM_PROGRAMA_SUCUPIRA`, `VM_AREA_CONHECIMENTO`, `VM_AREA_AVALIACAO` +- 29.546 registros (4.150 ativos, 25.396 históricos) + +## Documentação da API + +Após iniciar o backend, acesse: +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` + +## Desenvolvimento + +### Testes +```bash +cd backend +poetry run pytest +``` + +### Linting +```bash +poetry run black src/ +poetry run ruff check src/ +``` + +### Build para Produção + +**Backend:** +```bash +docker build -t ranking-capes-backend ./backend +``` + +**Frontend:** +```bash +cd frontend +npm run build +``` + +## Referências + +- Documentação dos critérios: `.claude/rules/ranking-consultores-capes.md` +- Queries e mapeamentos: `.claude/rules/ranking-queries-elasticsearch.md` diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..b47b6cf --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,16 @@ +ES_URL=http://localhost:9200 +ES_INDEX=atuacapes__1763197236 +ES_USER=seu_usuario_elastic +ES_PASSWORD=sua_senha_elastic +ES_DEFAULT_SOURCE_FIELDS=id,dadosPessoais.nome,dadosPessoais.cpf,atuacoes + +ORACLE_USER=seu_usuario +ORACLE_PASSWORD=sua_senha +ORACLE_DSN=host:1521/service_name + +API_HOST=0.0.0.0 +API_PORT=8000 +API_RELOAD=true +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 + +LOG_LEVEL=INFO diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1c124b6 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + gcc \ + wget \ + unzip \ + && (apt-get install -y libaio1t64 || apt-get install -y libaio1 || true) \ + && rm -rf /var/lib/apt/lists/* + +RUN wget https://download.oracle.com/otn_software/linux/instantclient/2115000/instantclient-basic-linux.x64-21.15.0.0.0dbru.zip \ + && unzip instantclient-basic-linux.x64-21.15.0.0.0dbru.zip -d /opt/oracle \ + && rm instantclient-basic-linux.x64-21.15.0.0.0dbru.zip \ + && sh -c "echo /opt/oracle/instantclient_21_15 > /etc/ld.so.conf.d/oracle-instantclient.conf" \ + && ln -sf /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1 || true \ + && ldconfig + +ENV LD_LIBRARY_PATH=/opt/oracle/instantclient_21_15:$LD_LIBRARY_PATH +ENV PATH=/opt/oracle/instantclient_21_15:$PATH + +RUN pip install --upgrade pip + +COPY requirements.txt ./ +RUN pip install -r requirements.txt + +COPY ./src ./src + +EXPOSE 8000 + +CMD ["python", "-m", "uvicorn", "src.interface.api.app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 0000000..5837fde --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,6 @@ +# This file is automatically generated by poetry +# Run `poetry lock` to regenerate it + +[[package]] +name = "fastapi" +version = "0.109.0" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..bf0dbd5 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,45 @@ +[tool.poetry] +name = "ranking-capes" +version = "0.1.0" +description = "Sistema de Ranking de Consultores CAPES - DDD Architecture" +authors = ["CAPES"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +fastapi = "^0.109.0" +uvicorn = {extras = ["standard"], version = "^0.27.0"} +pydantic = "^2.5.3" +pydantic-settings = "^2.1.0" +elasticsearch = "^8.11.1" +cx-Oracle = "^8.3.0" +python-dateutil = "^2.8.2" +httpx = "^0.26.0" +python-dotenv = "^1.0.0" +rich = "^13.7.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.4" +pytest-asyncio = "^0.23.3" +pytest-cov = "^4.1.0" +black = "^24.1.1" +ruff = "^0.1.14" +mypy = "^1.8.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 100 +target-version = ["py311"] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.mypy] +python_version = "3.11" +strict = true +warn_return_any = true +warn_unused_configs = true diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..ca29160 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +pydantic==2.5.3 +pydantic-settings==2.1.0 +cx-Oracle==8.3.0 +python-dateutil==2.8.2 +httpx==0.26.0 +python-dotenv==1.0.0 +aiohttp==3.9.1 diff --git a/backend/src/__init__.py b/backend/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/application/__init__.py b/backend/src/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/application/dtos/__init__.py b/backend/src/application/dtos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/application/dtos/consultor_dto.py b/backend/src/application/dtos/consultor_dto.py new file mode 100644 index 0000000..609089d --- /dev/null +++ b/backend/src/application/dtos/consultor_dto.py @@ -0,0 +1,98 @@ +from dataclasses import dataclass, asdict +from typing import List, Optional, Dict, Any +from datetime import datetime + + +@dataclass +class PeriodoDTO: + inicio: str + fim: Optional[str] + ativo: bool + anos_decorridos: float + + +@dataclass +class CoordenacaoCapesDTO: + tipo: str + area_avaliacao: str + periodo: PeriodoDTO + areas_adicionais: List[str] + ja_coordenou_antes: bool + + +@dataclass +class CoordenacaoProgramaDTO: + id_programa: int + nome_programa: str + codigo_programa: str + nota_ppg: str + modalidade: str + area_avaliacao: str + periodo: PeriodoDTO + + +@dataclass +class ConsultoriaDTO: + total_eventos: int + eventos_recentes: int + primeiro_evento: str + ultimo_evento: str + vezes_responsavel: int + areas: List[str] + + +@dataclass +class PremiacaoDTO: + tipo: str + nome_premio: str + ano: int + pontos: int + + +@dataclass +class ComponentePontuacaoDTO: + base: int + tempo: int + extras: int + bonus: int + retorno: int + total: int + + +@dataclass +class PontuacaoCompletaDTO: + componente_a: ComponentePontuacaoDTO + componente_b: ComponentePontuacaoDTO + componente_c: ComponentePontuacaoDTO + componente_d: ComponentePontuacaoDTO + pontuacao_total: int + + +@dataclass +class ConsultorResumoDTO: + id_pessoa: int + nome: str + anos_atuacao: float + ativo: bool + veterano: bool + pontuacao_total: int + rank: Optional[int] = None + + +@dataclass +class ConsultorDetalhadoDTO: + id_pessoa: int + nome: str + cpf: Optional[str] + anos_atuacao: float + ativo: bool + veterano: bool + coordenacoes_capes: List[CoordenacaoCapesDTO] + coordenacoes_programas: List[CoordenacaoProgramaDTO] + consultoria: Optional[ConsultoriaDTO] + premiacoes: List[PremiacaoDTO] + pontuacao: PontuacaoCompletaDTO + rank: Optional[int] = None + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) diff --git a/backend/src/application/use_cases/__init__.py b/backend/src/application/use_cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/application/use_cases/obter_consultor.py b/backend/src/application/use_cases/obter_consultor.py new file mode 100644 index 0000000..ee5594b --- /dev/null +++ b/backend/src/application/use_cases/obter_consultor.py @@ -0,0 +1,23 @@ +from typing import Optional + +from ...domain.repositories.consultor_repository import ConsultorRepository +from ..dtos.consultor_dto import ConsultorDetalhadoDTO +from .obter_ranking import ObterRankingUseCase + + +class ObterConsultorUseCase: + def __init__(self, repository: ConsultorRepository): + self.repository = repository + self.ranking_use_case = ObterRankingUseCase(repository) + + async def executar(self, id_pessoa: int) -> Optional[ConsultorDetalhadoDTO]: + consultor = await self.repository.buscar_por_id(id_pessoa) + if not consultor: + return None + + ranking_completo = await self.repository.buscar_ranking(limite=1000) + rank = next( + (idx + 1 for idx, c in enumerate(ranking_completo) if c.id_pessoa == id_pessoa), None + ) + + return self.ranking_use_case._converter_para_dto_detalhado(consultor, rank or 0) diff --git a/backend/src/application/use_cases/obter_ranking.py b/backend/src/application/use_cases/obter_ranking.py new file mode 100644 index 0000000..f0b69f1 --- /dev/null +++ b/backend/src/application/use_cases/obter_ranking.py @@ -0,0 +1,145 @@ +from typing import List, Optional +from datetime import datetime + +from ...domain.repositories.consultor_repository import ConsultorRepository +from ...domain.entities.consultor import Consultor +from ..dtos.consultor_dto import ( + ConsultorResumoDTO, + ConsultorDetalhadoDTO, + PeriodoDTO, + CoordenacaoCapesDTO, + CoordenacaoProgramaDTO, + ConsultoriaDTO, + PremiacaoDTO, + ComponentePontuacaoDTO, + PontuacaoCompletaDTO, +) + + +class ObterRankingUseCase: + def __init__(self, repository: ConsultorRepository): + self.repository = repository + + async def executar( + self, limite: int = 100, componente: Optional[str] = None + ) -> List[ConsultorResumoDTO]: + consultores = await self.repository.buscar_ranking(limite=limite, componente=componente) + + return [ + ConsultorResumoDTO( + id_pessoa=c.id_pessoa, + nome=c.nome, + anos_atuacao=c.anos_atuacao, + ativo=c.ativo, + veterano=c.veterano, + pontuacao_total=c.pontuacao_total, + rank=idx + 1, + ) + for idx, c in enumerate(consultores) + ] + + async def executar_detalhado( + self, limite: int = 100, componente: Optional[str] = None + ) -> List[ConsultorDetalhadoDTO]: + consultores = await self.repository.buscar_ranking(limite=limite, componente=componente) + + return [self._converter_para_dto_detalhado(c, idx + 1) for idx, c in enumerate(consultores)] + + def _converter_para_dto_detalhado( + self, consultor: Consultor, rank: int + ) -> ConsultorDetalhadoDTO: + return ConsultorDetalhadoDTO( + id_pessoa=consultor.id_pessoa, + nome=consultor.nome, + cpf=consultor.cpf, + anos_atuacao=consultor.anos_atuacao, + ativo=consultor.ativo, + veterano=consultor.veterano, + coordenacoes_capes=[ + CoordenacaoCapesDTO( + tipo=cc.tipo, + area_avaliacao=cc.area_avaliacao, + periodo=PeriodoDTO( + inicio=cc.periodo.inicio.isoformat(), + fim=cc.periodo.fim.isoformat() if cc.periodo.fim else None, + ativo=cc.periodo.ativo, + anos_decorridos=cc.periodo.anos_decorridos, + ), + areas_adicionais=cc.areas_adicionais, + ja_coordenou_antes=cc.ja_coordenou_antes, + ) + for cc in consultor.coordenacoes_capes + ], + coordenacoes_programas=[ + CoordenacaoProgramaDTO( + id_programa=cp.id_programa, + nome_programa=cp.nome_programa, + codigo_programa=cp.codigo_programa, + nota_ppg=cp.nota_ppg, + modalidade=cp.modalidade, + area_avaliacao=cp.area_avaliacao, + periodo=PeriodoDTO( + inicio=cp.periodo.inicio.isoformat(), + fim=cp.periodo.fim.isoformat() if cp.periodo.fim else None, + ativo=cp.periodo.ativo, + anos_decorridos=cp.periodo.anos_decorridos, + ), + ) + for cp in consultor.coordenacoes_programas + ], + consultoria=ConsultoriaDTO( + total_eventos=consultor.consultoria.total_eventos, + eventos_recentes=consultor.consultoria.eventos_recentes, + primeiro_evento=consultor.consultoria.primeiro_evento.isoformat(), + ultimo_evento=consultor.consultoria.ultimo_evento.isoformat(), + vezes_responsavel=consultor.consultoria.vezes_responsavel, + areas=consultor.consultoria.areas, + ) + if consultor.consultoria + else None, + premiacoes=[ + PremiacaoDTO( + tipo=p.tipo, + nome_premio=p.nome_premio, + ano=p.ano, + pontos=p.pontos, + ) + for p in consultor.premiacoes + ], + pontuacao=PontuacaoCompletaDTO( + componente_a=ComponentePontuacaoDTO( + base=consultor.pontuacao.componente_a.base, + tempo=consultor.pontuacao.componente_a.tempo, + extras=consultor.pontuacao.componente_a.extras, + bonus=consultor.pontuacao.componente_a.bonus, + retorno=consultor.pontuacao.componente_a.retorno, + total=consultor.pontuacao.componente_a.total, + ), + componente_b=ComponentePontuacaoDTO( + base=consultor.pontuacao.componente_b.base, + tempo=consultor.pontuacao.componente_b.tempo, + extras=consultor.pontuacao.componente_b.extras, + bonus=consultor.pontuacao.componente_b.bonus, + retorno=0, + total=consultor.pontuacao.componente_b.total, + ), + componente_c=ComponentePontuacaoDTO( + base=consultor.pontuacao.componente_c.base, + tempo=consultor.pontuacao.componente_c.tempo, + extras=consultor.pontuacao.componente_c.extras, + bonus=consultor.pontuacao.componente_c.bonus, + retorno=0, + total=consultor.pontuacao.componente_c.total, + ), + componente_d=ComponentePontuacaoDTO( + base=consultor.pontuacao.componente_d.base, + tempo=consultor.pontuacao.componente_d.tempo, + extras=consultor.pontuacao.componente_d.extras, + bonus=consultor.pontuacao.componente_d.bonus, + retorno=0, + total=consultor.pontuacao.componente_d.total, + ), + pontuacao_total=consultor.pontuacao.total, + ), + rank=rank, + ) diff --git a/backend/src/domain/__init__.py b/backend/src/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/domain/entities/__init__.py b/backend/src/domain/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/domain/entities/consultor.py b/backend/src/domain/entities/consultor.py new file mode 100644 index 0000000..e246efe --- /dev/null +++ b/backend/src/domain/entities/consultor.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass, field +from typing import List, Optional +from datetime import datetime + +from ..value_objects.periodo import Periodo +from ..value_objects.pontuacao import PontuacaoCompleta + + +@dataclass +class CoordenacaoCapes: + tipo: str + area_avaliacao: str + periodo: Periodo + areas_adicionais: List[str] = field(default_factory=list) + ja_coordenou_antes: bool = False + + +@dataclass +class CoordenacaoPrograma: + id_programa: int + nome_programa: str + codigo_programa: str + nota_ppg: str + modalidade: str + area_avaliacao: str + periodo: Periodo + + +@dataclass +class Consultoria: + total_eventos: int + eventos_recentes: int + primeiro_evento: datetime + ultimo_evento: datetime + vezes_responsavel: int + areas: List[str] = field(default_factory=list) + + +@dataclass +class Premiacao: + tipo: str + nome_premio: str + ano: int + pontos: int + + +@dataclass +class Consultor: + id_pessoa: int + nome: str + cpf: Optional[str] = None + coordenacoes_capes: List[CoordenacaoCapes] = field(default_factory=list) + coordenacoes_programas: List[CoordenacaoPrograma] = field(default_factory=list) + consultoria: Optional[Consultoria] = None + premiacoes: List[Premiacao] = field(default_factory=list) + pontuacao: Optional[PontuacaoCompleta] = None + + @property + def anos_atuacao(self) -> float: + if not self.consultoria: + return 0.0 + dias = (datetime.now() - self.consultoria.primeiro_evento).days + return round(dias / 365.25, 1) + + @property + def ativo(self) -> bool: + if not self.consultoria: + return False + return self.consultoria.eventos_recentes > 0 + + @property + def veterano(self) -> bool: + return self.anos_atuacao >= 10.0 + + @property + def pontuacao_total(self) -> int: + return self.pontuacao.total if self.pontuacao else 0 diff --git a/backend/src/domain/repositories/__init__.py b/backend/src/domain/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/domain/repositories/consultor_repository.py b/backend/src/domain/repositories/consultor_repository.py new file mode 100644 index 0000000..508c0b9 --- /dev/null +++ b/backend/src/domain/repositories/consultor_repository.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + +from ..entities.consultor import Consultor + + +class ConsultorRepository(ABC): + @abstractmethod + async def buscar_por_id(self, id_pessoa: int) -> Optional[Consultor]: + pass + + @abstractmethod + async def buscar_todos( + self, limite: int = 100, offset: int = 0, filtro_ativo: Optional[bool] = None + ) -> List[Consultor]: + pass + + @abstractmethod + async def buscar_ranking( + self, limite: int = 100, componente: Optional[str] = None + ) -> List[Consultor]: + pass + + @abstractmethod + async def contar_total(self, filtro_ativo: Optional[bool] = None) -> int: + pass diff --git a/backend/src/domain/services/calculador_pontuacao.py b/backend/src/domain/services/calculador_pontuacao.py new file mode 100644 index 0000000..c6f2226 --- /dev/null +++ b/backend/src/domain/services/calculador_pontuacao.py @@ -0,0 +1,93 @@ +from datetime import datetime, timedelta +from typing import List + +from ..entities.consultor import ( + Consultor, + CoordenacaoCapes, + CoordenacaoPrograma, + Consultoria, + Premiacao, +) +from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta + + +class CalculadorPontuacao: + @staticmethod + def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao: + if not coordenacoes: + return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0) + + coord_atual = next((c for c in coordenacoes if c.periodo.ativo), None) + if not coord_atual: + return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0) + + base_map = {"CA": 200, "CAJ": 150, "CAJ-MP": 120, "CAM": 100} + tempo_max_map = {"CA": 100, "CAJ": 80, "CAJ-MP": 60, "CAM": 50} + bonus_atual_map = {"CA": 30, "CAJ": 20, "CAJ-MP": 15, "CAM": 10} + + base = base_map.get(coord_atual.tipo, 0) + anos = coord_atual.periodo.anos_decorridos + tempo = min(int(anos * 10), tempo_max_map.get(coord_atual.tipo, 0)) + + extras = min(len(coord_atual.areas_adicionais) * 20, 100) + bonus = bonus_atual_map.get(coord_atual.tipo, 0) if coord_atual.periodo.ativo else 0 + retorno = 20 if coord_atual.ja_coordenou_antes else 0 + + return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno) + + @staticmethod + def calcular_componente_b(coordenacoes: List[CoordenacaoPrograma]) -> ComponentePontuacao: + if not coordenacoes: + return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) + + base = 70 + anos_totais = sum(c.periodo.anos_decorridos for c in coordenacoes) + tempo = min(int(anos_totais * 5), 50) + + programas_distintos = len({c.id_programa for c in coordenacoes}) + extras = min((programas_distintos - 1) * 20, 40) + + coord_ativa = any(c.periodo.ativo for c in coordenacoes) + bonus = 20 if coord_ativa else 0 + + return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus) + + @staticmethod + def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao: + if not consultoria: + return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) + + base = 150 if consultoria.eventos_recentes > 0 else 100 + + anos = (datetime.now() - consultoria.primeiro_evento).days / 365.25 + tempo = min(int(anos * 5), 50) + + extras_eventos = min(consultoria.total_eventos * 2, 20) + extras_responsavel = min(consultoria.vezes_responsavel * 5, 25) + extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0 + extras = extras_eventos + extras_responsavel + extras_areas + + bonus = 0 + + return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus) + + @staticmethod + def calcular_componente_d(premiacoes: List[Premiacao]) -> ComponentePontuacao: + if not premiacoes: + return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0) + + total_pontos = sum(p.pontos for p in premiacoes) + total_pontos = min(total_pontos, 180) + + return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0) + + @classmethod + def calcular_pontuacao_completa(cls, consultor: Consultor) -> PontuacaoCompleta: + comp_a = cls.calcular_componente_a(consultor.coordenacoes_capes) + comp_b = cls.calcular_componente_b(consultor.coordenacoes_programas) + comp_c = cls.calcular_componente_c(consultor.consultoria) + comp_d = cls.calcular_componente_d(consultor.premiacoes) + + return PontuacaoCompleta( + componente_a=comp_a, componente_b=comp_b, componente_c=comp_c, componente_d=comp_d + ) diff --git a/backend/src/domain/value_objects/__init__.py b/backend/src/domain/value_objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/domain/value_objects/periodo.py b/backend/src/domain/value_objects/periodo.py new file mode 100644 index 0000000..1f18765 --- /dev/null +++ b/backend/src/domain/value_objects/periodo.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + + +@dataclass(frozen=True) +class Periodo: + inicio: datetime + fim: Optional[datetime] = None + + @property + def ativo(self) -> bool: + return self.fim is None + + @property + def anos_decorridos(self) -> float: + fim = self.fim if self.fim else datetime.now() + dias = (fim - self.inicio).days + return round(dias / 365.25, 1) + + def __post_init__(self) -> None: + if self.fim and self.fim < self.inicio: + raise ValueError("Data de fim não pode ser anterior à data de início") diff --git a/backend/src/domain/value_objects/pontuacao.py b/backend/src/domain/value_objects/pontuacao.py new file mode 100644 index 0000000..8fe14bf --- /dev/null +++ b/backend/src/domain/value_objects/pontuacao.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass +from typing import Dict + + +@dataclass(frozen=True) +class ComponentePontuacao: + base: int + tempo: int + extras: int + bonus: int + retorno: int = 0 + + @property + def total(self) -> int: + return self.base + self.tempo + self.extras + self.bonus + self.retorno + + +@dataclass(frozen=True) +class PontuacaoCompleta: + componente_a: ComponentePontuacao + componente_b: ComponentePontuacao + componente_c: ComponentePontuacao + componente_d: ComponentePontuacao + + @property + def total(self) -> int: + return ( + self.componente_a.total + + self.componente_b.total + + self.componente_c.total + + self.componente_d.total + ) + + @property + def detalhamento(self) -> Dict[str, Dict[str, int]]: + return { + "componente_a": { + "base": self.componente_a.base, + "tempo": self.componente_a.tempo, + "extras": self.componente_a.extras, + "bonus": self.componente_a.bonus, + "retorno": self.componente_a.retorno, + "total": self.componente_a.total, + }, + "componente_b": { + "base": self.componente_b.base, + "tempo": self.componente_b.tempo, + "extras": self.componente_b.extras, + "bonus": self.componente_b.bonus, + "total": self.componente_b.total, + }, + "componente_c": { + "base": self.componente_c.base, + "tempo": self.componente_c.tempo, + "extras": self.componente_c.extras, + "bonus": self.componente_c.bonus, + "total": self.componente_c.total, + }, + "componente_d": { + "base": self.componente_d.base, + "tempo": self.componente_d.tempo, + "extras": self.componente_d.extras, + "bonus": self.componente_d.bonus, + "total": self.componente_d.total, + }, + "pontuacao_total": self.total, + } diff --git a/backend/src/infrastructure/__init__.py b/backend/src/infrastructure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/infrastructure/elasticsearch/__init__.py b/backend/src/infrastructure/elasticsearch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/infrastructure/elasticsearch/client.py b/backend/src/infrastructure/elasticsearch/client.py new file mode 100644 index 0000000..685e469 --- /dev/null +++ b/backend/src/infrastructure/elasticsearch/client.py @@ -0,0 +1,104 @@ +import httpx +from typing import Optional, Dict, Any + + +class ElasticsearchClient: + def __init__(self, url: str, index: str, user: str = "", password: str = ""): + self.url = url.rstrip("/") + self.index = index + self.user = user + self.password = password + self._client: Optional[httpx.AsyncClient] = None + + async def connect(self) -> None: + auth = None + if self.user and self.password: + auth = httpx.BasicAuth(self.user, self.password) + + self._client = httpx.AsyncClient( + auth=auth, + headers={ + "Content-Type": "application/json", + "Accept": "application/json" + }, + verify=False, + timeout=120.0 + ) + + async def close(self) -> None: + if self._client: + await self._client.aclose() + + @property + def client(self) -> httpx.AsyncClient: + if not self._client: + raise RuntimeError("Cliente Elasticsearch não conectado. Execute connect() primeiro.") + return self._client + + async def buscar_por_id(self, id_pessoa: int) -> Optional[dict]: + try: + query = { + "query": {"term": {"id": id_pessoa}}, + "_source": ["id", "dadosPessoais", "atuacoes"], + "size": 1, + } + + response = await self.client.post( + f"{self.url}/{self.index}/_search", + json=query + ) + response.raise_for_status() + + data = response.json() + hits = data.get("hits", {}).get("hits", []) + return hits[0]["_source"] if hits else None + except Exception as e: + raise RuntimeError(f"Erro ao buscar consultor {id_pessoa}: {e}") + + async def buscar_com_atuacoes(self, size: int = 1000, from_: int = 0) -> list: + try: + query = { + "query": { + "nested": { + "path": "atuacoes", + "query": {"exists": {"field": "atuacoes.tipo"}} + } + }, + "_source": ["id", "dadosPessoais", "atuacoes"], + "size": size, + "from": from_, + "sort": [{"id": "asc"}], + } + + response = await self.client.post( + f"{self.url}/{self.index}/_search", + json=query + ) + response.raise_for_status() + + data = response.json() + return [hit["_source"] for hit in data.get("hits", {}).get("hits", [])] + except Exception as e: + raise RuntimeError(f"Erro ao buscar consultores: {e}") + + async def contar_com_atuacoes(self) -> int: + try: + query = { + "query": { + "nested": { + "path": "atuacoes", + "query": {"exists": {"field": "atuacoes.tipo"}} + } + } + } + + response = await self.client.post( + f"{self.url}/{self.index}/_count", + json=query + ) + response.raise_for_status() + + data = response.json() + return data.get("count", 0) + except Exception as e: + raise RuntimeError(f"Erro ao contar consultores: {e}") diff --git a/backend/src/infrastructure/mcp/__init__.py b/backend/src/infrastructure/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/infrastructure/oracle/__init__.py b/backend/src/infrastructure/oracle/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/infrastructure/oracle/client.py b/backend/src/infrastructure/oracle/client.py new file mode 100644 index 0000000..3443cbd --- /dev/null +++ b/backend/src/infrastructure/oracle/client.py @@ -0,0 +1,112 @@ +import cx_Oracle +from typing import List, Dict, Any, Optional +from contextlib import contextmanager + + +class OracleClient: + def __init__(self, user: str, password: str, dsn: str): + self.user = user + self.password = password + self.dsn = dsn + self._pool: Optional[cx_Oracle.SessionPool] = None + self._connected = False + + def connect(self) -> None: + try: + self._pool = cx_Oracle.SessionPool( + user=self.user, + password=self.password, + dsn=self.dsn, + min=2, + max=10, + increment=1, + encoding="UTF-8", + ) + self._connected = True + except Exception as e: + print(f"AVISO Oracle: {e}") + self._connected = False + + def close(self) -> None: + if self._pool: + try: + self._pool.close() + except: + pass + + @property + def is_connected(self) -> bool: + return self._connected and self._pool is not None + + @contextmanager + def get_connection(self): + if not self._pool: + raise RuntimeError("Pool Oracle não conectado. Execute connect() primeiro.") + conn = self._pool.acquire() + try: + yield conn + finally: + self._pool.release(conn) + + def executar_query(self, query: str, params: Optional[dict] = None) -> List[Dict[str, Any]]: + if not self.is_connected: + return [] + try: + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(query, params or {}) + columns = [col[0] for col in cursor.description] + rows = cursor.fetchall() + cursor.close() + return [dict(zip(columns, row)) for row in rows] + except Exception as e: + print(f"AVISO Oracle: falha ao executar query: {e}") + self._connected = False + return [] + + def buscar_coordenacoes_programa(self, id_pessoa: int) -> List[Dict[str, Any]]: + query = """ + SELECT + c.ID_PESSOA, + c.ID_PROGRAMA_SNPG, + p.NM_PROGRAMA, + p.CD_PROGRAMA_PPG, + p.DS_CONCEITO AS NOTA_PPG, + p.NM_PROGRAMA_MODALIDADE, + aa.NM_AREA_AVALIACAO, + c.DT_INICIO_VIGENCIA, + c.DT_FIM_VIGENCIA + FROM SUCUPIRA_PAINEL.VM_COORDENADOR c + INNER JOIN SUCUPIRA_PAINEL.VM_PROGRAMA_SUCUPIRA p + ON c.ID_PROGRAMA_SNPG = p.ID_PROGRAMA + LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_CONHECIMENTO ac + ON p.ID_AREA_CONHECIMENTO_ATUAL = ac.ID_AREA_CONHECIMENTO + LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_AVALIACAO aa + ON ac.ID_AREA_AVALIACAO = aa.ID_AREA_AVALIACAO + WHERE c.ID_PESSOA = :id_pessoa + ORDER BY c.DT_INICIO_VIGENCIA DESC + """ + return self.executar_query(query, {"id_pessoa": id_pessoa}) + + def buscar_todas_coordenacoes_programa(self) -> List[Dict[str, Any]]: + query = """ + SELECT + c.ID_PESSOA, + c.ID_PROGRAMA_SNPG, + p.NM_PROGRAMA, + p.CD_PROGRAMA_PPG, + p.DS_CONCEITO AS NOTA_PPG, + p.NM_PROGRAMA_MODALIDADE, + aa.NM_AREA_AVALIACAO, + c.DT_INICIO_VIGENCIA, + c.DT_FIM_VIGENCIA + FROM SUCUPIRA_PAINEL.VM_COORDENADOR c + INNER JOIN SUCUPIRA_PAINEL.VM_PROGRAMA_SUCUPIRA p + ON c.ID_PROGRAMA_SNPG = p.ID_PROGRAMA + LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_CONHECIMENTO ac + ON p.ID_AREA_CONHECIMENTO_ATUAL = ac.ID_AREA_CONHECIMENTO + LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_AVALIACAO aa + ON ac.ID_AREA_AVALIACAO = aa.ID_AREA_AVALIACAO + ORDER BY c.ID_PESSOA, c.DT_INICIO_VIGENCIA DESC + """ + return self.executar_query(query) diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py new file mode 100644 index 0000000..4ead580 --- /dev/null +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -0,0 +1,287 @@ +from typing import List, Optional, Dict, Any +from datetime import datetime, timedelta +from dateutil import parser as date_parser +import asyncio + +from ...domain.entities.consultor import ( + Consultor, + CoordenacaoCapes, + CoordenacaoPrograma, + Consultoria, + Premiacao, +) +from ...domain.repositories.consultor_repository import ConsultorRepository +from ...domain.services.calculador_pontuacao import CalculadorPontuacao +from ...domain.value_objects.periodo import Periodo +from ..elasticsearch.client import ElasticsearchClient +from ..oracle.client import OracleClient + + +class RankingCache: + def __init__(self, ttl_seconds: int = 300): + self.ttl = ttl_seconds + self._cache: List[Consultor] = [] + self._last_update: Optional[datetime] = None + self._loading = False + self._lock = asyncio.Lock() + + def is_valid(self) -> bool: + if not self._cache or not self._last_update: + return False + return (datetime.now() - self._last_update).total_seconds() < self.ttl + + def get(self) -> List[Consultor]: + return self._cache + + def set(self, consultores: List[Consultor]) -> None: + self._cache = consultores + self._last_update = datetime.now() + + +_ranking_cache = RankingCache(ttl_seconds=300) + + +class ConsultorRepositoryImpl(ConsultorRepository): + def __init__(self, es_client: ElasticsearchClient, oracle_client: OracleClient): + self.es_client = es_client + self.oracle_client = oracle_client + self.calculador = CalculadorPontuacao() + self.es_disponivel = True + + def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]: + if not date_str: + return None + try: + return date_parser.parse(date_str, dayfirst=True) + except: + return None + + def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]: + consultorias = [ + a for a in atuacoes if a.get("tipo") in ["Consultor", "Histórico de Consultoria"] + ] + if not consultorias: + return None + + datas_inicio = [ + self._parse_date(c.get("inicio")) + for c in consultorias + ] + datas_inicio = [d for d in datas_inicio if d] + + datas_fim = [ + self._parse_date(c.get("fim")) + for c in consultorias + ] + datas_fim = [d for d in datas_fim if d] + + if not datas_inicio: + return None + + limite_recente = datetime.now() - timedelta(days=730) + eventos_recentes = sum(1 for d in datas_fim if d >= limite_recente) + + areas = list({c.get("areaAvaliacao", "N/A") for c in consultorias if c.get("areaAvaliacao")}) + + vezes_responsavel = sum(1 for c in consultorias if c.get("responsavel", False)) + + return Consultoria( + total_eventos=len(consultorias), + eventos_recentes=eventos_recentes, + primeiro_evento=min(datas_inicio), + ultimo_evento=max(datas_fim) if datas_fim else datetime.now(), + vezes_responsavel=vezes_responsavel, + areas=areas, + ) + + def _extrair_coordenacoes_capes( + self, atuacoes: List[Dict[str, Any]] + ) -> List[CoordenacaoCapes]: + coordenacoes = [ + a + for a in atuacoes + if a.get("tipo") + in [ + "Coordenação de Área de Avaliação", + "Histórico de Coordenação de Área de Avaliação", + ] + ] + + resultado = [] + for coord in coordenacoes: + inicio = self._parse_date(coord.get("inicio")) + if not inicio: + continue + + tipo = self._inferir_tipo_coordenacao(coord) + fim = self._parse_date(coord.get("fim")) + + resultado.append( + CoordenacaoCapes( + tipo=tipo, + area_avaliacao=coord.get("areaAvaliacao", "N/A"), + periodo=Periodo(inicio=inicio, fim=fim), + areas_adicionais=[], + ja_coordenou_antes=False, + ) + ) + + return resultado + + def _inferir_tipo_coordenacao(self, coord: Dict[str, Any]) -> str: + nome = coord.get("nome", "").lower() + if "câmara" in nome or "camara" in nome: + return "CAM" + elif "mestrado profissional" in nome: + return "CAJ-MP" + elif "adjunta" in nome: + return "CAJ" + else: + return "CA" + + def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]: + premiacoes_data = [ + a + for a in atuacoes + if a.get("tipo") + in [ + "Premiação Prêmio", + "Avaliação Prêmio", + "Inscrição Prêmio", + ] + ] + + premiacoes = [] + for prem in premiacoes_data: + pontos = self._calcular_pontos_premiacao(prem.get("tipo", "")) + inicio = self._parse_date(prem.get("inicio")) + ano = inicio.year if inicio else datetime.now().year + + premiacoes.append( + Premiacao( + tipo=prem.get("tipo", "N/A"), + nome_premio=prem.get("descricao", "N/A"), + ano=ano, + pontos=pontos, + ) + ) + + return premiacoes + + def _calcular_pontos_premiacao(self, tipo: str) -> int: + mapa = { + "Premiação Prêmio": 60, + "Avaliação Prêmio": 40, + "Inscrição Prêmio": 20, + } + return mapa.get(tipo, 0) + + async def _construir_consultor(self, doc: Dict[str, Any]) -> Consultor: + id_pessoa = doc["id"] + dados_pessoais = doc.get("dadosPessoais", {}) + atuacoes = doc.get("atuacoes", []) + + consultoria = self._extrair_consultoria(atuacoes) + coordenacoes_capes = self._extrair_coordenacoes_capes(atuacoes) + premiacoes = self._extrair_premiacoes(atuacoes) + + coordenacoes_programas_raw = [] + if self.oracle_client.is_connected: + try: + coordenacoes_programas_raw = self.oracle_client.buscar_coordenacoes_programa(id_pessoa) + except Exception as e: + print(f"AVISO Oracle: erro ao buscar coordenacoes do programa para {id_pessoa}: {e}") + coordenacoes_programas = [ + CoordenacaoPrograma( + id_programa=c["ID_PROGRAMA_SNPG"], + nome_programa=c["NM_PROGRAMA"], + codigo_programa=c["CD_PROGRAMA_PPG"], + nota_ppg=c["NOTA_PPG"] or "N/A", + modalidade=c["NM_PROGRAMA_MODALIDADE"] or "N/A", + area_avaliacao=c["NM_AREA_AVALIACAO"] or "N/A", + periodo=Periodo( + inicio=c["DT_INICIO_VIGENCIA"], + fim=c["DT_FIM_VIGENCIA"], + ), + ) + for c in coordenacoes_programas_raw + ] + + consultor = Consultor( + id_pessoa=id_pessoa, + nome=dados_pessoais.get("nome", "N/A"), + cpf=dados_pessoais.get("cpf"), + coordenacoes_capes=coordenacoes_capes, + coordenacoes_programas=coordenacoes_programas, + consultoria=consultoria, + premiacoes=premiacoes, + ) + + consultor.pontuacao = self.calculador.calcular_pontuacao_completa(consultor) + + return consultor + + async def buscar_por_id(self, id_pessoa: int) -> Optional[Consultor]: + try: + doc = await self.es_client.buscar_por_id(id_pessoa) + except Exception as e: + print(f"AVISO Elasticsearch: falha ao buscar consultor {id_pessoa}: {e}") + return None + if not doc: + return None + return await self._construir_consultor(doc) + + async def buscar_todos( + self, limite: int = 100, offset: int = 0, filtro_ativo: Optional[bool] = None + ) -> List[Consultor]: + if not self.es_client._client or getattr(self.es_client._client, "is_closed", False): + self.es_disponivel = False + return [] + try: + docs = await self.es_client.buscar_com_atuacoes(size=limite, from_=offset) + self.es_disponivel = True + except Exception as e: + print(f"AVISO Elasticsearch: falha ao buscar consultores: {e}") + self.es_disponivel = False + return [] + consultores = [await self._construir_consultor(doc) for doc in docs] + + if filtro_ativo is not None: + consultores = [c for c in consultores if c.ativo == filtro_ativo] + + return consultores + + async def buscar_ranking( + self, limite: int = 100, componente: Optional[str] = None + ) -> List[Consultor]: + global _ranking_cache + + if _ranking_cache.is_valid(): + consultores_ordenados = _ranking_cache.get() + return consultores_ordenados[:limite] + + async with _ranking_cache._lock: + if _ranking_cache.is_valid(): + return _ranking_cache.get()[:limite] + + tamanho_busca = 1000 + consultores = await self.buscar_todos(limite=tamanho_busca) + consultores_ordenados = sorted( + consultores, key=lambda c: c.pontuacao_total, reverse=True + ) + _ranking_cache.set(consultores_ordenados) + + return consultores_ordenados[:limite] + + async def contar_total(self, filtro_ativo: Optional[bool] = None) -> int: + if not self.es_disponivel: + return 0 + if not self.es_client._client or getattr(self.es_client._client, "is_closed", False): + self.es_disponivel = False + return 0 + try: + return await self.es_client.contar_com_atuacoes() + except Exception as e: + print(f"AVISO Elasticsearch: falha ao contar consultores: {e}") + self.es_disponivel = False + return 0 diff --git a/backend/src/interface/__init__.py b/backend/src/interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/interface/api/__init__.py b/backend/src/interface/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/interface/api/app.py b/backend/src/interface/api/app.py new file mode 100644 index 0000000..f3002c4 --- /dev/null +++ b/backend/src/interface/api/app.py @@ -0,0 +1,50 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager + +from .routes import router +from .config import settings +from .dependencies import es_client, oracle_client + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await es_client.connect() + try: + oracle_client.connect() + except Exception as e: + print(f"AVISO: Oracle não conectou: {e}. Sistema rodando sem Coordenação PPG.") + yield + await es_client.close() + try: + oracle_client.close() + except: + pass + + +app = FastAPI( + title="Ranking de Consultores CAPES", + description="Sistema de Ranking de Consultores CAPES baseado na Minuta Técnica", + version="1.0.0", + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins_list, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(router) + + +@app.get("/") +async def root(): + return { + "message": "API Ranking CAPES", + "version": "1.0.0", + "docs": "/docs", + "health": "/api/v1/health", + } diff --git a/backend/src/interface/api/config.py b/backend/src/interface/api/config.py new file mode 100644 index 0000000..ce81a45 --- /dev/null +++ b/backend/src/interface/api/config.py @@ -0,0 +1,30 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing import List + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + + ES_URL: str = "http://localhost:9200" + ES_INDEX: str = "atuacapes__1763197236" + ES_USER: str = "" + ES_PASSWORD: str = "" + + ORACLE_USER: str + ORACLE_PASSWORD: str + ORACLE_DSN: str + + API_HOST: str = "0.0.0.0" + API_PORT: int = 8000 + API_RELOAD: bool = True + + CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173" + + LOG_LEVEL: str = "INFO" + + @property + def cors_origins_list(self) -> List[str]: + return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] + + +settings = Settings() diff --git a/backend/src/interface/api/dependencies.py b/backend/src/interface/api/dependencies.py new file mode 100644 index 0000000..7f9e879 --- /dev/null +++ b/backend/src/interface/api/dependencies.py @@ -0,0 +1,25 @@ +from ...infrastructure.elasticsearch.client import ElasticsearchClient +from ...infrastructure.oracle.client import OracleClient +from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl +from .config import settings + + +es_client = ElasticsearchClient( + url=settings.ES_URL, + index=settings.ES_INDEX, + user=settings.ES_USER, + password=settings.ES_PASSWORD +) + +oracle_client = OracleClient( + user=settings.ORACLE_USER, password=settings.ORACLE_PASSWORD, dsn=settings.ORACLE_DSN +) + +_repository: ConsultorRepositoryImpl = None + + +def get_repository() -> ConsultorRepositoryImpl: + global _repository + if _repository is None: + _repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_client) + return _repository diff --git a/backend/src/interface/api/routes.py b/backend/src/interface/api/routes.py new file mode 100644 index 0000000..03b9ffc --- /dev/null +++ b/backend/src/interface/api/routes.py @@ -0,0 +1,77 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Optional + +from ...application.use_cases.obter_ranking import ObterRankingUseCase +from ...application.use_cases.obter_consultor import ObterConsultorUseCase +from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl +from ..schemas.consultor_schema import ( + RankingResponseSchema, + RankingDetalhadoResponseSchema, + ConsultorDetalhadoSchema, + ConsultorResumoSchema, +) +from .dependencies import get_repository + +router = APIRouter(prefix="/api/v1", tags=["ranking"]) + + +@router.get("/ranking", response_model=RankingResponseSchema) +async def obter_ranking( + limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), + offset: int = Query(default=0, ge=0, description="Offset para paginação"), + componente: Optional[str] = Query( + default=None, description="Filtrar por componente (a, b, c, d)" + ), + repository: ConsultorRepositoryImpl = Depends(get_repository), +): + use_case = ObterRankingUseCase(repository=repository) + consultores_dto = await use_case.executar(limite=limite, componente=componente) + + total = await repository.contar_total() + + consultores_schema = [ + ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto + ] + + return RankingResponseSchema( + total=total, limite=limite, offset=offset, consultores=consultores_schema + ) + + +@router.get("/ranking/detalhado", response_model=RankingDetalhadoResponseSchema) +async def obter_ranking_detalhado( + limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), + componente: Optional[str] = Query( + default=None, description="Filtrar por componente (a, b, c, d)" + ), + repository: ConsultorRepositoryImpl = Depends(get_repository), +): + use_case = ObterRankingUseCase(repository=repository) + consultores_dto = await use_case.executar_detalhado(limite=limite, componente=componente) + + total = await repository.contar_total() + + consultores_schema = [ + ConsultorDetalhadoSchema(**dto.to_dict()) for dto in consultores_dto + ] + + return RankingDetalhadoResponseSchema(total=total, limite=limite, consultores=consultores_schema) + + +@router.get("/consultor/{id_pessoa}", response_model=ConsultorDetalhadoSchema) +async def obter_consultor( + id_pessoa: int, + repository: ConsultorRepositoryImpl = Depends(get_repository), +): + use_case = ObterConsultorUseCase(repository=repository) + consultor = await use_case.executar(id_pessoa=id_pessoa) + + if not consultor: + raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado") + + return consultor + + +@router.get("/health") +async def health_check(): + return {"status": "ok", "message": "API Ranking CAPES funcionando"} diff --git a/backend/src/interface/schemas/__init__.py b/backend/src/interface/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/interface/schemas/consultor_schema.py b/backend/src/interface/schemas/consultor_schema.py new file mode 100644 index 0000000..872dee1 --- /dev/null +++ b/backend/src/interface/schemas/consultor_schema.py @@ -0,0 +1,98 @@ +from pydantic import BaseModel, Field +from typing import List, Optional + + +class PeriodoSchema(BaseModel): + inicio: str + fim: Optional[str] = None + ativo: bool + anos_decorridos: float + + +class CoordenacaoCapesSchema(BaseModel): + tipo: str + area_avaliacao: str + periodo: PeriodoSchema + areas_adicionais: List[str] + ja_coordenou_antes: bool + + +class CoordenacaoProgramaSchema(BaseModel): + id_programa: int + nome_programa: str + codigo_programa: str + nota_ppg: str + modalidade: str + area_avaliacao: str + periodo: PeriodoSchema + + +class ConsultoriaSchema(BaseModel): + total_eventos: int + eventos_recentes: int + primeiro_evento: str + ultimo_evento: str + vezes_responsavel: int + areas: List[str] + + +class PremiacaoSchema(BaseModel): + tipo: str + nome_premio: str + ano: int + pontos: int + + +class ComponentePontuacaoSchema(BaseModel): + base: int + tempo: int + extras: int + bonus: int + retorno: int + total: int + + +class PontuacaoCompletaSchema(BaseModel): + componente_a: ComponentePontuacaoSchema + componente_b: ComponentePontuacaoSchema + componente_c: ComponentePontuacaoSchema + componente_d: ComponentePontuacaoSchema + pontuacao_total: int + + +class ConsultorResumoSchema(BaseModel): + id_pessoa: int + nome: str + anos_atuacao: float + ativo: bool + veterano: bool + pontuacao_total: int + rank: Optional[int] = None + + +class ConsultorDetalhadoSchema(BaseModel): + id_pessoa: int + nome: str + cpf: Optional[str] = None + anos_atuacao: float + ativo: bool + veterano: bool + coordenacoes_capes: List[CoordenacaoCapesSchema] + coordenacoes_programas: List[CoordenacaoProgramaSchema] + consultoria: Optional[ConsultoriaSchema] = None + premiacoes: List[PremiacaoSchema] + pontuacao: PontuacaoCompletaSchema + rank: Optional[int] = None + + +class RankingResponseSchema(BaseModel): + total: int + limite: int + offset: int + consultores: List[ConsultorResumoSchema] + + +class RankingDetalhadoResponseSchema(BaseModel): + total: int + limite: int + consultores: List[ConsultorDetalhadoSchema] diff --git a/backend/src/main.py b/backend/src/main.py new file mode 100644 index 0000000..6300a1e --- /dev/null +++ b/backend/src/main.py @@ -0,0 +1,12 @@ +import uvicorn +from interface.api.app import app +from interface.api.config import settings + +if __name__ == "__main__": + uvicorn.run( + "interface.api.app:app", + host=settings.API_HOST, + port=settings.API_PORT, + reload=settings.API_RELOAD, + log_level=settings.LOG_LEVEL.lower(), + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1716692 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +services: + backend: + container_name: ranking_backend + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "8000:8000" + env_file: + - ./backend/.env + environment: + - API_HOST=0.0.0.0 + - API_PORT=8000 + - API_RELOAD=true + - CORS_ORIGINS=http://localhost:5173,http://frontend:5173 + - LOG_LEVEL=INFO + volumes: + - ./backend/src:/app/src + - /etc/localtime:/etc/localtime:ro + networks: + - shared_network + restart: unless-stopped + + frontend: + container_name: ranking_frontend + build: + context: ./frontend + dockerfile: Dockerfile + depends_on: + - backend + ports: + - "5173:5173" + volumes: + - ./frontend/src:/app/src + - ./frontend/index.html:/app/index.html + - ./frontend/vite.config.js:/app/vite.config.js + - /etc/localtime:/etc/localtime:ro + links: + - backend + networks: + - shared_network + restart: unless-stopped + +networks: + shared_network: + external: true diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..db6ef60 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/frontend/index.html b/frontend/frontend/index.html new file mode 100644 index 0000000..e69de29 diff --git a/frontend/frontend/package.json b/frontend/frontend/package.json new file mode 100644 index 0000000..e69de29 diff --git a/frontend/frontend/src/App.css b/frontend/frontend/src/App.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/frontend/frontend/src/App.css @@ -0,0 +1 @@ + diff --git a/frontend/frontend/src/App.jsx b/frontend/frontend/src/App.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/frontend/src/index.css b/frontend/frontend/src/index.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/frontend/src/main.jsx b/frontend/frontend/src/main.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/frontend/vite.config.js b/frontend/frontend/vite.config.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9faeb77 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Ranking de Consultores CAPES + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..393ef37 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_pass http://backend:8000/api; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + gzip on; + gzip_vary on; + gzip_min_length 10240; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json; + gzip_disable "MSIE [1-6]\."; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..960b3c5 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1796 @@ +{ + "name": "ranking-capes-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ranking-capes-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.5", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz", + "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..274b6de --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "ranking-capes-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "axios": "^1.6.5" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..abd08dd --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,97 @@ +.container { + width: 100%; +} + +.loading, +.error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + text-align: center; + gap: 1rem; +} + +.loading { + font-size: 1.2rem; + color: var(--accent-2); +} + +.error { + background: rgba(255, 59, 48, 0.1); + border: 1px solid rgba(255, 59, 48, 0.3); + border-radius: 12px; + padding: 2rem; +} + +.error h2 { + color: #ff3b30; + margin-bottom: 0.5rem; +} + +.error button { + margin-top: 1rem; + padding: 0.6rem 1.2rem; + background: var(--accent); + border: none; + border-radius: 8px; + color: white; + font-weight: 600; + cursor: pointer; + transition: background 200ms; +} + +.error button:hover { + background: var(--accent-2); +} + +.controls { + margin: 1.5rem 0; + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: center; +} + +.controls label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: var(--muted); +} + +.controls select { + padding: 0.5rem 1rem; + background: rgba(255,255,255,0.06); + border: 1px solid var(--stroke); + border-radius: 8px; + color: var(--text); + font-size: 0.9rem; + cursor: pointer; + transition: border-color 200ms; +} + +.controls select:hover { + border-color: var(--accent-2); +} + +.ranking-list { + display: flex; + flex-direction: column; +} + +footer { + text-align: center; + margin-top: 2.4rem; + padding: 1.4rem 0 0.8rem; + color: var(--muted); + border-top: 1px solid var(--stroke); + font-size: 0.9rem; +} + +footer p + p { + margin-top: 0.5rem; + font-size: 0.8rem; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..baa5c29 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,84 @@ +import { useState, useEffect } from 'react'; +import Header from './components/Header'; +import ConsultorCard from './components/ConsultorCard'; +import { rankingService } from './services/api'; +import './App.css'; + +function App() { + const [consultores, setConsultores] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [total, setTotal] = useState(0); + const [limite, setLimite] = useState(10); + + useEffect(() => { + loadRanking(); + }, [limite]); + + const loadRanking = async () => { + try { + setLoading(true); + setError(null); + const response = await rankingService.getRanking(limite); + setConsultores(response.consultores); + setTotal(response.total); + } catch (err) { + console.error('Erro ao carregar ranking:', err); + setError('Erro ao carregar ranking. Verifique se a API está rodando.'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+
Carregando ranking...
+
+ ); + } + + if (error) { + return ( +
+
+

Erro

+

{error}

+ +
+
+ ); + } + + return ( +
+
+ +
+ +
+ +
+ {consultores.map((consultor) => ( + + ))} +
+ +
+

Dados: ATUACAPES (Elasticsearch) + SUCUPIRA_PAINEL (Oracle)

+

Critérios: Minuta Técnica - Ranking AtuaCAPES | Clique em qualquer consultor para ver detalhes

+
+
+ ); +} + +export default App; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ConsultorCard.css b/frontend/src/components/ConsultorCard.css new file mode 100644 index 0000000..6f1cff9 --- /dev/null +++ b/frontend/src/components/ConsultorCard.css @@ -0,0 +1,322 @@ +.ranking-card { + position: relative; + background: linear-gradient(155deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); + border: 1px solid var(--stroke); + border-radius: 18px; + overflow: hidden; + box-shadow: var(--shadow); + transition: transform 200ms ease, border 200ms ease, box-shadow 200ms ease; + cursor: pointer; + margin-bottom: 1rem; +} + +.ranking-card::before { + content: ""; + position: absolute; + inset: -40% auto auto -40%; + width: 60%; + height: 160%; + background: radial-gradient(circle at center, rgba(79,70,229,0.18), transparent 55%); + transform: rotate(-8deg); + opacity: 0.8; + pointer-events: none; +} + +.ranking-card:hover { + transform: translateY(-4px); + border-color: rgba(79,70,229,0.4); + box-shadow: 0 15px 35px rgba(0,0,0,0.5); +} + +.ranking-card.expanded { + border-color: rgba(22,169,250,0.5); + box-shadow: 0 22px 38px rgba(0,0,0,0.55); +} + +.card-main { + position: relative; + display: grid; + grid-template-columns: 70px 1fr auto; + align-items: center; + gap: 1.35rem; + padding: 1.25rem 1.6rem; + z-index: 1; +} + +.rank { + width: 62px; + height: 62px; + display: grid; + place-items: center; + font-size: 1.35rem; + font-weight: 700; + color: var(--text); + border-radius: 14px; + border: 1px solid var(--stroke); + background: linear-gradient(145deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03)); + text-shadow: 0 2px 12px rgba(0,0,0,0.35); +} + +.rank-1 { + background: linear-gradient(145deg, #fbbf24, #f59e0b); + color: #0f172a; +} + +.rank-2 { + background: linear-gradient(145deg, #cbd5e1, #94a3b8); + color: #0f172a; +} + +.rank-3 { + background: linear-gradient(145deg, #fb923c, #f97316); + color: #0f172a; +} + +.card-info { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.consultant-name { + font-size: 1.15rem; + font-weight: 700; + letter-spacing: -0.2px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.65rem; +} + +.consultant-area { + color: var(--muted); + font-size: 0.95rem; +} + +.badge { + font-size: 0.72rem; + padding: 0.28rem 0.55rem; + border-radius: 999px; + letter-spacing: 0.4px; + border: 1px solid rgba(255,255,255,0.25); + text-transform: uppercase; + font-weight: 600; +} + +.badge-ativo { + background: linear-gradient(120deg, var(--success), #16a34a); + color: #fff; +} + +.badge-historico { + background: linear-gradient(120deg, #64748b, #475569); + color: #fff; +} + +.badge-veterano { + background: linear-gradient(120deg, var(--gold), #f59e0b); + color: #0f172a; +} + +.card-stats { + display: flex; + align-items: center; + gap: 1.4rem; + background: rgba(255,255,255,0.04); + padding: 0.65rem 0.9rem; + border-radius: 12px; + border: 1px solid var(--stroke); +} + +.stat { + text-align: center; + min-width: 78px; +} + +.stat-value { + font-size: 1.2rem; + font-weight: 700; +} + +.stat-label { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--muted); +} + +.score-value { + font-size: 1.9rem; + font-weight: 800; + background: linear-gradient(120deg, var(--accent), var(--accent-2)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.expand-icon { + margin-left: 0.5rem; + color: var(--muted); + transition: transform 200ms ease, color 200ms ease; + font-size: 0.9rem; +} + +.ranking-card:hover .expand-icon { + color: var(--accent-2); +} + +.ranking-card.expanded .expand-icon { + transform: rotate(180deg); + color: var(--accent-2); +} + +.card-details { + padding: 0 1.6rem 1.35rem; + border-top: 1px solid var(--stroke); + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)); + position: relative; +} + +.details-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 1.1rem 1.25rem; + padding-top: 1.1rem; +} + +.detail-section { + background: rgba(255,255,255,0.04); + border: 1px solid var(--stroke); + border-radius: 12px; + padding: 0.9rem 1rem 0.75rem; + overflow: hidden; +} + +.detail-section h4 { + color: var(--accent-2); + font-size: 0.78rem; + letter-spacing: 0.9px; + text-transform: uppercase; + margin-bottom: 0.6rem; +} + +.score-breakdown, +.score-breakdown-total { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + margin-top: 0.4rem; +} + +.score-breakdown .score-item, +.score-breakdown-total .score-item { + width: 58px; + height: 58px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.score-item { + background: var(--bg-veil); + border: 1px solid var(--stroke); + border-radius: 10px; + text-align: center; +} + +.score-item.score-total { + background: linear-gradient(145deg, rgba(79,70,229,0.2), rgba(22,169,250,0.2)); + border-color: rgba(79,70,229,0.5); +} + +.score-item-value { + font-size: 1.05rem; + font-weight: 700; +} + +.score-item-label { + font-size: 0.7rem; + letter-spacing: 0.8px; + color: var(--muted); +} + +.extra-details { + margin-top: 1.5rem; + background: rgba(255,255,255,0.02); + border: 1px solid var(--stroke); + border-radius: 12px; + padding: 1rem; +} + +.extra-details h4 { + color: var(--accent-2); + font-size: 0.82rem; + letter-spacing: 0.6px; + text-transform: uppercase; + margin-bottom: 0.8rem; +} + +.list-items { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.list-item { + display: flex; + align-items: center; + gap: 0.8rem; + padding: 0.5rem; + background: rgba(255,255,255,0.03); + border-radius: 8px; + font-size: 0.85rem; +} + +.list-item .muted { + color: var(--muted); + font-size: 0.8rem; +} + +@media (max-width: 1200px) { + .details-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 900px) { + .card-main { + grid-template-columns: 56px 1fr; + align-items: flex-start; + } + + .card-stats { + width: 100%; + justify-content: space-between; + margin-top: 0.65rem; + grid-column: 1 / -1; + } + + .details-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .card-stats { + flex-wrap: wrap; + gap: 0.85rem; + } + + .stat { + flex: 1 1 40%; + } + + .score-value { + font-size: 1.6rem; + } + + .details-grid { + grid-template-columns: 1fr; + } +} diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx new file mode 100644 index 0000000..c49e029 --- /dev/null +++ b/frontend/src/components/ConsultorCard.jsx @@ -0,0 +1,216 @@ +import React, { useState } from 'react'; +import './ConsultorCard.css'; + +const ConsultorCard = ({ consultor }) => { + const [expanded, setExpanded] = useState(false); + + const getRankClass = (rank) => { + if (rank === 1) return 'rank-1'; + if (rank === 2) return 'rank-2'; + if (rank === 3) return 'rank-3'; + return ''; + }; + + const formatDate = (dateStr) => { + if (!dateStr) return 'Atual'; + return new Date(dateStr).toLocaleDateString('pt-BR'); + }; + + const { pontuacao } = consultor; + const { consultoria } = consultor; + + return ( +
setExpanded(!expanded)}> +
+
#{consultor.rank}
+ +
+
+ {consultor.nome} + {consultor.ativo && ATIVO} + {!consultor.ativo && HISTÓRICO} + {consultor.veterano && VETERANO} +
+
+ {consultor.anos_atuacao} anos de atuação + {consultoria && ` | Desde ${formatDate(consultoria.primeiro_evento)}`} +
+
+ +
+ {consultoria && ( + <> +
+
{consultoria.total_eventos}
+
Eventos
+
+
+
{consultoria.eventos_recentes}
+
Recentes
+
+
+
{consultoria.vezes_responsavel}
+
Responsável
+
+ + )} +
+
{consultor.pontuacao_total}
+
Score
+
+
{expanded ? '▲' : '▼'}
+
+
+ + {expanded && ( +
+
+
+

Pontuação Total

+
+
+
0 ? 'var(--accent-2)' : 'var(--muted)' }}> + {pontuacao.componente_a.total} +
+
COMP A
+
+
+
0 ? 'var(--success)' : 'var(--muted)' }}> + {pontuacao.componente_b.total} +
+
COMP B
+
+
+
0 ? 'var(--gold)' : 'var(--muted)' }}> + {pontuacao.componente_c.total} +
+
COMP C
+
+
+
0 ? 'var(--bronze)' : 'var(--muted)' }}> + {pontuacao.componente_d.total} +
+
COMP D
+
+
+
{pontuacao.pontuacao_total}
+
TOTAL
+
+
+
+ + + + + + + + +
+ + {consultor.coordenacoes_capes?.length > 0 && ( +
+

Coordenações CAPES

+
+ {consultor.coordenacoes_capes.map((coord, idx) => ( +
+ {coord.tipo} + {coord.area_avaliacao} + + {formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)} + +
+ ))} +
+
+ )} + + {consultor.coordenacoes_programas?.length > 0 && ( +
+

Coordenações de Programa (PPG)

+
+ {consultor.coordenacoes_programas.map((coord, idx) => ( +
+ {coord.nota_ppg} + {coord.nome_programa} + {coord.area_avaliacao} + + {formatDate(coord.periodo.inicio)} - {formatDate(coord.periodo.fim)} + +
+ ))} +
+
+ )} + + {consultor.premiacoes?.length > 0 && ( +
+

Premiações

+
+ {consultor.premiacoes.map((prem, idx) => ( +
+ {prem.pontos} pts + {prem.nome_premio} + {prem.ano} +
+ ))} +
+
+ )} +
+ )} +
+ ); +}; + +const ComponenteDetalhes = ({ titulo, componente, cor }) => ( +
+

{titulo}

+
+
+
{componente.base}
+
BASE
+
+
+
{componente.tempo}
+
TEMPO
+
+
+
{componente.extras}
+
EXTRAS
+
+
+
{componente.bonus}
+
BÔNUS
+
+ {componente.retorno > 0 && ( +
+
{componente.retorno}
+
RETORNO
+
+ )} +
+
{componente.total}
+
TOTAL
+
+
+
+); + +export default ConsultorCard; diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css new file mode 100644 index 0000000..e837321 --- /dev/null +++ b/frontend/src/components/Header.css @@ -0,0 +1,107 @@ +.header { + padding: 1.5rem 1.25rem 1.75rem; + background: linear-gradient(145deg, rgba(79,70,229,0.18), rgba(22,169,250,0.14)); + border: 1px solid var(--stroke); + border-radius: 18px; + box-shadow: var(--shadow); + position: relative; + overflow: hidden; + margin-bottom: 1.75rem; +} + +.header::after { + content: ""; + position: absolute; + inset: 0; + background: radial-gradient(circle at 30% 0%, rgba(255,255,255,0.18), transparent 35%); + pointer-events: none; +} + +.header-content { + position: relative; + z-index: 1; +} + +.header h1 { + font-size: clamp(1.9rem, 3vw, 2.5rem); + font-weight: 700; + letter-spacing: -0.5px; + margin-bottom: 0.35rem; +} + +.subtitle { + color: var(--silver); + font-size: 0.95rem; + line-height: 1.6; + margin-bottom: 0.6rem; +} + +.meta { + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.6rem; + padding: 0.4rem 0.75rem; + background: rgba(255,255,255,0.08); + border: 1px solid var(--stroke); + border-radius: 999px; + font-size: 0.85rem; + color: var(--muted); +} + +.criteria-box { + background: rgba(79,70,229,0.1); + border: 1px solid rgba(79,70,229,0.3); + border-radius: 12px; + padding: 1rem; + margin-top: 1.25rem; +} + +.criteria-box h3 { + color: var(--accent-2); + font-size: 0.9rem; + margin-bottom: 0.75rem; + text-align: center; +} + +.criteria-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +.criteria-section { + background: rgba(255,255,255,0.04); + border: 1px solid var(--stroke); + border-radius: 10px; + padding: 0.9rem; +} + +.criteria-section h4 { + color: var(--accent-2); + font-size: 0.82rem; + margin-bottom: 0.6rem; + letter-spacing: 0.4px; +} + +.criteria-section ul { + list-style: none; + font-size: 0.8rem; + line-height: 1.7; + color: var(--muted); +} + +.criteria-section li { + padding: 0.2rem 0; + border-bottom: 1px dashed rgba(255,255,255,0.05); +} + +.criteria-section li:last-child { + border-bottom: none; +} + +@media (max-width: 768px) { + .criteria-grid { + grid-template-columns: 1fr; + } +} diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..f2511e0 --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import './Header.css'; + +const Header = ({ total }) => { + const dataGeracao = new Date().toLocaleDateString('pt-BR'); + + return ( +
+
+

Ranking de Consultores CAPES

+

+ Sistema completo de pontuação baseado na Minuta Técnica | + 4 Componentes: Coordenação CAPES + PPG + Consultoria + Premiações +

+
+ Gerado em {dataGeracao} | Total: {total} consultores +
+ +
+

Componentes de Pontuação

+
+
+

A - Coordenação CAPES (máx 450 pts)

+
    +
  • CA: 200 base + 100 tempo + 100 áreas + 20 retorno + 30 bônus
  • +
  • CAJ: 150 base + 80 tempo + 100 áreas + 20 retorno + 20 bônus
  • +
  • CAJ-MP: 120 base + 60 tempo + 100 áreas + 20 retorno + 15 bônus
  • +
  • CAM: 100 base + 50 tempo + 100 áreas + 20 retorno + 10 bônus
  • +
+
+ +
+

B - Coordenação Programa PPG (máx 180 pts)

+
    +
  • Base: 70 pts
  • +
  • Tempo: 5 pts/ano (máx 50 pts)
  • +
  • Programas extras: 20 pts/programa (máx 40 pts)
  • +
  • Bônus ativo: 20 pts
  • +
+
+ +
+

C - Consultoria (máx 230 pts)

+
    +
  • Ativo: 150 pts base | Histórico: 100 pts base
  • +
  • Tempo: 5 pts/ano (máx 50 pts)
  • +
  • Eventos: 2 pts/evento (máx 20 pts)
  • +
  • Responsável: 5 pts/vez (máx 25 pts)
  • +
  • Áreas extras: 10 pts/área (máx 30 pts)
  • +
+
+ +
+

D - Premiações (máx 180 pts)

+
    +
  • Premiação: 60 pts
  • +
  • Avaliação: 40 pts
  • +
  • Inscrição: 20 pts
  • +
  • Total máximo: 180 pts
  • +
+
+
+
+
+
+ ); +}; + +export default Header; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..29287b5 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,49 @@ +:root { + --bg-base: #0b1220; + --bg-veil: rgba(255,255,255,0.03); + --card: rgba(255,255,255,0.06); + --card-strong: rgba(255,255,255,0.12); + --stroke: rgba(255,255,255,0.09); + --accent: #4f46e5; + --accent-2: #16a9fa; + --success: #22c55e; + --gold: #fbbf24; + --silver: #cbd5e1; + --bronze: #fb923c; + --text: #f8fafc; + --muted: #94a3b8; + --shadow: 0 25px 50px -12px rgba(0,0,0,0.45); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, sans-serif; + background: radial-gradient(circle at 15% 20%, rgba(79,70,229,0.18), transparent 25%), + radial-gradient(circle at 75% 0%, rgba(22,169,250,0.18), transparent 26%), + radial-gradient(circle at 80% 80%, rgba(34,197,94,0.12), transparent 30%), + var(--bg-base); + color: var(--text); + min-height: 100vh; + padding: 2.25rem; + position: relative; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0)); + pointer-events: none; + backdrop-filter: blur(12px); + z-index: -1; +} + +#root { + max-width: 1280px; + margin: 0 auto; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..3d88954 --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,29 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api/v1', + headers: { + 'Content-Type': 'application/json', + }, +}); + +export const rankingService = { + async getRanking(limite = 100, componente = null) { + const params = { limite }; + if (componente) params.componente = componente; + const response = await api.get('/ranking/detalhado', { params }); + return response.data; + }, + + async getConsultor(idPessoa) { + const response = await api.get(`/consultor/${idPessoa}`); + return response.data; + }, + + async getHealth() { + const response = await api.get('/health'); + return response.data; + }, +}; + +export default api; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..1f23be9 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: 'http://backend:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/index-teste.html b/index-teste.html new file mode 100644 index 0000000..f2dfa8e --- /dev/null +++ b/index-teste.html @@ -0,0 +1,152 @@ + + + + + +Ranking de Consultores CAPES - Teste + + + + + + +
+
+

Ranking de Consultores CAPES

+

Sistema completo - 4 Componentes (Coordenação CAPES + PPG + Consultoria + Premiações)

+
+ +
+ +
+ +
+
Carregando ranking...
+
+
+ + + +