Initial project import

This commit is contained in:
Lucas
2026-06-26 18:07:04 -03:00
commit 6443c0e2dc
48 changed files with 3948 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
.venv/
venv/
env/
__pycache__/
**/__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
resultados/
logs/
*.log
temp/
*.tmp
*.temp
~$*
.env
.env.*

View File

@@ -0,0 +1,7 @@
{
"folders": [
{
"path": ".."
}
]
}

View File

@@ -0,0 +1,7 @@
{
"folders": [
{
"path": ".."
}
]
}

18
Abrir Planejador.vbs Normal file
View File

@@ -0,0 +1,18 @@
Set shell = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
baseDir = fso.GetParentFolderName(WScript.ScriptFullName)
pythonw = baseDir & "\.venv\Scripts\pythonw.exe"
python = baseDir & "\.venv\Scripts\python.exe"
app = baseDir & "\web_app.py"
If fso.FileExists(pythonw) Then
exe = pythonw
ElseIf fso.FileExists(python) Then
exe = python
Else
exe = "pythonw"
End If
shell.CurrentDirectory = baseDir
shell.Run """" & exe & """ """ & app & """", 0, False

216
README.md Normal file
View File

@@ -0,0 +1,216 @@
# Planejador Diario de Missoes e Sobreaviso
Projeto em Python para gerar a escala diaria de missao acionada, missao local e sobreaviso usando Programacao Linear Inteira Mista (MILP).
## 1. Estrutura do projeto
```text
dados/ Arquivos de entrada do planejamento
resultados/ Planilhas geradas pelo planejador
src/planejador_missao/ Codigo-fonte Python do modelo
scripts/ Scripts numerados de execucao, validacao e teste
.vscode/ Arquivos de workspace do VS Code
web_app.py Interface web HTML para abrir no navegador
run_planner.py Ponto de entrada para executar o planejamento
requirements.txt Dependencias Python
Abrir Planejador.vbs Atalho para abrir a interface sem prompt
```
A execucao oficial do projeto e feita somente em Python.
## 2. Preparacao do ambiente
No PowerShell, a partir da raiz do projeto:
```powershell
cd "C:\caminho\para\planejador_missao_limpo_milp"
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
```
A pasta `.venv` nao acompanha a entrega oficial. Ela deve ser recriada pelo usuario com os comandos acima.
## 3. Como executar pelo app no navegador
Forma recomendada:
1. De dois cliques em `Abrir Planejador.vbs`.
2. Aguarde o navegador abrir em `http://127.0.0.1:8050`.
Se preferir executar manualmente pelo PowerShell, use:
Na raiz do projeto:
```powershell
python web_app.py
```
O navegador abre automaticamente em `http://127.0.0.1:8050`. Se nao abrir, copie esse endereco no Google Chrome.
O app permite:
- editar data e criterios de otimizacao;
- informar a missao de rota acionada;
- marcar condicao das aeronaves;
- selecionar tripulantes disponiveis;
- consultar a aba `Quadrinhos`, com proxima OI, horas do ano, SBV e falta para 50 horas;
- salvar os arquivos de entrada em `dados/`;
- gerar a escala e abrir a ultima planilha.
A versao final usa Python e interface web local.
## 4. Como executar pelo terminal
Na raiz do projeto:
```powershell
python scripts\00_main.py
```
Ao final, o programa informa:
- data de planejamento usada;
- caminho da planilha Excel gerada;
- quantidade de colunas candidatas criadas;
- quantidade de escalas selecionadas pelo MILP.
## 5. Arquivos de entrada
Os arquivos de entrada ficam em `dados/`.
| Arquivo | Finalidade |
| --- | --- |
| `Modelagem_C98_ETA2_local.xlsx` ou `Modelagem C98 ETA2.xlsx` | Cadastro base de tripulantes, qualificacoes, projetos, soldo e metas de horas. |
| `catalogo_ois.xlsx` | Catalogo de OIs por aeronave, subprograma, ordem e tipo de missao. |
| `indisponibilidades_2026.xlsx` | Periodos em que cada tripulante nao pode ser escalado. |
| `parametros_missao.csv` | Data de planejamento e criterios de otimizacao. |
| `aeronaves_disponiveis.csv` | Condicao diaria de cada aeronave. |
| `rotas_acionadas.csv` | Missao de rota acionada para atendimento obrigatorio. |
| `tripulantes_disponiveis.csv` | Selecao manual dos tripulantes disponiveis no dia. |
| `progresso_ois_2026.xlsx` | OIs ja concluidas manualmente. |
| `historico_horas_voadas.csv` | Historico acumulado gerado automaticamente pelo planejador. |
## 6. Saida gerada
O planejador cria uma planilha em:
```text
resultados/planejamento_diario_YYYY-MM-DD.xlsx
```
Quando ja existe arquivo para a mesma data, o sistema acrescenta o horario ao nome para preservar a saida anterior.
A aba `ESCALA DIARIA` contem tres blocos:
- `MISSAO ACIONADA`;
- `VOOS LOCAIS`;
- `SOBREAVISO`.
Apos a solucao, o arquivo `dados/historico_horas_voadas.csv` tambem e atualizado com horas voadas e sobreavisos registrados.
## 7. Scripts e validacao com voos de 2025
O fluxo pelo VS Code pode ser executado com:
```powershell
python scripts\00_main.py --modo diario
```
Para conferir rapidamente o ambiente:
```powershell
python scripts\09_testar_instalacao.py
```
Para validar o modelo com dados historicos, coloque os voos reais de 2025 em:
```text
dados/validacao/voos_2025.csv
```
O arquivo deve ter as colunas:
```text
data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv
```
Se a planilha `Quadro de Voo 2025 (2).xlsx` estiver em `dados/`, a validacao importa a aba `VOOS` automaticamente:
```powershell
python scripts\00_main.py --validacao 2025
```
Tambem e possivel chamar diretamente:
```powershell
python scripts\07_validacao_2025.py
```
Os relatorios sao salvos em:
```text
resultados/validacao/validacao_2025_resumo.xlsx
resultados/validacao/validacao_2025_detalhada.xlsx
resultados/validacao/validacao_2025_metricas.csv
resultados/validacao/validacao_2025_barras.png
```
Ele contem:
- `metricas`: comparacao entre escala real 2025 e escala otimizada;
- `comparativo_trips`: horas reais, horas otimizadas e delta por tripulante;
- `voos_2025_slots`: voos historicos agregados em slots;
- `escala_otimizada`: redistribuicao proposta pelo MILP.
Os scripts antigos de importacao/validacao foram preservados em `scripts/_arquivados/`.
## 8. Metodo de otimizacao
O modelo monta previamente todas as colunas candidatas viaveis. Cada coluna representa uma possivel escala de dupla para:
- rota acionada;
- missao local;
- sobreaviso.
Cada coluna recebe uma variavel binaria no MILP:
```text
x[i] = 1 se a coluna candidata i for escolhida
x[i] = 0 caso contrario
```
A funcao objetivo maximiza o score operacional, com prioridade para:
1. atender rotas acionadas;
2. aproveitar missoes locais para progressao operacional;
3. escolher sobreavisos conforme o criterio definido.
Restricoes principais:
- cada tripulante aparece no maximo uma vez na escala;
- cada aeronave livre recebe exatamente uma cobertura principal, por rota acionada ou sobreaviso;
- cada aeronave executa no maximo uma rota acionada;
- cada aeronave executa no maximo uma missao local;
- rota acionada e missao local nao podem usar a mesma aeronave no mesmo dia;
- toda rota acionada informada deve ser atendida exatamente uma vez.
O resolvedor usado e `scipy.optimize.milp`, um resolvedor exato de programacao inteira disponivel diretamente em Python.
## 9. Organizacao do codigo
O codigo em `src/planejador_missao/` esta dividido por blocos funcionais:
| Arquivo | Responsabilidade |
| --- | --- |
| `data_io.py` | Leitura das planilhas/CSVs e escrita do historico. |
| `rules.py` | Regras de disponibilidade, qualificacao e progresso operacional. |
| `candidates.py` | Geracao das colunas candidatas para o MILP. |
| `optimizer.py` | Formulacao e solucao do modelo MILP. |
| `report.py` | Registro de horas e criacao da planilha Excel. |
| `quadrinhos.py` | Resumo operacional da aba Quadrinhos. |
| `validacao.py` | Validacao retrospectiva com voos historicos. |
| `main.py` | Orquestracao do fluxo completo. |
| `utils.py` | Normalizacao de textos, datas, booleanos e horas. |
Os comentarios no codigo indicam os blocos do fluxo e as restricoes relevantes do modelo, sem repetir conceitos elementares da linguagem.

View File

@@ -0,0 +1,3 @@
@echo off
wscript.exe "%~dp0Abrir Planejador.vbs"
exit /b

View File

@@ -0,0 +1,19 @@
Set shell = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
atalhosDir = fso.GetParentFolderName(WScript.ScriptFullName)
baseDir = fso.GetParentFolderName(atalhosDir)
pythonw = baseDir & "\.venv\Scripts\pythonw.exe"
python = baseDir & "\.venv\Scripts\python.exe"
app = baseDir & "\web_app.py"
If fso.FileExists(pythonw) Then
exe = pythonw
ElseIf fso.FileExists(python) Then
exe = python
Else
exe = "pythonw"
End If
shell.CurrentDirectory = baseDir
shell.Run """" & exe & """ """ & app & """", 0, False

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
aeronave,disponivel_sede,em_pane,em_missao_rota,hora_livre
C98,True,False,False,50:00
C97,True,False,False,50:00
C95,True,False,False,50:00
1 aeronave disponivel_sede em_pane em_missao_rota hora_livre
2 C98 True False False 50:00
3 C97 True False False 50:00
4 C95 True False False 50:00

BIN
dados/assets/app_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
dados/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
dados/catalogo_ois.xlsx Normal file

Binary file not shown.

View File

@@ -0,0 +1,49 @@
data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv,origem_registro,registrado_em
2026-06-18,C97,MISSAO_LOCAL,AEU,TRIP2,,1.5,0,escala_diaria,2026-06-18 10:35:24
2026-06-18,C97,MISSAO_LOCAL,LPS,TRIP1,17TL17D31,1.5,0,escala_diaria,2026-06-18 10:35:24
2026-06-18,C95,ROTA_ACIONADA,ISA,TRIP1,69TF01DN03,6.0,0,escala_diaria,2026-06-18 10:35:24
2026-06-18,C95,ROTA_ACIONADA,MES,TRIP2,,6.0,0,escala_diaria,2026-06-18 10:35:24
2026-06-18,C97,SBV,MAT,TRIP1,,0.0,1,escala_diaria,2026-06-18 10:35:24
2026-06-18,C97,SBV,STS,TRIP2,,0.0,1,escala_diaria,2026-06-18 10:35:24
2026-06-22,C97,ROTA_ACIONADA,JVT,TRIP1,00TF86D01,6.0,0,escala_diaria,2026-06-22 08:49:59
2026-06-22,C97,ROTA_ACIONADA,AEU,TRIP2,,6.0,0,escala_diaria,2026-06-22 08:49:59
2026-06-22,C95,MISSAO_LOCAL,ISA,TRIP1,69TF01D04,1.0,0,escala_diaria,2026-06-22 08:49:59
2026-06-22,C95,MISSAO_LOCAL,MES,TRIP2,,1.0,0,escala_diaria,2026-06-22 08:49:59
2026-06-22,C95,SBV,BRJ,TRIP1,,0.0,1,escala_diaria,2026-06-22 08:49:59
2026-06-22,C95,SBV,KVN,TRIP2,,0.0,1,escala_diaria,2026-06-22 08:49:59
2026-06-26,C95,ROTA_ACIONADA,SOL,TRIP1,69TS01D01,6.0,0,escala_diaria,2026-06-26 09:41:10
2026-06-26,C95,ROTA_ACIONADA,MES,TRIP2,,6.0,0,escala_diaria,2026-06-26 09:41:10
2026-06-26,C97,MISSAO_LOCAL,MHL,TRIP1,01TF01D21,1.5,0,escala_diaria,2026-06-26 09:41:10
2026-06-26,C97,MISSAO_LOCAL,MAT,TRIP2,,1.5,0,escala_diaria,2026-06-26 09:41:10
2026-06-26,C97,SBV,SEI,TRIP1,,0.0,1,escala_diaria,2026-06-26 09:41:10
2026-06-26,C97,SBV,LPS,TRIP2,,0.0,1,escala_diaria,2026-06-26 09:41:10
2026-06-26,C95,ROTA_ACIONADA,SOL,TRIP1,69TS01D02,6.0,0,escala_diaria,2026-06-26 12:32:06
2026-06-26,C95,ROTA_ACIONADA,MES,TRIP2,,6.0,0,escala_diaria,2026-06-26 12:32:06
2026-06-26,C98,MISSAO_LOCAL,MCH,TRIP1,01TL01D31,1.0,0,escala_diaria,2026-06-26 12:32:06
2026-06-26,C98,MISSAO_LOCAL,SLS,TRIP2,,1.0,0,escala_diaria,2026-06-26 12:32:06
2026-06-26,C98,SBV,GMS,TRIP1,,0.0,1,escala_diaria,2026-06-26 12:32:06
2026-06-26,C98,SBV,LRS,TRIP2,,0.0,1,escala_diaria,2026-06-26 12:32:06
2026-06-26,C97,MISSAO_LOCAL,LPS,TRIP1,17TL17D32,1.5,0,escala_diaria,2026-06-26 12:32:06
2026-06-26,C97,MISSAO_LOCAL,MAT,TRIP2,,1.5,0,escala_diaria,2026-06-26 12:32:06
2026-06-26,C97,SBV,FIA,TRIP1,,0.0,1,escala_diaria,2026-06-26 12:32:06
2026-06-26,C97,SBV,AEU,TRIP2,,0.0,1,escala_diaria,2026-06-26 12:32:06
2026-06-26,C97,ROTA_ACIONADA,LPS,TRIP1,01TL01D41,6.0,0,escala_diaria,2026-06-26 13:35:48
2026-06-26,C97,ROTA_ACIONADA,MAT,TRIP2,,6.0,0,escala_diaria,2026-06-26 13:35:48
2026-06-26,C98,MISSAO_LOCAL,MCH,TRIP1,04TL01N32,1.0,0,escala_diaria,2026-06-26 13:35:48
2026-06-26,C98,MISSAO_LOCAL,SLS,TRIP2,,1.0,0,escala_diaria,2026-06-26 13:35:48
2026-06-26,C98,SBV,CFF,TRIP1,,0.0,1,escala_diaria,2026-06-26 13:35:48
2026-06-26,C98,SBV,SUG,TRIP2,,0.0,1,escala_diaria,2026-06-26 13:35:48
2026-06-26,C95,MISSAO_LOCAL,SOL,TRIP1,06TT36D01,1.0,0,escala_diaria,2026-06-26 13:35:48
2026-06-26,C95,MISSAO_LOCAL,MES,TRIP2,,1.0,0,escala_diaria,2026-06-26 13:35:48
2026-06-26,C95,SBV,ISA,TRIP1,,0.0,1,escala_diaria,2026-06-26 13:35:48
2026-06-26,C95,SBV,ATA,TRIP2,,0.0,1,escala_diaria,2026-06-26 13:35:48
2026-06-26,C97,ROTA_ACIONADA,HCK,TRIP1,00TF86D01,6.0,0,escala_diaria,2026-06-26 14:29:27
2026-06-26,C97,ROTA_ACIONADA,AEU,TRIP2,,6.0,0,escala_diaria,2026-06-26 14:29:27
2026-06-26,C98,MISSAO_LOCAL,LRS,TRIP1,01TT01D01,1.0,0,escala_diaria,2026-06-26 14:29:27
2026-06-26,C98,MISSAO_LOCAL,CFF,TRIP2,,1.0,0,escala_diaria,2026-06-26 14:29:27
2026-06-26,C98,SBV,SLS,TRIP1,,0.0,1,escala_diaria,2026-06-26 14:29:27
2026-06-26,C98,SBV,MCH,TRIP2,,0.0,1,escala_diaria,2026-06-26 14:29:27
2026-06-26,C95,MISSAO_LOCAL,SOL,TRIP1,06TT36D02,1.0,0,escala_diaria,2026-06-26 14:29:27
2026-06-26,C95,MISSAO_LOCAL,MES,TRIP2,,1.0,0,escala_diaria,2026-06-26 14:29:27
2026-06-26,C95,SBV,ISA,TRIP1,,0.0,1,escala_diaria,2026-06-26 14:29:27
2026-06-26,C95,SBV,KVN,TRIP2,,0.0,1,escala_diaria,2026-06-26 14:29:27
1 data aeronave tipo_escala tripulante funcao oi horas_voadas sbv origem_registro registrado_em
2 2026-06-18 C97 MISSAO_LOCAL AEU TRIP2 1.5 0 escala_diaria 2026-06-18 10:35:24
3 2026-06-18 C97 MISSAO_LOCAL LPS TRIP1 17TL17D31 1.5 0 escala_diaria 2026-06-18 10:35:24
4 2026-06-18 C95 ROTA_ACIONADA ISA TRIP1 69TF01DN03 6.0 0 escala_diaria 2026-06-18 10:35:24
5 2026-06-18 C95 ROTA_ACIONADA MES TRIP2 6.0 0 escala_diaria 2026-06-18 10:35:24
6 2026-06-18 C97 SBV MAT TRIP1 0.0 1 escala_diaria 2026-06-18 10:35:24
7 2026-06-18 C97 SBV STS TRIP2 0.0 1 escala_diaria 2026-06-18 10:35:24
8 2026-06-22 C97 ROTA_ACIONADA JVT TRIP1 00TF86D01 6.0 0 escala_diaria 2026-06-22 08:49:59
9 2026-06-22 C97 ROTA_ACIONADA AEU TRIP2 6.0 0 escala_diaria 2026-06-22 08:49:59
10 2026-06-22 C95 MISSAO_LOCAL ISA TRIP1 69TF01D04 1.0 0 escala_diaria 2026-06-22 08:49:59
11 2026-06-22 C95 MISSAO_LOCAL MES TRIP2 1.0 0 escala_diaria 2026-06-22 08:49:59
12 2026-06-22 C95 SBV BRJ TRIP1 0.0 1 escala_diaria 2026-06-22 08:49:59
13 2026-06-22 C95 SBV KVN TRIP2 0.0 1 escala_diaria 2026-06-22 08:49:59
14 2026-06-26 C95 ROTA_ACIONADA SOL TRIP1 69TS01D01 6.0 0 escala_diaria 2026-06-26 09:41:10
15 2026-06-26 C95 ROTA_ACIONADA MES TRIP2 6.0 0 escala_diaria 2026-06-26 09:41:10
16 2026-06-26 C97 MISSAO_LOCAL MHL TRIP1 01TF01D21 1.5 0 escala_diaria 2026-06-26 09:41:10
17 2026-06-26 C97 MISSAO_LOCAL MAT TRIP2 1.5 0 escala_diaria 2026-06-26 09:41:10
18 2026-06-26 C97 SBV SEI TRIP1 0.0 1 escala_diaria 2026-06-26 09:41:10
19 2026-06-26 C97 SBV LPS TRIP2 0.0 1 escala_diaria 2026-06-26 09:41:10
20 2026-06-26 C95 ROTA_ACIONADA SOL TRIP1 69TS01D02 6.0 0 escala_diaria 2026-06-26 12:32:06
21 2026-06-26 C95 ROTA_ACIONADA MES TRIP2 6.0 0 escala_diaria 2026-06-26 12:32:06
22 2026-06-26 C98 MISSAO_LOCAL MCH TRIP1 01TL01D31 1.0 0 escala_diaria 2026-06-26 12:32:06
23 2026-06-26 C98 MISSAO_LOCAL SLS TRIP2 1.0 0 escala_diaria 2026-06-26 12:32:06
24 2026-06-26 C98 SBV GMS TRIP1 0.0 1 escala_diaria 2026-06-26 12:32:06
25 2026-06-26 C98 SBV LRS TRIP2 0.0 1 escala_diaria 2026-06-26 12:32:06
26 2026-06-26 C97 MISSAO_LOCAL LPS TRIP1 17TL17D32 1.5 0 escala_diaria 2026-06-26 12:32:06
27 2026-06-26 C97 MISSAO_LOCAL MAT TRIP2 1.5 0 escala_diaria 2026-06-26 12:32:06
28 2026-06-26 C97 SBV FIA TRIP1 0.0 1 escala_diaria 2026-06-26 12:32:06
29 2026-06-26 C97 SBV AEU TRIP2 0.0 1 escala_diaria 2026-06-26 12:32:06
30 2026-06-26 C97 ROTA_ACIONADA LPS TRIP1 01TL01D41 6.0 0 escala_diaria 2026-06-26 13:35:48
31 2026-06-26 C97 ROTA_ACIONADA MAT TRIP2 6.0 0 escala_diaria 2026-06-26 13:35:48
32 2026-06-26 C98 MISSAO_LOCAL MCH TRIP1 04TL01N32 1.0 0 escala_diaria 2026-06-26 13:35:48
33 2026-06-26 C98 MISSAO_LOCAL SLS TRIP2 1.0 0 escala_diaria 2026-06-26 13:35:48
34 2026-06-26 C98 SBV CFF TRIP1 0.0 1 escala_diaria 2026-06-26 13:35:48
35 2026-06-26 C98 SBV SUG TRIP2 0.0 1 escala_diaria 2026-06-26 13:35:48
36 2026-06-26 C95 MISSAO_LOCAL SOL TRIP1 06TT36D01 1.0 0 escala_diaria 2026-06-26 13:35:48
37 2026-06-26 C95 MISSAO_LOCAL MES TRIP2 1.0 0 escala_diaria 2026-06-26 13:35:48
38 2026-06-26 C95 SBV ISA TRIP1 0.0 1 escala_diaria 2026-06-26 13:35:48
39 2026-06-26 C95 SBV ATA TRIP2 0.0 1 escala_diaria 2026-06-26 13:35:48
40 2026-06-26 C97 ROTA_ACIONADA HCK TRIP1 00TF86D01 6.0 0 escala_diaria 2026-06-26 14:29:27
41 2026-06-26 C97 ROTA_ACIONADA AEU TRIP2 6.0 0 escala_diaria 2026-06-26 14:29:27
42 2026-06-26 C98 MISSAO_LOCAL LRS TRIP1 01TT01D01 1.0 0 escala_diaria 2026-06-26 14:29:27
43 2026-06-26 C98 MISSAO_LOCAL CFF TRIP2 1.0 0 escala_diaria 2026-06-26 14:29:27
44 2026-06-26 C98 SBV SLS TRIP1 0.0 1 escala_diaria 2026-06-26 14:29:27
45 2026-06-26 C98 SBV MCH TRIP2 0.0 1 escala_diaria 2026-06-26 14:29:27
46 2026-06-26 C95 MISSAO_LOCAL SOL TRIP1 06TT36D02 1.0 0 escala_diaria 2026-06-26 14:29:27
47 2026-06-26 C95 MISSAO_LOCAL MES TRIP2 1.0 0 escala_diaria 2026-06-26 14:29:27
48 2026-06-26 C95 SBV ISA TRIP1 0.0 1 escala_diaria 2026-06-26 14:29:27
49 2026-06-26 C95 SBV KVN TRIP2 0.0 1 escala_diaria 2026-06-26 14:29:27

Binary file not shown.

View File

@@ -0,0 +1,4 @@
parametro,valor
data_planejamento,hoje
criterio_missao,meta_50
criterio_sbv,equalizar_quadrinhos
1 parametro valor
2 data_planejamento hoje
3 criterio_missao meta_50
4 criterio_sbv equalizar_quadrinhos

Binary file not shown.

View File

@@ -0,0 +1 @@
tripulante,aeronave,qualificacao_manual,horas_voadas_manual,sbv_manual,ultima_data_voo_manual,observacao
1 tripulante aeronave qualificacao_manual horas_voadas_manual sbv_manual ultima_data_voo_manual observacao

View File

@@ -0,0 +1,2 @@
origem,destino,inicio,fim,tev_horas,pernoite_dias,rota
NT,NT,2026-06-14 08:00,2026-06-17 13:00,6,3,NT-RF-*FZ*-RF-NT
1 origem destino inicio fim tev_horas pernoite_dias rota
2 NT NT 2026-06-14 08:00 2026-06-17 13:00 6 3 NT-RF-*FZ*-RF-NT

View File

@@ -0,0 +1,33 @@
tripulante,disponivel
AEU,True
ATA,True
BEN,True
BMK,True
BRI,True
BRJ,True
CFF,True
DIL,True
DOG,True
FIA,True
GMR,True
GMS,True
HCK,True
ISA,True
JVT,True
KVN,True
LPS,True
LRS,True
MAT,True
MCH,True
MDO,True
MES,True
MHL,True
NSC,True
PFR,True
RCH,True
SCA,True
SEI,True
SLS,True
SOL,True
STS,True
SUG,True
1 tripulante disponivel
2 AEU True
3 ATA True
4 BEN True
5 BMK True
6 BRI True
7 BRJ True
8 CFF True
9 DIL True
10 DOG True
11 FIA True
12 GMR True
13 GMS True
14 HCK True
15 ISA True
16 JVT True
17 KVN True
18 LPS True
19 LRS True
20 MAT True
21 MCH True
22 MDO True
23 MES True
24 MHL True
25 NSC True
26 PFR True
27 RCH True
28 SCA True
29 SEI True
30 SLS True
31 SOL True
32 STS True
33 SUG True

View File

@@ -0,0 +1,769 @@
slot_id,data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv
1,2025-01-02,C98,ROTA,SLS,PILOTO,001,6.67,0
1,2025-01-02,C98,ROTA,DOG,COPILOTO,001,6.67,0
2,2025-01-07,C98,ROTA,SLS,PILOTO,002,6.67,0
2,2025-01-07,C98,ROTA,CFF,COPILOTO,002,6.67,0
3,2025-01-09,C95,LOCAL,JVT,PILOTO,-,0.08,0
3,2025-01-09,C95,LOCAL,GMR,COPILOTO,-,0.08,0
4,2025-01-09,C95,LOCAL,HCK,PILOTO,-,0.08,0
4,2025-01-09,C95,LOCAL,CAR,COPILOTO,-,0.08,0
5,2025-01-09,C97,LOCAL,MES,PILOTO,-,0.08,0
5,2025-01-09,C97,LOCAL,SEI,COPILOTO,-,0.08,0
6,2025-01-09,C97,LOCAL,MAT,PILOTO,-,0.08,0
6,2025-01-09,C97,LOCAL,STS,COPILOTO,-,0.08,0
7,2025-01-15,C98,LOCAL,HAI,PILOTO,011,0.08,0
7,2025-01-15,C98,LOCAL,GMS,COPILOTO,011,0.08,0
8,2025-01-15,C95,ROTA,RAY,PILOTO,003,1.33,0
8,2025-01-15,C95,ROTA,HCK,COPILOTO,003,1.33,0
9,2025-01-20,C97,LOCAL,MES,PILOTO,004,1.0,0
9,2025-01-20,C97,LOCAL,AEU,COPILOTO,004,1.0,0
10,2025-01-20,C97,ROTA,MAT,PILOTO,005,5.67,0
10,2025-01-20,C97,ROTA,STS,COPILOTO,005,5.67,0
11,2025-01-21,C98,LOCAL,CNI,PILOTO,006,1.0,0
11,2025-01-21,C98,LOCAL,DOG,COPILOTO,006,1.0,0
12,2025-01-22,C98,ROTA,SLS,PILOTO,007,13.0,0
12,2025-01-22,C98,ROTA,GMS,COPILOTO,007,13.0,0
13,2025-01-24,C98,ROTA,CFF,PILOTO,009,13.67,0
13,2025-01-24,C98,ROTA,GMS,COPILOTO,009,13.67,0
14,2025-01-24,C98,LOCAL,FER,PILOTO,008,1.0,0
14,2025-01-24,C98,LOCAL,MCH,COPILOTO,008,1.0,0
15,2025-01-25,C98,ROTA,SLS,PILOTO,010,6.67,0
15,2025-01-25,C98,ROTA,DOG,COPILOTO,010,6.67,0
16,2025-01-29,C98,ROTA,FER,PILOTO,012,12.33,0
16,2025-01-29,C98,ROTA,DOG,COPILOTO,012,12.33,0
17,2025-02-03,C98,LOCAL,HAI,PILOTO,014,1.0,0
17,2025-02-03,C98,LOCAL,MED,COPILOTO,014,1.0,0
18,2025-02-03,C98,LOCAL,HAI,PILOTO,015,1.0,0
18,2025-02-03,C98,LOCAL,FIA,COPILOTO,015,1.0,0
19,2025-02-04,C98,ROTA,MED,PILOTO,016,5.92,0
19,2025-02-04,C98,ROTA,CNI,COPILOTO,016,5.92,0
20,2025-02-06,C98,ROTA,FER,PILOTO,018,10.5,0
20,2025-02-06,C98,ROTA,CNI,COPILOTO,018,10.5,0
21,2025-02-07,C98,ROTA,HAI,PILOTO,020,2.0,0
21,2025-02-07,C98,ROTA,FIA,COPILOTO,020,2.0,0
22,2025-02-07,C97,ROTA,AEU,PILOTO,021,7.58,0
22,2025-02-07,C97,ROTA,SEI,COPILOTO,021,7.58,0
23,2025-02-08,C98,ROTA,CFF,PILOTO,022,10.5,0
23,2025-02-08,C98,ROTA,FER,COPILOTO,022,10.5,0
24,2025-02-09,C97,ROTA,AEU,PILOTO,023,9.42,0
24,2025-02-09,C97,ROTA,SEI,COPILOTO,023,9.42,0
25,2025-02-11,C98,ROTA,FER,PILOTO,026,3.33,0
25,2025-02-11,C98,ROTA,DOG,COPILOTO,026,3.33,0
26,2025-02-12,C98,LOCAL,FIA,PILOTO,027,1.0,0
26,2025-02-12,C98,LOCAL,GMS,COPILOTO,027,1.0,0
27,2025-02-12,C98,ROTA,FER,PILOTO,028,1.67,0
27,2025-02-12,C98,ROTA,DOG,COPILOTO,028,1.67,0
28,2025-02-12,C98,ROTA,MCH,PILOTO,028,1.67,0
28,2025-02-12,C98,ROTA,CNI,COPILOTO,028,1.67,0
29,2025-02-12,C98,ROTA,MCH,PILOTO,026,3.33,0
29,2025-02-12,C98,ROTA,CNI,COPILOTO,026,3.33,0
30,2025-02-13,C98,LOCAL,BRI,PILOTO,029,1.0,0
30,2025-02-13,C98,LOCAL,HAI,COPILOTO,029,1.0,0
31,2025-02-13,C98,LOCAL,SLS,PILOTO,027,0.5,0
31,2025-02-13,C98,LOCAL,DOG,COPILOTO,027,0.5,0
32,2025-02-14,C97,ROTA,AEU,PILOTO,030,5.83,0
32,2025-02-14,C97,ROTA,SEI,COPILOTO,030,5.83,0
33,2025-02-14,C98,LOCAL,BRI,PILOTO,031,1.0,0
33,2025-02-14,C98,LOCAL,FIA,COPILOTO,031,1.0,0
34,2025-02-16,C95,ROTA,RAY,PILOTO,032,4.83,0
34,2025-02-16,C95,ROTA,PCN,COPILOTO,032,4.83,0
35,2025-02-16,C97,ROTA,AEU,PILOTO,033,10.67,0
35,2025-02-16,C97,ROTA,SEI,COPILOTO,033,10.67,0
36,2025-02-17,C98,LOCAL,CNI,PILOTO,CPT,2.0,0
36,2025-02-17,C98,LOCAL,LOU,COPILOTO,CPT,2.0,0
37,2025-02-17,C98,LOCAL,CNI,PILOTO,CPT,2.0,0
37,2025-02-17,C98,LOCAL,PFR,COPILOTO,CPT,2.0,0
38,2025-02-17,C98,LOCAL,FER,PILOTO,CPT,2.0,0
38,2025-02-17,C98,LOCAL,BEN,COPILOTO,CPT,2.0,0
39,2025-02-17,C98,LOCAL,FER,PILOTO,CPT,2.0,0
39,2025-02-17,C98,LOCAL,RCH,COPILOTO,CPT,2.0,0
40,2025-02-18,C98,LOCAL,CNI,PILOTO,CPT,1.0,0
40,2025-02-18,C98,LOCAL,LOU,COPILOTO,CPT,1.0,0
41,2025-02-18,C98,LOCAL,FER,PILOTO,CPT,2.0,0
41,2025-02-18,C98,LOCAL,SUG,COPILOTO,CPT,2.0,0
42,2025-02-18,C98,LOCAL,FER,PILOTO,CPT,2.0,0
42,2025-02-18,C98,LOCAL,LRS,COPILOTO,CPT,2.0,0
43,2025-02-18,C97,ROTA,AEU,PILOTO,034,7.83,0
43,2025-02-18,C97,ROTA,SEI,COPILOTO,034,7.83,0
44,2025-02-18,C98,LOCAL,MED,PILOTO,036,1.5,0
44,2025-02-18,C98,LOCAL,CNI,COPILOTO,036,1.5,0
45,2025-02-18,C98,LOCAL,FIA,PILOTO,037,1.0,0
45,2025-02-18,C98,LOCAL,CNI,COPILOTO,037,1.0,0
46,2025-02-20,C95,LOCAL,PCN,PILOTO,039,0.08,0
46,2025-02-20,C95,LOCAL,JVT,COPILOTO,039,0.08,0
47,2025-02-20,C98,LOCAL,CNI,PILOTO,040,1.5,0
47,2025-02-20,C98,LOCAL,PFR,COPILOTO,040,1.5,0
48,2025-02-20,C98,LOCAL,CNI,PILOTO,041,1.5,0
48,2025-02-20,C98,LOCAL,BEN,COPILOTO,041,1.5,0
49,2025-02-20,C95,LOCAL,MHL,PILOTO,042,1.0,0
49,2025-02-20,C95,LOCAL,KVN,COPILOTO,042,1.0,0
50,2025-02-21,C97,ROTA,AEU,PILOTO,048,4.0,0
50,2025-02-21,C97,ROTA,SEI,COPILOTO,048,4.0,0
51,2025-02-21,C95,ROTA,CAR,PILOTO,043,3.58,0
51,2025-02-21,C95,ROTA,BRJ,COPILOTO,043,3.58,0
52,2025-02-21,C98,LOCAL,MED,PILOTO,044,1.5,0
52,2025-02-21,C98,LOCAL,RCH,COPILOTO,044,1.5,0
53,2025-02-21,C98,LOCAL,FER,PILOTO,045,1.5,0
53,2025-02-21,C98,LOCAL,LRS,COPILOTO,045,1.5,0
54,2025-02-21,C95,LOCAL,MHL,PILOTO,046,1.0,0
54,2025-02-21,C95,LOCAL,ISA,COPILOTO,046,1.0,0
55,2025-02-24,C98,LOCAL,FER,PILOTO,050,1.5,0
55,2025-02-24,C98,LOCAL,SUG,COPILOTO,050,1.5,0
56,2025-02-24,C98,LOCAL,FIA,PILOTO,051,1.5,0
56,2025-02-24,C98,LOCAL,BEN,COPILOTO,051,1.5,0
57,2025-02-24,C97,LOCAL,AEU,PILOTO,047,0.08,0
57,2025-02-24,C97,LOCAL,MAT,COPILOTO,047,0.08,0
58,2025-02-24,C95,LOCAL,MHL,PILOTO,052,1.0,0
58,2025-02-24,C95,LOCAL,ISA,COPILOTO,052,1.0,0
59,2025-02-24,C95,LOCAL,MHL,PILOTO,053,1.0,0
59,2025-02-24,C95,LOCAL,KVN,COPILOTO,053,1.0,0
60,2025-02-25,C98,ROTA,CNI,PILOTO,054,12.42,0
60,2025-02-25,C98,ROTA,BEN,COPILOTO,054,12.42,0
61,2025-02-25,C97,LOCAL,MES,PILOTO,055,2.0,0
61,2025-02-25,C97,LOCAL,AEU,COPILOTO,055,2.0,0
62,2025-02-25,C97,LOCAL,MES,PILOTO,056,1.5,0
62,2025-02-25,C97,LOCAL,HCK,COPILOTO,056,1.5,0
63,2025-02-26,C97,LOCAL,MES,PILOTO,057,1.5,0
63,2025-02-26,C97,LOCAL,HCK,COPILOTO,057,1.5,0
64,2025-02-27,C98,LOCAL,FER,PILOTO,058,1.5,0
64,2025-02-27,C98,LOCAL,LOU,COPILOTO,058,1.5,0
65,2025-02-27,C98,LOCAL,MED,PILOTO,059,1.5,0
65,2025-02-27,C98,LOCAL,PFR,COPILOTO,059,1.5,0
66,2025-02-27,C95,LOCAL,MHL,PILOTO,060,1.0,0
66,2025-02-27,C95,LOCAL,BRI,COPILOTO,060,1.0,0
67,2025-02-28,C98,LOCAL,MED,PILOTO,061,0.67,0
67,2025-02-28,C98,LOCAL,PFR,COPILOTO,061,0.67,0
68,2025-02-28,C95,LOCAL,JVT,PILOTO,066,0.08,0
68,2025-02-28,C95,LOCAL,HCK,COPILOTO,066,0.08,0
69,2025-02-28,C95,ROTA,RAY,PILOTO,062,9.25,0
69,2025-02-28,C95,ROTA,ISA,COPILOTO,062,9.25,0
70,2025-03-01,C95,ROTA,CAR,PILOTO,063,13.58,0
70,2025-03-01,C95,ROTA,BMK,COPILOTO,063,13.58,0
71,2025-03-01,C95,ROTA,MHL,PILOTO,064,7.42,0
71,2025-03-01,C95,ROTA,KVN,COPILOTO,064,7.42,0
72,2025-03-05,C97,ROTA,MES,PILOTO,065,2.0,0
72,2025-03-05,C97,ROTA,LPS,COPILOTO,065,2.0,0
73,2025-03-05,C95,LOCAL,RAY,PILOTO,067,1.5,0
73,2025-03-05,C95,LOCAL,BRI,COPILOTO,067,1.5,0
74,2025-03-05,C98,LOCAL,FER,PILOTO,068,1.0,0
74,2025-03-05,C98,LOCAL,PFR,COPILOTO,068,1.0,0
75,2025-03-06,C95,ROTA,PCN,PILOTO,069,3.92,0
75,2025-03-06,C95,ROTA,JVT,COPILOTO,069,3.92,0
76,2025-03-06,C98,LOCAL,FIA,PILOTO,070,1.5,0
76,2025-03-06,C98,LOCAL,LRS,COPILOTO,070,1.5,0
77,2025-03-06,C98,LOCAL,CNI,PILOTO,071,1.5,0
77,2025-03-06,C98,LOCAL,RCH,COPILOTO,071,1.5,0
78,2025-03-06,C97,LOCAL,MES,PILOTO,072,2.0,0
78,2025-03-06,C97,LOCAL,AEU,COPILOTO,072,2.0,0
79,2025-03-06,C98,LOCAL,CFF,PILOTO,073,0.08,0
79,2025-03-06,C98,LOCAL,GMS,COPILOTO,073,0.08,0
80,2025-03-06,C98,LOCAL,BRI,PILOTO,074,1.0,0
80,2025-03-06,C98,LOCAL,CNI,COPILOTO,074,1.0,0
81,2025-03-06,C97,LOCAL,MES,PILOTO,075,1.0,0
81,2025-03-06,C97,LOCAL,HCK,COPILOTO,075,1.0,0
82,2025-03-08,C97,ROTA,MES,PILOTO,077,19.33,0
82,2025-03-08,C97,ROTA,LPS,COPILOTO,077,19.33,0
83,2025-03-10,C98,LOCAL,FER,PILOTO,078,1.5,0
83,2025-03-10,C98,LOCAL,LRS,COPILOTO,078,1.5,0
84,2025-03-10,C98,LOCAL,GMS,PILOTO,079,1.0,0
84,2025-03-10,C98,LOCAL,DOG,COPILOTO,079,1.0,0
85,2025-03-11,C97,LOCAL,AEU,PILOTO,080,1.5,0
85,2025-03-11,C97,LOCAL,JVT,COPILOTO,080,1.5,0
86,2025-03-11,C97,LOCAL,AEU,PILOTO,081,1.0,0
86,2025-03-11,C97,LOCAL,MAT,COPILOTO,081,1.0,0
87,2025-03-12,C97,LOCAL,AEU,PILOTO,082,1.5,0
87,2025-03-12,C97,LOCAL,JVT,COPILOTO,082,1.5,0
88,2025-03-12,C98,LOCAL,CNI,PILOTO,083,1.5,0
88,2025-03-12,C98,LOCAL,SUG,COPILOTO,083,1.5,0
89,2025-03-12,C98,LOCAL,HAI,PILOTO,084,1.0,0
89,2025-03-12,C98,LOCAL,MED,COPILOTO,084,1.0,0
90,2025-03-13,C98,LOCAL,CNI,PILOTO,085,1.0,0
90,2025-03-13,C98,LOCAL,FER,COPILOTO,085,1.0,0
91,2025-03-14,C95,ROTA,MHL,PILOTO,086,0.75,0
91,2025-03-14,C95,ROTA,ISA,COPILOTO,086,0.75,0
92,2025-03-15,C97,ROTA,MAT,PILOTO,087,9.75,0
92,2025-03-15,C97,ROTA,LPS,COPILOTO,087,9.75,0
93,2025-03-16,C95,ROTA,MHL,PILOTO,086,0.75,0
93,2025-03-16,C95,ROTA,ISA,COPILOTO,086,0.75,0
94,2025-03-17,C97,LOCAL,MES,PILOTO,089,1.0,0
94,2025-03-17,C97,LOCAL,JVT,COPILOTO,089,1.0,0
95,2025-03-18,C98,ROTA,CNI,PILOTO,088,6.67,0
95,2025-03-18,C98,ROTA,PFR,COPILOTO,088,6.67,0
96,2025-03-18,C95,ROTA,PCN,PILOTO,090,5.33,0
96,2025-03-18,C95,ROTA,KVN,COPILOTO,090,5.33,0
97,2025-03-18,C95,LOCAL,MES,PILOTO,091,1.5,0
97,2025-03-18,C95,LOCAL,MHL,COPILOTO,091,1.5,0
98,2025-03-18,C98,LOCAL,MCH,PILOTO,092,0.08,0
98,2025-03-18,C98,LOCAL,DOG,COPILOTO,092,0.08,0
99,2025-03-19,C98,ROTA,SLS,PILOTO,094,6.67,0
99,2025-03-19,C98,ROTA,CFF,COPILOTO,094,6.67,0
100,2025-03-19,C95,ROTA,MES,PILOTO,093,12.83,0
100,2025-03-19,C95,ROTA,RAY,COPILOTO,093,12.83,0
101,2025-03-20,C98,LOCAL,FIA,PILOTO,095,1.5,0
101,2025-03-20,C98,LOCAL,SLS,COPILOTO,095,1.5,0
102,2025-03-20,C98,LOCAL,FER,PILOTO,096,1.0,0
102,2025-03-20,C98,LOCAL,LRS,COPILOTO,096,1.0,0
103,2025-03-20,C98,LOCAL,FER,PILOTO,097,1.0,0
103,2025-03-20,C98,LOCAL,BEN,COPILOTO,097,1.0,0
104,2025-03-21,C95,ROTA,BRI,PILOTO,098,10.08,0
104,2025-03-21,C95,ROTA,RAY,COPILOTO,098,10.08,0
105,2025-03-22,C97,ROTA,MES,PILOTO,099,10.5,0
105,2025-03-22,C97,ROTA,MAT,COPILOTO,099,10.5,0
106,2025-03-23,C95,ROTA,JVT,PILOTO,100,1.5,0
106,2025-03-23,C95,ROTA,ISA,COPILOTO,100,1.5,0
107,2025-03-23,C97,ROTA,MES,PILOTO,099,4.5,0
107,2025-03-23,C97,ROTA,SEI,COPILOTO,099,4.5,0
108,2025-03-24,C95,ROTA,HCK,PILOTO,101,8.67,0
108,2025-03-24,C95,ROTA,KVN,COPILOTO,101,8.67,0
109,2025-03-24,C98,ROTA,FER,PILOTO,102,18.75,0
109,2025-03-24,C98,ROTA,LRS,COPILOTO,102,18.75,0
110,2025-03-27,C95,ROTA,CAR,PILOTO,104,3.67,0
110,2025-03-27,C95,ROTA,BRJ,COPILOTO,104,3.67,0
111,2025-03-27,C95,ROTA,PCN,PILOTO,105,8.33,0
111,2025-03-27,C95,ROTA,GMR,COPILOTO,105,8.33,0
112,2025-03-27,C97,ROTA,MES,PILOTO,106,9.17,0
112,2025-03-27,C97,ROTA,JVT,COPILOTO,106,9.17,0
113,2025-03-29,C97,ROTA,MAT,PILOTO,107,10.58,0
113,2025-03-29,C97,ROTA,SEI,COPILOTO,107,10.58,0
114,2025-03-29,C95,ROTA,MHL,PILOTO,108,9.5,0
114,2025-03-29,C95,ROTA,BMK,COPILOTO,108,9.5,0
115,2025-03-31,C95,ROTA,JVT,PILOTO,110,4.75,0
115,2025-03-31,C95,ROTA,KVN,COPILOTO,110,4.75,0
116,2025-03-31,C98,LOCAL,HAI,PILOTO,111,0.33,0
116,2025-03-31,C98,LOCAL,MCH,COPILOTO,111,0.33,0
117,2025-04-01,C95,LOCAL,BRI,PILOTO,112,1.0,0
117,2025-04-01,C95,LOCAL,MHL,COPILOTO,112,1.0,0
118,2025-04-02,C98,LOCAL,FIA,PILOTO,113,0.67,0
118,2025-04-02,C98,LOCAL,LOU,COPILOTO,113,0.67,0
119,2025-04-02,C98,LOCAL,AEU,PILOTO,CPT,2.0,0
119,2025-04-02,C98,LOCAL,RAY,COPILOTO,CPT,2.0,0
120,2025-04-02,C98,LOCAL,MED,PILOTO,114,1.0,0
120,2025-04-02,C98,LOCAL,SUG,COPILOTO,114,1.0,0
121,2025-04-02,C97,ROTA,MAT,PILOTO,109,2.75,0
121,2025-04-02,C97,ROTA,LPS,COPILOTO,109,2.75,0
122,2025-04-03,C97,LOCAL,AEU,PILOTO,115,1.5,0
122,2025-04-03,C97,LOCAL,MAT,COPILOTO,115,1.5,0
123,2025-04-03,C97,LOCAL,AEU,PILOTO,116,1.0,0
123,2025-04-03,C97,LOCAL,SEI,COPILOTO,116,1.0,0
124,2025-04-03,C98,LOCAL,CNI,PILOTO,117,1.0,0
124,2025-04-03,C98,LOCAL,LOU,COPILOTO,117,1.0,0
125,2025-04-04,C97,LOCAL,AEU,PILOTO,118,0.5,0
125,2025-04-04,C97,LOCAL,MAT,COPILOTO,118,0.5,0
126,2025-04-05,C95,ROTA,CAR,PILOTO,119,4.75,0
126,2025-04-05,C95,ROTA,GMR,COPILOTO,119,4.75,0
127,2025-04-06,C97,ROTA,MES,PILOTO,120,15.42,0
127,2025-04-06,C97,ROTA,JVT,COPILOTO,120,15.42,0
128,2025-04-07,C98,LOCAL,FER,PILOTO,121,1.5,0
128,2025-04-07,C98,LOCAL,RAY,COPILOTO,121,1.5,0
129,2025-04-08,C98,ROTA,BRI,PILOTO,122,5.33,0
129,2025-04-08,C98,ROTA,FER,COPILOTO,122,5.33,0
130,2025-04-11,C97,ROTA,MES,PILOTO,125,13.58,0
130,2025-04-11,C97,ROTA,HCK,COPILOTO,125,13.58,0
131,2025-04-11,C95,ROTA,PCN,PILOTO,123,3.92,0
131,2025-04-11,C95,ROTA,BMK,COPILOTO,123,3.92,0
132,2025-04-14,C98,LOCAL,MED,PILOTO,124,1.5,0
132,2025-04-14,C98,LOCAL,AEU,COPILOTO,124,1.5,0
133,2025-04-14,C95,LOCAL,RAY,PILOTO,126,1.0,0
133,2025-04-14,C95,LOCAL,CAR,COPILOTO,126,1.0,0
134,2025-04-14,C98,LOCAL,BRI,PILOTO,127,1.0,0
134,2025-04-14,C98,LOCAL,SLS,COPILOTO,127,1.0,0
135,2025-04-15,C95,ROTA,CAR,PILOTO,128,9.58,0
135,2025-04-15,C95,ROTA,ISA,COPILOTO,128,9.58,0
136,2025-04-15,C98,LOCAL,MED,PILOTO,129,1.5,0
136,2025-04-15,C98,LOCAL,RAY,COPILOTO,129,1.5,0
137,2025-04-15,C98,LOCAL,FER,PILOTO,130,1.5,0
137,2025-04-15,C98,LOCAL,AEU,COPILOTO,130,1.5,0
138,2025-04-15,C98,LOCAL,FIA,PILOTO,131,1.0,0
138,2025-04-15,C98,LOCAL,RCH,COPILOTO,131,1.0,0
139,2025-04-17,C97,LOCAL,MES,PILOTO,132,1.0,0
139,2025-04-17,C97,LOCAL,MAT,COPILOTO,132,1.0,0
140,2025-04-18,C95,ROTA,BRJ,PILOTO,133,7.5,0
140,2025-04-18,C95,ROTA,BMK,COPILOTO,133,7.5,0
141,2025-04-21,C98,ROTA,MED,PILOTO,134,11.92,0
141,2025-04-21,C98,ROTA,SUG,COPILOTO,134,11.92,0
142,2025-04-22,C95,ROTA,BRI,PILOTO,135,12.67,0
142,2025-04-22,C95,ROTA,GMR,COPILOTO,135,12.67,0
143,2025-04-22,C98,LOCAL,CFF,PILOTO,136,0.08,0
143,2025-04-22,C98,LOCAL,DOG,COPILOTO,136,0.08,0
144,2025-04-25,C98,ROTA,MED,PILOTO,137,6.58,0
144,2025-04-25,C98,ROTA,MCH,COPILOTO,137,6.58,0
145,2025-04-26,C98,ROTA,MCH,PILOTO,139,11.5,0
145,2025-04-26,C98,ROTA,DOG,COPILOTO,139,11.5,0
146,2025-04-29,C98,LOCAL,SLS,PILOTO,141,1.0,0
146,2025-04-29,C98,LOCAL,FER,COPILOTO,141,1.0,0
147,2025-04-30,C97,ROTA,SEI,PILOTO,140,11.5,0
147,2025-04-30,C97,ROTA,LPS,COPILOTO,140,11.5,0
148,2025-05-04,C97,ROTA,MAT,PILOTO,142,5.67,0
148,2025-05-04,C97,ROTA,JVT,COPILOTO,142,5.67,0
149,2025-05-05,C95,ROTA,BRJ,PILOTO,143,7.25,0
149,2025-05-05,C95,ROTA,ISA,COPILOTO,143,7.25,0
150,2025-05-05,C98,LOCAL,HAI,PILOTO,144,1.0,0
150,2025-05-05,C98,LOCAL,AEU,COPILOTO,144,1.0,0
151,2025-05-05,C98,LOCAL,CNI,PILOTO,145,1.0,0
151,2025-05-05,C98,LOCAL,RAY,COPILOTO,145,1.0,0
152,2025-05-06,C98,ROTA,MED,PILOTO,146,6.33,0
152,2025-05-06,C98,ROTA,PFR,COPILOTO,146,6.33,0
153,2025-05-07,C95,ROTA,PCN,PILOTO,147,3.67,0
153,2025-05-07,C95,ROTA,GMR,COPILOTO,147,3.67,0
154,2025-05-07,C98,LOCAL,SLS,PILOTO,148,1.0,0
154,2025-05-07,C98,LOCAL,GMS,COPILOTO,148,1.0,0
155,2025-05-08,C98,ROTA,FER,PILOTO,150,6.67,0
155,2025-05-08,C98,ROTA,MCH,COPILOTO,150,6.67,0
156,2025-05-08,C98,ROTA,HAI,PILOTO,149,6.33,0
156,2025-05-08,C98,ROTA,LRS,COPILOTO,149,6.33,0
157,2025-05-09,C95,ROTA,CAR,PILOTO,151,3.58,0
157,2025-05-09,C95,ROTA,KVN,COPILOTO,151,3.58,0
158,2025-05-09,C98,ROTA,MED,PILOTO,152,17.75,0
158,2025-05-09,C98,ROTA,BEN,COPILOTO,152,17.75,0
159,2025-05-09,C95,ROTA,RAY,PILOTO,153,5.33,0
159,2025-05-09,C95,ROTA,BMK,COPILOTO,153,5.33,0
160,2025-05-10,C95,ROTA,BRJ,PILOTO,154,11.58,0
160,2025-05-10,C95,ROTA,ISA,COPILOTO,154,11.58,0
161,2025-05-12,C95,ROTA,PCN,PILOTO,155,7.58,0
161,2025-05-12,C95,ROTA,GMR,COPILOTO,155,7.58,0
162,2025-05-13,C98,ROTA,BRI,PILOTO,156,4.83,0
162,2025-05-13,C98,ROTA,RAY,COPILOTO,156,4.83,0
163,2025-05-14,C95,ROTA,CAR,PILOTO,157,8.0,0
163,2025-05-14,C95,ROTA,KVN,COPILOTO,157,8.0,0
164,2025-05-14,C95,ROTA,CAR,PILOTO,157,8.75,0
164,2025-05-14,C95,ROTA,KVN,COPILOTO,157,8.75,0
165,2025-05-19,C95,ROTA,HCK,PILOTO,158,3.58,0
165,2025-05-19,C95,ROTA,BMK,COPILOTO,158,3.58,0
166,2025-05-20,C95,ROTA,MHL,PILOTO,159,11.17,0
166,2025-05-20,C95,ROTA,GMR,COPILOTO,159,11.17,0
167,2025-05-20,C97,LOCAL,MAT,PILOTO,160,1.0,0
167,2025-05-20,C97,LOCAL,SEI,COPILOTO,160,1.0,0
168,2025-05-21,C98,LOCAL,CNI,PILOTO,161,1.0,0
168,2025-05-21,C98,LOCAL,MCH,COPILOTO,161,1.0,0
169,2025-05-22,C98,ROTA,AEU,PILOTO,162,9.42,0
169,2025-05-22,C98,ROTA,SLS,COPILOTO,162,9.42,0
170,2025-05-22,C97,ROTA,MAT,PILOTO,163,11.08,0
170,2025-05-22,C97,ROTA,JVT,COPILOTO,163,11.08,0
171,2025-05-23,C98,LOCAL,FIA,PILOTO,164,1.0,0
171,2025-05-23,C98,LOCAL,DOG,COPILOTO,164,1.0,0
172,2025-05-25,C97,ROTA,SEI,PILOTO,165,9.67,0
172,2025-05-25,C97,ROTA,LPS,COPILOTO,165,9.67,0
173,2025-05-26,C98,ROTA,HAI,PILOTO,166,11.92,0
173,2025-05-26,C98,ROTA,PFR,COPILOTO,166,11.92,0
174,2025-05-27,C98,ROTA,BRI,PILOTO,166,3.33,0
174,2025-05-27,C98,ROTA,LRS,COPILOTO,166,3.33,0
175,2025-05-29,C98,ROTA,MED,PILOTO,167,4.92,0
175,2025-05-29,C98,ROTA,RCH,COPILOTO,167,4.92,0
176,2025-05-29,C98,LOCAL,MCH,PILOTO,168,1.0,0
176,2025-05-29,C98,LOCAL,DOG,COPILOTO,168,1.0,0
177,2025-05-30,C98,ROTA,SLS,PILOTO,169,14.33,0
177,2025-05-30,C98,ROTA,PFR,COPILOTO,169,14.33,0
178,2025-06-01,C98,ROTA,MCH,PILOTO,170,3.33,0
178,2025-06-01,C98,ROTA,GMS,COPILOTO,170,3.33,0
179,2025-06-01,C98,ROTA,MED,PILOTO,170,6.83,0
179,2025-06-01,C98,ROTA,LRS,COPILOTO,170,6.83,0
180,2025-06-02,C98,ROTA,CNI,PILOTO,171,5.67,0
180,2025-06-02,C98,ROTA,RCH,COPILOTO,171,5.67,0
181,2025-06-03,C98,ROTA,CNI,PILOTO,171,11.67,0
181,2025-06-03,C98,ROTA,RCH,COPILOTO,171,11.67,0
182,2025-06-03,C98,ROTA,BRI,PILOTO,172,2.0,0
182,2025-06-03,C98,ROTA,LRS,COPILOTO,172,2.0,0
183,2025-06-03,C98,ROTA,MCH,PILOTO,172,4.0,0
183,2025-06-03,C98,ROTA,DOG,COPILOTO,172,4.0,0
184,2025-06-04,C98,ROTA,MCH,PILOTO,173,5.0,0
184,2025-06-04,C98,ROTA,GMS,COPILOTO,173,5.0,0
185,2025-06-06,C98,ROTA,SLS,PILOTO,174,13.33,0
185,2025-06-06,C98,ROTA,LRS,COPILOTO,174,13.33,0
186,2025-06-06,C97,LOCAL,MAT,PILOTO,175,1.0,0
186,2025-06-06,C97,LOCAL,SEI,COPILOTO,175,1.0,0
187,2025-06-07,C97,ROTA,SEI,PILOTO,178,11.92,0
187,2025-06-07,C97,ROTA,LPS,COPILOTO,178,11.92,0
188,2025-06-07,C95,LOCAL,MES,PILOTO,179,1.0,0
188,2025-06-07,C95,LOCAL,BMK,COPILOTO,179,1.0,0
189,2025-06-08,C97,ROTA,MES,PILOTO,176,9.5,0
189,2025-06-08,C97,ROTA,AEU,COPILOTO,176,9.5,0
190,2025-06-08,C98,ROTA,MCH,PILOTO,177,16.33,0
190,2025-06-08,C98,ROTA,GMS,COPILOTO,177,16.33,0
191,2025-06-10,C95,ROTA,PCN,PILOTO,180,9.33,0
191,2025-06-10,C95,ROTA,ISA,COPILOTO,180,9.33,0
192,2025-06-12,C95,ROTA,GMR,PILOTO,181,11.5,0
192,2025-06-12,C95,ROTA,BMK,COPILOTO,181,11.5,0
193,2025-06-12,C98,ROTA,HAI,PILOTO,182,11.08,0
193,2025-06-12,C98,ROTA,SUG,COPILOTO,182,11.08,0
194,2025-06-12,C97,LOCAL,AEU,PILOTO,183,1.5,0
194,2025-06-12,C97,LOCAL,MHL,COPILOTO,183,1.5,0
195,2025-06-14,C95,ROTA,MHL,PILOTO,184,1.5,0
195,2025-06-14,C95,ROTA,JVT,COPILOTO,184,1.5,0
196,2025-06-15,C95,ROTA,BRJ,PILOTO,185,9.33,0
196,2025-06-15,C95,ROTA,ISA,COPILOTO,185,9.33,0
197,2025-06-17,C95,LOCAL,HCK,PILOTO,186,0.67,0
197,2025-06-17,C95,LOCAL,KVN,COPILOTO,186,0.67,0
198,2025-06-18,C95,ROTA,BMK,PILOTO,187,9.5,0
198,2025-06-18,C95,ROTA,ISA,COPILOTO,187,9.5,0
199,2025-06-18,C98,LOCAL,MCH,PILOTO,189,1.0,0
199,2025-06-18,C98,LOCAL,GMS,COPILOTO,189,1.0,0
200,2025-06-18,C95,ROTA,GMR,PILOTO,191,8.0,0
200,2025-06-18,C95,ROTA,KVN,COPILOTO,191,8.0,0
201,2025-06-21,C98,ROTA,SLS,PILOTO,192,2.0,0
201,2025-06-21,C98,ROTA,AEU,COPILOTO,192,2.0,0
202,2025-06-21,C97,ROTA,MAT,PILOTO,193,10.0,0
202,2025-06-21,C97,ROTA,JVT,COPILOTO,193,10.0,0
203,2025-06-23,C97,LOCAL,MAT,PILOTO,190,1.5,0
203,2025-06-23,C97,LOCAL,MHL,COPILOTO,190,1.5,0
204,2025-06-23,C98,LOCAL,MCH,PILOTO,194,1.0,0
204,2025-06-23,C98,LOCAL,DOG,COPILOTO,194,1.0,0
205,2025-06-24,C97,LOCAL,AEU,PILOTO,195,1.0,0
205,2025-06-24,C97,LOCAL,MHL,COPILOTO,195,1.0,0
206,2025-06-25,C97,LOCAL,AEU,PILOTO,198,1.0,0
206,2025-06-25,C97,LOCAL,MES,COPILOTO,198,1.0,0
207,2025-06-25,C98,LOCAL,SLS,PILOTO,199,1.0,0
207,2025-06-25,C98,LOCAL,CFF,COPILOTO,199,1.0,0
208,2025-06-25,C98,LOCAL,CNI,PILOTO,200,1.0,0
208,2025-06-25,C98,LOCAL,FER,COPILOTO,200,1.0,0
209,2025-06-26,C97,ROTA,MAT,PILOTO,202,9.92,0
209,2025-06-26,C97,ROTA,MHL,COPILOTO,202,9.92,0
210,2025-06-26,C98,LOCAL,MED,PILOTO,201,0.67,0
210,2025-06-26,C98,LOCAL,CFF,COPILOTO,201,0.67,0
211,2025-06-26,C98,LOCAL,FER,PILOTO,203,0.25,0
211,2025-06-26,C98,LOCAL,MCH,COPILOTO,203,0.25,0
212,2025-06-26,C98,LOCAL,FER,PILOTO,204,0.25,0
212,2025-06-26,C98,LOCAL,DOG,COPILOTO,204,0.25,0
213,2025-06-27,C95,LOCAL,BRI,PILOTO,205,1.0,0
213,2025-06-27,C95,LOCAL,RAY,COPILOTO,205,1.0,0
214,2025-06-29,C97,ROTA,MAT,PILOTO,207,10.67,0
214,2025-06-29,C97,ROTA,MHL,COPILOTO,207,10.67,0
215,2025-06-30,C98,ROTA,CNI,PILOTO,208,6.67,0
215,2025-06-30,C98,ROTA,PFR,COPILOTO,208,6.67,0
216,2025-07-01,C98,ROTA,SLS,PILOTO,211,10.92,0
216,2025-07-01,C98,ROTA,SUG,COPILOTO,211,10.92,0
217,2025-07-01,C95,LOCAL,RAY,PILOTO,206,1.0,0
217,2025-07-01,C95,LOCAL,BMK,COPILOTO,206,1.0,0
218,2025-07-01,C95,ROTA,PCN,PILOTO,209,5.92,0
218,2025-07-01,C95,ROTA,KVN,COPILOTO,209,5.92,0
219,2025-07-02,C98,ROTA,RAY,PILOTO,212,3.17,0
219,2025-07-02,C98,ROTA,CNI,COPILOTO,212,3.17,0
220,2025-07-04,C98,ROTA,AEU,PILOTO,214,5.0,0
220,2025-07-04,C98,ROTA,SLS,COPILOTO,214,5.0,0
221,2025-07-04,C98,ROTA,MED,PILOTO,213,6.92,0
221,2025-07-04,C98,ROTA,RAY,COPILOTO,213,6.92,0
222,2025-07-04,C97,LOCAL,MES,PILOTO,210,1.5,0
222,2025-07-04,C97,LOCAL,FIA,COPILOTO,210,1.5,0
223,2025-07-05,C97,LOCAL,MAT,PILOTO,216,1.5,0
223,2025-07-05,C97,LOCAL,FIA,COPILOTO,216,1.5,0
224,2025-07-07,C98,ROTA,FER,PILOTO,215,5.92,0
224,2025-07-07,C98,ROTA,RCH,COPILOTO,215,5.92,0
225,2025-07-07,C97,LOCAL,AEU,PILOTO,217,1.0,0
225,2025-07-07,C97,LOCAL,FIA,COPILOTO,217,1.0,0
226,2025-07-10,C98,ROTA,CNI,PILOTO,218,12.17,0
226,2025-07-10,C98,ROTA,SUG,COPILOTO,218,12.17,0
227,2025-07-11,C97,ROTA,MAT,PILOTO,219,6.83,0
227,2025-07-11,C97,ROTA,FIA,COPILOTO,219,6.83,0
228,2025-07-11,C98,ROTA,FER,PILOTO,220,13.67,0
228,2025-07-11,C98,ROTA,LRS,COPILOTO,220,13.67,0
229,2025-07-12,C97,ROTA,FIA,PILOTO,221,10.25,0
229,2025-07-12,C97,ROTA,LPS,COPILOTO,221,10.25,0
230,2025-07-12,C98,ROTA,CFF,PILOTO,223,8.0,0
230,2025-07-12,C98,ROTA,MCH,COPILOTO,223,8.0,0
231,2025-07-14,C98,ROTA,GMS,PILOTO,224,4.83,0
231,2025-07-14,C98,ROTA,DOG,COPILOTO,224,4.83,0
232,2025-07-14,C98,ROTA,MCH,PILOTO,222,9.75,0
232,2025-07-14,C98,ROTA,CFF,COPILOTO,222,9.75,0
233,2025-07-15,C98,ROTA,CNI,PILOTO,225,4.83,0
233,2025-07-15,C98,ROTA,PFR,COPILOTO,225,4.83,0
234,2025-07-16,C97,ROTA,FIA,PILOTO,226,6.17,0
234,2025-07-16,C97,ROTA,LPS,COPILOTO,226,6.17,0
235,2025-07-19,C98,ROTA,GMS,PILOTO,228,17.83,0
235,2025-07-19,C98,ROTA,DOG,COPILOTO,228,17.83,0
236,2025-07-21,C98,ROTA,SLS,PILOTO,228,3.33,0
236,2025-07-21,C98,ROTA,AEU,COPILOTO,228,3.33,0
237,2025-08-01,C98,LOCAL,CFF,PILOTO,188,1.0,0
237,2025-08-01,C98,LOCAL,MCH,COPILOTO,188,1.0,0
238,2025-08-04,C95,LOCAL,BMK,PILOTO,229,0.08,0
238,2025-08-04,C95,LOCAL,KVN,COPILOTO,229,0.08,0
239,2025-08-06,C98,ROTA,MCH,PILOTO,231,6.67,0
239,2025-08-06,C98,ROTA,GMS,COPILOTO,231,6.67,0
240,2025-08-12,C95,LOCAL,BRI,PILOTO,232,1.0,0
240,2025-08-12,C95,LOCAL,GMR,COPILOTO,232,1.0,0
241,2025-08-16,C97,LOCAL,MES,PILOTO,233,0.92,0
241,2025-08-16,C97,LOCAL,SEI,COPILOTO,233,0.92,0
242,2025-08-16,C97,LOCAL,FIA,PILOTO,234,0.08,0
242,2025-08-16,C97,LOCAL,LPS,COPILOTO,234,0.08,0
243,2025-08-18,C98,ROTA,RAY,PILOTO,235,6.67,0
243,2025-08-18,C98,ROTA,SLS,COPILOTO,235,6.67,0
244,2025-08-21,C95,LOCAL,BRI,PILOTO,236,1.0,0
244,2025-08-21,C95,LOCAL,HCK,COPILOTO,236,1.0,0
245,2025-08-25,C98,LOCAL,FIA,PILOTO,237,1.0,0
245,2025-08-25,C98,LOCAL,SLS,COPILOTO,237,1.0,0
246,2025-08-25,C98,LOCAL,FIA,PILOTO,238,0.08,0
246,2025-08-25,C98,LOCAL,DOG,COPILOTO,238,0.08,0
247,2025-08-26,C98,LOCAL,FIA,PILOTO,239,1.0,0
247,2025-08-26,C98,LOCAL,LRS,COPILOTO,239,1.0,0
248,2025-08-26,C97,LOCAL,MES,PILOTO,240,1.0,0
248,2025-08-26,C97,LOCAL,AEU,COPILOTO,240,1.0,0
249,2025-08-26,C98,LOCAL,FIA,PILOTO,241,1.0,0
249,2025-08-26,C98,LOCAL,MED,COPILOTO,241,1.0,0
250,2025-08-26,C95,LOCAL,BRI,PILOTO,242,1.0,0
250,2025-08-26,C95,LOCAL,PCN,COPILOTO,242,1.0,0
251,2025-08-27,C97,ROTA,FIA,PILOTO,243,10.5,0
251,2025-08-27,C97,ROTA,LPS,COPILOTO,243,10.5,0
252,2025-08-27,C98,LOCAL,MED,PILOTO,244,1.0,0
252,2025-08-27,C98,LOCAL,RCH,COPILOTO,244,1.0,0
253,2025-08-27,C98,LOCAL,MED,PILOTO,245,1.0,0
253,2025-08-27,C98,LOCAL,PFR,COPILOTO,245,1.0,0
254,2025-09-01,C97,LOCAL,AEU,PILOTO,246,1.0,0
254,2025-09-01,C97,LOCAL,LPS,COPILOTO,246,1.0,0
255,2025-09-02,C98,LOCAL,FIA,PILOTO,247,1.0,0
255,2025-09-02,C98,LOCAL,SLS,COPILOTO,247,1.0,0
256,2025-09-02,C95,LOCAL,BRI,PILOTO,248,1.0,0
256,2025-09-02,C95,LOCAL,CAR,COPILOTO,248,1.0,0
257,2025-09-02,C98,LOCAL,MED,PILOTO,249,0.08,0
257,2025-09-02,C98,LOCAL,SLS,COPILOTO,249,0.08,0
258,2025-09-03,C95,LOCAL,CAR,PILOTO,250,0.08,0
258,2025-09-03,C95,LOCAL,KVN,COPILOTO,250,0.08,0
259,2025-09-03,C95,LOCAL,CAR,PILOTO,251,0.08,0
259,2025-09-03,C95,LOCAL,ISA,COPILOTO,251,0.08,0
260,2025-09-05,C98,ROTA,MCH,PILOTO,252,4.17,0
260,2025-09-05,C98,ROTA,LRS,COPILOTO,252,4.17,0
261,2025-09-10,C95,ROTA,BRI,PILOTO,253,2.17,0
261,2025-09-10,C95,ROTA,BRJ,COPILOTO,253,2.17,0
262,2025-09-10,C98,LOCAL,GMS,PILOTO,254,0.08,0
262,2025-09-10,C98,LOCAL,LRS,COPILOTO,254,0.08,0
263,2025-09-11,C98,LOCAL,DOG,PILOTO,255,0.08,0
263,2025-09-11,C98,LOCAL,PFR,COPILOTO,255,0.08,0
264,2025-09-11,C98,LOCAL,FIA,PILOTO,256,1.0,0
264,2025-09-11,C98,LOCAL,CFF,COPILOTO,256,1.0,0
265,2025-09-12,C97,LOCAL,MES,PILOTO,257,0.08,0
265,2025-09-12,C97,LOCAL,SEI,COPILOTO,257,0.08,0
266,2025-09-15,C98,LOCAL,MED,PILOTO,258,1.0,0
266,2025-09-15,C98,LOCAL,SUG,COPILOTO,258,1.0,0
267,2025-09-16,C97,ROTA,FIA,PILOTO,260,8.67,0
267,2025-09-16,C97,ROTA,SEI,COPILOTO,260,8.67,0
268,2025-09-16,C98,ROTA,DOG,PILOTO,259,2.0,0
268,2025-09-16,C98,ROTA,SUG,COPILOTO,259,2.0,0
269,2025-09-17,C98,ROTA,CFF,PILOTO,261,0.67,0
269,2025-09-17,C98,ROTA,SUG,COPILOTO,261,0.67,0
270,2025-09-18,C97,ROTA,FIA,PILOTO,262,5.0,0
270,2025-09-18,C97,ROTA,AEU,COPILOTO,262,5.0,0
271,2025-09-22,C97,LOCAL,AEU,PILOTO,263,1.5,0
271,2025-09-22,C97,LOCAL,LPS,COPILOTO,263,1.5,0
272,2025-09-24,C95,LOCAL,HCK,PILOTO,264,0.08,0
272,2025-09-24,C95,LOCAL,CAR,COPILOTO,264,0.08,0
273,2025-09-25,C98,LOCAL,CFF,PILOTO,265,1.0,0
273,2025-09-25,C98,LOCAL,MCH,COPILOTO,265,1.0,0
274,2025-10-01,C98,LOCAL,MCH,PILOTO,268,0.08,0
274,2025-10-01,C98,LOCAL,GMS,COPILOTO,268,0.08,0
275,2025-09-27,C98,ROTA,CFF,PILOTO,266,20.58,0
275,2025-09-27,C98,ROTA,DOG,COPILOTO,266,20.58,0
276,2025-09-29,C97,ROTA,AEU,PILOTO,267,6.0,0
276,2025-09-29,C97,ROTA,LPS,COPILOTO,267,6.0,0
277,2025-10-02,C98,ROTA,CFF,PILOTO,269,8.5,0
277,2025-10-02,C98,ROTA,DOG,COPILOTO,269,8.5,0
278,2025-10-05,C95,LOCAL,BRI,PILOTO,270,3.0,0
278,2025-10-05,C95,LOCAL,CAR,COPILOTO,270,3.0,0
279,2025-10-05,C95,ROTA,PCN,PILOTO,271,3.5,0
279,2025-10-05,C95,ROTA,GMR,COPILOTO,271,3.5,0
280,2025-10-08,C95,ROTA,CAR,PILOTO,272,3.58,0
280,2025-10-08,C95,ROTA,KVN,COPILOTO,272,3.58,0
281,2025-10-11,C95,LOCAL,BRI,PILOTO,273,1.0,0
281,2025-10-11,C95,LOCAL,BRJ,COPILOTO,273,1.0,0
282,2025-10-11,C95,LOCAL,BRI,PILOTO,274,1.0,0
282,2025-10-11,C95,LOCAL,BMK,COPILOTO,274,1.0,0
283,2025-10-13,C97,LOCAL,MES,PILOTO,275,0.25,0
283,2025-10-13,C97,LOCAL,SEI,COPILOTO,275,0.25,0
284,2025-10-13,C98,LOCAL,MED,PILOTO,276,0.08,0
284,2025-10-13,C98,LOCAL,SLS,COPILOTO,276,0.08,0
285,2025-10-21,C98,LOCAL,FIA,PILOTO,277,0.67,0
285,2025-10-21,C98,LOCAL,BEN,COPILOTO,277,0.67,0
286,2025-10-21,C95,LOCAL,HCK,PILOTO,278,0.08,0
286,2025-10-21,C95,LOCAL,ISA,COPILOTO,278,0.08,0
287,2025-10-22,C98,LOCAL,GMS,PILOTO,279,1.0,0
287,2025-10-22,C98,LOCAL,DOG,COPILOTO,279,1.0,0
288,2025-10-23,C95,LOCAL,BRI,PILOTO,280,1.0,0
288,2025-10-23,C95,LOCAL,BRJ,COPILOTO,280,1.0,0
289,2025-10-23,C95,LOCAL,BRI,PILOTO,281,1.5,0
289,2025-10-23,C95,LOCAL,JVT,COPILOTO,281,1.5,0
290,2025-10-28,C95,LOCAL,BRI,PILOTO,282,1.5,0
290,2025-10-28,C95,LOCAL,PCN,COPILOTO,282,1.5,0
291,2025-10-29,C98,LOCAL,GMS,PILOTO,283,0.08,0
291,2025-10-29,C98,LOCAL,PFR,COPILOTO,283,0.08,0
292,2025-10-30,C98,LOCAL,MED,PILOTO,284,0.58,0
292,2025-10-30,C98,LOCAL,BEN,COPILOTO,284,0.58,0
293,2025-11-04,C98,LOCAL,MCH,PILOTO,285,0.08,0
293,2025-11-04,C98,LOCAL,DOG,COPILOTO,285,0.08,0
294,2025-11-08,C98,ROTA,CFF,PILOTO,286,10.83,0
294,2025-11-08,C98,ROTA,BEN,COPILOTO,286,10.83,0
295,2025-11-08,C95,LOCAL,BRI,PILOTO,288,2.0,0
295,2025-11-08,C95,LOCAL,PCN,COPILOTO,288,2.0,0
296,2025-11-11,C98,ROTA,MCH,PILOTO,287,5.92,0
296,2025-11-11,C98,ROTA,PFR,COPILOTO,287,5.92,0
297,2025-11-11,C95,LOCAL,PCN,PILOTO,289,0.08,0
297,2025-11-11,C95,LOCAL,BMK,COPILOTO,289,0.08,0
298,2025-11-11,C95,LOCAL,PCN,PILOTO,290,1.0,0
298,2025-11-11,C95,LOCAL,GMR,COPILOTO,290,1.0,0
299,2025-11-13,C98,ROTA,DOG,PILOTO,287,5.92,0
299,2025-11-13,C98,ROTA,LRS,COPILOTO,287,5.92,0
300,2025-11-24,C98,LOCAL,SLS,PILOTO,292,0.08,0
300,2025-11-24,C98,LOCAL,RCH,COPILOTO,292,0.08,0
301,2025-11-24,C98,ROTA,GMS,PILOTO,291,5.0,0
301,2025-11-24,C98,ROTA,BEN,COPILOTO,291,5.0,0
302,2025-11-24,C95,LOCAL,BRJ,PILOTO,293,0.08,0
302,2025-11-24,C95,LOCAL,ISA,COPILOTO,293,0.08,0
303,2025-11-27,C95,LOCAL,MHL,PILOTO,294,1.0,0
303,2025-11-27,C95,LOCAL,BRJ,COPILOTO,294,1.0,0
304,2025-12-01,C95,ROTA,BRI,PILOTO,295,1.5,0
304,2025-12-01,C95,ROTA,ISA,COPILOTO,295,1.5,0
305,2025-12-02,C95,ROTA,CAR,PILOTO,296,6.58,0
305,2025-12-02,C95,ROTA,KVN,COPILOTO,296,6.58,0
306,2025-12-03,C97,ROTA,MES,PILOTO,297,9.08,0
306,2025-12-03,C97,ROTA,SEI,COPILOTO,297,9.08,0
307,2025-12-03,C98,LOCAL,SLS,PILOTO,298,0.08,0
307,2025-12-03,C98,LOCAL,RCH,COPILOTO,298,0.08,0
308,2025-12-03,C95,LOCAL,JVT,PILOTO,299,0.67,0
308,2025-12-03,C95,LOCAL,GMR,COPILOTO,299,0.67,0
309,2025-12-03,C95,LOCAL,CAR,PILOTO,300,1.0,0
309,2025-12-03,C95,LOCAL,BMK,COPILOTO,300,1.0,0
310,2025-12-03,C95,LOCAL,CAR,PILOTO,300,1.0,0
310,2025-12-03,C95,LOCAL,BMK,COPILOTO,300,1.0,0
311,2025-12-03,C95,LOCAL,CAR,PILOTO,300,1.0,0
311,2025-12-03,C95,LOCAL,BMK,COPILOTO,300,1.0,0
312,2025-12-03,C95,LOCAL,JVT,PILOTO,299,0.67,0
312,2025-12-03,C95,LOCAL,GMR,COPILOTO,299,0.67,0
313,2025-12-03,C95,ROTA,BRJ,PILOTO,304,12.0,0
313,2025-12-03,C95,ROTA,ISA,COPILOTO,304,12.0,0
314,2025-12-04,C95,ROTA,GMR,PILOTO,302,2.67,0
314,2025-12-04,C95,ROTA,KVN,COPILOTO,302,2.67,0
315,2025-12-04,C95,ROTA,GMR,PILOTO,303,14.0,0
315,2025-12-04,C95,ROTA,KVN,COPILOTO,303,14.0,0
316,2025-12-05,C95,LOCAL,BRI,PILOTO,301,1.5,0
316,2025-12-05,C95,LOCAL,CAR,COPILOTO,301,1.5,0
317,2025-12-05,C98,LOCAL,SLS,PILOTO,305,1.0,0
317,2025-12-05,C98,LOCAL,MCH,COPILOTO,305,1.0,0
318,2025-12-05,C98,LOCAL,SLS,PILOTO,306,1.0,0
318,2025-12-05,C98,LOCAL,SUG,COPILOTO,306,1.0,0
319,2025-12-05,C98,ROTA,DOG,PILOTO,308,5.0,0
319,2025-12-05,C98,ROTA,BEN,COPILOTO,308,5.0,0
320,2025-12-06,C98,ROTA,GMS,PILOTO,309,3.5,0
320,2025-12-06,C98,ROTA,LRS,COPILOTO,309,3.5,0
321,2025-12-06,C98,ROTA,MCH,PILOTO,310,2.08,0
321,2025-12-06,C98,ROTA,RCH,COPILOTO,310,2.08,0
322,2025-12-06,C95,LOCAL,BRI,PILOTO,307,2.5,0
322,2025-12-06,C95,LOCAL,CAR,COPILOTO,307,2.5,0
323,2025-12-07,C98,ROTA,CFF,PILOTO,313,3.5,0
323,2025-12-07,C98,ROTA,RCH,COPILOTO,313,3.5,0
324,2025-12-07,C98,ROTA,DOG,PILOTO,314,2.08,0
324,2025-12-07,C98,ROTA,BEN,COPILOTO,314,2.08,0
325,2025-12-08,C95,LOCAL,CAR,PILOTO,311,0.67,0
325,2025-12-08,C95,LOCAL,KVN,COPILOTO,311,0.67,0
326,2025-12-08,C95,LOCAL,JVT,PILOTO,312,1.0,0
326,2025-12-08,C95,LOCAL,GMR,COPILOTO,312,1.0,0
327,2025-12-08,C95,LOCAL,JVT,PILOTO,312,1.0,0
327,2025-12-08,C95,LOCAL,GMR,COPILOTO,312,1.0,0
328,2025-12-08,C95,LOCAL,JVT,PILOTO,312,1.0,0
328,2025-12-08,C95,LOCAL,GMR,COPILOTO,312,1.0,0
329,2025-12-08,C95,LOCAL,CAR,PILOTO,311,0.67,0
329,2025-12-08,C95,LOCAL,KVN,COPILOTO,311,0.67,0
330,2025-12-08,C97,LOCAL,MES,PILOTO,315,1.0,0
330,2025-12-08,C97,LOCAL,AEU,COPILOTO,315,1.0,0
331,2025-12-08,C98,LOCAL,CNI,PILOTO,316,1.0,0
331,2025-12-08,C98,LOCAL,FIA,COPILOTO,316,1.0,0
332,2025-12-09,C95,ROTA,HCK,PILOTO,317,6.75,0
332,2025-12-09,C95,ROTA,BRJ,COPILOTO,317,6.75,0
333,2025-12-09,C98,LOCAL,FIA,PILOTO,318,1.5,0
333,2025-12-09,C98,LOCAL,CFF,COPILOTO,318,1.5,0
334,2025-12-09,C95,LOCAL,CAR,PILOTO,319,1.0,0
334,2025-12-09,C95,LOCAL,KVN,COPILOTO,319,1.0,0
335,2025-12-09,C98,ROTA,GMS,PILOTO,320,6.0,0
335,2025-12-09,C98,ROTA,PFR,COPILOTO,320,6.0,0
336,2025-12-09,C98,LOCAL,MED,PILOTO,321,0.08,0
336,2025-12-09,C98,LOCAL,RCH,COPILOTO,321,0.08,0
337,2025-12-10,C95,ROTA,BMK,PILOTO,322,6.75,0
337,2025-12-10,C95,ROTA,BRJ,COPILOTO,322,6.75,0
338,2025-12-10,C98,LOCAL,FIA,PILOTO,323,1.5,0
338,2025-12-10,C98,LOCAL,GMS,COPILOTO,323,1.5,0
339,2025-12-10,C98,LOCAL,MED,PILOTO,324,1.0,0
339,2025-12-10,C98,LOCAL,CFF,COPILOTO,324,1.0,0
340,2025-12-11,C95,ROTA,CAR,PILOTO,325,4.83,0
340,2025-12-11,C95,ROTA,KVN,COPILOTO,325,4.83,0
341,2025-12-12,C97,LOCAL,FIA,PILOTO,326,1.0,0
341,2025-12-12,C97,LOCAL,AEU,COPILOTO,326,1.0,0
342,2025-12-12,C95,LOCAL,JVT,PILOTO,327,0.67,0
342,2025-12-12,C95,LOCAL,BMK,COPILOTO,327,0.67,0
343,2025-12-12,C95,LOCAL,BRI,PILOTO,328,1.0,0
343,2025-12-12,C95,LOCAL,HCK,COPILOTO,328,1.0,0
344,2025-12-12,C95,LOCAL,BRI,PILOTO,328,1.0,0
344,2025-12-12,C95,LOCAL,HCK,COPILOTO,328,1.0,0
345,2025-12-12,C95,LOCAL,BRI,PILOTO,328,1.0,0
345,2025-12-12,C95,LOCAL,HCK,COPILOTO,328,1.0,0
346,2025-12-12,C95,LOCAL,GMR,PILOTO,327,0.67,0
346,2025-12-12,C95,LOCAL,HCK,COPILOTO,327,0.67,0
347,2025-12-12,C95,LOCAL,RAY,PILOTO,329,1.0,0
347,2025-12-12,C95,LOCAL,KVN,COPILOTO,329,1.0,0
348,2025-12-12,C95,LOCAL,MHL,PILOTO,329,1.0,0
348,2025-12-12,C95,LOCAL,ISA,COPILOTO,329,1.0,0
349,2025-12-12,C95,LOCAL,MHL,PILOTO,330,1.0,0
349,2025-12-12,C95,LOCAL,ISA,COPILOTO,330,1.0,0
350,2025-12-12,C95,LOCAL,RAY,PILOTO,330,1.0,0
350,2025-12-12,C95,LOCAL,KVN,COPILOTO,330,1.0,0
351,2025-12-12,C98,LOCAL,MED,PILOTO,331,1.0,0
351,2025-12-12,C98,LOCAL,GMS,COPILOTO,331,1.0,0
352,2025-12-13,C95,LOCAL,RAY,PILOTO,333,1.0,0
352,2025-12-13,C95,LOCAL,CAR,COPILOTO,333,1.0,0
353,2025-12-13,C95,LOCAL,HCK,PILOTO,334,1.0,0
353,2025-12-13,C95,LOCAL,ISA,COPILOTO,334,1.0,0
354,2025-12-13,C95,LOCAL,RAY,PILOTO,335,1.0,0
354,2025-12-13,C95,LOCAL,CAR,COPILOTO,335,1.0,0
355,2025-12-14,C95,ROTA,JVT,PILOTO,332,11.33,0
355,2025-12-14,C95,ROTA,BMK,COPILOTO,332,11.33,0
356,2025-12-15,C95,LOCAL,MHL,PILOTO,336,1.0,0
356,2025-12-15,C95,LOCAL,MES,COPILOTO,336,1.0,0
357,2025-12-15,C95,LOCAL,BRJ,PILOTO,336,1.0,0
357,2025-12-15,C95,LOCAL,KVN,COPILOTO,336,1.0,0
358,2025-12-15,C95,LOCAL,MES,PILOTO,337,1.0,0
358,2025-12-15,C95,LOCAL,MHL,COPILOTO,337,1.0,0
359,2025-12-15,C95,LOCAL,BRJ,PILOTO,337,1.0,0
359,2025-12-15,C95,LOCAL,KVN,COPILOTO,337,1.0,0
360,2025-12-15,C98,LOCAL,CFF,PILOTO,338,1.0,0
360,2025-12-15,C98,LOCAL,BEN,COPILOTO,338,1.0,0
361,2025-12-16,C95,ROTA,BRJ,PILOTO,339,5.33,0
361,2025-12-16,C95,ROTA,KVN,COPILOTO,339,5.33,0
362,2025-12-16,C98,ROTA,CFF,PILOTO,340,2.08,0
362,2025-12-16,C98,ROTA,BEN,COPILOTO,340,2.08,0
363,2025-12-17,C95,LOCAL,CAR,PILOTO,342,1.0,0
363,2025-12-17,C95,LOCAL,MES,COPILOTO,342,1.0,0
364,2025-12-17,C95,LOCAL,GMR,PILOTO,342,1.0,0
364,2025-12-17,C95,LOCAL,BMK,COPILOTO,342,1.0,0
365,2025-12-17,C95,LOCAL,MES,PILOTO,343,1.0,0
365,2025-12-17,C95,LOCAL,CAR,COPILOTO,343,1.0,0
366,2025-12-17,C95,LOCAL,BMK,PILOTO,343,1.0,0
366,2025-12-17,C95,LOCAL,GMR,COPILOTO,343,1.0,0
367,2025-12-18,C97,ROTA,AEU,PILOTO,341,10.17,0
367,2025-12-18,C97,ROTA,MHL,COPILOTO,341,10.17,0
368,2025-12-18,C98,LOCAL,FIA,PILOTO,344,1.0,0
368,2025-12-18,C98,LOCAL,DOG,COPILOTO,344,1.0,0
369,2025-12-19,C95,ROTA,BRI,PILOTO,345,5.67,0
369,2025-12-19,C95,ROTA,HCK,COPILOTO,345,5.67,0
370,2025-12-20,C98,ROTA,MCH,PILOTO,347,1.17,0
370,2025-12-20,C98,ROTA,SUG,COPILOTO,347,1.17,0
371,2025-12-23,C95,ROTA,GMR,PILOTO,346,6.42,0
371,2025-12-23,C95,ROTA,ISA,COPILOTO,346,6.42,0
372,2025-12-23,C97,LOCAL,MES,PILOTO,349,0.5,0
372,2025-12-23,C97,LOCAL,MHL,COPILOTO,349,0.5,0
373,2025-12-23,C98,ROTA,MCH,PILOTO,350,0.92,0
373,2025-12-23,C98,ROTA,RCH,COPILOTO,350,0.92,0
374,2025-12-28,C95,ROTA,CAR,PILOTO,352,3.92,0
374,2025-12-28,C95,ROTA,HCK,COPILOTO,352,3.92,0
375,2025-12-28,C95,ROTA,JVT,PILOTO,353,10.0,0
375,2025-12-28,C95,ROTA,BRJ,COPILOTO,353,10.0,0
376,2025-12-29,C95,ROTA,BMK,PILOTO,354,3.92,0
376,2025-12-29,C95,ROTA,KVN,COPILOTO,354,3.92,0
377,2025-12-29,C97,LOCAL,AEU,PILOTO,348,0.75,0
377,2025-12-29,C97,LOCAL,LPS,COPILOTO,348,0.75,0
378,2025-12-29,C97,LOCAL,AEU,PILOTO,351,0.75,0
378,2025-12-29,C97,LOCAL,SEI,COPILOTO,351,0.75,0
379,2025-12-29,C95,LOCAL,MES,PILOTO,355,1.0,0
379,2025-12-29,C95,LOCAL,CAR,COPILOTO,355,1.0,0
380,2025-12-29,C95,LOCAL,ISA,PILOTO,355,1.0,0
380,2025-12-29,C95,LOCAL,MHL,COPILOTO,355,1.0,0
381,2025-12-29,C95,LOCAL,MHL,PILOTO,356,1.0,0
381,2025-12-29,C95,LOCAL,RAY,COPILOTO,356,1.0,0
382,2025-12-29,C95,LOCAL,CAR,PILOTO,356,1.0,0
382,2025-12-29,C95,LOCAL,HCK,COPILOTO,356,1.0,0
383,2025-12-30,C95,ROTA,BRI,PILOTO,357,3.42,0
383,2025-12-30,C95,ROTA,KVN,COPILOTO,357,3.42,0
384,2025-12-30,C97,LOCAL,FIA,PILOTO,358,0.75,0
384,2025-12-30,C97,LOCAL,SEI,COPILOTO,358,0.75,0
1 slot_id data aeronave tipo_escala tripulante funcao oi horas_voadas sbv
2 1 2025-01-02 C98 ROTA SLS PILOTO 001 6.67 0
3 1 2025-01-02 C98 ROTA DOG COPILOTO 001 6.67 0
4 2 2025-01-07 C98 ROTA SLS PILOTO 002 6.67 0
5 2 2025-01-07 C98 ROTA CFF COPILOTO 002 6.67 0
6 3 2025-01-09 C95 LOCAL JVT PILOTO - 0.08 0
7 3 2025-01-09 C95 LOCAL GMR COPILOTO - 0.08 0
8 4 2025-01-09 C95 LOCAL HCK PILOTO - 0.08 0
9 4 2025-01-09 C95 LOCAL CAR COPILOTO - 0.08 0
10 5 2025-01-09 C97 LOCAL MES PILOTO - 0.08 0
11 5 2025-01-09 C97 LOCAL SEI COPILOTO - 0.08 0
12 6 2025-01-09 C97 LOCAL MAT PILOTO - 0.08 0
13 6 2025-01-09 C97 LOCAL STS COPILOTO - 0.08 0
14 7 2025-01-15 C98 LOCAL HAI PILOTO 011 0.08 0
15 7 2025-01-15 C98 LOCAL GMS COPILOTO 011 0.08 0
16 8 2025-01-15 C95 ROTA RAY PILOTO 003 1.33 0
17 8 2025-01-15 C95 ROTA HCK COPILOTO 003 1.33 0
18 9 2025-01-20 C97 LOCAL MES PILOTO 004 1.0 0
19 9 2025-01-20 C97 LOCAL AEU COPILOTO 004 1.0 0
20 10 2025-01-20 C97 ROTA MAT PILOTO 005 5.67 0
21 10 2025-01-20 C97 ROTA STS COPILOTO 005 5.67 0
22 11 2025-01-21 C98 LOCAL CNI PILOTO 006 1.0 0
23 11 2025-01-21 C98 LOCAL DOG COPILOTO 006 1.0 0
24 12 2025-01-22 C98 ROTA SLS PILOTO 007 13.0 0
25 12 2025-01-22 C98 ROTA GMS COPILOTO 007 13.0 0
26 13 2025-01-24 C98 ROTA CFF PILOTO 009 13.67 0
27 13 2025-01-24 C98 ROTA GMS COPILOTO 009 13.67 0
28 14 2025-01-24 C98 LOCAL FER PILOTO 008 1.0 0
29 14 2025-01-24 C98 LOCAL MCH COPILOTO 008 1.0 0
30 15 2025-01-25 C98 ROTA SLS PILOTO 010 6.67 0
31 15 2025-01-25 C98 ROTA DOG COPILOTO 010 6.67 0
32 16 2025-01-29 C98 ROTA FER PILOTO 012 12.33 0
33 16 2025-01-29 C98 ROTA DOG COPILOTO 012 12.33 0
34 17 2025-02-03 C98 LOCAL HAI PILOTO 014 1.0 0
35 17 2025-02-03 C98 LOCAL MED COPILOTO 014 1.0 0
36 18 2025-02-03 C98 LOCAL HAI PILOTO 015 1.0 0
37 18 2025-02-03 C98 LOCAL FIA COPILOTO 015 1.0 0
38 19 2025-02-04 C98 ROTA MED PILOTO 016 5.92 0
39 19 2025-02-04 C98 ROTA CNI COPILOTO 016 5.92 0
40 20 2025-02-06 C98 ROTA FER PILOTO 018 10.5 0
41 20 2025-02-06 C98 ROTA CNI COPILOTO 018 10.5 0
42 21 2025-02-07 C98 ROTA HAI PILOTO 020 2.0 0
43 21 2025-02-07 C98 ROTA FIA COPILOTO 020 2.0 0
44 22 2025-02-07 C97 ROTA AEU PILOTO 021 7.58 0
45 22 2025-02-07 C97 ROTA SEI COPILOTO 021 7.58 0
46 23 2025-02-08 C98 ROTA CFF PILOTO 022 10.5 0
47 23 2025-02-08 C98 ROTA FER COPILOTO 022 10.5 0
48 24 2025-02-09 C97 ROTA AEU PILOTO 023 9.42 0
49 24 2025-02-09 C97 ROTA SEI COPILOTO 023 9.42 0
50 25 2025-02-11 C98 ROTA FER PILOTO 026 3.33 0
51 25 2025-02-11 C98 ROTA DOG COPILOTO 026 3.33 0
52 26 2025-02-12 C98 LOCAL FIA PILOTO 027 1.0 0
53 26 2025-02-12 C98 LOCAL GMS COPILOTO 027 1.0 0
54 27 2025-02-12 C98 ROTA FER PILOTO 028 1.67 0
55 27 2025-02-12 C98 ROTA DOG COPILOTO 028 1.67 0
56 28 2025-02-12 C98 ROTA MCH PILOTO 028 1.67 0
57 28 2025-02-12 C98 ROTA CNI COPILOTO 028 1.67 0
58 29 2025-02-12 C98 ROTA MCH PILOTO 026 3.33 0
59 29 2025-02-12 C98 ROTA CNI COPILOTO 026 3.33 0
60 30 2025-02-13 C98 LOCAL BRI PILOTO 029 1.0 0
61 30 2025-02-13 C98 LOCAL HAI COPILOTO 029 1.0 0
62 31 2025-02-13 C98 LOCAL SLS PILOTO 027 0.5 0
63 31 2025-02-13 C98 LOCAL DOG COPILOTO 027 0.5 0
64 32 2025-02-14 C97 ROTA AEU PILOTO 030 5.83 0
65 32 2025-02-14 C97 ROTA SEI COPILOTO 030 5.83 0
66 33 2025-02-14 C98 LOCAL BRI PILOTO 031 1.0 0
67 33 2025-02-14 C98 LOCAL FIA COPILOTO 031 1.0 0
68 34 2025-02-16 C95 ROTA RAY PILOTO 032 4.83 0
69 34 2025-02-16 C95 ROTA PCN COPILOTO 032 4.83 0
70 35 2025-02-16 C97 ROTA AEU PILOTO 033 10.67 0
71 35 2025-02-16 C97 ROTA SEI COPILOTO 033 10.67 0
72 36 2025-02-17 C98 LOCAL CNI PILOTO CPT 2.0 0
73 36 2025-02-17 C98 LOCAL LOU COPILOTO CPT 2.0 0
74 37 2025-02-17 C98 LOCAL CNI PILOTO CPT 2.0 0
75 37 2025-02-17 C98 LOCAL PFR COPILOTO CPT 2.0 0
76 38 2025-02-17 C98 LOCAL FER PILOTO CPT 2.0 0
77 38 2025-02-17 C98 LOCAL BEN COPILOTO CPT 2.0 0
78 39 2025-02-17 C98 LOCAL FER PILOTO CPT 2.0 0
79 39 2025-02-17 C98 LOCAL RCH COPILOTO CPT 2.0 0
80 40 2025-02-18 C98 LOCAL CNI PILOTO CPT 1.0 0
81 40 2025-02-18 C98 LOCAL LOU COPILOTO CPT 1.0 0
82 41 2025-02-18 C98 LOCAL FER PILOTO CPT 2.0 0
83 41 2025-02-18 C98 LOCAL SUG COPILOTO CPT 2.0 0
84 42 2025-02-18 C98 LOCAL FER PILOTO CPT 2.0 0
85 42 2025-02-18 C98 LOCAL LRS COPILOTO CPT 2.0 0
86 43 2025-02-18 C97 ROTA AEU PILOTO 034 7.83 0
87 43 2025-02-18 C97 ROTA SEI COPILOTO 034 7.83 0
88 44 2025-02-18 C98 LOCAL MED PILOTO 036 1.5 0
89 44 2025-02-18 C98 LOCAL CNI COPILOTO 036 1.5 0
90 45 2025-02-18 C98 LOCAL FIA PILOTO 037 1.0 0
91 45 2025-02-18 C98 LOCAL CNI COPILOTO 037 1.0 0
92 46 2025-02-20 C95 LOCAL PCN PILOTO 039 0.08 0
93 46 2025-02-20 C95 LOCAL JVT COPILOTO 039 0.08 0
94 47 2025-02-20 C98 LOCAL CNI PILOTO 040 1.5 0
95 47 2025-02-20 C98 LOCAL PFR COPILOTO 040 1.5 0
96 48 2025-02-20 C98 LOCAL CNI PILOTO 041 1.5 0
97 48 2025-02-20 C98 LOCAL BEN COPILOTO 041 1.5 0
98 49 2025-02-20 C95 LOCAL MHL PILOTO 042 1.0 0
99 49 2025-02-20 C95 LOCAL KVN COPILOTO 042 1.0 0
100 50 2025-02-21 C97 ROTA AEU PILOTO 048 4.0 0
101 50 2025-02-21 C97 ROTA SEI COPILOTO 048 4.0 0
102 51 2025-02-21 C95 ROTA CAR PILOTO 043 3.58 0
103 51 2025-02-21 C95 ROTA BRJ COPILOTO 043 3.58 0
104 52 2025-02-21 C98 LOCAL MED PILOTO 044 1.5 0
105 52 2025-02-21 C98 LOCAL RCH COPILOTO 044 1.5 0
106 53 2025-02-21 C98 LOCAL FER PILOTO 045 1.5 0
107 53 2025-02-21 C98 LOCAL LRS COPILOTO 045 1.5 0
108 54 2025-02-21 C95 LOCAL MHL PILOTO 046 1.0 0
109 54 2025-02-21 C95 LOCAL ISA COPILOTO 046 1.0 0
110 55 2025-02-24 C98 LOCAL FER PILOTO 050 1.5 0
111 55 2025-02-24 C98 LOCAL SUG COPILOTO 050 1.5 0
112 56 2025-02-24 C98 LOCAL FIA PILOTO 051 1.5 0
113 56 2025-02-24 C98 LOCAL BEN COPILOTO 051 1.5 0
114 57 2025-02-24 C97 LOCAL AEU PILOTO 047 0.08 0
115 57 2025-02-24 C97 LOCAL MAT COPILOTO 047 0.08 0
116 58 2025-02-24 C95 LOCAL MHL PILOTO 052 1.0 0
117 58 2025-02-24 C95 LOCAL ISA COPILOTO 052 1.0 0
118 59 2025-02-24 C95 LOCAL MHL PILOTO 053 1.0 0
119 59 2025-02-24 C95 LOCAL KVN COPILOTO 053 1.0 0
120 60 2025-02-25 C98 ROTA CNI PILOTO 054 12.42 0
121 60 2025-02-25 C98 ROTA BEN COPILOTO 054 12.42 0
122 61 2025-02-25 C97 LOCAL MES PILOTO 055 2.0 0
123 61 2025-02-25 C97 LOCAL AEU COPILOTO 055 2.0 0
124 62 2025-02-25 C97 LOCAL MES PILOTO 056 1.5 0
125 62 2025-02-25 C97 LOCAL HCK COPILOTO 056 1.5 0
126 63 2025-02-26 C97 LOCAL MES PILOTO 057 1.5 0
127 63 2025-02-26 C97 LOCAL HCK COPILOTO 057 1.5 0
128 64 2025-02-27 C98 LOCAL FER PILOTO 058 1.5 0
129 64 2025-02-27 C98 LOCAL LOU COPILOTO 058 1.5 0
130 65 2025-02-27 C98 LOCAL MED PILOTO 059 1.5 0
131 65 2025-02-27 C98 LOCAL PFR COPILOTO 059 1.5 0
132 66 2025-02-27 C95 LOCAL MHL PILOTO 060 1.0 0
133 66 2025-02-27 C95 LOCAL BRI COPILOTO 060 1.0 0
134 67 2025-02-28 C98 LOCAL MED PILOTO 061 0.67 0
135 67 2025-02-28 C98 LOCAL PFR COPILOTO 061 0.67 0
136 68 2025-02-28 C95 LOCAL JVT PILOTO 066 0.08 0
137 68 2025-02-28 C95 LOCAL HCK COPILOTO 066 0.08 0
138 69 2025-02-28 C95 ROTA RAY PILOTO 062 9.25 0
139 69 2025-02-28 C95 ROTA ISA COPILOTO 062 9.25 0
140 70 2025-03-01 C95 ROTA CAR PILOTO 063 13.58 0
141 70 2025-03-01 C95 ROTA BMK COPILOTO 063 13.58 0
142 71 2025-03-01 C95 ROTA MHL PILOTO 064 7.42 0
143 71 2025-03-01 C95 ROTA KVN COPILOTO 064 7.42 0
144 72 2025-03-05 C97 ROTA MES PILOTO 065 2.0 0
145 72 2025-03-05 C97 ROTA LPS COPILOTO 065 2.0 0
146 73 2025-03-05 C95 LOCAL RAY PILOTO 067 1.5 0
147 73 2025-03-05 C95 LOCAL BRI COPILOTO 067 1.5 0
148 74 2025-03-05 C98 LOCAL FER PILOTO 068 1.0 0
149 74 2025-03-05 C98 LOCAL PFR COPILOTO 068 1.0 0
150 75 2025-03-06 C95 ROTA PCN PILOTO 069 3.92 0
151 75 2025-03-06 C95 ROTA JVT COPILOTO 069 3.92 0
152 76 2025-03-06 C98 LOCAL FIA PILOTO 070 1.5 0
153 76 2025-03-06 C98 LOCAL LRS COPILOTO 070 1.5 0
154 77 2025-03-06 C98 LOCAL CNI PILOTO 071 1.5 0
155 77 2025-03-06 C98 LOCAL RCH COPILOTO 071 1.5 0
156 78 2025-03-06 C97 LOCAL MES PILOTO 072 2.0 0
157 78 2025-03-06 C97 LOCAL AEU COPILOTO 072 2.0 0
158 79 2025-03-06 C98 LOCAL CFF PILOTO 073 0.08 0
159 79 2025-03-06 C98 LOCAL GMS COPILOTO 073 0.08 0
160 80 2025-03-06 C98 LOCAL BRI PILOTO 074 1.0 0
161 80 2025-03-06 C98 LOCAL CNI COPILOTO 074 1.0 0
162 81 2025-03-06 C97 LOCAL MES PILOTO 075 1.0 0
163 81 2025-03-06 C97 LOCAL HCK COPILOTO 075 1.0 0
164 82 2025-03-08 C97 ROTA MES PILOTO 077 19.33 0
165 82 2025-03-08 C97 ROTA LPS COPILOTO 077 19.33 0
166 83 2025-03-10 C98 LOCAL FER PILOTO 078 1.5 0
167 83 2025-03-10 C98 LOCAL LRS COPILOTO 078 1.5 0
168 84 2025-03-10 C98 LOCAL GMS PILOTO 079 1.0 0
169 84 2025-03-10 C98 LOCAL DOG COPILOTO 079 1.0 0
170 85 2025-03-11 C97 LOCAL AEU PILOTO 080 1.5 0
171 85 2025-03-11 C97 LOCAL JVT COPILOTO 080 1.5 0
172 86 2025-03-11 C97 LOCAL AEU PILOTO 081 1.0 0
173 86 2025-03-11 C97 LOCAL MAT COPILOTO 081 1.0 0
174 87 2025-03-12 C97 LOCAL AEU PILOTO 082 1.5 0
175 87 2025-03-12 C97 LOCAL JVT COPILOTO 082 1.5 0
176 88 2025-03-12 C98 LOCAL CNI PILOTO 083 1.5 0
177 88 2025-03-12 C98 LOCAL SUG COPILOTO 083 1.5 0
178 89 2025-03-12 C98 LOCAL HAI PILOTO 084 1.0 0
179 89 2025-03-12 C98 LOCAL MED COPILOTO 084 1.0 0
180 90 2025-03-13 C98 LOCAL CNI PILOTO 085 1.0 0
181 90 2025-03-13 C98 LOCAL FER COPILOTO 085 1.0 0
182 91 2025-03-14 C95 ROTA MHL PILOTO 086 0.75 0
183 91 2025-03-14 C95 ROTA ISA COPILOTO 086 0.75 0
184 92 2025-03-15 C97 ROTA MAT PILOTO 087 9.75 0
185 92 2025-03-15 C97 ROTA LPS COPILOTO 087 9.75 0
186 93 2025-03-16 C95 ROTA MHL PILOTO 086 0.75 0
187 93 2025-03-16 C95 ROTA ISA COPILOTO 086 0.75 0
188 94 2025-03-17 C97 LOCAL MES PILOTO 089 1.0 0
189 94 2025-03-17 C97 LOCAL JVT COPILOTO 089 1.0 0
190 95 2025-03-18 C98 ROTA CNI PILOTO 088 6.67 0
191 95 2025-03-18 C98 ROTA PFR COPILOTO 088 6.67 0
192 96 2025-03-18 C95 ROTA PCN PILOTO 090 5.33 0
193 96 2025-03-18 C95 ROTA KVN COPILOTO 090 5.33 0
194 97 2025-03-18 C95 LOCAL MES PILOTO 091 1.5 0
195 97 2025-03-18 C95 LOCAL MHL COPILOTO 091 1.5 0
196 98 2025-03-18 C98 LOCAL MCH PILOTO 092 0.08 0
197 98 2025-03-18 C98 LOCAL DOG COPILOTO 092 0.08 0
198 99 2025-03-19 C98 ROTA SLS PILOTO 094 6.67 0
199 99 2025-03-19 C98 ROTA CFF COPILOTO 094 6.67 0
200 100 2025-03-19 C95 ROTA MES PILOTO 093 12.83 0
201 100 2025-03-19 C95 ROTA RAY COPILOTO 093 12.83 0
202 101 2025-03-20 C98 LOCAL FIA PILOTO 095 1.5 0
203 101 2025-03-20 C98 LOCAL SLS COPILOTO 095 1.5 0
204 102 2025-03-20 C98 LOCAL FER PILOTO 096 1.0 0
205 102 2025-03-20 C98 LOCAL LRS COPILOTO 096 1.0 0
206 103 2025-03-20 C98 LOCAL FER PILOTO 097 1.0 0
207 103 2025-03-20 C98 LOCAL BEN COPILOTO 097 1.0 0
208 104 2025-03-21 C95 ROTA BRI PILOTO 098 10.08 0
209 104 2025-03-21 C95 ROTA RAY COPILOTO 098 10.08 0
210 105 2025-03-22 C97 ROTA MES PILOTO 099 10.5 0
211 105 2025-03-22 C97 ROTA MAT COPILOTO 099 10.5 0
212 106 2025-03-23 C95 ROTA JVT PILOTO 100 1.5 0
213 106 2025-03-23 C95 ROTA ISA COPILOTO 100 1.5 0
214 107 2025-03-23 C97 ROTA MES PILOTO 099 4.5 0
215 107 2025-03-23 C97 ROTA SEI COPILOTO 099 4.5 0
216 108 2025-03-24 C95 ROTA HCK PILOTO 101 8.67 0
217 108 2025-03-24 C95 ROTA KVN COPILOTO 101 8.67 0
218 109 2025-03-24 C98 ROTA FER PILOTO 102 18.75 0
219 109 2025-03-24 C98 ROTA LRS COPILOTO 102 18.75 0
220 110 2025-03-27 C95 ROTA CAR PILOTO 104 3.67 0
221 110 2025-03-27 C95 ROTA BRJ COPILOTO 104 3.67 0
222 111 2025-03-27 C95 ROTA PCN PILOTO 105 8.33 0
223 111 2025-03-27 C95 ROTA GMR COPILOTO 105 8.33 0
224 112 2025-03-27 C97 ROTA MES PILOTO 106 9.17 0
225 112 2025-03-27 C97 ROTA JVT COPILOTO 106 9.17 0
226 113 2025-03-29 C97 ROTA MAT PILOTO 107 10.58 0
227 113 2025-03-29 C97 ROTA SEI COPILOTO 107 10.58 0
228 114 2025-03-29 C95 ROTA MHL PILOTO 108 9.5 0
229 114 2025-03-29 C95 ROTA BMK COPILOTO 108 9.5 0
230 115 2025-03-31 C95 ROTA JVT PILOTO 110 4.75 0
231 115 2025-03-31 C95 ROTA KVN COPILOTO 110 4.75 0
232 116 2025-03-31 C98 LOCAL HAI PILOTO 111 0.33 0
233 116 2025-03-31 C98 LOCAL MCH COPILOTO 111 0.33 0
234 117 2025-04-01 C95 LOCAL BRI PILOTO 112 1.0 0
235 117 2025-04-01 C95 LOCAL MHL COPILOTO 112 1.0 0
236 118 2025-04-02 C98 LOCAL FIA PILOTO 113 0.67 0
237 118 2025-04-02 C98 LOCAL LOU COPILOTO 113 0.67 0
238 119 2025-04-02 C98 LOCAL AEU PILOTO CPT 2.0 0
239 119 2025-04-02 C98 LOCAL RAY COPILOTO CPT 2.0 0
240 120 2025-04-02 C98 LOCAL MED PILOTO 114 1.0 0
241 120 2025-04-02 C98 LOCAL SUG COPILOTO 114 1.0 0
242 121 2025-04-02 C97 ROTA MAT PILOTO 109 2.75 0
243 121 2025-04-02 C97 ROTA LPS COPILOTO 109 2.75 0
244 122 2025-04-03 C97 LOCAL AEU PILOTO 115 1.5 0
245 122 2025-04-03 C97 LOCAL MAT COPILOTO 115 1.5 0
246 123 2025-04-03 C97 LOCAL AEU PILOTO 116 1.0 0
247 123 2025-04-03 C97 LOCAL SEI COPILOTO 116 1.0 0
248 124 2025-04-03 C98 LOCAL CNI PILOTO 117 1.0 0
249 124 2025-04-03 C98 LOCAL LOU COPILOTO 117 1.0 0
250 125 2025-04-04 C97 LOCAL AEU PILOTO 118 0.5 0
251 125 2025-04-04 C97 LOCAL MAT COPILOTO 118 0.5 0
252 126 2025-04-05 C95 ROTA CAR PILOTO 119 4.75 0
253 126 2025-04-05 C95 ROTA GMR COPILOTO 119 4.75 0
254 127 2025-04-06 C97 ROTA MES PILOTO 120 15.42 0
255 127 2025-04-06 C97 ROTA JVT COPILOTO 120 15.42 0
256 128 2025-04-07 C98 LOCAL FER PILOTO 121 1.5 0
257 128 2025-04-07 C98 LOCAL RAY COPILOTO 121 1.5 0
258 129 2025-04-08 C98 ROTA BRI PILOTO 122 5.33 0
259 129 2025-04-08 C98 ROTA FER COPILOTO 122 5.33 0
260 130 2025-04-11 C97 ROTA MES PILOTO 125 13.58 0
261 130 2025-04-11 C97 ROTA HCK COPILOTO 125 13.58 0
262 131 2025-04-11 C95 ROTA PCN PILOTO 123 3.92 0
263 131 2025-04-11 C95 ROTA BMK COPILOTO 123 3.92 0
264 132 2025-04-14 C98 LOCAL MED PILOTO 124 1.5 0
265 132 2025-04-14 C98 LOCAL AEU COPILOTO 124 1.5 0
266 133 2025-04-14 C95 LOCAL RAY PILOTO 126 1.0 0
267 133 2025-04-14 C95 LOCAL CAR COPILOTO 126 1.0 0
268 134 2025-04-14 C98 LOCAL BRI PILOTO 127 1.0 0
269 134 2025-04-14 C98 LOCAL SLS COPILOTO 127 1.0 0
270 135 2025-04-15 C95 ROTA CAR PILOTO 128 9.58 0
271 135 2025-04-15 C95 ROTA ISA COPILOTO 128 9.58 0
272 136 2025-04-15 C98 LOCAL MED PILOTO 129 1.5 0
273 136 2025-04-15 C98 LOCAL RAY COPILOTO 129 1.5 0
274 137 2025-04-15 C98 LOCAL FER PILOTO 130 1.5 0
275 137 2025-04-15 C98 LOCAL AEU COPILOTO 130 1.5 0
276 138 2025-04-15 C98 LOCAL FIA PILOTO 131 1.0 0
277 138 2025-04-15 C98 LOCAL RCH COPILOTO 131 1.0 0
278 139 2025-04-17 C97 LOCAL MES PILOTO 132 1.0 0
279 139 2025-04-17 C97 LOCAL MAT COPILOTO 132 1.0 0
280 140 2025-04-18 C95 ROTA BRJ PILOTO 133 7.5 0
281 140 2025-04-18 C95 ROTA BMK COPILOTO 133 7.5 0
282 141 2025-04-21 C98 ROTA MED PILOTO 134 11.92 0
283 141 2025-04-21 C98 ROTA SUG COPILOTO 134 11.92 0
284 142 2025-04-22 C95 ROTA BRI PILOTO 135 12.67 0
285 142 2025-04-22 C95 ROTA GMR COPILOTO 135 12.67 0
286 143 2025-04-22 C98 LOCAL CFF PILOTO 136 0.08 0
287 143 2025-04-22 C98 LOCAL DOG COPILOTO 136 0.08 0
288 144 2025-04-25 C98 ROTA MED PILOTO 137 6.58 0
289 144 2025-04-25 C98 ROTA MCH COPILOTO 137 6.58 0
290 145 2025-04-26 C98 ROTA MCH PILOTO 139 11.5 0
291 145 2025-04-26 C98 ROTA DOG COPILOTO 139 11.5 0
292 146 2025-04-29 C98 LOCAL SLS PILOTO 141 1.0 0
293 146 2025-04-29 C98 LOCAL FER COPILOTO 141 1.0 0
294 147 2025-04-30 C97 ROTA SEI PILOTO 140 11.5 0
295 147 2025-04-30 C97 ROTA LPS COPILOTO 140 11.5 0
296 148 2025-05-04 C97 ROTA MAT PILOTO 142 5.67 0
297 148 2025-05-04 C97 ROTA JVT COPILOTO 142 5.67 0
298 149 2025-05-05 C95 ROTA BRJ PILOTO 143 7.25 0
299 149 2025-05-05 C95 ROTA ISA COPILOTO 143 7.25 0
300 150 2025-05-05 C98 LOCAL HAI PILOTO 144 1.0 0
301 150 2025-05-05 C98 LOCAL AEU COPILOTO 144 1.0 0
302 151 2025-05-05 C98 LOCAL CNI PILOTO 145 1.0 0
303 151 2025-05-05 C98 LOCAL RAY COPILOTO 145 1.0 0
304 152 2025-05-06 C98 ROTA MED PILOTO 146 6.33 0
305 152 2025-05-06 C98 ROTA PFR COPILOTO 146 6.33 0
306 153 2025-05-07 C95 ROTA PCN PILOTO 147 3.67 0
307 153 2025-05-07 C95 ROTA GMR COPILOTO 147 3.67 0
308 154 2025-05-07 C98 LOCAL SLS PILOTO 148 1.0 0
309 154 2025-05-07 C98 LOCAL GMS COPILOTO 148 1.0 0
310 155 2025-05-08 C98 ROTA FER PILOTO 150 6.67 0
311 155 2025-05-08 C98 ROTA MCH COPILOTO 150 6.67 0
312 156 2025-05-08 C98 ROTA HAI PILOTO 149 6.33 0
313 156 2025-05-08 C98 ROTA LRS COPILOTO 149 6.33 0
314 157 2025-05-09 C95 ROTA CAR PILOTO 151 3.58 0
315 157 2025-05-09 C95 ROTA KVN COPILOTO 151 3.58 0
316 158 2025-05-09 C98 ROTA MED PILOTO 152 17.75 0
317 158 2025-05-09 C98 ROTA BEN COPILOTO 152 17.75 0
318 159 2025-05-09 C95 ROTA RAY PILOTO 153 5.33 0
319 159 2025-05-09 C95 ROTA BMK COPILOTO 153 5.33 0
320 160 2025-05-10 C95 ROTA BRJ PILOTO 154 11.58 0
321 160 2025-05-10 C95 ROTA ISA COPILOTO 154 11.58 0
322 161 2025-05-12 C95 ROTA PCN PILOTO 155 7.58 0
323 161 2025-05-12 C95 ROTA GMR COPILOTO 155 7.58 0
324 162 2025-05-13 C98 ROTA BRI PILOTO 156 4.83 0
325 162 2025-05-13 C98 ROTA RAY COPILOTO 156 4.83 0
326 163 2025-05-14 C95 ROTA CAR PILOTO 157 8.0 0
327 163 2025-05-14 C95 ROTA KVN COPILOTO 157 8.0 0
328 164 2025-05-14 C95 ROTA CAR PILOTO 157 8.75 0
329 164 2025-05-14 C95 ROTA KVN COPILOTO 157 8.75 0
330 165 2025-05-19 C95 ROTA HCK PILOTO 158 3.58 0
331 165 2025-05-19 C95 ROTA BMK COPILOTO 158 3.58 0
332 166 2025-05-20 C95 ROTA MHL PILOTO 159 11.17 0
333 166 2025-05-20 C95 ROTA GMR COPILOTO 159 11.17 0
334 167 2025-05-20 C97 LOCAL MAT PILOTO 160 1.0 0
335 167 2025-05-20 C97 LOCAL SEI COPILOTO 160 1.0 0
336 168 2025-05-21 C98 LOCAL CNI PILOTO 161 1.0 0
337 168 2025-05-21 C98 LOCAL MCH COPILOTO 161 1.0 0
338 169 2025-05-22 C98 ROTA AEU PILOTO 162 9.42 0
339 169 2025-05-22 C98 ROTA SLS COPILOTO 162 9.42 0
340 170 2025-05-22 C97 ROTA MAT PILOTO 163 11.08 0
341 170 2025-05-22 C97 ROTA JVT COPILOTO 163 11.08 0
342 171 2025-05-23 C98 LOCAL FIA PILOTO 164 1.0 0
343 171 2025-05-23 C98 LOCAL DOG COPILOTO 164 1.0 0
344 172 2025-05-25 C97 ROTA SEI PILOTO 165 9.67 0
345 172 2025-05-25 C97 ROTA LPS COPILOTO 165 9.67 0
346 173 2025-05-26 C98 ROTA HAI PILOTO 166 11.92 0
347 173 2025-05-26 C98 ROTA PFR COPILOTO 166 11.92 0
348 174 2025-05-27 C98 ROTA BRI PILOTO 166 3.33 0
349 174 2025-05-27 C98 ROTA LRS COPILOTO 166 3.33 0
350 175 2025-05-29 C98 ROTA MED PILOTO 167 4.92 0
351 175 2025-05-29 C98 ROTA RCH COPILOTO 167 4.92 0
352 176 2025-05-29 C98 LOCAL MCH PILOTO 168 1.0 0
353 176 2025-05-29 C98 LOCAL DOG COPILOTO 168 1.0 0
354 177 2025-05-30 C98 ROTA SLS PILOTO 169 14.33 0
355 177 2025-05-30 C98 ROTA PFR COPILOTO 169 14.33 0
356 178 2025-06-01 C98 ROTA MCH PILOTO 170 3.33 0
357 178 2025-06-01 C98 ROTA GMS COPILOTO 170 3.33 0
358 179 2025-06-01 C98 ROTA MED PILOTO 170 6.83 0
359 179 2025-06-01 C98 ROTA LRS COPILOTO 170 6.83 0
360 180 2025-06-02 C98 ROTA CNI PILOTO 171 5.67 0
361 180 2025-06-02 C98 ROTA RCH COPILOTO 171 5.67 0
362 181 2025-06-03 C98 ROTA CNI PILOTO 171 11.67 0
363 181 2025-06-03 C98 ROTA RCH COPILOTO 171 11.67 0
364 182 2025-06-03 C98 ROTA BRI PILOTO 172 2.0 0
365 182 2025-06-03 C98 ROTA LRS COPILOTO 172 2.0 0
366 183 2025-06-03 C98 ROTA MCH PILOTO 172 4.0 0
367 183 2025-06-03 C98 ROTA DOG COPILOTO 172 4.0 0
368 184 2025-06-04 C98 ROTA MCH PILOTO 173 5.0 0
369 184 2025-06-04 C98 ROTA GMS COPILOTO 173 5.0 0
370 185 2025-06-06 C98 ROTA SLS PILOTO 174 13.33 0
371 185 2025-06-06 C98 ROTA LRS COPILOTO 174 13.33 0
372 186 2025-06-06 C97 LOCAL MAT PILOTO 175 1.0 0
373 186 2025-06-06 C97 LOCAL SEI COPILOTO 175 1.0 0
374 187 2025-06-07 C97 ROTA SEI PILOTO 178 11.92 0
375 187 2025-06-07 C97 ROTA LPS COPILOTO 178 11.92 0
376 188 2025-06-07 C95 LOCAL MES PILOTO 179 1.0 0
377 188 2025-06-07 C95 LOCAL BMK COPILOTO 179 1.0 0
378 189 2025-06-08 C97 ROTA MES PILOTO 176 9.5 0
379 189 2025-06-08 C97 ROTA AEU COPILOTO 176 9.5 0
380 190 2025-06-08 C98 ROTA MCH PILOTO 177 16.33 0
381 190 2025-06-08 C98 ROTA GMS COPILOTO 177 16.33 0
382 191 2025-06-10 C95 ROTA PCN PILOTO 180 9.33 0
383 191 2025-06-10 C95 ROTA ISA COPILOTO 180 9.33 0
384 192 2025-06-12 C95 ROTA GMR PILOTO 181 11.5 0
385 192 2025-06-12 C95 ROTA BMK COPILOTO 181 11.5 0
386 193 2025-06-12 C98 ROTA HAI PILOTO 182 11.08 0
387 193 2025-06-12 C98 ROTA SUG COPILOTO 182 11.08 0
388 194 2025-06-12 C97 LOCAL AEU PILOTO 183 1.5 0
389 194 2025-06-12 C97 LOCAL MHL COPILOTO 183 1.5 0
390 195 2025-06-14 C95 ROTA MHL PILOTO 184 1.5 0
391 195 2025-06-14 C95 ROTA JVT COPILOTO 184 1.5 0
392 196 2025-06-15 C95 ROTA BRJ PILOTO 185 9.33 0
393 196 2025-06-15 C95 ROTA ISA COPILOTO 185 9.33 0
394 197 2025-06-17 C95 LOCAL HCK PILOTO 186 0.67 0
395 197 2025-06-17 C95 LOCAL KVN COPILOTO 186 0.67 0
396 198 2025-06-18 C95 ROTA BMK PILOTO 187 9.5 0
397 198 2025-06-18 C95 ROTA ISA COPILOTO 187 9.5 0
398 199 2025-06-18 C98 LOCAL MCH PILOTO 189 1.0 0
399 199 2025-06-18 C98 LOCAL GMS COPILOTO 189 1.0 0
400 200 2025-06-18 C95 ROTA GMR PILOTO 191 8.0 0
401 200 2025-06-18 C95 ROTA KVN COPILOTO 191 8.0 0
402 201 2025-06-21 C98 ROTA SLS PILOTO 192 2.0 0
403 201 2025-06-21 C98 ROTA AEU COPILOTO 192 2.0 0
404 202 2025-06-21 C97 ROTA MAT PILOTO 193 10.0 0
405 202 2025-06-21 C97 ROTA JVT COPILOTO 193 10.0 0
406 203 2025-06-23 C97 LOCAL MAT PILOTO 190 1.5 0
407 203 2025-06-23 C97 LOCAL MHL COPILOTO 190 1.5 0
408 204 2025-06-23 C98 LOCAL MCH PILOTO 194 1.0 0
409 204 2025-06-23 C98 LOCAL DOG COPILOTO 194 1.0 0
410 205 2025-06-24 C97 LOCAL AEU PILOTO 195 1.0 0
411 205 2025-06-24 C97 LOCAL MHL COPILOTO 195 1.0 0
412 206 2025-06-25 C97 LOCAL AEU PILOTO 198 1.0 0
413 206 2025-06-25 C97 LOCAL MES COPILOTO 198 1.0 0
414 207 2025-06-25 C98 LOCAL SLS PILOTO 199 1.0 0
415 207 2025-06-25 C98 LOCAL CFF COPILOTO 199 1.0 0
416 208 2025-06-25 C98 LOCAL CNI PILOTO 200 1.0 0
417 208 2025-06-25 C98 LOCAL FER COPILOTO 200 1.0 0
418 209 2025-06-26 C97 ROTA MAT PILOTO 202 9.92 0
419 209 2025-06-26 C97 ROTA MHL COPILOTO 202 9.92 0
420 210 2025-06-26 C98 LOCAL MED PILOTO 201 0.67 0
421 210 2025-06-26 C98 LOCAL CFF COPILOTO 201 0.67 0
422 211 2025-06-26 C98 LOCAL FER PILOTO 203 0.25 0
423 211 2025-06-26 C98 LOCAL MCH COPILOTO 203 0.25 0
424 212 2025-06-26 C98 LOCAL FER PILOTO 204 0.25 0
425 212 2025-06-26 C98 LOCAL DOG COPILOTO 204 0.25 0
426 213 2025-06-27 C95 LOCAL BRI PILOTO 205 1.0 0
427 213 2025-06-27 C95 LOCAL RAY COPILOTO 205 1.0 0
428 214 2025-06-29 C97 ROTA MAT PILOTO 207 10.67 0
429 214 2025-06-29 C97 ROTA MHL COPILOTO 207 10.67 0
430 215 2025-06-30 C98 ROTA CNI PILOTO 208 6.67 0
431 215 2025-06-30 C98 ROTA PFR COPILOTO 208 6.67 0
432 216 2025-07-01 C98 ROTA SLS PILOTO 211 10.92 0
433 216 2025-07-01 C98 ROTA SUG COPILOTO 211 10.92 0
434 217 2025-07-01 C95 LOCAL RAY PILOTO 206 1.0 0
435 217 2025-07-01 C95 LOCAL BMK COPILOTO 206 1.0 0
436 218 2025-07-01 C95 ROTA PCN PILOTO 209 5.92 0
437 218 2025-07-01 C95 ROTA KVN COPILOTO 209 5.92 0
438 219 2025-07-02 C98 ROTA RAY PILOTO 212 3.17 0
439 219 2025-07-02 C98 ROTA CNI COPILOTO 212 3.17 0
440 220 2025-07-04 C98 ROTA AEU PILOTO 214 5.0 0
441 220 2025-07-04 C98 ROTA SLS COPILOTO 214 5.0 0
442 221 2025-07-04 C98 ROTA MED PILOTO 213 6.92 0
443 221 2025-07-04 C98 ROTA RAY COPILOTO 213 6.92 0
444 222 2025-07-04 C97 LOCAL MES PILOTO 210 1.5 0
445 222 2025-07-04 C97 LOCAL FIA COPILOTO 210 1.5 0
446 223 2025-07-05 C97 LOCAL MAT PILOTO 216 1.5 0
447 223 2025-07-05 C97 LOCAL FIA COPILOTO 216 1.5 0
448 224 2025-07-07 C98 ROTA FER PILOTO 215 5.92 0
449 224 2025-07-07 C98 ROTA RCH COPILOTO 215 5.92 0
450 225 2025-07-07 C97 LOCAL AEU PILOTO 217 1.0 0
451 225 2025-07-07 C97 LOCAL FIA COPILOTO 217 1.0 0
452 226 2025-07-10 C98 ROTA CNI PILOTO 218 12.17 0
453 226 2025-07-10 C98 ROTA SUG COPILOTO 218 12.17 0
454 227 2025-07-11 C97 ROTA MAT PILOTO 219 6.83 0
455 227 2025-07-11 C97 ROTA FIA COPILOTO 219 6.83 0
456 228 2025-07-11 C98 ROTA FER PILOTO 220 13.67 0
457 228 2025-07-11 C98 ROTA LRS COPILOTO 220 13.67 0
458 229 2025-07-12 C97 ROTA FIA PILOTO 221 10.25 0
459 229 2025-07-12 C97 ROTA LPS COPILOTO 221 10.25 0
460 230 2025-07-12 C98 ROTA CFF PILOTO 223 8.0 0
461 230 2025-07-12 C98 ROTA MCH COPILOTO 223 8.0 0
462 231 2025-07-14 C98 ROTA GMS PILOTO 224 4.83 0
463 231 2025-07-14 C98 ROTA DOG COPILOTO 224 4.83 0
464 232 2025-07-14 C98 ROTA MCH PILOTO 222 9.75 0
465 232 2025-07-14 C98 ROTA CFF COPILOTO 222 9.75 0
466 233 2025-07-15 C98 ROTA CNI PILOTO 225 4.83 0
467 233 2025-07-15 C98 ROTA PFR COPILOTO 225 4.83 0
468 234 2025-07-16 C97 ROTA FIA PILOTO 226 6.17 0
469 234 2025-07-16 C97 ROTA LPS COPILOTO 226 6.17 0
470 235 2025-07-19 C98 ROTA GMS PILOTO 228 17.83 0
471 235 2025-07-19 C98 ROTA DOG COPILOTO 228 17.83 0
472 236 2025-07-21 C98 ROTA SLS PILOTO 228 3.33 0
473 236 2025-07-21 C98 ROTA AEU COPILOTO 228 3.33 0
474 237 2025-08-01 C98 LOCAL CFF PILOTO 188 1.0 0
475 237 2025-08-01 C98 LOCAL MCH COPILOTO 188 1.0 0
476 238 2025-08-04 C95 LOCAL BMK PILOTO 229 0.08 0
477 238 2025-08-04 C95 LOCAL KVN COPILOTO 229 0.08 0
478 239 2025-08-06 C98 ROTA MCH PILOTO 231 6.67 0
479 239 2025-08-06 C98 ROTA GMS COPILOTO 231 6.67 0
480 240 2025-08-12 C95 LOCAL BRI PILOTO 232 1.0 0
481 240 2025-08-12 C95 LOCAL GMR COPILOTO 232 1.0 0
482 241 2025-08-16 C97 LOCAL MES PILOTO 233 0.92 0
483 241 2025-08-16 C97 LOCAL SEI COPILOTO 233 0.92 0
484 242 2025-08-16 C97 LOCAL FIA PILOTO 234 0.08 0
485 242 2025-08-16 C97 LOCAL LPS COPILOTO 234 0.08 0
486 243 2025-08-18 C98 ROTA RAY PILOTO 235 6.67 0
487 243 2025-08-18 C98 ROTA SLS COPILOTO 235 6.67 0
488 244 2025-08-21 C95 LOCAL BRI PILOTO 236 1.0 0
489 244 2025-08-21 C95 LOCAL HCK COPILOTO 236 1.0 0
490 245 2025-08-25 C98 LOCAL FIA PILOTO 237 1.0 0
491 245 2025-08-25 C98 LOCAL SLS COPILOTO 237 1.0 0
492 246 2025-08-25 C98 LOCAL FIA PILOTO 238 0.08 0
493 246 2025-08-25 C98 LOCAL DOG COPILOTO 238 0.08 0
494 247 2025-08-26 C98 LOCAL FIA PILOTO 239 1.0 0
495 247 2025-08-26 C98 LOCAL LRS COPILOTO 239 1.0 0
496 248 2025-08-26 C97 LOCAL MES PILOTO 240 1.0 0
497 248 2025-08-26 C97 LOCAL AEU COPILOTO 240 1.0 0
498 249 2025-08-26 C98 LOCAL FIA PILOTO 241 1.0 0
499 249 2025-08-26 C98 LOCAL MED COPILOTO 241 1.0 0
500 250 2025-08-26 C95 LOCAL BRI PILOTO 242 1.0 0
501 250 2025-08-26 C95 LOCAL PCN COPILOTO 242 1.0 0
502 251 2025-08-27 C97 ROTA FIA PILOTO 243 10.5 0
503 251 2025-08-27 C97 ROTA LPS COPILOTO 243 10.5 0
504 252 2025-08-27 C98 LOCAL MED PILOTO 244 1.0 0
505 252 2025-08-27 C98 LOCAL RCH COPILOTO 244 1.0 0
506 253 2025-08-27 C98 LOCAL MED PILOTO 245 1.0 0
507 253 2025-08-27 C98 LOCAL PFR COPILOTO 245 1.0 0
508 254 2025-09-01 C97 LOCAL AEU PILOTO 246 1.0 0
509 254 2025-09-01 C97 LOCAL LPS COPILOTO 246 1.0 0
510 255 2025-09-02 C98 LOCAL FIA PILOTO 247 1.0 0
511 255 2025-09-02 C98 LOCAL SLS COPILOTO 247 1.0 0
512 256 2025-09-02 C95 LOCAL BRI PILOTO 248 1.0 0
513 256 2025-09-02 C95 LOCAL CAR COPILOTO 248 1.0 0
514 257 2025-09-02 C98 LOCAL MED PILOTO 249 0.08 0
515 257 2025-09-02 C98 LOCAL SLS COPILOTO 249 0.08 0
516 258 2025-09-03 C95 LOCAL CAR PILOTO 250 0.08 0
517 258 2025-09-03 C95 LOCAL KVN COPILOTO 250 0.08 0
518 259 2025-09-03 C95 LOCAL CAR PILOTO 251 0.08 0
519 259 2025-09-03 C95 LOCAL ISA COPILOTO 251 0.08 0
520 260 2025-09-05 C98 ROTA MCH PILOTO 252 4.17 0
521 260 2025-09-05 C98 ROTA LRS COPILOTO 252 4.17 0
522 261 2025-09-10 C95 ROTA BRI PILOTO 253 2.17 0
523 261 2025-09-10 C95 ROTA BRJ COPILOTO 253 2.17 0
524 262 2025-09-10 C98 LOCAL GMS PILOTO 254 0.08 0
525 262 2025-09-10 C98 LOCAL LRS COPILOTO 254 0.08 0
526 263 2025-09-11 C98 LOCAL DOG PILOTO 255 0.08 0
527 263 2025-09-11 C98 LOCAL PFR COPILOTO 255 0.08 0
528 264 2025-09-11 C98 LOCAL FIA PILOTO 256 1.0 0
529 264 2025-09-11 C98 LOCAL CFF COPILOTO 256 1.0 0
530 265 2025-09-12 C97 LOCAL MES PILOTO 257 0.08 0
531 265 2025-09-12 C97 LOCAL SEI COPILOTO 257 0.08 0
532 266 2025-09-15 C98 LOCAL MED PILOTO 258 1.0 0
533 266 2025-09-15 C98 LOCAL SUG COPILOTO 258 1.0 0
534 267 2025-09-16 C97 ROTA FIA PILOTO 260 8.67 0
535 267 2025-09-16 C97 ROTA SEI COPILOTO 260 8.67 0
536 268 2025-09-16 C98 ROTA DOG PILOTO 259 2.0 0
537 268 2025-09-16 C98 ROTA SUG COPILOTO 259 2.0 0
538 269 2025-09-17 C98 ROTA CFF PILOTO 261 0.67 0
539 269 2025-09-17 C98 ROTA SUG COPILOTO 261 0.67 0
540 270 2025-09-18 C97 ROTA FIA PILOTO 262 5.0 0
541 270 2025-09-18 C97 ROTA AEU COPILOTO 262 5.0 0
542 271 2025-09-22 C97 LOCAL AEU PILOTO 263 1.5 0
543 271 2025-09-22 C97 LOCAL LPS COPILOTO 263 1.5 0
544 272 2025-09-24 C95 LOCAL HCK PILOTO 264 0.08 0
545 272 2025-09-24 C95 LOCAL CAR COPILOTO 264 0.08 0
546 273 2025-09-25 C98 LOCAL CFF PILOTO 265 1.0 0
547 273 2025-09-25 C98 LOCAL MCH COPILOTO 265 1.0 0
548 274 2025-10-01 C98 LOCAL MCH PILOTO 268 0.08 0
549 274 2025-10-01 C98 LOCAL GMS COPILOTO 268 0.08 0
550 275 2025-09-27 C98 ROTA CFF PILOTO 266 20.58 0
551 275 2025-09-27 C98 ROTA DOG COPILOTO 266 20.58 0
552 276 2025-09-29 C97 ROTA AEU PILOTO 267 6.0 0
553 276 2025-09-29 C97 ROTA LPS COPILOTO 267 6.0 0
554 277 2025-10-02 C98 ROTA CFF PILOTO 269 8.5 0
555 277 2025-10-02 C98 ROTA DOG COPILOTO 269 8.5 0
556 278 2025-10-05 C95 LOCAL BRI PILOTO 270 3.0 0
557 278 2025-10-05 C95 LOCAL CAR COPILOTO 270 3.0 0
558 279 2025-10-05 C95 ROTA PCN PILOTO 271 3.5 0
559 279 2025-10-05 C95 ROTA GMR COPILOTO 271 3.5 0
560 280 2025-10-08 C95 ROTA CAR PILOTO 272 3.58 0
561 280 2025-10-08 C95 ROTA KVN COPILOTO 272 3.58 0
562 281 2025-10-11 C95 LOCAL BRI PILOTO 273 1.0 0
563 281 2025-10-11 C95 LOCAL BRJ COPILOTO 273 1.0 0
564 282 2025-10-11 C95 LOCAL BRI PILOTO 274 1.0 0
565 282 2025-10-11 C95 LOCAL BMK COPILOTO 274 1.0 0
566 283 2025-10-13 C97 LOCAL MES PILOTO 275 0.25 0
567 283 2025-10-13 C97 LOCAL SEI COPILOTO 275 0.25 0
568 284 2025-10-13 C98 LOCAL MED PILOTO 276 0.08 0
569 284 2025-10-13 C98 LOCAL SLS COPILOTO 276 0.08 0
570 285 2025-10-21 C98 LOCAL FIA PILOTO 277 0.67 0
571 285 2025-10-21 C98 LOCAL BEN COPILOTO 277 0.67 0
572 286 2025-10-21 C95 LOCAL HCK PILOTO 278 0.08 0
573 286 2025-10-21 C95 LOCAL ISA COPILOTO 278 0.08 0
574 287 2025-10-22 C98 LOCAL GMS PILOTO 279 1.0 0
575 287 2025-10-22 C98 LOCAL DOG COPILOTO 279 1.0 0
576 288 2025-10-23 C95 LOCAL BRI PILOTO 280 1.0 0
577 288 2025-10-23 C95 LOCAL BRJ COPILOTO 280 1.0 0
578 289 2025-10-23 C95 LOCAL BRI PILOTO 281 1.5 0
579 289 2025-10-23 C95 LOCAL JVT COPILOTO 281 1.5 0
580 290 2025-10-28 C95 LOCAL BRI PILOTO 282 1.5 0
581 290 2025-10-28 C95 LOCAL PCN COPILOTO 282 1.5 0
582 291 2025-10-29 C98 LOCAL GMS PILOTO 283 0.08 0
583 291 2025-10-29 C98 LOCAL PFR COPILOTO 283 0.08 0
584 292 2025-10-30 C98 LOCAL MED PILOTO 284 0.58 0
585 292 2025-10-30 C98 LOCAL BEN COPILOTO 284 0.58 0
586 293 2025-11-04 C98 LOCAL MCH PILOTO 285 0.08 0
587 293 2025-11-04 C98 LOCAL DOG COPILOTO 285 0.08 0
588 294 2025-11-08 C98 ROTA CFF PILOTO 286 10.83 0
589 294 2025-11-08 C98 ROTA BEN COPILOTO 286 10.83 0
590 295 2025-11-08 C95 LOCAL BRI PILOTO 288 2.0 0
591 295 2025-11-08 C95 LOCAL PCN COPILOTO 288 2.0 0
592 296 2025-11-11 C98 ROTA MCH PILOTO 287 5.92 0
593 296 2025-11-11 C98 ROTA PFR COPILOTO 287 5.92 0
594 297 2025-11-11 C95 LOCAL PCN PILOTO 289 0.08 0
595 297 2025-11-11 C95 LOCAL BMK COPILOTO 289 0.08 0
596 298 2025-11-11 C95 LOCAL PCN PILOTO 290 1.0 0
597 298 2025-11-11 C95 LOCAL GMR COPILOTO 290 1.0 0
598 299 2025-11-13 C98 ROTA DOG PILOTO 287 5.92 0
599 299 2025-11-13 C98 ROTA LRS COPILOTO 287 5.92 0
600 300 2025-11-24 C98 LOCAL SLS PILOTO 292 0.08 0
601 300 2025-11-24 C98 LOCAL RCH COPILOTO 292 0.08 0
602 301 2025-11-24 C98 ROTA GMS PILOTO 291 5.0 0
603 301 2025-11-24 C98 ROTA BEN COPILOTO 291 5.0 0
604 302 2025-11-24 C95 LOCAL BRJ PILOTO 293 0.08 0
605 302 2025-11-24 C95 LOCAL ISA COPILOTO 293 0.08 0
606 303 2025-11-27 C95 LOCAL MHL PILOTO 294 1.0 0
607 303 2025-11-27 C95 LOCAL BRJ COPILOTO 294 1.0 0
608 304 2025-12-01 C95 ROTA BRI PILOTO 295 1.5 0
609 304 2025-12-01 C95 ROTA ISA COPILOTO 295 1.5 0
610 305 2025-12-02 C95 ROTA CAR PILOTO 296 6.58 0
611 305 2025-12-02 C95 ROTA KVN COPILOTO 296 6.58 0
612 306 2025-12-03 C97 ROTA MES PILOTO 297 9.08 0
613 306 2025-12-03 C97 ROTA SEI COPILOTO 297 9.08 0
614 307 2025-12-03 C98 LOCAL SLS PILOTO 298 0.08 0
615 307 2025-12-03 C98 LOCAL RCH COPILOTO 298 0.08 0
616 308 2025-12-03 C95 LOCAL JVT PILOTO 299 0.67 0
617 308 2025-12-03 C95 LOCAL GMR COPILOTO 299 0.67 0
618 309 2025-12-03 C95 LOCAL CAR PILOTO 300 1.0 0
619 309 2025-12-03 C95 LOCAL BMK COPILOTO 300 1.0 0
620 310 2025-12-03 C95 LOCAL CAR PILOTO 300 1.0 0
621 310 2025-12-03 C95 LOCAL BMK COPILOTO 300 1.0 0
622 311 2025-12-03 C95 LOCAL CAR PILOTO 300 1.0 0
623 311 2025-12-03 C95 LOCAL BMK COPILOTO 300 1.0 0
624 312 2025-12-03 C95 LOCAL JVT PILOTO 299 0.67 0
625 312 2025-12-03 C95 LOCAL GMR COPILOTO 299 0.67 0
626 313 2025-12-03 C95 ROTA BRJ PILOTO 304 12.0 0
627 313 2025-12-03 C95 ROTA ISA COPILOTO 304 12.0 0
628 314 2025-12-04 C95 ROTA GMR PILOTO 302 2.67 0
629 314 2025-12-04 C95 ROTA KVN COPILOTO 302 2.67 0
630 315 2025-12-04 C95 ROTA GMR PILOTO 303 14.0 0
631 315 2025-12-04 C95 ROTA KVN COPILOTO 303 14.0 0
632 316 2025-12-05 C95 LOCAL BRI PILOTO 301 1.5 0
633 316 2025-12-05 C95 LOCAL CAR COPILOTO 301 1.5 0
634 317 2025-12-05 C98 LOCAL SLS PILOTO 305 1.0 0
635 317 2025-12-05 C98 LOCAL MCH COPILOTO 305 1.0 0
636 318 2025-12-05 C98 LOCAL SLS PILOTO 306 1.0 0
637 318 2025-12-05 C98 LOCAL SUG COPILOTO 306 1.0 0
638 319 2025-12-05 C98 ROTA DOG PILOTO 308 5.0 0
639 319 2025-12-05 C98 ROTA BEN COPILOTO 308 5.0 0
640 320 2025-12-06 C98 ROTA GMS PILOTO 309 3.5 0
641 320 2025-12-06 C98 ROTA LRS COPILOTO 309 3.5 0
642 321 2025-12-06 C98 ROTA MCH PILOTO 310 2.08 0
643 321 2025-12-06 C98 ROTA RCH COPILOTO 310 2.08 0
644 322 2025-12-06 C95 LOCAL BRI PILOTO 307 2.5 0
645 322 2025-12-06 C95 LOCAL CAR COPILOTO 307 2.5 0
646 323 2025-12-07 C98 ROTA CFF PILOTO 313 3.5 0
647 323 2025-12-07 C98 ROTA RCH COPILOTO 313 3.5 0
648 324 2025-12-07 C98 ROTA DOG PILOTO 314 2.08 0
649 324 2025-12-07 C98 ROTA BEN COPILOTO 314 2.08 0
650 325 2025-12-08 C95 LOCAL CAR PILOTO 311 0.67 0
651 325 2025-12-08 C95 LOCAL KVN COPILOTO 311 0.67 0
652 326 2025-12-08 C95 LOCAL JVT PILOTO 312 1.0 0
653 326 2025-12-08 C95 LOCAL GMR COPILOTO 312 1.0 0
654 327 2025-12-08 C95 LOCAL JVT PILOTO 312 1.0 0
655 327 2025-12-08 C95 LOCAL GMR COPILOTO 312 1.0 0
656 328 2025-12-08 C95 LOCAL JVT PILOTO 312 1.0 0
657 328 2025-12-08 C95 LOCAL GMR COPILOTO 312 1.0 0
658 329 2025-12-08 C95 LOCAL CAR PILOTO 311 0.67 0
659 329 2025-12-08 C95 LOCAL KVN COPILOTO 311 0.67 0
660 330 2025-12-08 C97 LOCAL MES PILOTO 315 1.0 0
661 330 2025-12-08 C97 LOCAL AEU COPILOTO 315 1.0 0
662 331 2025-12-08 C98 LOCAL CNI PILOTO 316 1.0 0
663 331 2025-12-08 C98 LOCAL FIA COPILOTO 316 1.0 0
664 332 2025-12-09 C95 ROTA HCK PILOTO 317 6.75 0
665 332 2025-12-09 C95 ROTA BRJ COPILOTO 317 6.75 0
666 333 2025-12-09 C98 LOCAL FIA PILOTO 318 1.5 0
667 333 2025-12-09 C98 LOCAL CFF COPILOTO 318 1.5 0
668 334 2025-12-09 C95 LOCAL CAR PILOTO 319 1.0 0
669 334 2025-12-09 C95 LOCAL KVN COPILOTO 319 1.0 0
670 335 2025-12-09 C98 ROTA GMS PILOTO 320 6.0 0
671 335 2025-12-09 C98 ROTA PFR COPILOTO 320 6.0 0
672 336 2025-12-09 C98 LOCAL MED PILOTO 321 0.08 0
673 336 2025-12-09 C98 LOCAL RCH COPILOTO 321 0.08 0
674 337 2025-12-10 C95 ROTA BMK PILOTO 322 6.75 0
675 337 2025-12-10 C95 ROTA BRJ COPILOTO 322 6.75 0
676 338 2025-12-10 C98 LOCAL FIA PILOTO 323 1.5 0
677 338 2025-12-10 C98 LOCAL GMS COPILOTO 323 1.5 0
678 339 2025-12-10 C98 LOCAL MED PILOTO 324 1.0 0
679 339 2025-12-10 C98 LOCAL CFF COPILOTO 324 1.0 0
680 340 2025-12-11 C95 ROTA CAR PILOTO 325 4.83 0
681 340 2025-12-11 C95 ROTA KVN COPILOTO 325 4.83 0
682 341 2025-12-12 C97 LOCAL FIA PILOTO 326 1.0 0
683 341 2025-12-12 C97 LOCAL AEU COPILOTO 326 1.0 0
684 342 2025-12-12 C95 LOCAL JVT PILOTO 327 0.67 0
685 342 2025-12-12 C95 LOCAL BMK COPILOTO 327 0.67 0
686 343 2025-12-12 C95 LOCAL BRI PILOTO 328 1.0 0
687 343 2025-12-12 C95 LOCAL HCK COPILOTO 328 1.0 0
688 344 2025-12-12 C95 LOCAL BRI PILOTO 328 1.0 0
689 344 2025-12-12 C95 LOCAL HCK COPILOTO 328 1.0 0
690 345 2025-12-12 C95 LOCAL BRI PILOTO 328 1.0 0
691 345 2025-12-12 C95 LOCAL HCK COPILOTO 328 1.0 0
692 346 2025-12-12 C95 LOCAL GMR PILOTO 327 0.67 0
693 346 2025-12-12 C95 LOCAL HCK COPILOTO 327 0.67 0
694 347 2025-12-12 C95 LOCAL RAY PILOTO 329 1.0 0
695 347 2025-12-12 C95 LOCAL KVN COPILOTO 329 1.0 0
696 348 2025-12-12 C95 LOCAL MHL PILOTO 329 1.0 0
697 348 2025-12-12 C95 LOCAL ISA COPILOTO 329 1.0 0
698 349 2025-12-12 C95 LOCAL MHL PILOTO 330 1.0 0
699 349 2025-12-12 C95 LOCAL ISA COPILOTO 330 1.0 0
700 350 2025-12-12 C95 LOCAL RAY PILOTO 330 1.0 0
701 350 2025-12-12 C95 LOCAL KVN COPILOTO 330 1.0 0
702 351 2025-12-12 C98 LOCAL MED PILOTO 331 1.0 0
703 351 2025-12-12 C98 LOCAL GMS COPILOTO 331 1.0 0
704 352 2025-12-13 C95 LOCAL RAY PILOTO 333 1.0 0
705 352 2025-12-13 C95 LOCAL CAR COPILOTO 333 1.0 0
706 353 2025-12-13 C95 LOCAL HCK PILOTO 334 1.0 0
707 353 2025-12-13 C95 LOCAL ISA COPILOTO 334 1.0 0
708 354 2025-12-13 C95 LOCAL RAY PILOTO 335 1.0 0
709 354 2025-12-13 C95 LOCAL CAR COPILOTO 335 1.0 0
710 355 2025-12-14 C95 ROTA JVT PILOTO 332 11.33 0
711 355 2025-12-14 C95 ROTA BMK COPILOTO 332 11.33 0
712 356 2025-12-15 C95 LOCAL MHL PILOTO 336 1.0 0
713 356 2025-12-15 C95 LOCAL MES COPILOTO 336 1.0 0
714 357 2025-12-15 C95 LOCAL BRJ PILOTO 336 1.0 0
715 357 2025-12-15 C95 LOCAL KVN COPILOTO 336 1.0 0
716 358 2025-12-15 C95 LOCAL MES PILOTO 337 1.0 0
717 358 2025-12-15 C95 LOCAL MHL COPILOTO 337 1.0 0
718 359 2025-12-15 C95 LOCAL BRJ PILOTO 337 1.0 0
719 359 2025-12-15 C95 LOCAL KVN COPILOTO 337 1.0 0
720 360 2025-12-15 C98 LOCAL CFF PILOTO 338 1.0 0
721 360 2025-12-15 C98 LOCAL BEN COPILOTO 338 1.0 0
722 361 2025-12-16 C95 ROTA BRJ PILOTO 339 5.33 0
723 361 2025-12-16 C95 ROTA KVN COPILOTO 339 5.33 0
724 362 2025-12-16 C98 ROTA CFF PILOTO 340 2.08 0
725 362 2025-12-16 C98 ROTA BEN COPILOTO 340 2.08 0
726 363 2025-12-17 C95 LOCAL CAR PILOTO 342 1.0 0
727 363 2025-12-17 C95 LOCAL MES COPILOTO 342 1.0 0
728 364 2025-12-17 C95 LOCAL GMR PILOTO 342 1.0 0
729 364 2025-12-17 C95 LOCAL BMK COPILOTO 342 1.0 0
730 365 2025-12-17 C95 LOCAL MES PILOTO 343 1.0 0
731 365 2025-12-17 C95 LOCAL CAR COPILOTO 343 1.0 0
732 366 2025-12-17 C95 LOCAL BMK PILOTO 343 1.0 0
733 366 2025-12-17 C95 LOCAL GMR COPILOTO 343 1.0 0
734 367 2025-12-18 C97 ROTA AEU PILOTO 341 10.17 0
735 367 2025-12-18 C97 ROTA MHL COPILOTO 341 10.17 0
736 368 2025-12-18 C98 LOCAL FIA PILOTO 344 1.0 0
737 368 2025-12-18 C98 LOCAL DOG COPILOTO 344 1.0 0
738 369 2025-12-19 C95 ROTA BRI PILOTO 345 5.67 0
739 369 2025-12-19 C95 ROTA HCK COPILOTO 345 5.67 0
740 370 2025-12-20 C98 ROTA MCH PILOTO 347 1.17 0
741 370 2025-12-20 C98 ROTA SUG COPILOTO 347 1.17 0
742 371 2025-12-23 C95 ROTA GMR PILOTO 346 6.42 0
743 371 2025-12-23 C95 ROTA ISA COPILOTO 346 6.42 0
744 372 2025-12-23 C97 LOCAL MES PILOTO 349 0.5 0
745 372 2025-12-23 C97 LOCAL MHL COPILOTO 349 0.5 0
746 373 2025-12-23 C98 ROTA MCH PILOTO 350 0.92 0
747 373 2025-12-23 C98 ROTA RCH COPILOTO 350 0.92 0
748 374 2025-12-28 C95 ROTA CAR PILOTO 352 3.92 0
749 374 2025-12-28 C95 ROTA HCK COPILOTO 352 3.92 0
750 375 2025-12-28 C95 ROTA JVT PILOTO 353 10.0 0
751 375 2025-12-28 C95 ROTA BRJ COPILOTO 353 10.0 0
752 376 2025-12-29 C95 ROTA BMK PILOTO 354 3.92 0
753 376 2025-12-29 C95 ROTA KVN COPILOTO 354 3.92 0
754 377 2025-12-29 C97 LOCAL AEU PILOTO 348 0.75 0
755 377 2025-12-29 C97 LOCAL LPS COPILOTO 348 0.75 0
756 378 2025-12-29 C97 LOCAL AEU PILOTO 351 0.75 0
757 378 2025-12-29 C97 LOCAL SEI COPILOTO 351 0.75 0
758 379 2025-12-29 C95 LOCAL MES PILOTO 355 1.0 0
759 379 2025-12-29 C95 LOCAL CAR COPILOTO 355 1.0 0
760 380 2025-12-29 C95 LOCAL ISA PILOTO 355 1.0 0
761 380 2025-12-29 C95 LOCAL MHL COPILOTO 355 1.0 0
762 381 2025-12-29 C95 LOCAL MHL PILOTO 356 1.0 0
763 381 2025-12-29 C95 LOCAL RAY COPILOTO 356 1.0 0
764 382 2025-12-29 C95 LOCAL CAR PILOTO 356 1.0 0
765 382 2025-12-29 C95 LOCAL HCK COPILOTO 356 1.0 0
766 383 2025-12-30 C95 ROTA BRI PILOTO 357 3.42 0
767 383 2025-12-30 C95 ROTA KVN COPILOTO 357 3.42 0
768 384 2025-12-30 C97 LOCAL FIA PILOTO 358 0.75 0
769 384 2025-12-30 C97 LOCAL SEI COPILOTO 358 0.75 0

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
pandas>=3.0,<4
numpy>=2.4,<3
scipy>=1.18,<2
openpyxl>=3.1,<4

14
run_planner.py Normal file
View File

@@ -0,0 +1,14 @@
"""Ponto de entrada do planejador diario de missoes."""
from pathlib import Path
from src.planejador_missao.main import executar_planejamento
if __name__ == "__main__":
resultado = executar_planejamento(Path(__file__).resolve().parent)
print("\n=== Planejamento concluido ===")
print(f"Data: {resultado['data_planejamento']}")
print(f"Arquivo de saida: {resultado['arquivo_saida']}")
print(f"Colunas candidatas: {resultado['total_candidatas']}")
print(f"Escalas selecionadas: {resultado['total_selecionadas']}")

103
scripts/00_main.py Normal file
View File

@@ -0,0 +1,103 @@
"""Orquestrador principal do Planejador Missao.
Entradas:
Arquivos em dados/, argumentos opcionais de linha de comando e configuracoes
centralizadas em scripts/01_config.py.
Saidas:
Planejamento diario em resultados/, historico operacional atualizado,
relatorios de validacao quando solicitado e logs em logs/execucao.log.
Papel no pipeline:
Executa o fluxo completo: leitura de dados, preparacao, geracao de colunas
candidatas, montagem/solucao do MILP, exportacao de resultados e validacao.
Exemplos:
python scripts/00_main.py --data 2026-01-02 --modo diario
python scripts/00_main.py --validacao 2025
"""
from __future__ import annotations
import argparse
import importlib
import time
import pandas as pd
config = importlib.import_module("01_config")
io_utils = importlib.import_module("02_io_utils")
def atualizar_data_planejamento(data: str) -> None:
"""Atualiza dados/parametros_missao.csv com a data solicitada."""
path = config.ARQUIVO_PARAMETROS
df = pd.read_csv(path) if path.exists() else pd.DataFrame(columns=["parametro", "valor"])
if "parametro" not in df.columns or "valor" not in df.columns:
raise ValueError("Erro: parametros_missao.csv deve conter as colunas parametro,valor.")
if (df["parametro"] == "data_planejamento").any():
df.loc[df["parametro"] == "data_planejamento", "valor"] = data
else:
df = pd.concat([df, pd.DataFrame([{"parametro": "data_planejamento", "valor": data}])], ignore_index=True)
df.to_csv(path, index=False)
def executar_diario(args: argparse.Namespace, logger) -> dict:
"""Executa o planejamento diario oficial em Python."""
io_utils.configurar_ambiente()
if args.data:
atualizar_data_planejamento(args.data)
logger.info("Data de planejamento ajustada para %s", args.data)
if args.aeronave:
logger.info("Filtro --aeronave recebido: %s. O modelo diario usa a disponibilidade em dados/aeronaves_disponiveis.csv.", args.aeronave)
from src.planejador_missao.main import executar_planejamento
inicio = time.perf_counter()
resultado = executar_planejamento(config.PROJECT_ROOT)
duracao = time.perf_counter() - inicio
logger.info("Data planejada: %s", resultado["data_planejamento"])
logger.info("Colunas candidatas: %s", resultado["total_candidatas"])
logger.info("Escalas selecionadas: %s", resultado["total_selecionadas"])
logger.info("Arquivo exportado: %s", resultado["arquivo_saida"])
logger.info("Tempo de execucao: %.1f s", duracao)
return resultado
def executar_validacao(ano: str, logger) -> dict:
"""Executa validacao retrospectiva suportada pelo projeto."""
if str(ano) != "2025":
raise ValueError("Erro: no momento a validacao disponivel e --validacao 2025.")
validacao = importlib.import_module("07_validacao_2025")
return validacao.executar_validacao_2025(importar=True, logger=logger)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Executa o Planejador Missao em Python.")
parser.add_argument("--data", help="Data do planejamento diario no formato AAAA-MM-DD.")
parser.add_argument("--aeronave", choices=config.AERONAVES, help="Aeronave de referencia para diagnostico.")
parser.add_argument("--modo", default="diario", choices=["diario"], help="Modo de execucao do planejamento.")
parser.add_argument("--validacao", help="Ano da validacao retrospectiva. Exemplo: --validacao 2025.")
return parser.parse_args()
def main() -> None:
args = parse_args()
logger = io_utils.configurar_logger("main")
if args.validacao:
saida = executar_validacao(args.validacao, logger)
print("\n=== Validacao concluida ===")
for path in saida["arquivos"]:
print(f"- {path}")
return
resultado = executar_diario(args, logger)
print("\n=== Planejamento diario concluido ===")
print(f"Data: {resultado['data_planejamento']}")
print(f"Arquivo: {resultado['arquivo_saida']}")
print(f"Colunas candidatas: {resultado['total_candidatas']}")
print(f"Escalas selecionadas: {resultado['total_selecionadas']}")
if __name__ == "__main__":
main()

72
scripts/01_config.py Normal file
View File

@@ -0,0 +1,72 @@
"""Configuracao central dos scripts do Planejador Missao.
Entradas:
Estrutura padrao do projeto, com pastas dados/, src/ e resultados/.
Saidas:
Constantes de caminho, parametros operacionais, nomes de arquivos e pesos
usados pelos scripts de execucao, validacao e teste.
Papel no pipeline:
Evita caminhos absolutos e concentra convencoes para que o projeto rode em
qualquer computador mantendo a mesma estrutura de diretorios.
"""
from __future__ import annotations
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
SRC_DIR = PROJECT_ROOT / "src"
DADOS_DIR = PROJECT_ROOT / "dados"
RESULTADOS_DIR = PROJECT_ROOT / "resultados"
VALIDACAO_DIR = RESULTADOS_DIR / "validacao"
LOGS_DIR = PROJECT_ROOT / "logs"
TEMP_DIR = PROJECT_ROOT / "temp"
ARQUIVO_PARAMETROS = DADOS_DIR / "parametros_missao.csv"
ARQUIVO_CADASTRO_LOCAL = DADOS_DIR / "Modelagem_C98_ETA2_local.xlsx"
ARQUIVO_CADASTRO_ORIGINAL = DADOS_DIR / "Modelagem C98 ETA2.xlsx"
ARQUIVO_CATALOGO_OIS = DADOS_DIR / "catalogo_ois.xlsx"
ARQUIVO_INDISPONIBILIDADES = DADOS_DIR / "indisponibilidades_2026.xlsx"
ARQUIVO_HISTORICO = DADOS_DIR / "historico_horas_voadas.csv"
ARQUIVO_QUADRO_VOO_2025 = DADOS_DIR / "Quadro de Voo 2025 (2).xlsx"
ARQUIVO_VOOS_2025 = DADOS_DIR / "validacao" / "voos_2025.csv"
LOG_EXECUCAO = LOGS_DIR / "execucao.log"
AERONAVES = ["C98", "C97", "C95"]
META_HORAS_PADRAO = 50.0
VALIDACAO_TEMPO_LIMITE_SEGUNDOS = 180.0
VALIDACAO_GAP_RELATIVO = 0.05
PESOS_OBJETIVO = {
"rota_acionada": 100000,
"missao_local": 1000,
"sobreaviso": 0,
"meta_50": 100,
"meta_110": 100,
"beneficio_paop": 10,
"custo_financeiro": -0.05,
}
ABAS = {
"cadastro": "BANCO DE DADOS 2026",
"catalogo_ois": "catalogo_ois",
"voos_2025": "VOOS",
"relatorio_diario": "ESCALA DIARIA",
}
ARQUIVOS_ENTRADA_OBRIGATORIOS = [
ARQUIVO_CATALOGO_OIS,
ARQUIVO_INDISPONIBILIDADES,
ARQUIVO_PARAMETROS,
]
def garantir_diretorios() -> None:
"""Cria diretorios de saida e apoio usados pelos scripts."""
for path in [RESULTADOS_DIR, VALIDACAO_DIR, LOGS_DIR, TEMP_DIR, ARQUIVO_VOOS_2025.parent]:
path.mkdir(parents=True, exist_ok=True)

85
scripts/02_io_utils.py Normal file
View File

@@ -0,0 +1,85 @@
"""Utilitarios de entrada, saida e log dos scripts.
Entradas:
Caminhos definidos em 01_config.py.
Saidas:
Validacoes de arquivos, diretorios criados, logs em logs/execucao.log e
funcoes pequenas para exportacao.
Papel no pipeline:
Padroniza mensagens de erro e evita repeticao de codigo de filesystem.
"""
from __future__ import annotations
import importlib
import logging
import sys
from datetime import datetime
from pathlib import Path
from typing import Iterable
import pandas as pd
config = importlib.import_module("01_config")
def configurar_ambiente() -> None:
"""Garante diretorios e deixa src/ importavel para execucoes pelo VS Code."""
config.garantir_diretorios()
project_root = str(config.PROJECT_ROOT)
if project_root not in sys.path:
sys.path.insert(0, project_root)
def configurar_logger(nome: str = "planejador_missao") -> logging.Logger:
"""Cria logger simples em arquivo e terminal."""
configurar_ambiente()
logger = logging.getLogger(nome)
logger.setLevel(logging.INFO)
logger.handlers.clear()
formato = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
file_handler = logging.FileHandler(config.LOG_EXECUCAO, encoding="utf-8")
file_handler.setFormatter(formato)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(file_handler)
logger.addHandler(stream_handler)
logger.info("Execucao iniciada em %s", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
return logger
def exigir_arquivos(paths: Iterable[Path]) -> None:
"""Interrompe a execucao com mensagem clara se algum arquivo faltar."""
ausentes = [path for path in paths if not path.exists()]
if ausentes:
lista = "\n".join(f"- {path}" for path in ausentes)
raise FileNotFoundError(f"Erro: arquivos obrigatorios ausentes:\n{lista}")
def exigir_colunas(df: pd.DataFrame, colunas: Iterable[str], origem: str) -> None:
"""Valida colunas obrigatorias de uma tabela carregada."""
faltantes = [col for col in colunas if col not in df.columns]
if faltantes:
raise ValueError(
f"Erro: coluna obrigatoria '{faltantes[0]}' nao encontrada em {origem}. "
"Verifique o arquivo de entrada."
)
def exportar_excel(path: Path, abas: dict[str, pd.DataFrame]) -> Path:
"""Exporta um workbook Excel com uma aba por DataFrame."""
path.parent.mkdir(parents=True, exist_ok=True)
with pd.ExcelWriter(path, engine="openpyxl") as writer:
for nome, df in abas.items():
df.to_excel(writer, sheet_name=nome[:31], index=False)
return path
def exportar_csv(path: Path, df: pd.DataFrame) -> Path:
"""Exporta CSV garantindo existencia do diretorio."""
path.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(path, index=False)
return path

View File

@@ -0,0 +1,73 @@
"""Preparacao das bases operacionais do Planejador Missao.
Entradas:
Planilhas e CSVs em dados/: cadastro, catalogo de OIs, indisponibilidades,
progresso, historico, aeronaves, tripulantes disponiveis e rotas acionadas.
Saidas:
Dicionario com DataFrames normalizados para geracao de colunas candidatas.
Papel no pipeline:
Concentra a leitura e a preparacao antes do MILP, mantendo rastreavel quais
arquivos alimentam cada execucao.
"""
from __future__ import annotations
import importlib
from pathlib import Path
config = importlib.import_module("01_config")
io_utils = importlib.import_module("02_io_utils")
def carregar_bases(base_dir: Path = config.PROJECT_ROOT) -> dict:
"""Le e normaliza todas as bases necessarias ao planejamento diario."""
io_utils.configurar_ambiente()
io_utils.exigir_arquivos(config.ARQUIVOS_ENTRADA_OBRIGATORIOS)
from src.planejador_missao.data_io import (
carregar_aeronaves,
carregar_cadastro,
carregar_catalogo_ois,
carregar_historico,
carregar_indisponibilidades,
carregar_parametros,
carregar_progresso_ois,
carregar_rotas,
carregar_tripulantes_disponiveis,
)
from src.planejador_missao.rules import combinar_progresso_com_historico
parametros = carregar_parametros(base_dir)
historico = carregar_historico(base_dir)
cadastro = carregar_cadastro(base_dir, historico)
catalogo = carregar_catalogo_ois(base_dir)
indisponibilidades = carregar_indisponibilidades(base_dir)
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
return {
"parametros": parametros,
"historico": historico,
"cadastro": cadastro,
"catalogo": catalogo,
"indisponibilidades": indisponibilidades,
"progresso": progresso,
"aeronaves": carregar_aeronaves(base_dir),
"disponiveis": carregar_tripulantes_disponiveis(base_dir),
"rotas": carregar_rotas(base_dir),
}
def main() -> None:
"""Executa uma leitura de diagnostico das bases."""
logger = io_utils.configurar_logger("preparar_dados")
bases = carregar_bases()
logger.info("Tripulantes no cadastro: %s", len(bases["cadastro"]))
logger.info("Tripulantes disponiveis: %s", len(bases["disponiveis"]))
logger.info("Aeronaves informadas: %s", len(bases["aeronaves"]))
logger.info("Rotas acionadas: %s", len(bases["rotas"]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
"""Geracao das colunas candidatas viaveis.
Entradas:
Bases preparadas por 03_preparar_dados.py.
Saidas:
DataFrame em que cada linha representa uma escala possivel: rota acionada,
missao local ou sobreaviso, com dupla, aeronave, score, custo e metadados.
Papel no pipeline:
Materializa o espaco de decisoes do MILP. A compatibilidade de aeronave,
indisponibilidade, qualificacao e regra de instrucao ja sao filtradas aqui.
"""
from __future__ import annotations
import importlib
preparar = importlib.import_module("03_preparar_dados")
io_utils = importlib.import_module("02_io_utils")
def gerar_colunas_do_dia(bases: dict):
"""Gera colunas candidatas para a data e parametros informados nas bases."""
from src.planejador_missao.candidates import gerar_colunas
parametros = bases["parametros"]
# Cada coluna candidata equivale a uma variavel x_j do MILP.
# As regras de dupla, qualificacao, instrucao e indisponibilidade sao
# aplicadas antes do solver para reduzir o modelo a alternativas viaveis.
return gerar_colunas(
parametros["data_planejamento"],
bases["rotas"],
bases["aeronaves"],
bases["cadastro"],
bases["indisponibilidades"],
bases["progresso"],
bases["catalogo"],
bases["disponiveis"],
parametros["criterio_missao"],
parametros["criterio_sbv"],
)
def main() -> None:
"""Executa diagnostico da geracao de colunas."""
logger = io_utils.configurar_logger("gerar_colunas")
bases = preparar.carregar_bases()
colunas = gerar_colunas_do_dia(bases)
logger.info("Colunas candidatas geradas: %s", len(colunas))
if colunas.empty:
logger.warning("Nenhuma coluna candidata viavel foi gerada.")
if __name__ == "__main__":
main()

58
scripts/05_modelo_milp.py Normal file
View File

@@ -0,0 +1,58 @@
"""Modelo MILP do Planejador Missao usando scipy.optimize.milp/HiGHS.
Entradas:
Colunas candidatas viaveis geradas por 04_gerar_colunas_candidatas.py.
Saidas:
Subconjunto de colunas selecionadas pelo solver, representando a escala
diaria otimizada.
Metodologia:
Variavel de decisao: x_j em {0,1}, onde cada j representa uma coluna
candidata. Como scipy.optimize.milp minimiza, o modelo minimiza -score_milp,
equivalente a maximizar o score operacional total.
Restricoes implementadas no motor src.planejador_missao.optimizer:
a) rota acionada obrigatoria coberta exatamente uma vez;
b) cada tripulante aparece no maximo uma vez no dia;
c) cada aeronave recebe cobertura principal por rota acionada ou SBV;
d) aeronave nao executa escalas conflitantes de rota/local no mesmo dia;
e) qualificacao, indisponibilidade e regras de instrucao entram no filtro de
colunas candidatas, logo colunas inviaveis nao chegam ao MILP;
f) regras especificas de rota, missao local e SBV sao preservadas por tipo.
"""
from __future__ import annotations
import importlib
gerador = importlib.import_module("04_gerar_colunas_candidatas")
io_utils = importlib.import_module("02_io_utils")
preparar = importlib.import_module("03_preparar_dados")
def resolver_escala(colunas, rotas, aeronaves):
"""Resolve o MILP e retorna as colunas escolhidas."""
from src.planejador_missao.optimizer import resolver_milp
if colunas.empty:
raise RuntimeError("Erro: nao ha colunas candidatas viaveis para montar o MILP.")
# O resolvedor cria x_j binario para cada linha de colunas.
# A funcao objetivo usa -score_milp porque o HiGHS via SciPy resolve
# minimizacao; isso preserva a interpretacao de maximizar score.
return resolver_milp(colunas, rotas, aeronaves)
def main() -> None:
"""Executa o MILP em modo diagnostico."""
logger = io_utils.configurar_logger("modelo_milp")
bases = preparar.carregar_bases()
colunas = gerador.gerar_colunas_do_dia(bases)
solucao = resolver_escala(colunas, bases["rotas"], bases["aeronaves"])
logger.info("Colunas candidatas: %s", len(colunas))
logger.info("Escalas selecionadas: %s", len(solucao))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,38 @@
"""Exportacao dos resultados do planejamento diario.
Entradas:
Solucao do MILP, catalogo de OIs e data de planejamento.
Saidas:
Planilha em resultados/planejamento_diario_YYYY-MM-DD.xlsx e registros de
historico operacional para atualizacao do acumulado.
Papel no pipeline:
Converte a solucao matematica em artefatos utilizaveis pelo esquadrao.
"""
from __future__ import annotations
import importlib
config = importlib.import_module("01_config")
io_utils = importlib.import_module("02_io_utils")
def exportar_planejamento(data_planejamento, solucao, catalogo):
"""Gera registros operacionais e planilha Excel da escala."""
from src.planejador_missao.report import gerar_excel, gerar_registros
registros = gerar_registros(solucao, catalogo)
arquivo = gerar_excel(config.PROJECT_ROOT, data_planejamento, solucao)
return {"arquivo": arquivo, "registros": registros}
def main() -> None:
"""Mostra instrucao de uso deste modulo dentro do pipeline."""
logger = io_utils.configurar_logger("exportar_resultados")
logger.info("Use python scripts/00_main.py para executar o fluxo completo.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,257 @@
"""Validacao retrospectiva com voos reais de 2025.
Entradas:
dados/Quadro de Voo 2025 (2).xlsx, aba VOOS, ou o CSV ja importado em
dados/validacao/voos_2025.csv.
Saidas:
resultados/validacao/validacao_2025_resumo.xlsx
resultados/validacao/validacao_2025_detalhada.xlsx
resultados/validacao/validacao_2025_metricas.csv
resultados/validacao/validacao_2025_barras.png
Papel no pipeline:
Mantem a demanda historica de 2025, gera candidatos compativeis para cada
slot historico e usa MILP para redistribuir tripulantes preservando horas.
"""
from __future__ import annotations
import importlib
import re
import struct
import zlib
from datetime import time, timedelta
from pathlib import Path
import pandas as pd
config = importlib.import_module("01_config")
io_utils = importlib.import_module("02_io_utils")
COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
def _normalizar_texto(valor: object) -> str:
from src.planejador_missao.utils import normalizar_texto
return normalizar_texto(valor)
def _horas(valor: object) -> float:
"""Converte TEV da planilha historica para horas decimais."""
if pd.isna(valor):
return 0.0
if isinstance(valor, timedelta):
return round(valor.total_seconds() / 3600, 2)
if isinstance(valor, time):
return round(valor.hour + valor.minute / 60 + valor.second / 3600, 2)
if isinstance(valor, str):
texto = valor.strip()
if not texto or texto == "-":
return 0.0
partes = texto.split(":")
if len(partes) >= 2:
return round(float(partes[0]) + float(partes[1]) / 60, 2)
return float(texto.replace(",", "."))
if isinstance(valor, (int, float)):
return round(float(valor) * 24, 2) if 0 < float(valor) < 1 else float(valor)
return 0.0
def _aeronave(valor: object) -> str:
"""Traduz matricula FAB para familia operacional C95/C97/C98."""
texto = _normalizar_texto(valor)
match = re.search(r"(\d{4})", texto)
if not match:
return ""
numero = match.group(1)
if numero.startswith("20"):
return "C97"
if numero.startswith("23"):
return "C95"
if numero.startswith("27"):
return "C98"
return ""
def _tripulantes(valor: object) -> list[str]:
"""Extrai a dupla principal da tripulacao historica."""
partes = re.split(r"\s*/\s*", str(valor))
trips = []
for parte in partes:
texto = _normalizar_texto(parte)
texto = re.sub(r"\([^)]*\)", " ", texto)
texto = re.sub(r"[^A-Z0-9]+", " ", texto).strip()
if texto and texto != "NAN":
trips.append(texto)
return trips[:2]
def importar_voos_2025(origem: Path = config.ARQUIVO_QUADRO_VOO_2025, destino: Path = config.ARQUIVO_VOOS_2025) -> pd.DataFrame:
"""Importa voos realizados de 2025 da aba VOOS para o CSV de validacao."""
io_utils.exigir_arquivos([origem])
raw = pd.read_excel(origem, sheet_name=config.ABAS["voos_2025"], header=None)
linhas = []
slot_id = 0
for _, row in raw.iterrows():
data = pd.to_datetime(row[2], errors="coerce")
if pd.isna(data) or data.year != 2025:
continue
if _normalizar_texto(row[41]) != "REALIZADO":
continue
aeronave = _aeronave(row[37])
if not aeronave:
continue
horas_voadas = _horas(row[30])
if horas_voadas <= 0:
continue
trips = _tripulantes(row[8])
if len(trips) < 2:
continue
slot_id += 1
tipo_escala = _normalizar_texto(row[1])
oi = _normalizar_texto(row[35])
if not oi or oi == "-":
oi = _normalizar_texto(row[32])
for idx, tripulante in enumerate(trips, start=1):
linhas.append(
{
"slot_id": slot_id,
"data": data.date().isoformat(),
"aeronave": aeronave,
"tipo_escala": tipo_escala,
"tripulante": tripulante,
"funcao": "PILOTO" if idx == 1 else "COPILOTO",
"oi": oi,
"horas_voadas": horas_voadas,
"sbv": 0,
}
)
df = pd.DataFrame(linhas, columns=COLUNAS_VALIDACAO)
io_utils.exportar_csv(destino, df)
return df
def _png_barras(path: Path, metricas: pd.DataFrame) -> Path:
"""Gera PNG simples sem depender de bibliotecas graficas externas."""
width, height = 760, 420
canvas = bytearray([255, 255, 255] * width * height)
def rect(x0: int, y0: int, x1: int, y1: int, color: tuple[int, int, int]) -> None:
x0, x1 = max(0, x0), min(width, x1)
y0, y1 = max(0, y0), min(height, y1)
for y in range(y0, y1):
for x in range(x0, x1):
i = (y * width + x) * 3
canvas[i : i + 3] = bytes(color)
rect(55, 45, 60, 365, (38, 57, 77))
rect(55, 360, 705, 365, (38, 57, 77))
valores = metricas.set_index("cenario")["desvio_padrao"].to_dict()
max_v = max(valores.values()) if valores else 1
cores = {"real_2025": (154, 52, 18), "otimizado_meta_50": (38, 115, 77)}
xs = {"real_2025": 190, "otimizado_meta_50": 430}
for cenario, valor in valores.items():
h = int((valor / max_v) * 270)
rect(xs[cenario], 360 - h, xs[cenario] + 110, 360, cores[cenario])
raw = b"".join(b"\x00" + canvas[y * width * 3 : (y + 1) * width * 3] for y in range(height))
png = b"\x89PNG\r\n\x1a\n"
for chunk_type, data in [
(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)),
(b"IDAT", zlib.compress(raw, 9)),
(b"IEND", b""),
]:
png += struct.pack(">I", len(data)) + chunk_type + data + struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(png)
return path
def executar_validacao_2025(importar: bool = True, logger=None) -> dict:
"""Executa a validacao completa e exporta os artefatos finais."""
io_utils.configurar_ambiente()
logger = logger or io_utils.configurar_logger("validacao_2025")
inicio = pd.Timestamp.now()
if importar or not config.ARQUIVO_VOOS_2025.exists():
voos_importados = importar_voos_2025()
logger.info("Voos 2025 importados: %s registros", len(voos_importados))
from src.planejador_missao.validacao import carregar_voos_2025, otimizar_equalizacao_50
voos = carregar_voos_2025(config.ARQUIVO_VOOS_2025)
if voos.empty:
raise RuntimeError("Erro: base de validacao 2025 vazia. Verifique dados/validacao/voos_2025.csv.")
if (voos["horas_voadas"] < 0).any():
raise RuntimeError("Erro: foram encontradas horas voadas negativas na validacao 2025.")
resultado = otimizar_equalizacao_50(
config.PROJECT_ROOT,
voos,
meta_horas=config.META_HORAS_PADRAO,
tempo_limite_segundos=config.VALIDACAO_TEMPO_LIMITE_SEGUNDOS,
gap_relativo=config.VALIDACAO_GAP_RELATIVO,
)
metricas = resultado["metricas"]
total_real = float(metricas.loc[metricas["cenario"] == "real_2025", "horas_total"].iloc[0])
total_otim = float(metricas.loc[metricas["cenario"] == "otimizado_meta_50", "horas_total"].iloc[0])
if abs(total_real - total_otim) > 0.01:
logger.warning("Horas nao preservadas: real=%s otimizado=%s", total_real, total_otim)
resumo_path = config.VALIDACAO_DIR / "validacao_2025_resumo.xlsx"
detalhada_path = config.VALIDACAO_DIR / "validacao_2025_detalhada.xlsx"
metricas_path = config.VALIDACAO_DIR / "validacao_2025_metricas.csv"
grafico_path = config.VALIDACAO_DIR / "validacao_2025_barras.png"
solver_df = pd.DataFrame([resultado.get("status_solver", {})])
io_utils.exportar_excel(resumo_path, {"solver": solver_df, "metricas": metricas})
io_utils.exportar_excel(
detalhada_path,
{
"solver": solver_df,
"metricas": metricas,
"comparativo_trips": resultado["comparativo"],
"voos_2025_slots": resultado["slots"],
"escala_otimizada": resultado["otimizados"],
},
)
io_utils.exportar_csv(metricas_path, metricas)
_png_barras(grafico_path, metricas)
duracao = (pd.Timestamp.now() - inicio).total_seconds()
logger.info("Slots historicos avaliados: %s", len(resultado["slots"]))
logger.info("Alocacoes otimizadas: %s", len(resultado["otimizados"]))
logger.info("Tripulantes comparados: %s", len(resultado["comparativo"]))
logger.info("Status solver: %s", resultado["status_solver"]["mensagem"])
logger.info("Tempo de validacao: %.1f s", duracao)
logger.info("Arquivos exportados: %s | %s | %s | %s", resumo_path, detalhada_path, metricas_path, grafico_path)
return {
"resultado": resultado,
"arquivos": [resumo_path, detalhada_path, metricas_path, grafico_path],
"duracao_segundos": duracao,
}
def main() -> None:
"""Ponto de entrada da validacao 2025."""
logger = io_utils.configurar_logger("validacao_2025")
saida = executar_validacao_2025(importar=True, logger=logger)
metricas = saida["resultado"]["metricas"]
print("\n=== Validacao 2025 concluida ===")
print(metricas.to_string(index=False))
print("\nArquivos gerados:")
for path in saida["arquivos"]:
print(f"- {path}")
if __name__ == "__main__":
main()

44
scripts/08_app_utils.py Normal file
View File

@@ -0,0 +1,44 @@
"""Funcoes auxiliares da interface web local.
Entradas:
Estrutura padrao do projeto e web_app.py.
Saidas:
Inicializacao local da interface em http://127.0.0.1:8050 quando chamado
diretamente.
Papel no pipeline:
Facilita acesso ao app Python com interface web local.
"""
from __future__ import annotations
import importlib
import subprocess
import sys
config = importlib.import_module("01_config")
io_utils = importlib.import_module("02_io_utils")
def comando_python_app() -> list[str]:
"""Retorna comando portavel para iniciar a interface web local."""
pythonw = config.PROJECT_ROOT / ".venv" / "Scripts" / "pythonw.exe"
python = config.PROJECT_ROOT / ".venv" / "Scripts" / "python.exe"
exe = pythonw if pythonw.exists() else python if python.exists() else sys.executable
return [str(exe), str(config.PROJECT_ROOT / "web_app.py")]
def abrir_app() -> None:
"""Inicia o app em segundo plano sem janela de console quando possivel."""
io_utils.configurar_ambiente()
subprocess.Popen(comando_python_app(), cwd=config.PROJECT_ROOT)
def main() -> None:
abrir_app()
print("Interface web local iniciada em http://127.0.0.1:8050")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,63 @@
"""Teste rapido de instalacao do Planejador Missao.
Entradas:
Ambiente Python atual, dependencias instaladas e arquivos em dados/.
Saidas:
Diagnostico amigavel no terminal e log em logs/execucao.log.
Papel no pipeline:
Permite verificar rapidamente se o computador esta pronto para executar o
planejador e o solver HiGHS/scipy.optimize.milp.
"""
from __future__ import annotations
import importlib
import sys
import numpy as np
from scipy.optimize import Bounds, LinearConstraint, milp
config = importlib.import_module("01_config")
io_utils = importlib.import_module("02_io_utils")
def testar_solver_minimo() -> None:
"""Resolve um MILP minimo para comprovar que scipy.optimize.milp funciona."""
c = np.array([-1.0, -2.0])
integrality = np.ones(2)
bounds = Bounds([0, 0], [1, 1])
constraints = [LinearConstraint([[1, 1]], -np.inf, 1)]
result = milp(c=c, integrality=integrality, bounds=bounds, constraints=constraints)
if not result.success:
raise RuntimeError(f"Erro: teste minimo do solver falhou: {result.message}")
def main() -> None:
"""Executa todas as verificacoes de ambiente."""
logger = io_utils.configurar_logger("teste_instalacao")
print(f"Python encontrado: {sys.version.split()[0]}")
for pacote in ["pandas", "numpy", "openpyxl", "scipy"]:
importlib.import_module(pacote)
print(f"Biblioteca OK: {pacote}")
config.garantir_diretorios()
for diretorio in [config.DADOS_DIR, config.RESULTADOS_DIR, config.SRC_DIR, config.LOGS_DIR, config.TEMP_DIR]:
if not diretorio.exists():
raise RuntimeError(f"Erro: diretorio obrigatorio ausente: {diretorio}")
print(f"Diretorio OK: {diretorio.name}")
cadastro = config.ARQUIVO_CADASTRO_LOCAL if config.ARQUIVO_CADASTRO_LOCAL.exists() else config.ARQUIVO_CADASTRO_ORIGINAL
io_utils.exigir_arquivos([cadastro, *config.ARQUIVOS_ENTRADA_OBRIGATORIOS])
print("Arquivos de entrada OK")
testar_solver_minimo()
print("Solver scipy.optimize.milp/HiGHS OK")
logger.info("Teste de instalacao concluido com sucesso.")
print("Ambiente OK para executar o Planejador Missao.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,136 @@
"""Importa a aba VOOS do Quadro de Voo 2025 para a base de validacao."""
from __future__ import annotations
import argparse
import re
import sys
from datetime import time, timedelta
from pathlib import Path
import pandas as pd
BASE_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(BASE_DIR))
from src.planejador_missao.utils import normalizar_texto
COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
def _horas(valor: object) -> float:
if pd.isna(valor):
return 0.0
if isinstance(valor, timedelta):
return round(valor.total_seconds() / 3600, 2)
if isinstance(valor, time):
return round(valor.hour + valor.minute / 60 + valor.second / 3600, 2)
if isinstance(valor, str):
texto = valor.strip()
if not texto or texto == "-":
return 0.0
partes = texto.split(":")
if len(partes) >= 2:
return round(float(partes[0]) + float(partes[1]) / 60, 2)
return float(texto.replace(",", "."))
if isinstance(valor, (int, float)):
return round(float(valor) * 24, 2) if 0 < float(valor) < 1 else float(valor)
return 0.0
def _aeronave(valor: object) -> str:
texto = normalizar_texto(valor)
match = re.search(r"(\d{4})", texto)
if not match:
return ""
numero = match.group(1)
if numero.startswith("20"):
return "C97"
if numero.startswith("23"):
return "C95"
if numero.startswith("27"):
return "C98"
return ""
def _tripulantes(valor: object) -> list[str]:
partes = re.split(r"\s*/\s*", str(valor))
trips = []
for parte in partes:
texto = normalizar_texto(parte)
texto = re.sub(r"\([^)]*\)", " ", texto)
texto = re.sub(r"[^A-Z0-9]+", " ", texto).strip()
if texto and texto != "NAN":
trips.append(texto)
return trips[:2]
def importar(origem: Path, destino: Path) -> pd.DataFrame:
raw = pd.read_excel(origem, sheet_name="VOOS", header=None)
linhas = []
slot_id = 0
for _, row in raw.iterrows():
data = pd.to_datetime(row[2], errors="coerce")
if pd.isna(data) or data.year != 2025:
continue
if normalizar_texto(row[41]) != "REALIZADO":
continue
aeronave = _aeronave(row[37])
if not aeronave:
continue
slot_id += 1
tipo_escala = normalizar_texto(row[1])
horas_voadas = _horas(row[30])
oi = normalizar_texto(row[35])
if not oi or oi == "-":
oi = normalizar_texto(row[32])
for idx, tripulante in enumerate(_tripulantes(row[8]), start=1):
linhas.append(
{
"slot_id": slot_id,
"data": data.date().isoformat(),
"aeronave": aeronave,
"tipo_escala": tipo_escala,
"tripulante": tripulante,
"funcao": "PILOTO" if idx == 1 else "COPILOTO",
"oi": oi,
"horas_voadas": horas_voadas,
"sbv": 0,
}
)
df = pd.DataFrame(linhas, columns=COLUNAS_VALIDACAO)
destino.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(destino, index=False)
return df
def main() -> None:
parser = argparse.ArgumentParser(description="Importa voos realizados de 2025 para a validacao.")
parser.add_argument(
"--origem",
type=Path,
default=BASE_DIR / "dados" / "Quadro de Voo 2025 (2).xlsx",
help="Planilha com aba VOOS.",
)
parser.add_argument(
"--destino",
type=Path,
default=BASE_DIR / "dados" / "validacao" / "voos_2025.csv",
help="CSV de validacao gerado.",
)
args = parser.parse_args()
df = importar(args.origem, args.destino)
print(f"Arquivo gerado: {args.destino}")
print(f"Registros exportados: {len(df)}")
print(f"Voos/slots exportados: {df['slot_id'].nunique() if not df.empty else 0}")
print(f"Tripulantes exportados: {df['tripulante'].nunique() if not df.empty else 0}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,57 @@
"""Executa a validacao retrospectiva com voos de 2025."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(BASE_DIR))
from src.planejador_missao.validacao import (
carregar_voos_2025,
criar_modelo_voos_2025,
otimizar_equalizacao_50,
salvar_relatorio_validacao,
)
def main() -> None:
parser = argparse.ArgumentParser(description="Valida o planejador com voos historicos de 2025.")
parser.add_argument("--meta", type=float, default=50.0, help="Meta de horas usada na equalizacao.")
parser.add_argument("--tempo-limite", type=float, default=180.0, help="Tempo limite do resolvedor MILP, em segundos.")
parser.add_argument("--gap", type=float, default=0.05, help="Gap relativo aceitavel para encerrar o MILP.")
args = parser.parse_args()
entrada = BASE_DIR / "dados" / "validacao" / "voos_2025.csv"
criar_modelo_voos_2025(entrada)
voos = carregar_voos_2025(entrada)
if voos.empty:
print("Base de validacao ainda vazia.")
print(f"Preencha o arquivo: {entrada}")
print("Colunas esperadas: data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv")
return
resultado = otimizar_equalizacao_50(
BASE_DIR,
voos,
meta_horas=args.meta,
tempo_limite_segundos=args.tempo_limite,
gap_relativo=args.gap,
)
arquivo = salvar_relatorio_validacao(BASE_DIR, resultado, meta_horas=args.meta)
print("\n=== Validacao 2025 concluida ===")
print(f"Meta de equalizacao: {args.meta:g} horas")
print(f"Voos/slots avaliados: {len(resultado['slots'])}")
print(f"Tripulantes comparados: {len(resultado['comparativo'])}")
print(f"Relatorio: {arquivo}")
print(f"Solver: {resultado['status_solver']['mensagem']}")
print("\nMetricas:")
print(resultado["metricas"].to_string(index=False))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,2 @@
"""Planejador de missao e sobreaviso por MILP."""

View File

@@ -0,0 +1,216 @@
"""Geracao de colunas candidatas para o modelo MILP."""
from __future__ import annotations
from datetime import date
from itertools import combinations
import pandas as pd
from .data_io import AERONAVES
from .rules import eh_instrutor_adaptado, piloto_disponivel, qualificacao_operacional
from .utils import normalizar_texto, parse_horas
def proxima_oi(tripulante: str, aeronave: str, subprograma: str, progresso: pd.DataFrame, catalogo: pd.DataFrame) -> dict | None:
ois = catalogo[(catalogo["aeronave"] == aeronave) & (catalogo["subprograma"] == subprograma)].sort_values("ordem")
if ois.empty:
return None
concluidas = set(
progresso[
(progresso["tripulante"] == tripulante)
& (progresso["aeronave"] == aeronave)
& (progresso["subprograma"] == subprograma)
& (progresso["concluida"] == True)
]["oi"]
)
pendentes = ois[~ois["oi"].isin(concluidas)]
if pendentes.empty:
return None
row = pendentes.iloc[0].to_dict()
row["tripulante"] = tripulante
return row
def candidatos_missao(data_missao: date, aeronave: str, tipo: str, cadastro: pd.DataFrame, indisponibilidades: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame, disponiveis: pd.DataFrame) -> pd.DataFrame:
qt_col = f"qt_{aeronave.lower()}"
permitidos = set(disponiveis["tripulante"])
linhas = []
for _, trip in cadastro[cadastro[qt_col]].iterrows():
trigrama = trip["tripulante"]
if trigrama not in permitidos or not piloto_disponivel(trigrama, data_missao, indisponibilidades):
continue
subprogramas = [sp for sp in str(trip[f"subprograma_{aeronave.lower()}"]).split() if sp.startswith("SP")]
for sp in subprogramas:
oi = proxima_oi(trigrama, aeronave, sp, progresso, catalogo)
if not oi:
continue
if tipo != "TODAS" and oi["tipo_missao"] != tipo:
continue
oi.update(
patente=trip.get("patente", ""),
qt=trip.get("qt", ""),
qualificacao=trip.get("qualificacao", ""),
horas_totais_2026=trip.get("horas_totais_2026", 0),
horas_realizadas_2026=trip.get("horas_realizadas_2026", 0),
sbv_realizados_2026=trip.get("sbv_realizados_2026", 0),
soldo=trip.get("soldo", 0),
)
linhas.append(oi)
if not linhas:
return pd.DataFrame()
df = pd.DataFrame(linhas)
df["horas_faltantes_paop"] = df["horas_totais_2026"] - df["horas_realizadas_2026"]
df["prioridade_tipo"] = df["tipo_missao"].map({"LOCAL": 1, "ROTA": 2}).fillna(3)
return df.sort_values(["horas_faltantes_paop", "prioridade_tipo", "aeronave", "subprograma", "ordem"], ascending=[False, True, True, True, True])
def aplicar_prioridades(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["bloqueado"] = False
df["prioridade_paop"] = 100.0
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["DOG", "MCH"])) & (df["subprograma"] == "SPQE-3"), "prioridade_paop"] = 1
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["DOG", "MCH"])) & (df["subprograma"] != "SPQE-3"), "prioridade_paop"] = 50
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["SCA", "DIL"])) & (df["subprograma"] == "SPMO-1"), "prioridade_paop"] = 1
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["SCA", "DIL"])) & (df["subprograma"] != "SPMO-1"), "prioridade_paop"] = 60
df.loc[(df["aeronave"] == "C98") & (df["subprograma"] == "SPFO-2") & (df["horas_realizadas_2026"] < 50), "bloqueado"] = True
return df.sort_values(["bloqueado", "prioridade_paop", "horas_faltantes_paop"], ascending=[True, True, False])
def score_dupla(p1, p2, h1, h2, s1, s2, sbv1, sbv2, criterio: str) -> float:
beneficio = (0 if pd.isna(p1) else 100 / p1) + (0 if pd.isna(p2) else 100 / p2)
horas_50 = (h1 < 50) + (h2 < 50)
horas_110 = (h1 < 110) + (h2 < 110)
custo = (0 if pd.isna(s1) else s1) + (0 if pd.isna(s2) else s2)
sbv = (0 if pd.isna(sbv1) else sbv1) + (0 if pd.isna(sbv2) else sbv2)
dif_sbv = abs((0 if pd.isna(sbv1) else sbv1) - (0 if pd.isna(sbv2) else sbv2))
if criterio == "menor_custo":
return -custo + 0.2 * beneficio + 20 * horas_50 + 10 * horas_110
if criterio == "meta_110":
return 100 * horas_110 + 5 * beneficio - 0.05 * custo
if criterio == "equalizar_quadrinhos":
return -sbv - 0.5 * dif_sbv + 0.1 * beneficio
return 100 * horas_50 + 10 * beneficio - 0.05 * custo
def instrutores_disponiveis(data_missao: date, aeronave: str, cadastro: pd.DataFrame, indisponibilidades: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame, disponiveis: pd.DataFrame) -> pd.DataFrame:
qt_col = f"qt_{aeronave.lower()}"
permitidos = set(disponiveis["tripulante"])
rows = []
for _, row in cadastro[cadastro[qt_col]].iterrows():
trip = row["tripulante"]
if trip in permitidos and piloto_disponivel(trip, data_missao, indisponibilidades) and eh_instrutor_adaptado(trip, aeronave, cadastro, progresso, catalogo):
rows.append(row)
return pd.DataFrame(rows)
def gerar_duplas(candidatos: pd.DataFrame, criterio: str, tipo_escala: str, aeronave: str, data_planejamento: date, cadastro: pd.DataFrame, indisponibilidades: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame, disponiveis: pd.DataFrame, rota_id=None, rota=None, observacao="") -> pd.DataFrame:
if candidatos.empty:
return pd.DataFrame()
livres = candidatos[~candidatos["bloqueado"]].drop_duplicates("tripulante")
linhas = []
if tipo_escala in {"ROTA_ACIONADA", "MISSAO_LOCAL"}:
instrutores = instrutores_disponiveis(data_planejamento, aeronave, cadastro, indisponibilidades, progresso, catalogo, disponiveis)
for _, aluno in livres.iterrows():
for _, inst in instrutores.iterrows():
if aluno["tripulante"] == inst["tripulante"]:
continue
linhas.append(_linha_coluna(data_planejamento, tipo_escala, aeronave, aluno, inst, criterio, rota_id, rota, observacao))
return pd.DataFrame(linhas)
for t1, t2 in combinations(livres["tripulante"].unique(), 2):
info1 = livres[livres["tripulante"] == t1].iloc[0]
info2 = livres[livres["tripulante"] == t2].iloc[0]
linha = _linha_coluna(data_planejamento, tipo_escala, aeronave, info1, info2, criterio, rota_id, rota, observacao)
linha["oi_trip1"] = ""
linha["oi_trip2"] = ""
linhas.append(linha)
return pd.DataFrame(linhas)
def _linha_coluna(data_planejamento, tipo_escala, aeronave, info1, info2, criterio, rota_id, rota, observacao):
h2 = info2.get("horas_realizadas_2026", 999)
p2 = info2.get("prioridade_paop", pd.NA)
score = score_dupla(info1.get("prioridade_paop", pd.NA), p2, info1.get("horas_realizadas_2026", 0), h2, info1.get("soldo", 0), info2.get("soldo", 0), info1.get("sbv_realizados_2026", 0), info2.get("sbv_realizados_2026", 0), criterio)
return {
"data": data_planejamento,
"tipo_escala": tipo_escala,
"rota_id": rota_id,
"aeronave": aeronave,
"origem": "" if rota is None else rota.get("origem", ""),
"destino": "" if rota is None else rota.get("destino", ""),
"inicio": pd.NaT if rota is None else rota.get("inicio", pd.NaT),
"fim": pd.NaT if rota is None else rota.get("fim", pd.NaT),
"tev_horas": pd.NA if rota is None else rota.get("tev_horas", pd.NA),
"pernoite_dias": pd.NA if rota is None else rota.get("pernoite_dias", pd.NA),
"rota": "" if rota is None else rota.get("rota", ""),
"trip1": info1["tripulante"],
"trip2": info2["tripulante"],
"oi_trip1": info1.get("oi", ""),
"oi_trip2": "",
"prioridade_paop_1": info1.get("prioridade_paop", pd.NA),
"prioridade_paop_2": p2,
"custo_financeiro": info1.get("soldo", 0) + info2.get("soldo", 0),
"score": score,
"observacao": observacao,
}
def gerar_colunas(data_planejamento, rotas, aeronaves, cadastro, indisponibilidades, progresso, catalogo, disponiveis, criterio_missao, criterio_sbv):
livres = aeronaves[(aeronaves["disponivel_sede"]) & (~aeronaves["em_pane"]) & (~aeronaves["em_missao_rota"])]["aeronave"].tolist()
colunas = []
for rota_id, rota in rotas.iterrows():
for av in livres:
horas_disp = horas_disponiveis(aeronaves, av)
horas_rota = parse_horas(rota.get("tev_horas"))
if horas_rota is None and pd.notna(rota.get("duracao_horas")):
horas_rota = float(rota.get("duracao_horas"))
if horas_rota is not None and horas_rota > horas_disp:
continue
candidatos = candidatos_missao(data_planejamento, av, "ROTA", cadastro, indisponibilidades, progresso, catalogo, disponiveis)
if len(candidatos) < 2:
candidatos = candidatos_missao(data_planejamento, av, "TODAS", cadastro, indisponibilidades, progresso, catalogo, disponiveis)
candidatos = filtrar_por_horas_aeronave(candidatos, horas_disp)
if candidatos.empty:
continue
colunas.append(gerar_duplas(aplicar_prioridades(candidatos), criterio_missao, "ROTA_ACIONADA", av, data_planejamento, cadastro, indisponibilidades, progresso, catalogo, disponiveis, rota_id=rota_id + 1, rota=rota, observacao="Missao de rota acionada"))
for av in livres:
horas_disp = horas_disponiveis(aeronaves, av)
candidatos_local = candidatos_missao(data_planejamento, av, "LOCAL", cadastro, indisponibilidades, progresso, catalogo, disponiveis)
candidatos_local = filtrar_por_horas_aeronave(candidatos_local, horas_disp)
if not candidatos_local.empty:
colunas.append(gerar_duplas(aplicar_prioridades(candidatos_local), criterio_missao, "MISSAO_LOCAL", av, data_planejamento, cadastro, indisponibilidades, progresso, catalogo, disponiveis, observacao="Missao local planejada para progressao operacional"))
candidatos_sbv = []
for _, row in cadastro[cadastro[f"qt_{av.lower()}"]].iterrows():
if row["tripulante"] in set(disponiveis["tripulante"]) and piloto_disponivel(row["tripulante"], data_planejamento, indisponibilidades):
if qualificacao_operacional(row["tripulante"], av, cadastro, progresso, catalogo) != "AL":
r = row.copy()
r["aeronave"] = av
r["subprograma"] = ""
r["oi"] = ""
r["prioridade_paop"] = max(1, 999 - float(r.get("horas_totais_2026", 0) - r.get("horas_realizadas_2026", 0)))
r["bloqueado"] = False
candidatos_sbv.append(r)
if len(candidatos_sbv) >= 2:
colunas.append(gerar_duplas(pd.DataFrame(candidatos_sbv), criterio_sbv, "SBV", av, data_planejamento, cadastro, indisponibilidades, progresso, catalogo, disponiveis, observacao="Sobreaviso para missao de rota"))
if not colunas:
return pd.DataFrame()
df = pd.concat(colunas, ignore_index=True)
bonus = {"ROTA_ACIONADA": 100000, "MISSAO_LOCAL": 1000, "SBV": 0}
df["coluna_id"] = range(1, len(df) + 1)
df["score_milp"] = df["score"] + df["tipo_escala"].map(bonus).fillna(0)
return df
def horas_disponiveis(aeronaves: pd.DataFrame, aeronave: str) -> float:
linha = aeronaves[aeronaves["aeronave"] == aeronave]
if linha.empty:
return float("inf")
horas = parse_horas(linha.iloc[0].get("hora_livre"))
return float("inf") if horas is None else horas
def filtrar_por_horas_aeronave(candidatos: pd.DataFrame, horas_disp: float) -> pd.DataFrame:
if candidatos.empty or horas_disp == float("inf") or "tev_horas" not in candidatos.columns:
return candidatos
horas_oi = candidatos["tev_horas"].map(parse_horas)
return candidatos[(horas_oi.isna()) | (horas_oi <= horas_disp)]

View File

@@ -0,0 +1,155 @@
"""Leitura das bases de entrada e escrita do historico operacional."""
from __future__ import annotations
from datetime import date, datetime
from pathlib import Path
import pandas as pd
from .utils import clean_columns, normalizar_texto, parse_bool, parse_data, parse_data_hora, read_csv
AERONAVES = ["C98", "C97", "C95"]
def carregar_parametros(base_dir: Path) -> dict:
default = {
"data_planejamento": "hoje",
"criterio_missao": "meta_50",
"criterio_sbv": "equalizar_quadrinhos",
}
path = base_dir / "dados" / "parametros_missao.csv"
if path.exists():
df = pd.read_csv(path)
if {"parametro", "valor"}.issubset(df.columns):
for _, row in df.iterrows():
chave = str(row["parametro"]).strip().lower()
if chave in default:
default[chave] = row["valor"]
default["data_planejamento"] = parse_data(default["data_planejamento"])
if default["criterio_missao"] == "paop":
default["criterio_missao"] = "meta_50"
return default
def carregar_cadastro(base_dir: Path, historico: pd.DataFrame) -> pd.DataFrame:
local = base_dir / "dados" / "Modelagem_C98_ETA2_local.xlsx"
original = base_dir / "dados" / "Modelagem C98 ETA2.xlsx"
arquivo = local if local.exists() else original
df = clean_columns(pd.read_excel(arquivo, sheet_name="BANCO DE DADOS 2026"))
resumo = pd.DataFrame(columns=["tripulante", "horas_historico", "sbv_historico"])
if not historico.empty:
resumo = (
historico.groupby("tripulante", as_index=False)
.agg(horas_historico=("horas_voadas", "sum"), sbv_historico=("sbv", "sum"))
)
cadastro = df.rename(
columns={
"subprograma_c98": "subprograma_c98",
"subprograma_c97": "subprograma_c97",
"subprograma_c95": "subprograma_c95",
}
).copy()
cadastro["tripulante"] = cadastro["tripulante"].map(normalizar_texto)
cadastro["qt"] = cadastro["qt"].map(normalizar_texto)
cadastro["qualificacao"] = cadastro["qualificacao"].map(normalizar_texto)
for av in AERONAVES:
cadastro[f"qt_{av.lower()}"] = cadastro["qt"].str.contains(av, na=False)
cadastro[f"subprograma_{av.lower()}"] = cadastro[f"subprograma_{av.lower()}"].map(normalizar_texto)
cadastro[f"horas_{av.lower()}"] = pd.to_numeric(cadastro[f"horas_{av.lower()}"], errors="coerce").fillna(0)
cadastro["soldo"] = pd.to_numeric(cadastro["soldo"], errors="coerce").fillna(0)
cadastro["horas_totais_2026"] = pd.to_numeric(cadastro["horas_totais_2026"], errors="coerce").fillna(0)
cadastro = cadastro.merge(resumo, how="left", on="tripulante")
cadastro["horas_realizadas_2026"] = cadastro["horas_historico"].fillna(0)
cadastro["sbv_realizados_2026"] = cadastro["sbv_historico"].fillna(0)
cadastro["disponivel"] = True
return cadastro.drop(columns=["horas_historico", "sbv_historico"], errors="ignore")
def carregar_catalogo_ois(base_dir: Path) -> pd.DataFrame:
df = pd.read_excel(base_dir / "dados" / "catalogo_ois.xlsx", sheet_name="catalogo_ois", dtype=str)
df = df.dropna(subset=["aeronave", "subprograma", "oi"]).copy()
for col in ["aeronave", "subprograma", "oi", "esforco", "tipo_missao"]:
df[col] = df[col].map(normalizar_texto)
df["ordem"] = pd.to_numeric(df["ordem"], errors="coerce")
df["eh_piloto"] = df.get("eh_piloto", "VERDADEIRO").fillna("VERDADEIRO").map(parse_bool)
df["conta_hora_voo"] = df["tipo_missao"].isin(["LOCAL", "ROTA"])
return df[df["eh_piloto"]].sort_values(["aeronave", "subprograma", "ordem"]).reset_index(drop=True)
def carregar_indisponibilidades(base_dir: Path) -> pd.DataFrame:
path = base_dir / "dados" / "indisponibilidades_2026.xlsx"
df = pd.read_excel(path)
df["tripulante"] = df["tripulante"].map(normalizar_texto)
df["inicio"] = pd.to_datetime(df["inicio"]).dt.date
df["fim"] = pd.to_datetime(df["fim"]).dt.date
return df
def carregar_historico(base_dir: Path) -> pd.DataFrame:
cols = ["data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv", "origem_registro", "registrado_em"]
df = read_csv(base_dir / "dados" / "historico_horas_voadas.csv", cols)
if df.empty:
return df
df["data"] = pd.to_datetime(df["data"], errors="coerce").dt.date
for col in ["aeronave", "tipo_escala", "tripulante", "funcao", "oi"]:
df[col] = df[col].map(normalizar_texto)
df["horas_voadas"] = pd.to_numeric(df["horas_voadas"], errors="coerce").fillna(0)
df["sbv"] = pd.to_numeric(df["sbv"], errors="coerce").fillna(0)
return df
def carregar_progresso_ois(base_dir: Path) -> pd.DataFrame:
path = base_dir / "dados" / "progresso_ois_2026.xlsx"
cols = ["tripulante", "aeronave", "subprograma", "oi", "horas_realizadas", "concluida"]
if not path.exists():
return pd.DataFrame(columns=cols)
df = pd.read_excel(path)
for col in ["tripulante", "aeronave", "subprograma", "oi"]:
df[col] = df[col].map(normalizar_texto)
df["horas_realizadas"] = pd.to_numeric(df["horas_realizadas"], errors="coerce").fillna(0)
df["concluida"] = df["concluida"].map(parse_bool)
return df[cols]
def carregar_aeronaves(base_dir: Path) -> pd.DataFrame:
cols = ["aeronave", "disponivel_sede", "em_pane", "em_missao_rota", "hora_livre"]
df = read_csv(base_dir / "dados" / "aeronaves_disponiveis.csv", cols)
if df.empty:
df = pd.DataFrame({"aeronave": AERONAVES, "disponivel_sede": True, "em_pane": False, "em_missao_rota": False, "hora_livre": "08:00"})
df["aeronave"] = df["aeronave"].map(normalizar_texto)
for col in ["disponivel_sede", "em_pane", "em_missao_rota"]:
df[col] = df[col].map(parse_bool)
return df
def carregar_tripulantes_disponiveis(base_dir: Path) -> pd.DataFrame:
df = read_csv(base_dir / "dados" / "tripulantes_disponiveis.csv", ["tripulante", "disponivel"])
df["tripulante"] = df["tripulante"].map(normalizar_texto)
df["disponivel"] = df["disponivel"].map(parse_bool)
return df[df["disponivel"]].reset_index(drop=True)
def carregar_rotas(base_dir: Path) -> pd.DataFrame:
cols = ["origem", "destino", "inicio", "fim", "tev_horas", "pernoite_dias", "rota"]
df = read_csv(base_dir / "dados" / "rotas_acionadas.csv", cols)
if df.empty:
return df
for col in ["origem", "destino", "rota"]:
df[col] = df[col].map(normalizar_texto)
df["inicio"] = df["inicio"].map(parse_data_hora)
df["fim"] = df["fim"].map(parse_data_hora)
df["tev_horas"] = pd.to_numeric(df["tev_horas"], errors="coerce")
df["pernoite_dias"] = pd.to_numeric(df["pernoite_dias"], errors="coerce").fillna((df["fim"] - df["inicio"]).dt.days).fillna(0)
df["duracao_horas"] = (df["fim"] - df["inicio"]).dt.total_seconds() / 3600
return df.dropna(subset=["origem", "destino"], how="all").reset_index(drop=True)
def salvar_historico(base_dir: Path, registros: pd.DataFrame) -> None:
path = base_dir / "dados" / "historico_horas_voadas.csv"
historico = carregar_historico(base_dir)
atualizado = pd.concat([historico, registros], ignore_index=True)
atualizado.to_csv(path, index=False)

View File

@@ -0,0 +1,64 @@
"""Fluxo principal do planejador em Python."""
from __future__ import annotations
from pathlib import Path
from .candidates import gerar_colunas
from .data_io import (
carregar_aeronaves,
carregar_cadastro,
carregar_catalogo_ois,
carregar_historico,
carregar_indisponibilidades,
carregar_parametros,
carregar_progresso_ois,
carregar_rotas,
carregar_tripulantes_disponiveis,
salvar_historico,
)
from .optimizer import resolver_milp
from .report import gerar_excel, gerar_registros
from .rules import combinar_progresso_com_historico
def executar_planejamento(base_dir: Path) -> dict:
# Bloco 01 - Entrada e estado operacional.
parametros = carregar_parametros(base_dir)
historico = carregar_historico(base_dir)
cadastro = carregar_cadastro(base_dir, historico)
catalogo = carregar_catalogo_ois(base_dir)
indisponibilidades = carregar_indisponibilidades(base_dir)
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
aeronaves = carregar_aeronaves(base_dir)
disponiveis = carregar_tripulantes_disponiveis(base_dir)
rotas = carregar_rotas(base_dir)
# Bloco 02 - Geracao das colunas candidatas.
colunas = gerar_colunas(
parametros["data_planejamento"],
rotas,
aeronaves,
cadastro,
indisponibilidades,
progresso,
catalogo,
disponiveis,
parametros["criterio_missao"],
parametros["criterio_sbv"],
)
# Bloco 03 - Modelo MILP e selecao final.
solucao = resolver_milp(colunas, rotas, aeronaves)
# Bloco 04 - Persistencia operacional e relatorio.
registros = gerar_registros(solucao, catalogo)
salvar_historico(base_dir, registros)
arquivo_saida = gerar_excel(base_dir, parametros["data_planejamento"], solucao)
return {
"data_planejamento": parametros["data_planejamento"],
"arquivo_saida": arquivo_saida,
"total_candidatas": len(colunas),
"total_selecionadas": len(solucao),
}

View File

@@ -0,0 +1,60 @@
"""Modelo MILP para escolher a combinacao final de escalas."""
from __future__ import annotations
import numpy as np
import pandas as pd
from scipy.optimize import Bounds, LinearConstraint, milp
def resolver_milp(colunas: pd.DataFrame, rotas: pd.DataFrame, aeronaves: pd.DataFrame) -> pd.DataFrame:
if colunas.empty:
return colunas
n = len(colunas)
integrality = np.ones(n)
bounds = Bounds(np.zeros(n), np.ones(n))
restricoes = []
# R1 - Cada tripulante aparece no maximo uma vez.
for trip in sorted(set(colunas["trip1"]).union(set(colunas["trip2"]))):
linha = np.zeros(n)
linha[(colunas["trip1"] == trip) | (colunas["trip2"] == trip)] = 1
restricoes.append(LinearConstraint(linha, -np.inf, 1))
livres = aeronaves[(aeronaves["disponivel_sede"]) & (~aeronaves["em_pane"]) & (~aeronaves["em_missao_rota"])]["aeronave"].unique()
for av in livres:
idx_rota = (colunas["aeronave"] == av) & (colunas["tipo_escala"] == "ROTA_ACIONADA")
idx_local = (colunas["aeronave"] == av) & (colunas["tipo_escala"] == "MISSAO_LOCAL")
idx_sbv = (colunas["aeronave"] == av) & (colunas["tipo_escala"] == "SBV")
if not (idx_rota | idx_sbv).any():
raise RuntimeError(f"Modelo inviavel: nao ha rota nem SBV candidato para a aeronave {av}.")
linha = np.zeros(n)
linha[idx_rota | idx_sbv] = 1
restricoes.append(LinearConstraint(linha, 1, 1))
if idx_rota.any():
linha = np.zeros(n)
linha[idx_rota] = 1
restricoes.append(LinearConstraint(linha, -np.inf, 1))
if idx_local.any():
linha = np.zeros(n)
linha[idx_local] = 1
restricoes.append(LinearConstraint(linha, -np.inf, 1))
if idx_rota.any() and idx_local.any():
linha = np.zeros(n)
linha[idx_rota | idx_local] = 1
restricoes.append(LinearConstraint(linha, -np.inf, 1))
for rota_id in range(1, len(rotas) + 1):
idx = (colunas["tipo_escala"] == "ROTA_ACIONADA") & (colunas["rota_id"] == rota_id)
if not idx.any():
raise RuntimeError(f"Modelo inviavel: nao ha coluna candidata para a rota acionada {rota_id}.")
linha = np.zeros(n)
linha[idx] = 1
restricoes.append(LinearConstraint(linha, 1, 1))
resultado = milp(c=-colunas["score_milp"].to_numpy(dtype=float), integrality=integrality, bounds=bounds, constraints=restricoes)
if not resultado.success:
raise RuntimeError(f"MILP nao encontrou solucao viavel: {resultado.message}")
selecionadas = resultado.x > 0.5
return colunas.loc[selecionadas].reset_index(drop=True)

View File

@@ -0,0 +1,100 @@
"""Resumo operacional por tripulante para a aba de quadrinhos."""
from __future__ import annotations
from pathlib import Path
import pandas as pd
from .candidates import proxima_oi
from .data_io import AERONAVES, carregar_cadastro, carregar_catalogo_ois, carregar_historico, carregar_progresso_ois
from .rules import combinar_progresso_com_historico, qualificacao_operacional
from .utils import normalizar_texto
def carregar_quadrinho_manual(base_dir: Path) -> pd.DataFrame:
path = base_dir / "dados" / "quadrinho_operacional_manual.csv"
cols = [
"tripulante",
"aeronave",
"qualificacao_manual",
"horas_voadas_manual",
"sbv_manual",
"ultima_data_voo_manual",
"observacao",
]
if not path.exists():
return pd.DataFrame(columns=cols)
df = pd.read_csv(path)
for col in cols:
if col not in df.columns:
df[col] = pd.NA
df = df[cols].copy()
df["tripulante"] = df["tripulante"].map(normalizar_texto)
df["aeronave"] = df["aeronave"].map(normalizar_texto)
df["horas_voadas_manual"] = pd.to_numeric(df["horas_voadas_manual"], errors="coerce").fillna(0)
df["sbv_manual"] = pd.to_numeric(df["sbv_manual"], errors="coerce").fillna(0)
return df
def gerar_quadrinhos(base_dir: Path) -> pd.DataFrame:
historico = carregar_historico(base_dir)
cadastro = carregar_cadastro(base_dir, historico)
catalogo = carregar_catalogo_ois(base_dir)
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
manual = carregar_quadrinho_manual(base_dir)
linhas = []
for _, trip in cadastro.sort_values("tripulante").iterrows():
trigrama = trip["tripulante"]
sbv_hist = float(trip.get("sbv_realizados_2026", 0) or 0)
horas_hist = float(trip.get("horas_realizadas_2026", 0) or 0)
for av in AERONAVES:
if not bool(trip.get(f"qt_{av.lower()}", False)):
continue
manual_row = manual[(manual["tripulante"] == trigrama) & (manual["aeronave"] == av)]
horas_manual = 0.0 if manual_row.empty else float(manual_row.iloc[0].get("horas_voadas_manual", 0) or 0)
sbv_manual = 0.0 if manual_row.empty else float(manual_row.iloc[0].get("sbv_manual", 0) or 0)
qual_manual = "" if manual_row.empty else normalizar_texto(manual_row.iloc[0].get("qualificacao_manual", ""))
obs = "" if manual_row.empty else str(manual_row.iloc[0].get("observacao", "") or "")
ultima_manual = "" if manual_row.empty else str(manual_row.iloc[0].get("ultima_data_voo_manual", "") or "")
proximas = []
subprogramas = [sp for sp in str(trip.get(f"subprograma_{av.lower()}", "")).split() if sp.startswith("SP")]
for sp in subprogramas:
oi = proxima_oi(trigrama, av, sp, progresso, catalogo)
if oi:
proximas.append(f"{sp}: {oi['oi']}")
else:
proximas.append(f"{sp}: concluido")
voos_trip = historico[(historico["tripulante"] == trigrama) & (historico["aeronave"] == av) & (historico["horas_voadas"] > 0)]
ultima_hist = "" if voos_trip.empty else max(voos_trip["data"]).isoformat()
linhas.append(
{
"tripulante": trigrama,
"aeronave": av,
"qualificacao": qual_manual or qualificacao_operacional(trigrama, av, cadastro, progresso, catalogo),
"proxima_oi": " | ".join(proximas) if proximas else "-",
"horas_aeronave_base": float(trip.get(f"horas_{av.lower()}", 0) or 0),
"horas_voadas_ano": horas_hist + horas_manual,
"sbv_ano": sbv_hist + sbv_manual,
"ultima_data_voo": ultima_manual or ultima_hist,
"meta_50_faltante": max(0.0, 50.0 - (horas_hist + horas_manual)),
"observacao": obs,
}
)
return pd.DataFrame(linhas)
def quadrinhos_para_api(base_dir: Path) -> dict:
df = gerar_quadrinhos(base_dir)
if df.empty:
return {"linhas": [], "resumo": {"tripulantes": 0, "abaixo_50": 0, "horas_media": 0}}
resumo = {
"tripulantes": int(df["tripulante"].nunique()),
"abaixo_50": int((df["horas_voadas_ano"] < 50).sum()),
"horas_media": round(float(df["horas_voadas_ano"].mean()), 1),
}
return {"linhas": df.fillna("").to_dict(orient="records"), "resumo": resumo}

View File

@@ -0,0 +1,117 @@
"""Registro de horas e geracao do relatorio Excel final."""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
from openpyxl.utils.dataframe import dataframe_to_rows
from .utils import parse_horas
def horas_previstas(linha: pd.Series, catalogo: pd.DataFrame) -> float:
if linha["tipo_escala"] == "SBV":
return 0.0
horas = parse_horas(linha.get("tev_horas"))
if horas:
return horas
oi = linha.get("oi_trip1", "")
match = catalogo[(catalogo["aeronave"] == linha["aeronave"]) & (catalogo["oi"] == oi)]
if not match.empty and bool(match.iloc[0]["conta_hora_voo"]):
return parse_horas(match.iloc[0]["tev_horas"]) or 0.0
return 0.0
def gerar_registros(solucao: pd.DataFrame, catalogo: pd.DataFrame) -> pd.DataFrame:
linhas = []
agora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
for _, row in solucao.iterrows():
horas = horas_previstas(row, catalogo)
sbv = 1 if row["tipo_escala"] == "SBV" else 0
for funcao, trip, oi in [("TRIP1", row["trip1"], row.get("oi_trip1", "")), ("TRIP2", row["trip2"], row.get("oi_trip2", ""))]:
linhas.append(
{
"data": row["data"],
"aeronave": row["aeronave"],
"tipo_escala": row["tipo_escala"],
"tripulante": trip,
"funcao": funcao,
"oi": oi if isinstance(oi, str) else "",
"horas_voadas": horas,
"sbv": sbv,
"origem_registro": "escala_diaria",
"registrado_em": agora,
}
)
return pd.DataFrame(linhas)
def bloco(solucao: pd.DataFrame, tipo: str) -> pd.DataFrame:
dados = solucao[solucao["tipo_escala"] == tipo]
if dados.empty:
return pd.DataFrame([{"AERONAVE": "-", "TRIP 1": "-", "TRIP 2": "-", "OI/FICHA": "-", "DETALHE": "Sem escala"}])
detalhe = dados["observacao"]
if tipo == "ROTA_ACIONADA":
detalhe = dados["rota"].where(dados["rota"].astype(str) != "", dados["origem"] + " - " + dados["destino"])
return pd.DataFrame(
{
"AERONAVE": dados["aeronave"],
"TRIP 1": dados["trip1"],
"TRIP 2": dados["trip2"],
"OI/FICHA": dados["oi_trip1"].replace("", "-"),
"DETALHE": detalhe,
}
)
def escrever_tabela(ws, titulo: str, dados: pd.DataFrame, row: int, col: int, cor: str) -> None:
fill_titulo = PatternFill("solid", fgColor=cor)
fill_header = PatternFill("solid", fgColor="D9EAF7")
borda = Border(*(Side(style="thin", color="BFBFBF") for _ in range(4)))
ws.merge_cells(start_row=row, start_column=col, end_row=row, end_column=col + 4)
cell = ws.cell(row=row, column=col, value=titulo)
cell.fill = fill_titulo
cell.font = Font(color="FFFFFF", bold=True)
cell.alignment = Alignment(horizontal="center")
for r_idx, linha in enumerate(dataframe_to_rows(dados, index=False, header=True), row + 1):
for c_idx, valor in enumerate(linha, col):
c = ws.cell(row=r_idx, column=c_idx, value=valor)
c.border = borda
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
if r_idx == row + 1:
c.fill = fill_header
c.font = Font(bold=True)
def gerar_excel(base_dir: Path, data_planejamento, solucao: pd.DataFrame) -> Path:
out_dir = base_dir / "resultados"
out_dir.mkdir(exist_ok=True)
arquivo = out_dir / f"planejamento_diario_{data_planejamento}.xlsx"
if arquivo.exists():
arquivo = out_dir / f"planejamento_diario_{data_planejamento}_{datetime.now():%H%M%S}.xlsx"
wb = Workbook()
ws = wb.active
ws.title = "ESCALA DIARIA"
ws.merge_cells("A1:Q1")
ws["A1"] = "Planejador diario de missoes e sobreaviso - 2 ETA"
ws["A1"].fill = PatternFill("solid", fgColor="1F4E78")
ws["A1"].font = Font(color="FFFFFF", bold=True, size=16)
ws["A1"].alignment = Alignment(horizontal="center")
ws.merge_cells("A2:Q2")
ws["A2"] = f"ESCALA DIARIA - {data_planejamento:%d/%m/%Y}"
ws["A2"].font = Font(color="1F4E78", bold=True, size=18)
ws["A2"].alignment = Alignment(horizontal="center")
escrever_tabela(ws, "MISSAO ACIONADA", bloco(solucao, "ROTA_ACIONADA"), 5, 1, "C00000")
escrever_tabela(ws, "VOOS LOCAIS", bloco(solucao, "MISSAO_LOCAL"), 5, 7, "548235")
escrever_tabela(ws, "SOBREAVISO", bloco(solucao, "SBV"), 5, 13, "8064A2")
for col in range(1, 18):
ws.column_dimensions[chr(64 + col)].width = 14 if col not in {5, 11, 17} else 24
ws.freeze_panes = "A5"
wb.save(arquivo)
return arquivo

View File

@@ -0,0 +1,108 @@
"""Regras operacionais de disponibilidade, qualificacao e progressao."""
from __future__ import annotations
from datetime import date
import pandas as pd
from .utils import normalizar_texto
def piloto_disponivel(tripulante: str, data_missao: date, indisponibilidades: pd.DataFrame) -> bool:
trip = normalizar_texto(tripulante)
conflitos = indisponibilidades[
(indisponibilidades["tripulante"] == trip)
& (indisponibilidades["inicio"] <= data_missao)
& (indisponibilidades["fim"] >= data_missao)
]
return conflitos.empty
def classe_principal(qualificacao: str) -> str:
qual = normalizar_texto(qualificacao)
for classe in ["IN", "PO", "PB", "AL"]:
if classe in qual.split("/"):
return classe
if classe in qual.split():
return classe
return "OUTRO"
def qualificacao_base_aeronave(tripulante: str, aeronave: str, cadastro: pd.DataFrame) -> str:
linha = cadastro[cadastro["tripulante"] == normalizar_texto(tripulante)]
if linha.empty:
return "OUTRO"
qt = [x for x in normalizar_texto(linha.iloc[0]["qt"]).split("/") if x]
quals = [x for x in normalizar_texto(linha.iloc[0]["qualificacao"]).split("/") if x]
if len(qt) == len(quals) and aeronave in qt:
return classe_principal(quals[qt.index(aeronave)])
return classe_principal(linha.iloc[0]["qualificacao"])
def subprograma_concluido(progresso: pd.DataFrame, tripulante: str, aeronave: str, subprograma: str, catalogo: pd.DataFrame) -> bool:
necessarias = set(catalogo[(catalogo["aeronave"] == aeronave) & (catalogo["subprograma"] == subprograma)]["oi"])
if not necessarias:
return False
concluidas = set(
progresso[
(progresso["tripulante"] == normalizar_texto(tripulante))
& (progresso["aeronave"] == aeronave)
& (progresso["subprograma"] == subprograma)
& (progresso["concluida"] == True)
]["oi"]
)
return necessarias.issubset(concluidas)
def qualificacao_operacional(tripulante: str, aeronave: str, cadastro: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame) -> str:
rank = {"OUTRO": 0, "AL": 1, "PB": 2, "PO": 3, "IN": 4}[qualificacao_base_aeronave(tripulante, aeronave, cadastro)]
if subprograma_concluido(progresso, tripulante, aeronave, "SPFO-1", catalogo):
rank = max(rank, 2)
if subprograma_concluido(progresso, tripulante, aeronave, "SPFO-2", catalogo):
rank = max(rank, 3)
if aeronave == "C98" and subprograma_concluido(progresso, tripulante, aeronave, "SPQE-3", catalogo):
rank = max(rank, 4)
if aeronave == "C97" and subprograma_concluido(progresso, tripulante, aeronave, "SPQE-4", catalogo):
rank = max(rank, 4)
return {0: "OUTRO", 1: "AL", 2: "PB", 3: "PO", 4: "IN"}[rank]
def concluiu_ois(progresso: pd.DataFrame, tripulante: str, aeronave: str, ois: list[str]) -> bool:
concluidas = set(
progresso[
(progresso["tripulante"] == normalizar_texto(tripulante))
& (progresso["aeronave"] == aeronave)
& (progresso["concluida"] == True)
]["oi"]
)
return set(ois).issubset(concluidas)
def eh_instrutor_adaptado(tripulante: str, aeronave: str, cadastro: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame) -> bool:
trip = normalizar_texto(tripulante)
instrutores_base = {
"C95": {"MES", "BRI"},
"C97": {"AEU", "MAT"},
"C98": {"BRI", "FIA", "MDO", "SLS", "CFF"},
}
if trip in instrutores_base.get(aeronave, set()):
return True
if aeronave == "C97" and trip in {"SEI", "LPS"}:
return concluiu_ois(progresso, trip, aeronave, ["01TL01D41", "04TL01N42"])
if aeronave == "C98" and trip in {"MCH", "DOG"}:
return concluiu_ois(progresso, trip, aeronave, ["01TL01D31", "04TL01N32"])
return qualificacao_operacional(trip, aeronave, cadastro, progresso, catalogo) == "IN"
def combinar_progresso_com_historico(progresso: pd.DataFrame, historico: pd.DataFrame, catalogo: pd.DataFrame) -> pd.DataFrame:
if historico.empty:
return progresso
voadas = historico[(historico["oi"] != "") & (historico["horas_voadas"] > 0)]
if voadas.empty:
return progresso
hist = voadas.merge(catalogo[["aeronave", "subprograma", "oi"]].drop_duplicates(), on=["aeronave", "oi"], how="left")
hist = hist.dropna(subset=["subprograma"])
hist = hist[["tripulante", "aeronave", "subprograma", "oi", "horas_voadas"]].rename(columns={"horas_voadas": "horas_realizadas"})
hist["concluida"] = True
return pd.concat([progresso, hist], ignore_index=True).drop_duplicates(["tripulante", "aeronave", "subprograma", "oi"])

View File

@@ -0,0 +1,84 @@
"""Utilitarios pequenos de normalizacao usados em todo o planejador."""
from __future__ import annotations
import math
import re
from datetime import date, datetime
from pathlib import Path
from typing import Any
import pandas as pd
def normalizar_texto(valor: Any) -> str:
if valor is None or (isinstance(valor, float) and math.isnan(valor)):
return ""
return re.sub(r"\s+", " ", str(valor).strip()).upper()
def parse_bool(valor: Any) -> bool:
return normalizar_texto(valor) in {"TRUE", "T", "1", "SIM", "S", "YES", "Y", "VERDADEIRO"}
def parse_data(valor: Any) -> date:
texto = normalizar_texto(valor).lower()
if texto in {"", "hoje", "today", "sysdate"}:
return date.today()
return pd.to_datetime(valor).date()
def parse_data_hora(valor: Any) -> pd.Timestamp:
if valor is None or str(valor).strip() == "":
return pd.NaT
return pd.to_datetime(valor, errors="coerce")
def parse_horas(valor: Any) -> float | None:
if valor is None or (isinstance(valor, float) and math.isnan(valor)):
return None
if isinstance(valor, (int, float)):
return float(valor)
texto = str(valor).strip().replace(",", ".")
if texto == "":
return None
if ":" in texto:
partes = texto.split(":")
try:
return float(partes[0]) + float(partes[1]) / 60
except (ValueError, IndexError):
return None
try:
return float(texto)
except ValueError:
return None
def clean_columns(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df.columns = [
normalizar_texto(c)
.lower()
.replace("º", "")
.replace("ç", "c")
.replace("ã", "a")
.replace("á", "a")
.replace("é", "e")
.replace("í", "i")
.replace("ó", "o")
.replace("ú", "u")
.replace(" ", "_")
.replace(".", "")
for c in df.columns
]
return df
def read_csv(path: Path, columns: list[str]) -> pd.DataFrame:
if not path.exists():
return pd.DataFrame(columns=columns)
df = pd.read_csv(path)
for col in columns:
if col not in df.columns:
df[col] = pd.NA
return df[columns]

View File

@@ -0,0 +1,251 @@
"""Validacao retrospectiva usando voos historicos."""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.optimize import Bounds, LinearConstraint, milp
from .data_io import carregar_cadastro
from .utils import normalizar_texto
COLUNAS_VOOS_2025 = ["data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
COLUNAS_OPCIONAIS_VOOS_2025 = ["slot_id"]
def criar_modelo_voos_2025(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists():
return
pd.DataFrame(columns=COLUNAS_VOOS_2025).to_csv(path, index=False)
def carregar_voos_2025(path: Path) -> pd.DataFrame:
df = pd.read_csv(path)
for col in COLUNAS_VOOS_2025:
if col not in df.columns:
df[col] = pd.NA
for col in COLUNAS_OPCIONAIS_VOOS_2025:
if col not in df.columns:
df[col] = pd.NA
df = df[COLUNAS_OPCIONAIS_VOOS_2025 + COLUNAS_VOOS_2025].copy()
df["data"] = pd.to_datetime(df["data"], errors="coerce").dt.date
for col in ["aeronave", "tipo_escala", "tripulante", "funcao", "oi"]:
df[col] = df[col].map(normalizar_texto)
df["horas_voadas"] = pd.to_numeric(df["horas_voadas"], errors="coerce").fillna(0)
df["sbv"] = pd.to_numeric(df["sbv"], errors="coerce").fillna(0)
return df.dropna(subset=["data"]).reset_index(drop=True)
def _montar_slots(voos: pd.DataFrame) -> pd.DataFrame:
voados = voos[voos["horas_voadas"] > 0].copy()
if voados.empty:
return pd.DataFrame(columns=["slot_id", "data", "aeronave", "tipo_escala", "horas_voadas"])
if "slot_id" in voados.columns and not voados["slot_id"].isna().all():
voos_unicos = (
voados.groupby(["slot_id", "data", "aeronave", "tipo_escala"], as_index=False)
.agg(
horas_voadas=("horas_voadas", "max"),
tripulantes_reais=("tripulante", lambda s: ", ".join(sorted(set(s)))),
)
)
slots = (
voos_unicos.groupby(["data", "aeronave", "tipo_escala"], as_index=False)
.agg(
horas_voadas=("horas_voadas", "sum"),
tripulantes_reais=("tripulantes_reais", lambda s: ", ".join(sorted(set(", ".join(s).split(", "))))),
)
.sort_values(["data", "aeronave", "tipo_escala"])
.reset_index(drop=True)
)
else:
slots = (
voados.groupby(["data", "aeronave", "tipo_escala"], as_index=False)
.agg(horas_voadas=("horas_voadas", "max"), tripulantes_reais=("tripulante", lambda s: ", ".join(sorted(set(s)))))
.sort_values(["data", "aeronave", "tipo_escala"])
.reset_index(drop=True)
)
slots["slot_id"] = slots.index + 1
return slots[["slot_id", "data", "aeronave", "tipo_escala", "horas_voadas", "tripulantes_reais"]]
def _metricas(horas: pd.Series, meta_horas: float) -> dict:
horas = horas.astype(float)
return {
"tripulantes": int(len(horas)),
"horas_total": round(float(horas.sum()), 2),
"horas_media": round(float(horas.mean()), 2) if len(horas) else 0,
"desvio_padrao": round(float(horas.std(ddof=0)), 2) if len(horas) else 0,
"menor_horas": round(float(horas.min()), 2) if len(horas) else 0,
"maior_horas": round(float(horas.max()), 2) if len(horas) else 0,
"abaixo_meta": int((horas < meta_horas).sum()),
"desvio_medio_meta": round(float((horas - meta_horas).abs().mean()), 2) if len(horas) else 0,
}
def otimizar_equalizacao_50(
base_dir: Path,
voos: pd.DataFrame,
meta_horas: float = 50.0,
tempo_limite_segundos: float = 180.0,
gap_relativo: float = 0.05,
) -> dict:
historico_vazio = pd.DataFrame(columns=["tripulante", "horas_voadas", "sbv"])
cadastro = carregar_cadastro(base_dir, historico_vazio)
trips_cadastro = set(cadastro["tripulante"])
if "slot_id" in voos.columns and not voos["slot_id"].isna().all():
slots_validos = (
voos.groupby("slot_id")["tripulante"]
.apply(lambda s: set(s).issubset(trips_cadastro))
.loc[lambda s: s]
.index
)
voos = voos[voos["slot_id"].isin(slots_validos)].copy()
else:
voos = voos[voos["tripulante"].isin(trips_cadastro)].copy()
slots = _montar_slots(voos)
if slots.empty:
raise RuntimeError("A base de 2025 nao possui linhas com horas_voadas maiores que zero.")
trips = sorted(set(voos["tripulante"]) & trips_cadastro)
if not trips:
raise RuntimeError("Nao ha tripulantes da base de 2025 presentes no cadastro atual.")
pares = []
for s_idx, slot in slots.iterrows():
qt_col = f"qt_{slot['aeronave'].lower()}"
for t_idx, trip in enumerate(trips):
linha = cadastro[cadastro["tripulante"] == trip]
if not linha.empty and bool(linha.iloc[0].get(qt_col, False)):
pares.append((s_idx, t_idx, float(slot["horas_voadas"])))
if not pares:
raise RuntimeError("Nao ha candidatos compativeis entre aeronaves dos voos 2025 e cadastro.")
n_x = len(pares)
n_t = len(trips)
hmax_idx = n_x + 2 * n_t
hmin_idx = hmax_idx + 1
n_vars = hmin_idx + 1
c = np.zeros(n_vars)
c[n_x : n_x + n_t] = 1
c[n_x + n_t :] = 1
c[hmax_idx] = 100
c[hmin_idx] = -100
integrality = np.zeros(n_vars)
integrality[:n_x] = 1
bounds = Bounds(np.zeros(n_vars), np.r_[np.ones(n_x), np.full(2 * n_t + 2, np.inf)])
constraints = []
for s_idx in range(len(slots)):
row = np.zeros(n_vars)
for var_idx, (slot_i, _, _) in enumerate(pares):
if slot_i == s_idx:
row[var_idx] = 1
constraints.append(LinearConstraint(row, 2, 2))
for data in sorted(slots["data"].unique()):
slots_dia = set(slots.index[slots["data"] == data])
for t_idx in range(n_t):
row = np.zeros(n_vars)
for var_idx, (slot_i, trip_i, _) in enumerate(pares):
if slot_i in slots_dia and trip_i == t_idx:
row[var_idx] = 1
constraints.append(LinearConstraint(row, -np.inf, 1))
for t_idx in range(n_t):
row = np.zeros(n_vars)
for var_idx, (_, trip_i, horas) in enumerate(pares):
if trip_i == t_idx:
row[var_idx] = horas
row[n_x + t_idx] = -1
row[n_x + n_t + t_idx] = 1
constraints.append(LinearConstraint(row, meta_horas, meta_horas))
row = np.zeros(n_vars)
for var_idx, (_, trip_i, horas) in enumerate(pares):
if trip_i == t_idx:
row[var_idx] = horas
row[hmax_idx] = -1
constraints.append(LinearConstraint(row, -np.inf, 0))
row = np.zeros(n_vars)
for var_idx, (_, trip_i, horas) in enumerate(pares):
if trip_i == t_idx:
row[var_idx] = horas
row[hmin_idx] = -1
constraints.append(LinearConstraint(row, 0, np.inf))
result = milp(
c=c,
integrality=integrality,
bounds=bounds,
constraints=constraints,
options={"time_limit": tempo_limite_segundos, "mip_rel_gap": gap_relativo},
)
if result.x is None:
raise RuntimeError(f"Validacao MILP inviavel: {result.message}")
escolhidos = result.x[:n_x] > 0.5
linhas = []
for var_idx, escolhido in enumerate(escolhidos):
if not escolhido:
continue
s_idx, t_idx, horas = pares[var_idx]
slot = slots.iloc[s_idx]
linhas.append(
{
"slot_id": int(slot["slot_id"]),
"data": slot["data"],
"aeronave": slot["aeronave"],
"tipo_escala": slot["tipo_escala"],
"tripulante_otimizado": trips[t_idx],
"horas_voadas": horas,
"tripulantes_reais": slot["tripulantes_reais"],
}
)
otimizados = pd.DataFrame(linhas)
horas_reais = voos.groupby("tripulante")["horas_voadas"].sum().reindex(trips, fill_value=0)
horas_otimizadas = otimizados.groupby("tripulante_otimizado")["horas_voadas"].sum().reindex(trips, fill_value=0)
comparativo = pd.DataFrame(
{
"tripulante": trips,
"horas_reais_2025": horas_reais.to_numpy(),
"horas_otimizadas_meta_50": horas_otimizadas.to_numpy(),
}
)
comparativo["delta_otimizado_menos_real"] = comparativo["horas_otimizadas_meta_50"] - comparativo["horas_reais_2025"]
return {
"slots": slots,
"otimizados": otimizados,
"comparativo": comparativo,
"metricas": pd.DataFrame(
[
{"cenario": "real_2025", **_metricas(comparativo["horas_reais_2025"], meta_horas)},
{"cenario": "otimizado_meta_50", **_metricas(comparativo["horas_otimizadas_meta_50"], meta_horas)},
]
),
"status_solver": {
"sucesso": bool(result.success),
"mensagem": str(result.message),
"fun_objetivo": float(result.fun) if result.fun is not None else None,
},
}
def salvar_relatorio_validacao(base_dir: Path, resultado: dict, meta_horas: float = 50.0) -> Path:
out_dir = base_dir / "resultados" / "validacao"
out_dir.mkdir(parents=True, exist_ok=True)
arquivo = out_dir / f"validacao_2025_meta_{int(meta_horas)}_{datetime.now():%Y%m%d_%H%M%S}.xlsx"
with pd.ExcelWriter(arquivo, engine="openpyxl") as writer:
pd.DataFrame([resultado.get("status_solver", {})]).to_excel(writer, sheet_name="solver", index=False)
resultado["metricas"].to_excel(writer, sheet_name="metricas", index=False)
resultado["comparativo"].to_excel(writer, sheet_name="comparativo_trips", index=False)
resultado["slots"].to_excel(writer, sheet_name="voos_2025_slots", index=False)
resultado["otimizados"].to_excel(writer, sheet_name="escala_otimizada", index=False)
return arquivo

578
web_app.py Normal file
View File

@@ -0,0 +1,578 @@
"""Interface web local em HTML para o planejador diario."""
from __future__ import annotations
import json
import mimetypes
import os
import threading
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
import pandas as pd
from src.planejador_missao.main import executar_planejamento
from src.planejador_missao.quadrinhos import quadrinhos_para_api
from src.planejador_missao.utils import normalizar_texto, parse_bool
BASE_DIR = Path(__file__).resolve().parent
DADOS_DIR = BASE_DIR / "dados"
HOST = "127.0.0.1"
PORT = 8050
def read_csv(path: Path, default: pd.DataFrame) -> pd.DataFrame:
if not path.exists():
return default.copy()
return pd.read_csv(path).fillna("")
def load_state() -> dict:
parametros_default = pd.DataFrame(
{
"parametro": ["data_planejamento", "criterio_missao", "criterio_sbv"],
"valor": ["hoje", "meta_50", "equalizar_quadrinhos"],
}
)
parametros_df = read_csv(DADOS_DIR / "parametros_missao.csv", parametros_default)
parametros = dict(zip(parametros_df["parametro"], parametros_df["valor"]))
aeronaves_default = pd.DataFrame(
{
"aeronave": ["C98", "C97", "C95"],
"disponivel_sede": [True, True, True],
"em_pane": [False, False, False],
"em_missao_rota": [False, False, False],
"hora_livre": ["08:00", "08:00", "08:00"],
}
)
aeronaves = read_csv(DADOS_DIR / "aeronaves_disponiveis.csv", aeronaves_default)
for col in ["disponivel_sede", "em_pane", "em_missao_rota"]:
aeronaves[col] = aeronaves[col].map(parse_bool)
rota_default = pd.DataFrame(
{
"origem": ["NT"],
"destino": [""],
"inicio": [pd.Timestamp.today().strftime("%Y-%m-%d 08:00")],
"fim": [pd.Timestamp.today().strftime("%Y-%m-%d 17:00")],
"tev_horas": [0],
"pernoite_dias": [0],
"rota": [""],
}
)
rotas = read_csv(DADOS_DIR / "rotas_acionadas.csv", rota_default)
rota = rota_default.iloc[0].to_dict() if rotas.empty else rotas.iloc[0].to_dict()
trips_default = pd.DataFrame({"tripulante": [], "disponivel": []})
tripulantes = read_csv(DADOS_DIR / "tripulantes_disponiveis.csv", trips_default)
if not tripulantes.empty:
tripulantes["tripulante"] = tripulantes["tripulante"].map(normalizar_texto)
tripulantes["disponivel"] = tripulantes["disponivel"].map(parse_bool)
tripulantes = tripulantes.sort_values("tripulante")
return {
"parametros": {
"data_planejamento": str(parametros.get("data_planejamento", "hoje")),
"criterio_missao": str(parametros.get("criterio_missao", "meta_50")),
"criterio_sbv": str(parametros.get("criterio_sbv", "equalizar_quadrinhos")),
},
"aeronaves": aeronaves.to_dict(orient="records"),
"rota": rota,
"tem_rota": str(rota.get("destino", "")).strip() != "",
"tripulantes": tripulantes.to_dict(orient="records"),
}
def save_state(payload: dict) -> None:
parametros = payload.get("parametros", {})
pd.DataFrame(
{
"parametro": ["data_planejamento", "criterio_missao", "criterio_sbv"],
"valor": [
parametros.get("data_planejamento", "hoje"),
parametros.get("criterio_missao", "meta_50"),
parametros.get("criterio_sbv", "equalizar_quadrinhos"),
],
}
).to_csv(DADOS_DIR / "parametros_missao.csv", index=False)
pd.DataFrame(payload.get("aeronaves", [])).to_csv(DADOS_DIR / "aeronaves_disponiveis.csv", index=False)
if payload.get("tem_rota", False):
rota = payload.get("rota", {})
pd.DataFrame(
{
"origem": [normalizar_texto(rota.get("origem", "NT"))],
"destino": [normalizar_texto(rota.get("destino", ""))],
"inicio": [str(rota.get("inicio", "")).strip()],
"fim": [str(rota.get("fim", "")).strip()],
"tev_horas": [str(rota.get("tev_horas", "0")).strip()],
"pernoite_dias": [str(rota.get("pernoite_dias", "0")).strip()],
"rota": [normalizar_texto(rota.get("rota", ""))],
}
).to_csv(DADOS_DIR / "rotas_acionadas.csv", index=False)
else:
pd.DataFrame(columns=["origem", "destino", "inicio", "fim", "tev_horas", "pernoite_dias", "rota"]).to_csv(
DADOS_DIR / "rotas_acionadas.csv", index=False
)
tripulantes = pd.DataFrame(payload.get("tripulantes", []))
if not tripulantes.empty:
tripulantes["tripulante"] = tripulantes["tripulante"].map(normalizar_texto)
tripulantes.to_csv(DADOS_DIR / "tripulantes_disponiveis.csv", index=False)
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
path = urlparse(self.path).path
if path == "/":
self._send_html(INDEX_HTML)
return
if path == "/api/state":
self._send_json(load_state())
return
if path == "/api/quadrinhos":
self._send_json(quadrinhos_para_api(BASE_DIR))
return
if path.startswith("/assets/"):
self._send_file(DADOS_DIR / "assets" / path.removeprefix("/assets/"))
return
self.send_error(404)
def do_POST(self) -> None:
path = urlparse(self.path).path
payload = self._read_json()
if path == "/api/save":
save_state(payload)
self._send_json({"ok": True, "message": "Entradas salvas em dados/."})
return
if path == "/api/run":
save_state(payload)
resultado = executar_planejamento(BASE_DIR)
self._send_json(
{
"ok": True,
"message": "Escala gerada com sucesso.",
"data_planejamento": str(resultado["data_planejamento"]),
"arquivo_saida": str(resultado["arquivo_saida"]),
"total_candidatas": resultado["total_candidatas"],
"total_selecionadas": resultado["total_selecionadas"],
}
)
return
if path == "/api/open-latest":
arquivos = sorted((BASE_DIR / "resultados").glob("planejamento_diario_*.xlsx"), key=lambda p: p.stat().st_mtime)
if not arquivos:
self._send_json({"ok": False, "message": "Nenhuma planilha encontrada."}, status=404)
return
os.startfile(arquivos[-1])
self._send_json({"ok": True, "message": f"Abrindo {arquivos[-1]}"})
return
self.send_error(404)
def _read_json(self) -> dict:
length = int(self.headers.get("Content-Length", "0"))
if length == 0:
return {}
return json.loads(self.rfile.read(length).decode("utf-8"))
def _send_json(self, payload: dict, status: int = 200) -> None:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _send_html(self, html: str) -> None:
data = html.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _send_file(self, path: Path) -> None:
try:
resolved = path.resolve()
assets_dir = (DADOS_DIR / "assets").resolve()
if not resolved.is_file() or assets_dir not in resolved.parents:
self.send_error(404)
return
data = resolved.read_bytes()
content_type = mimetypes.guess_type(str(resolved))[0] or "application/octet-stream"
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
except OSError:
self.send_error(404)
def log_message(self, format: str, *args) -> None:
return
INDEX_HTML = r"""<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Planejador Diario de Missoes</title>
<link rel="icon" href="/assets/favicon.ico">
<style>
:root {
--ink:#172033; --muted:#667085; --line:#d7deea; --brand:#1f4e78; --brand-2:#2c6b9f;
--bg:#eef3f8; --panel:#ffffff; --soft:#f7fafc; --ok:#26734d; --ok-2:#1d6b46; --warn:#9a3412;
--shadow:0 12px 28px rgba(23,32,51,.10); --shadow-soft:0 5px 16px rgba(23,32,51,.07);
}
* { box-sizing: border-box; }
body { margin:0; font-family: Segoe UI, Arial, sans-serif; color:var(--ink); background:var(--bg); }
header { background:linear-gradient(135deg, #12334f, #1f4e78 58%, #2f7a9f); color:white; padding:18px 26px; box-shadow:var(--shadow); }
.topbar { max-width:1280px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; gap:18px; }
.brand { display:flex; align-items:center; gap:14px; min-width:0; }
.brand img { width:48px; height:48px; object-fit:contain; background:white; border-radius:6px; padding:5px; }
h1 { font-size:22px; line-height:1.15; margin:0; color:white; letter-spacing:0; }
.subtitle { margin-top:4px; color:#dceaf5; font-size:13px; }
main { padding:22px 24px 32px; max-width:1280px; margin:0 auto; }
.grid { display:grid; grid-template-columns: minmax(0, 1fr) minmax(360px, .9fr); gap:18px; align-items:start; }
section { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:18px; box-shadow:var(--shadow-soft); }
h2 { font-size:15px; margin:0 0 14px; color:var(--brand); display:flex; align-items:center; gap:8px; }
h2::before { content:""; width:4px; height:18px; border-radius:4px; background:var(--brand-2); display:inline-block; }
label { display:block; font-size:12px; font-weight:700; color:#344054; margin-bottom:6px; }
input, select { width:100%; padding:10px 11px; border:1px solid #c7d3e0; border-radius:6px; font-size:14px; background:white; color:var(--ink); outline:none; transition:border-color .15s, box-shadow .15s; }
input:focus, select:focus { border-color:var(--brand-2); box-shadow:0 0 0 3px rgba(47,122,159,.14); }
.row { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:12px; margin-bottom:12px; }
.check { display:flex; align-items:center; gap:8px; margin:9px 0; font-size:14px; color:#26364a; }
.check input { width:16px; height:16px; accent-color:var(--brand); }
.aircraft { border-top:1px solid var(--line); padding-top:13px; margin-top:13px; }
.aircraft:first-of-type { border-top:0; padding-top:0; margin-top:0; }
.aircraft strong { display:block; margin-bottom:9px; color:#122b43; font-size:14px; }
.crewbar { display:flex; gap:8px; margin-bottom:10px; }
button { border:0; border-radius:6px; padding:10px 14px; font-weight:700; cursor:pointer; background:#e7edf3; color:#172033; transition:transform .12s, box-shadow .12s, background .12s; }
button:hover { transform:translateY(-1px); box-shadow:0 5px 12px rgba(23,32,51,.13); }
button.primary { background:#ffffff; color:var(--brand); }
button.success { background:var(--ok); color:white; }
main button.primary { background:var(--brand); color:white; }
main button.success { background:var(--ok-2); color:white; }
button:disabled { opacity:.55; cursor:wait; }
.crew { height:290px; overflow:auto; border:1px solid var(--line); border-radius:8px; padding:10px; display:grid; grid-template-columns: repeat(4, minmax(78px, 1fr)); gap:6px 10px; background:var(--soft); }
.crew label { display:flex; align-items:center; gap:7px; font-size:13px; font-weight:600; margin:0; background:white; border:1px solid #e1e8f0; border-radius:5px; padding:6px 7px; }
.crew input { width:15px; height:15px; accent-color:var(--brand); }
pre { white-space:pre-wrap; background:#101928; color:#dbe7f2; border-radius:8px; padding:15px; min-height:132px; margin:0; font-size:13px; line-height:1.5; border:1px solid #26374d; }
.actions { display:flex; flex-wrap:wrap; gap:10px; }
.tabs { display:flex; gap:8px; margin-bottom:18px; border-bottom:1px solid var(--line); }
.tab { border-radius:7px 7px 0 0; background:#dfe8f2; color:#1f2937; padding:10px 14px; box-shadow:none; }
.tab:hover { box-shadow:none; }
.tab.active { background:var(--brand); color:white; }
.view { display:none; }
.view.active { display:block; }
.summary { display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:10px; margin-bottom:12px; }
.metric { border:1px solid var(--line); border-radius:8px; padding:12px; background:var(--soft); color:#475467; font-weight:700; }
.metric b { display:block; color:var(--brand); font-size:20px; margin-top:3px; }
.toolbar { display:grid; grid-template-columns: 1.5fr 180px 180px; gap:10px; margin-bottom:10px; }
.tablewrap { overflow:auto; border:1px solid var(--line); border-radius:8px; background:white; max-height:520px; }
table { width:100%; border-collapse:collapse; font-size:13px; min-width:980px; }
th, td { border-bottom:1px solid var(--line); padding:9px 10px; text-align:left; vertical-align:top; }
th { background:#e8f0f8; color:#243b53; position:sticky; top:0; z-index:1; }
tbody tr:nth-child(even) { background:#fafcff; }
td.num { text-align:right; font-variant-numeric: tabular-nums; }
@media (max-width: 900px) { .topbar { align-items:flex-start; flex-direction:column; } .grid { grid-template-columns:1fr; } .crew { grid-template-columns: repeat(2, minmax(80px, 1fr)); } }
@media (max-width: 900px) { .summary, .toolbar { grid-template-columns:1fr; } }
</style>
</head>
<body>
<header>
<div class="topbar">
<div class="brand">
<img src="/assets/app_logo.png" alt="">
<div>
<h1>Planejador Diario de Missoes</h1>
<div class="subtitle">Escala, sobreaviso e acompanhamento operacional</div>
</div>
</div>
<div class="actions">
<button onclick="save()">Salvar entradas</button>
<button class="primary" onclick="run()">Gerar escala</button>
<button class="success" onclick="openLatest()">Abrir planilha</button>
</div>
</div>
</header>
<main>
<div class="tabs">
<button class="tab active" data-view="planejamento" onclick="showView('planejamento')">Planejamento</button>
<button class="tab" data-view="quadrinhos" onclick="showView('quadrinhos')">Quadrinhos</button>
</div>
<div id="planejamento" class="view active">
<div class="grid">
<section>
<h2>Parametros</h2>
<div class="row">
<div><label>Data</label><input id="data_planejamento" placeholder="hoje ou AAAA-MM-DD"></div>
<div><label>Criterio missao</label><select id="criterio_missao"><option>meta_50</option><option>meta_110</option><option>menor_custo</option></select></div>
</div>
<div class="row">
<div><label>Criterio SBV</label><select id="criterio_sbv"><option>equalizar_quadrinhos</option><option>meta_50</option><option>meta_110</option></select></div>
</div>
</section>
<section>
<h2>Aeronaves</h2>
<div id="aeronaves"></div>
</section>
<section>
<h2>Missao acionada</h2>
<label class="check"><input id="tem_rota" type="checkbox"> Existe missao de rota acionada</label>
<div class="row">
<div><label>Origem</label><input id="origem"></div>
<div><label>Destino</label><input id="destino"></div>
</div>
<div class="row">
<div><label>Inicio</label><input id="inicio" placeholder="AAAA-MM-DD HH:MM"></div>
<div><label>Fim</label><input id="fim" placeholder="AAAA-MM-DD HH:MM"></div>
</div>
<div class="row">
<div><label>TEV horas</label><input id="tev_horas"></div>
<div><label>Pernoites</label><input id="pernoite_dias"></div>
</div>
<div><label>Rota resumida</label><input id="rota"></div>
</section>
<section>
<h2>Tripulantes disponiveis</h2>
<div class="crewbar">
<button onclick="setCrew(true)">Marcar todos</button>
<button onclick="setCrew(false)">Desmarcar todos</button>
</div>
<div id="tripulantes" class="crew"></div>
</section>
<section style="grid-column: 1 / -1;">
<h2>Status</h2>
<pre id="log">Carregando dados...</pre>
</section>
</div>
</div>
<div id="quadrinhos" class="view">
<section>
<h2>Quadrinho operacional</h2>
<div class="summary">
<div class="metric">Tripulantes<b id="qtd_trip">-</b></div>
<div class="metric">Quadrinhos abaixo de 50h<b id="qtd_abaixo50">-</b></div>
<div class="metric">Media de horas no ano<b id="media_horas">-</b></div>
</div>
<div class="toolbar">
<input id="q_busca" placeholder="Filtrar por tripulante, OI ou observacao" oninput="renderQuadrinhos()">
<select id="q_aeronave" onchange="renderQuadrinhos()"><option value="">Todas aeronaves</option><option>C98</option><option>C97</option><option>C95</option></select>
<select id="q_meta" onchange="renderQuadrinhos()"><option value="">Todos</option><option value="abaixo50">Abaixo de 50h</option></select>
</div>
<div class="tablewrap">
<table>
<thead>
<tr>
<th>Trip</th><th>Aeronave</th><th>Qualificacao</th><th>Proxima OI</th>
<th>Horas base</th><th>Horas ano</th><th>SBV</th><th>Faltam 50h</th><th>Ultimo voo</th><th>Obs</th>
</tr>
</thead>
<tbody id="quadrinhos_body"><tr><td colspan="10">Carregando...</td></tr></tbody>
</table>
</div>
</section>
</div>
</main>
<script>
let state = null;
let quadrinhos = [];
async function load() {
const res = await fetch('/api/state');
state = await res.json();
document.querySelector('#data_planejamento').value = state.parametros.data_planejamento;
document.querySelector('#criterio_missao').value = state.parametros.criterio_missao;
document.querySelector('#criterio_sbv').value = state.parametros.criterio_sbv;
document.querySelector('#tem_rota').checked = state.tem_rota;
for (const key of ['origem','destino','inicio','fim','tev_horas','pernoite_dias','rota']) {
document.querySelector('#' + key).value = state.rota[key] ?? '';
}
renderAircraft();
renderCrew();
log('Pronto. Edite as entradas e clique em Salvar e gerar escala.');
loadQuadrinhos();
}
function showView(name) {
for (const view of document.querySelectorAll('.view')) view.classList.toggle('active', view.id === name);
for (const tab of document.querySelectorAll('.tab')) tab.classList.toggle('active', tab.dataset.view === name);
if (name === 'quadrinhos' && quadrinhos.length === 0) loadQuadrinhos();
}
async function loadQuadrinhos() {
const res = await fetch('/api/quadrinhos');
const data = await res.json();
quadrinhos = data.linhas ?? [];
document.querySelector('#qtd_trip').textContent = data.resumo?.tripulantes ?? 0;
document.querySelector('#qtd_abaixo50').textContent = data.resumo?.abaixo_50 ?? 0;
document.querySelector('#media_horas').textContent = `${data.resumo?.horas_media ?? 0} h`;
renderQuadrinhos();
}
function renderQuadrinhos() {
const root = document.querySelector('#quadrinhos_body');
const busca = (document.querySelector('#q_busca')?.value ?? '').trim().toUpperCase();
const aeronave = document.querySelector('#q_aeronave')?.value ?? '';
const meta = document.querySelector('#q_meta')?.value ?? '';
const rows = quadrinhos.filter((q) => {
const texto = `${q.tripulante} ${q.aeronave} ${q.qualificacao} ${q.proxima_oi} ${q.observacao}`.toUpperCase();
return (!busca || texto.includes(busca)) && (!aeronave || q.aeronave === aeronave) && (!meta || Number(q.horas_voadas_ano) < 50);
});
if (rows.length === 0) {
root.innerHTML = '<tr><td colspan="10">Nenhum quadrinho encontrado.</td></tr>';
return;
}
root.innerHTML = rows.map((q) => `
<tr>
<td>${q.tripulante}</td>
<td>${q.aeronave}</td>
<td>${q.qualificacao}</td>
<td>${q.proxima_oi}</td>
<td class="num">${fmt(q.horas_aeronave_base)}</td>
<td class="num">${fmt(q.horas_voadas_ano)}</td>
<td class="num">${fmt(q.sbv_ano)}</td>
<td class="num">${fmt(q.meta_50_faltante)}</td>
<td>${q.ultima_data_voo ?? ''}</td>
<td>${q.observacao ?? ''}</td>
</tr>
`).join('');
}
function fmt(value) {
const n = Number(value);
if (!Number.isFinite(n)) return '';
return n.toLocaleString('pt-BR', { maximumFractionDigits: 1 });
}
function renderAircraft() {
const root = document.querySelector('#aeronaves');
root.innerHTML = '';
for (const av of state.aeronaves) {
const item = document.createElement('div');
item.className = 'aircraft';
item.innerHTML = `
<strong>${av.aeronave}</strong>
<label class="check"><input type="checkbox" data-av="${av.aeronave}" data-field="disponivel_sede" ${av.disponivel_sede ? 'checked' : ''}> Disponivel em sede</label>
<label class="check"><input type="checkbox" data-av="${av.aeronave}" data-field="em_pane" ${av.em_pane ? 'checked' : ''}> Em pane</label>
<label class="check"><input type="checkbox" data-av="${av.aeronave}" data-field="em_missao_rota" ${av.em_missao_rota ? 'checked' : ''}> Em missao de rota</label>
<label>Hora livre</label><input value="${av.hora_livre ?? ''}" data-av="${av.aeronave}" data-field="hora_livre">
`;
root.appendChild(item);
}
}
function renderCrew() {
const root = document.querySelector('#tripulantes');
root.innerHTML = '';
for (const trip of state.tripulantes) {
const label = document.createElement('label');
label.innerHTML = `<input type="checkbox" data-trip="${trip.tripulante}" ${trip.disponivel ? 'checked' : ''}> ${trip.tripulante}`;
root.appendChild(label);
}
}
function collect() {
const payload = {
parametros: {
data_planejamento: document.querySelector('#data_planejamento').value,
criterio_missao: document.querySelector('#criterio_missao').value,
criterio_sbv: document.querySelector('#criterio_sbv').value,
},
tem_rota: document.querySelector('#tem_rota').checked,
rota: {},
aeronaves: [],
tripulantes: [],
};
for (const key of ['origem','destino','inicio','fim','tev_horas','pernoite_dias','rota']) {
payload.rota[key] = document.querySelector('#' + key).value;
}
for (const av of state.aeronaves) {
const out = { aeronave: av.aeronave };
for (const field of ['disponivel_sede','em_pane','em_missao_rota']) {
out[field] = document.querySelector(`[data-av="${av.aeronave}"][data-field="${field}"]`).checked;
}
out.hora_livre = document.querySelector(`[data-av="${av.aeronave}"][data-field="hora_livre"]`).value;
payload.aeronaves.push(out);
}
for (const input of document.querySelectorAll('[data-trip]')) {
payload.tripulantes.push({ tripulante: input.dataset.trip, disponivel: input.checked });
}
return payload;
}
async function save() {
setBusy(true);
try {
const res = await fetch('/api/save', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(collect()) });
const data = await res.json();
log(data.message);
} finally {
setBusy(false);
}
}
async function run() {
setBusy(true);
log('Gerando escala. Aguarde...');
try {
const res = await fetch('/api/run', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(collect()) });
const data = await res.json();
if (!data.ok) throw new Error(data.message);
log(`${data.message}\nData: ${data.data_planejamento}\nColunas candidatas: ${data.total_candidatas}\nEscalas selecionadas: ${data.total_selecionadas}\nArquivo: ${data.arquivo_saida}`);
loadQuadrinhos();
} catch (err) {
log('Erro ao gerar escala:\n' + err.message);
} finally {
setBusy(false);
}
}
async function openLatest() {
const res = await fetch('/api/open-latest', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
const data = await res.json();
log(data.message);
}
function setCrew(value) {
for (const input of document.querySelectorAll('[data-trip]')) input.checked = value;
}
function setBusy(value) {
for (const btn of document.querySelectorAll('button')) btn.disabled = value;
}
function log(text) {
document.querySelector('#log').textContent = text;
}
load();
</script>
</body>
</html>
"""
def main() -> None:
server = ThreadingHTTPServer((HOST, PORT), Handler)
url = f"http://{HOST}:{PORT}"
threading.Timer(0.5, lambda: webbrowser.open(url)).start()
print(f"Interface web aberta em {url}")
print("Pressione Ctrl+C para encerrar.")
server.serve_forever()
if __name__ == "__main__":
main()