Abimael Torcate
Claude
commited on
Commit
·
d54ba19
0
Parent(s):
Initial commit: Complete checklist management app with AI reporting
Browse filesFeatures:
- Multi-page Streamlit application for checklist management
- PostgreSQL integration with time tracking analytics
- Process tracking with 6-month deadline monitoring
- AI-powered analysis and executive reports with OpenAI GPT-4
- PDF export functionality with ReportLab
- Comprehensive dashboard with Plotly visualizations
- Environment variable configuration for Hugging Face Spaces deployment
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- .env.example +2 -0
- .gitignore +204 -0
- README.md +100 -0
- app.py +76 -0
- pages/analytics.py +197 -0
- pages/criar_checklist.py +130 -0
- pages/dashboard.py +108 -0
- pages/dashboard_geral.py +501 -0
- pages/relatorio_ia.py +325 -0
- prompt_analise_checklists.txt +30 -0
- prompt_relatorio_executivo.txt +48 -0
- requirements.txt +9 -0
- setup_database.py +28 -0
- update_database.py +41 -0
- utils/database.py +628 -0
.env.example
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configurações para Relatórios com IA
|
| 2 |
+
OPENAI_API_KEY=sua_chave_openai_aqui
|
.gitignore
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py,cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
#Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# poetry
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 102 |
+
#poetry.lock
|
| 103 |
+
|
| 104 |
+
# pdm
|
| 105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 106 |
+
#pdm.lock
|
| 107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 108 |
+
# in version control.
|
| 109 |
+
# https://pdm.fming.dev/#use-with-ide
|
| 110 |
+
.pdm.toml
|
| 111 |
+
|
| 112 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 113 |
+
__pypackages__/
|
| 114 |
+
|
| 115 |
+
# Celery stuff
|
| 116 |
+
celerybeat-schedule
|
| 117 |
+
celerybeat.pid
|
| 118 |
+
|
| 119 |
+
# SageMath parsed files
|
| 120 |
+
*.sage.py
|
| 121 |
+
|
| 122 |
+
# Environments
|
| 123 |
+
.env
|
| 124 |
+
.venv
|
| 125 |
+
env/
|
| 126 |
+
venv/
|
| 127 |
+
ENV/
|
| 128 |
+
env.bak/
|
| 129 |
+
venv.bak/
|
| 130 |
+
|
| 131 |
+
# Spyder project settings
|
| 132 |
+
.spyderproject
|
| 133 |
+
.spyproject
|
| 134 |
+
|
| 135 |
+
# Rope project settings
|
| 136 |
+
.ropeproject
|
| 137 |
+
|
| 138 |
+
# mkdocs documentation
|
| 139 |
+
/site
|
| 140 |
+
|
| 141 |
+
# mypy
|
| 142 |
+
.mypy_cache/
|
| 143 |
+
.dmypy.json
|
| 144 |
+
dmypy.json
|
| 145 |
+
|
| 146 |
+
# Pyre type checker
|
| 147 |
+
.pyre/
|
| 148 |
+
|
| 149 |
+
# pytype static type analyzer
|
| 150 |
+
.pytype/
|
| 151 |
+
|
| 152 |
+
# Cython debug symbols
|
| 153 |
+
cython_debug/
|
| 154 |
+
|
| 155 |
+
# PyCharm
|
| 156 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 157 |
+
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
| 158 |
+
# project, it is common to ignore common working directory configurations.
|
| 159 |
+
.idea/
|
| 160 |
+
|
| 161 |
+
# VS Code
|
| 162 |
+
.vscode/
|
| 163 |
+
|
| 164 |
+
# Arquivos de teste e desenvolvimento
|
| 165 |
+
test_*.py
|
| 166 |
+
*_test.py
|
| 167 |
+
tests/
|
| 168 |
+
test/
|
| 169 |
+
|
| 170 |
+
# Arquivos de criação e configuração do banco
|
| 171 |
+
create_tables.sql
|
| 172 |
+
add_numero_processo.sql
|
| 173 |
+
test_db.py
|
| 174 |
+
|
| 175 |
+
# Arquivos de configuração local
|
| 176 |
+
.env.local
|
| 177 |
+
.env.development
|
| 178 |
+
config.ini
|
| 179 |
+
|
| 180 |
+
# Arquivos de backup
|
| 181 |
+
*.bak
|
| 182 |
+
*.backup
|
| 183 |
+
*.tmp
|
| 184 |
+
|
| 185 |
+
# Logs
|
| 186 |
+
*.log
|
| 187 |
+
logs/
|
| 188 |
+
|
| 189 |
+
# Dados temporários
|
| 190 |
+
temp/
|
| 191 |
+
tmp/
|
| 192 |
+
|
| 193 |
+
# OS generated files
|
| 194 |
+
.DS_Store
|
| 195 |
+
.DS_Store?
|
| 196 |
+
._*
|
| 197 |
+
.Spotlight-V100
|
| 198 |
+
.Trashes
|
| 199 |
+
ehthumbs.db
|
| 200 |
+
Thumbs.db
|
| 201 |
+
|
| 202 |
+
# Specific project files to ignore
|
| 203 |
+
CLAUDE.md
|
| 204 |
+
README_local.md
|
README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🗂️ Gerenciador de Checklists
|
| 2 |
+
|
| 3 |
+
Aplicativo completo para gerenciamento de checklists com análise de tempo e relatórios inteligentes.
|
| 4 |
+
|
| 5 |
+
## 🚀 Funcionalidades
|
| 6 |
+
|
| 7 |
+
- ✅ **Criação de Checklists Personalizados**
|
| 8 |
+
- 📊 **Dashboard Geral com Analytics**
|
| 9 |
+
- ⏱️ **Análise de Tempo e Movimentos**
|
| 10 |
+
- 🎯 **Controle de Prazos (6 meses)**
|
| 11 |
+
- 🤖 **Relatórios com IA (GPT-4)**
|
| 12 |
+
- 📄 **Export para PDF**
|
| 13 |
+
- 🔢 **Rastreamento por Número de Processo**
|
| 14 |
+
|
| 15 |
+
## 🛠️ Tecnologias
|
| 16 |
+
|
| 17 |
+
- **Frontend**: Streamlit
|
| 18 |
+
- **Banco de Dados**: PostgreSQL
|
| 19 |
+
- **IA**: OpenAI GPT-4
|
| 20 |
+
- **Visualização**: Plotly, Matplotlib, Seaborn
|
| 21 |
+
- **PDF**: ReportLab
|
| 22 |
+
|
| 23 |
+
## 📦 Dependências
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## ⚙️ Configuração
|
| 30 |
+
|
| 31 |
+
### Variáveis de Ambiente
|
| 32 |
+
|
| 33 |
+
O aplicativo utiliza as seguintes variáveis de ambiente:
|
| 34 |
+
|
| 35 |
+
#### Banco de Dados
|
| 36 |
+
- `DB_HOST`: Host do PostgreSQL
|
| 37 |
+
- `DB_PORT`: Porta (padrão: 5432)
|
| 38 |
+
- `DB_NAME`: Nome do banco
|
| 39 |
+
- `DB_USER`: Usuário
|
| 40 |
+
- `DB_PASSWORD`: Senha
|
| 41 |
+
|
| 42 |
+
#### IA (OpenAI)
|
| 43 |
+
- `OPENAI_API_KEY`: Chave da API OpenAI para relatórios
|
| 44 |
+
|
| 45 |
+
### Para Desenvolvimento Local
|
| 46 |
+
|
| 47 |
+
Crie um arquivo `.env`:
|
| 48 |
+
```env
|
| 49 |
+
OPENAI_API_KEY=sua_chave_openai_aqui
|
| 50 |
+
DB_HOST=seu_host
|
| 51 |
+
DB_PORT=5432
|
| 52 |
+
DB_NAME=checklist
|
| 53 |
+
DB_USER=usuario
|
| 54 |
+
DB_PASSWORD=senha
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## 🚀 Como Usar
|
| 58 |
+
|
| 59 |
+
1. **Criar Checklist**: Clique em "➕ Novo Checklist"
|
| 60 |
+
2. **Visualizar**: Acesse via "📊 Dashboard Geral"
|
| 61 |
+
3. **Análise**: Use "🤖 Relatórios IA" para insights
|
| 62 |
+
4. **Monitorar**: Acompanhe prazos e progresso
|
| 63 |
+
|
| 64 |
+
## 📊 Analytics
|
| 65 |
+
|
| 66 |
+
- Tempo médio por processo
|
| 67 |
+
- Taxa de conclusão
|
| 68 |
+
- Análise de riscos de prazo
|
| 69 |
+
- Identificação de gargalos
|
| 70 |
+
- Relatórios executivos
|
| 71 |
+
|
| 72 |
+
## 🤖 IA e Relatórios
|
| 73 |
+
|
| 74 |
+
- Análise automática de padrões
|
| 75 |
+
- Identificação de ineficiências
|
| 76 |
+
- Recomendações estratégicas
|
| 77 |
+
- Relatórios executivos em PDF
|
| 78 |
+
|
| 79 |
+
## 🏗️ Estrutura do Projeto
|
| 80 |
+
|
| 81 |
+
```
|
| 82 |
+
checklist/
|
| 83 |
+
├── app.py # Aplicação principal
|
| 84 |
+
├── pages/ # Páginas do Streamlit
|
| 85 |
+
│ ├── criar_checklist.py # Criação de checklists
|
| 86 |
+
│ ├── dashboard.py # Dashboard individual
|
| 87 |
+
│ ├── dashboard_geral.py # Dashboard geral
|
| 88 |
+
│ ├── analytics.py # Análise de tempo
|
| 89 |
+
│ └── relatorio_ia.py # Relatórios com IA
|
| 90 |
+
├── utils/
|
| 91 |
+
│ └── database.py # Conexão e operações DB
|
| 92 |
+
├── requirements.txt # Dependências Python
|
| 93 |
+
└── README.md # Este arquivo
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
## 📈 Métricas Importantes
|
| 97 |
+
|
| 98 |
+
- **Prazo Ideal**: 6 meses por processo
|
| 99 |
+
- **Status de Prazo**: NO_PRAZO, EM_RISCO, RISCO_ATRASO, ATRASADO
|
| 100 |
+
- **Análise de Velocidade**: Baseada em interações dos usuários
|
app.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Aplicativo de Gerenciamento de Checklists"""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from utils.database import get_all_checklists, test_connection
|
| 5 |
+
|
| 6 |
+
st.set_page_config(
|
| 7 |
+
page_title="Gerenciador de Checklists",
|
| 8 |
+
page_icon="✅",
|
| 9 |
+
layout="centered",
|
| 10 |
+
initial_sidebar_state="collapsed"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
def init_session_state():
|
| 14 |
+
if 'current_checklist_id' not in st.session_state:
|
| 15 |
+
st.session_state.current_checklist_id = None
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
init_session_state()
|
| 19 |
+
|
| 20 |
+
st.title("🗂️ Gerenciador de Checklists")
|
| 21 |
+
st.markdown("---")
|
| 22 |
+
|
| 23 |
+
# Testar conexão com banco
|
| 24 |
+
if not test_connection():
|
| 25 |
+
st.error("❌ Erro ao conectar com o banco de dados!")
|
| 26 |
+
st.stop()
|
| 27 |
+
|
| 28 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 29 |
+
|
| 30 |
+
with col2:
|
| 31 |
+
st.markdown("### Bem-vindo!")
|
| 32 |
+
st.markdown("Crie e gerencie seus checklists personalizados de forma simples e eficiente.")
|
| 33 |
+
|
| 34 |
+
st.markdown("")
|
| 35 |
+
|
| 36 |
+
col1, col2, col3 = st.columns(3)
|
| 37 |
+
with col1:
|
| 38 |
+
if st.button("➕ Novo Checklist", type="primary", use_container_width=True):
|
| 39 |
+
st.switch_page("pages/criar_checklist.py")
|
| 40 |
+
with col2:
|
| 41 |
+
if st.button("📊 Dashboard Geral", use_container_width=True):
|
| 42 |
+
st.switch_page("pages/dashboard_geral.py")
|
| 43 |
+
with col3:
|
| 44 |
+
if st.button("🤖 Relatórios IA", use_container_width=True):
|
| 45 |
+
st.switch_page("pages/relatorio_ia.py")
|
| 46 |
+
|
| 47 |
+
# Buscar checklists do banco
|
| 48 |
+
try:
|
| 49 |
+
checklists = get_all_checklists()
|
| 50 |
+
|
| 51 |
+
if checklists:
|
| 52 |
+
st.markdown("### Seus Checklists")
|
| 53 |
+
for checklist in checklists:
|
| 54 |
+
col_name, col_process, col_date, col_btn = st.columns([3, 2, 1.5, 1])
|
| 55 |
+
with col_name:
|
| 56 |
+
st.write(f"📋 {checklist['name']}")
|
| 57 |
+
with col_process:
|
| 58 |
+
if checklist['numero_processo']:
|
| 59 |
+
st.caption(f"🔢 {checklist['numero_processo']}")
|
| 60 |
+
else:
|
| 61 |
+
st.caption("🔢 Sem processo")
|
| 62 |
+
with col_date:
|
| 63 |
+
st.caption(checklist['created_at'].strftime("%d/%m/%Y"))
|
| 64 |
+
with col_btn:
|
| 65 |
+
if st.button("Ver", key=f"view_{checklist['id']}"):
|
| 66 |
+
st.session_state.current_checklist_id = checklist['id']
|
| 67 |
+
st.switch_page("pages/dashboard.py")
|
| 68 |
+
else:
|
| 69 |
+
st.info("Você ainda não tem checklists. Clique em 'Novo Checklist' para criar o primeiro!")
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
st.error(f"Erro ao buscar checklists: {str(e)}")
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
main()
|
| 76 |
+
|
pages/analytics.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Página de análises de tempo e movimentos do checklist"""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
import plotly.graph_objects as go
|
| 7 |
+
from utils.database import get_checklist_analytics, get_checklist_with_items
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="Análises de Tempo",
|
| 12 |
+
page_icon="📊",
|
| 13 |
+
layout="wide"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
def format_time(seconds):
|
| 17 |
+
"""Formata segundos em formato legível"""
|
| 18 |
+
if seconds is None or seconds == 0:
|
| 19 |
+
return "0s"
|
| 20 |
+
|
| 21 |
+
if seconds < 60:
|
| 22 |
+
return f"{int(seconds)}s"
|
| 23 |
+
elif seconds < 3600:
|
| 24 |
+
minutes = int(seconds // 60)
|
| 25 |
+
secs = int(seconds % 60)
|
| 26 |
+
return f"{minutes}m {secs}s"
|
| 27 |
+
else:
|
| 28 |
+
hours = int(seconds // 3600)
|
| 29 |
+
minutes = int((seconds % 3600) // 60)
|
| 30 |
+
return f"{hours}h {minutes}m"
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
if 'current_checklist_id' not in st.session_state or st.session_state.current_checklist_id is None:
|
| 34 |
+
st.error("Nenhum checklist selecionado!")
|
| 35 |
+
if st.button("← Voltar para Início"):
|
| 36 |
+
st.switch_page("app.py")
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
checklist_id = st.session_state.current_checklist_id
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
# Buscar dados do checklist
|
| 43 |
+
checklist = get_checklist_with_items(checklist_id)
|
| 44 |
+
analytics = get_checklist_analytics(checklist_id)
|
| 45 |
+
|
| 46 |
+
if not checklist:
|
| 47 |
+
st.error("Checklist não encontrado!")
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
# Header
|
| 51 |
+
col1, col2 = st.columns([5, 1])
|
| 52 |
+
with col1:
|
| 53 |
+
st.title(f"📊 Análises: {checklist['name']}")
|
| 54 |
+
if checklist['numero_processo']:
|
| 55 |
+
st.caption(f"🔢 Processo: {checklist['numero_processo']}")
|
| 56 |
+
with col2:
|
| 57 |
+
if st.button("← Voltar"):
|
| 58 |
+
st.switch_page("pages/dashboard.py")
|
| 59 |
+
|
| 60 |
+
st.markdown("---")
|
| 61 |
+
|
| 62 |
+
# Estatísticas gerais
|
| 63 |
+
stats = analytics['stats']
|
| 64 |
+
time_analysis = analytics['time_analysis']
|
| 65 |
+
|
| 66 |
+
if stats['first_interaction'] and stats['last_interaction']:
|
| 67 |
+
total_time = stats['last_interaction'] - stats['first_interaction']
|
| 68 |
+
total_minutes = total_time.total_seconds() / 60
|
| 69 |
+
else:
|
| 70 |
+
total_minutes = 0
|
| 71 |
+
|
| 72 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 73 |
+
|
| 74 |
+
with col1:
|
| 75 |
+
st.metric("Total de Items", stats['total_items'])
|
| 76 |
+
with col2:
|
| 77 |
+
st.metric("Items Concluídos", stats['completed_items'])
|
| 78 |
+
with col3:
|
| 79 |
+
completion_rate = (stats['completed_items'] / stats['total_items']) * 100 if stats['total_items'] > 0 else 0
|
| 80 |
+
st.metric("Taxa de Conclusão", f"{completion_rate:.1f}%")
|
| 81 |
+
with col4:
|
| 82 |
+
st.metric("Tempo Total", format_time(total_minutes * 60))
|
| 83 |
+
|
| 84 |
+
st.markdown("---")
|
| 85 |
+
|
| 86 |
+
# Análise por item
|
| 87 |
+
if time_analysis:
|
| 88 |
+
st.markdown("### 📈 Análise de Tempo por Item")
|
| 89 |
+
|
| 90 |
+
# Criar DataFrame para visualização
|
| 91 |
+
df_time = pd.DataFrame(time_analysis)
|
| 92 |
+
|
| 93 |
+
# Converter colunas para tipos numéricos corretos
|
| 94 |
+
df_time['total_seconds_spent'] = pd.to_numeric(df_time['total_seconds_spent'], errors='coerce').fillna(0)
|
| 95 |
+
df_time['avg_seconds_per_completion'] = pd.to_numeric(df_time['avg_seconds_per_completion'], errors='coerce').fillna(0)
|
| 96 |
+
df_time['times_worked'] = pd.to_numeric(df_time['times_worked'], errors='coerce').fillna(0).astype(int)
|
| 97 |
+
|
| 98 |
+
df_time['tempo_formatado'] = df_time['total_seconds_spent'].apply(lambda x: format_time(x) if x else "0s")
|
| 99 |
+
df_time['tempo_medio_formatado'] = df_time['avg_seconds_per_completion'].apply(lambda x: format_time(x) if x else "0s")
|
| 100 |
+
df_time['vezes_trabalhado'] = df_time['times_worked']
|
| 101 |
+
|
| 102 |
+
# Gráfico de barras - Tempo total por item
|
| 103 |
+
if not df_time.empty and df_time['total_seconds_spent'].sum() > 0:
|
| 104 |
+
fig_time = px.bar(
|
| 105 |
+
df_time.sort_values('total_seconds_spent', ascending=False),
|
| 106 |
+
x='item_text',
|
| 107 |
+
y='total_seconds_spent',
|
| 108 |
+
title="Tempo Total Gasto por Item",
|
| 109 |
+
labels={'total_seconds_spent': 'Tempo (segundos)', 'item_text': 'Item'}
|
| 110 |
+
)
|
| 111 |
+
fig_time.update_layout(xaxis_tickangle=-45)
|
| 112 |
+
st.plotly_chart(fig_time, use_container_width=True)
|
| 113 |
+
|
| 114 |
+
# Gráfico de pizza - Distribuição do tempo
|
| 115 |
+
df_positive = df_time[df_time['total_seconds_spent'] > 0]
|
| 116 |
+
if not df_positive.empty:
|
| 117 |
+
fig_pie = px.pie(
|
| 118 |
+
df_positive,
|
| 119 |
+
values='total_seconds_spent',
|
| 120 |
+
names='item_text',
|
| 121 |
+
title="Distribuição do Tempo por Item"
|
| 122 |
+
)
|
| 123 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
| 124 |
+
|
| 125 |
+
# Tabela detalhada
|
| 126 |
+
st.markdown("### 📋 Detalhes por Item")
|
| 127 |
+
|
| 128 |
+
df_display = df_time[['item_text', 'vezes_trabalhado', 'tempo_formatado', 'tempo_medio_formatado']].copy()
|
| 129 |
+
df_display.columns = ['Item', 'Vezes Trabalhado', 'Tempo Total', 'Tempo Médio']
|
| 130 |
+
|
| 131 |
+
# Ordenar pela coluna numérica original
|
| 132 |
+
df_display = df_display.loc[df_time.sort_values('total_seconds_spent', ascending=False).index]
|
| 133 |
+
|
| 134 |
+
st.dataframe(df_display, use_container_width=True)
|
| 135 |
+
|
| 136 |
+
# Insights automáticos
|
| 137 |
+
st.markdown("### 🔍 Insights")
|
| 138 |
+
|
| 139 |
+
col1, col2 = st.columns(2)
|
| 140 |
+
|
| 141 |
+
with col1:
|
| 142 |
+
st.markdown("#### Items mais demorados")
|
| 143 |
+
if not df_time.empty and df_time['total_seconds_spent'].sum() > 0:
|
| 144 |
+
top_slow = df_time.sort_values('total_seconds_spent', ascending=False).head(3)
|
| 145 |
+
for _, item in top_slow.iterrows():
|
| 146 |
+
if item['total_seconds_spent'] > 0:
|
| 147 |
+
st.write(f"• **{item['item_text']}**: {item['tempo_formatado']}")
|
| 148 |
+
else:
|
| 149 |
+
st.info("Nenhum dado de tempo disponível ainda.")
|
| 150 |
+
|
| 151 |
+
with col2:
|
| 152 |
+
st.markdown("#### Items mais retrabalhados")
|
| 153 |
+
if not df_time.empty and df_time['times_worked'].sum() > 0:
|
| 154 |
+
top_rework = df_time.sort_values('times_worked', ascending=False).head(3)
|
| 155 |
+
for _, item in top_rework.iterrows():
|
| 156 |
+
if item['times_worked'] > 1:
|
| 157 |
+
st.write(f"• **{item['item_text']}**: {item['times_worked']} vezes")
|
| 158 |
+
else:
|
| 159 |
+
st.info("Nenhum retrabalho identificado.")
|
| 160 |
+
|
| 161 |
+
# Recomendações
|
| 162 |
+
st.markdown("#### 💡 Recomendações")
|
| 163 |
+
|
| 164 |
+
if not df_time.empty and df_time['total_seconds_spent'].sum() > 0:
|
| 165 |
+
mean_time = df_time['total_seconds_spent'].mean()
|
| 166 |
+
slow_items = df_time[df_time['total_seconds_spent'] > mean_time]['item_text'].tolist()
|
| 167 |
+
rework_items = df_time[df_time['times_worked'] > 1]['item_text'].tolist()
|
| 168 |
+
|
| 169 |
+
if slow_items:
|
| 170 |
+
st.warning(f"**Items que demandam mais tempo:** {', '.join(slow_items[:3])}")
|
| 171 |
+
st.write("💡 Considere revisar estes items ou dividir em subtarefas menores.")
|
| 172 |
+
|
| 173 |
+
if rework_items:
|
| 174 |
+
st.info(f"**Items com retrabalho:** {', '.join(rework_items[:3])}")
|
| 175 |
+
st.write("💡 Analise se estes items precisam de mais clareza ou recursos adicionais.")
|
| 176 |
+
|
| 177 |
+
if not slow_items and not rework_items:
|
| 178 |
+
st.success("✅ Ótimo trabalho! O checklist está sendo executado de forma eficiente.")
|
| 179 |
+
elif not df_time.empty:
|
| 180 |
+
st.info("📊 Marque e desmarque alguns items para gerar recomendações baseadas no tempo de execução.")
|
| 181 |
+
|
| 182 |
+
else:
|
| 183 |
+
st.info("📊 Ainda não há dados suficientes para análise. Continue marcando os items do checklist para gerar insights.")
|
| 184 |
+
|
| 185 |
+
# Mostrar checklist atual
|
| 186 |
+
st.markdown("### 📋 Status Atual dos Items")
|
| 187 |
+
for item in checklist['items']:
|
| 188 |
+
status = "✅" if item['is_checked'] else "⏳"
|
| 189 |
+
st.write(f"{status} {item['text']}")
|
| 190 |
+
|
| 191 |
+
except Exception as e:
|
| 192 |
+
st.error(f"Erro ao carregar análises: {str(e)}")
|
| 193 |
+
if st.button("← Voltar para Dashboard"):
|
| 194 |
+
st.switch_page("pages/dashboard.py")
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
main()
|
pages/criar_checklist.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Página para criar novo checklist"""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from utils.database import save_checklist as save_checklist_db
|
| 5 |
+
|
| 6 |
+
st.set_page_config(
|
| 7 |
+
page_title="Criar Novo Checklist",
|
| 8 |
+
page_icon="✅",
|
| 9 |
+
layout="centered"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
def init_form_state():
|
| 13 |
+
if 'form_items' not in st.session_state:
|
| 14 |
+
st.session_state.form_items = []
|
| 15 |
+
if 'item_to_remove' not in st.session_state:
|
| 16 |
+
st.session_state.item_to_remove = None
|
| 17 |
+
|
| 18 |
+
def add_item():
|
| 19 |
+
st.session_state.form_items.append("")
|
| 20 |
+
|
| 21 |
+
def remove_item(index):
|
| 22 |
+
st.session_state.form_items.pop(index)
|
| 23 |
+
|
| 24 |
+
def save_checklist(name, items, numero_processo):
|
| 25 |
+
if not name:
|
| 26 |
+
st.error("Por favor, dê um nome ao seu checklist!")
|
| 27 |
+
return False
|
| 28 |
+
|
| 29 |
+
if not numero_processo:
|
| 30 |
+
st.error("Por favor, informe o número do processo!")
|
| 31 |
+
return False
|
| 32 |
+
|
| 33 |
+
if not items or all(item == "" for item in items):
|
| 34 |
+
st.error("Por favor, adicione pelo menos um item ao checklist!")
|
| 35 |
+
return False
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# Salvar no banco de dados
|
| 39 |
+
checklist_id = save_checklist_db(name, items, numero_processo)
|
| 40 |
+
st.session_state.current_checklist_id = checklist_id
|
| 41 |
+
|
| 42 |
+
# Limpar os dados para próximo uso
|
| 43 |
+
st.session_state.form_items = []
|
| 44 |
+
if 'should_clear' not in st.session_state:
|
| 45 |
+
st.session_state.should_clear = False
|
| 46 |
+
st.session_state.should_clear = True
|
| 47 |
+
|
| 48 |
+
return True
|
| 49 |
+
except Exception as e:
|
| 50 |
+
st.error(f"Erro ao salvar checklist: {str(e)}")
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
def main():
|
| 54 |
+
init_form_state()
|
| 55 |
+
|
| 56 |
+
st.title("📝 Criar Novo Checklist")
|
| 57 |
+
|
| 58 |
+
col1, col2 = st.columns([5, 1])
|
| 59 |
+
with col1:
|
| 60 |
+
st.markdown("---")
|
| 61 |
+
with col2:
|
| 62 |
+
if st.button("← Voltar"):
|
| 63 |
+
st.switch_page("app.py")
|
| 64 |
+
|
| 65 |
+
# Verifica se deve limpar o formulário após salvar
|
| 66 |
+
if 'should_clear' in st.session_state and st.session_state.should_clear:
|
| 67 |
+
st.session_state.should_clear = False
|
| 68 |
+
st.switch_page("pages/dashboard.py")
|
| 69 |
+
|
| 70 |
+
if st.session_state.item_to_remove is not None:
|
| 71 |
+
remove_item(st.session_state.item_to_remove)
|
| 72 |
+
st.session_state.item_to_remove = None
|
| 73 |
+
st.rerun()
|
| 74 |
+
|
| 75 |
+
with st.container():
|
| 76 |
+
col1, col2 = st.columns(2)
|
| 77 |
+
|
| 78 |
+
with col1:
|
| 79 |
+
checklist_name = st.text_input(
|
| 80 |
+
"Nome do Checklist",
|
| 81 |
+
key="checklist_name_input",
|
| 82 |
+
placeholder="Ex: Tarefas do Projeto X"
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
with col2:
|
| 86 |
+
numero_processo = st.text_input(
|
| 87 |
+
"Número do Processo",
|
| 88 |
+
key="numero_processo_input",
|
| 89 |
+
placeholder="Ex: PROC-2024-001"
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
st.markdown("### Items do Checklist")
|
| 93 |
+
|
| 94 |
+
if not st.session_state.form_items:
|
| 95 |
+
st.info("Clique em 'Adicionar Item' para começar a criar seu checklist")
|
| 96 |
+
|
| 97 |
+
current_items = []
|
| 98 |
+
for i, item in enumerate(st.session_state.form_items):
|
| 99 |
+
col1, col2 = st.columns([5, 1])
|
| 100 |
+
with col1:
|
| 101 |
+
item_value = st.text_input(
|
| 102 |
+
f"Item {i+1}",
|
| 103 |
+
value=item,
|
| 104 |
+
key=f"item_{i}",
|
| 105 |
+
placeholder="Digite o item do checklist"
|
| 106 |
+
)
|
| 107 |
+
current_items.append(item_value)
|
| 108 |
+
with col2:
|
| 109 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 110 |
+
if st.button("🗑️", key=f"remove_{i}", help="Remover item"):
|
| 111 |
+
st.session_state.item_to_remove = i
|
| 112 |
+
st.rerun()
|
| 113 |
+
|
| 114 |
+
st.markdown("")
|
| 115 |
+
|
| 116 |
+
col1, col2, col3 = st.columns([2, 2, 2])
|
| 117 |
+
|
| 118 |
+
with col1:
|
| 119 |
+
if st.button("➕ Adicionar Item", use_container_width=True):
|
| 120 |
+
add_item()
|
| 121 |
+
st.rerun()
|
| 122 |
+
|
| 123 |
+
with col3:
|
| 124 |
+
if st.button("💾 Salvar Checklist", type="primary", use_container_width=True):
|
| 125 |
+
if save_checklist(checklist_name, current_items, numero_processo):
|
| 126 |
+
st.success("Checklist criado com sucesso!")
|
| 127 |
+
st.balloons()
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
main()
|
pages/dashboard.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Página do dashboard para visualizar e interagir com o checklist"""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from utils.database import get_checklist_with_items, toggle_item as toggle_item_db
|
| 5 |
+
|
| 6 |
+
st.set_page_config(
|
| 7 |
+
page_title="Dashboard do Checklist",
|
| 8 |
+
page_icon="✅",
|
| 9 |
+
layout="centered"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
def get_progress(items):
|
| 13 |
+
if not items:
|
| 14 |
+
return 0
|
| 15 |
+
checked_count = sum(1 for item in items if item['is_checked'])
|
| 16 |
+
return (checked_count / len(items)) * 100
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
if 'current_checklist_id' not in st.session_state or st.session_state.current_checklist_id is None:
|
| 20 |
+
st.error("Nenhum checklist selecionado!")
|
| 21 |
+
if st.button("← Voltar para Início"):
|
| 22 |
+
st.switch_page("app.py")
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
+
checklist_id = st.session_state.current_checklist_id
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
# Buscar checklist do banco
|
| 29 |
+
checklist = get_checklist_with_items(checklist_id)
|
| 30 |
+
|
| 31 |
+
if not checklist:
|
| 32 |
+
st.error("Checklist não encontrado!")
|
| 33 |
+
if st.button("← Voltar para Início"):
|
| 34 |
+
st.switch_page("app.py")
|
| 35 |
+
return
|
| 36 |
+
|
| 37 |
+
col1, col2, col3 = st.columns([4, 1, 1])
|
| 38 |
+
with col1:
|
| 39 |
+
st.title(f"📋 {checklist['name']}")
|
| 40 |
+
if checklist['numero_processo']:
|
| 41 |
+
st.caption(f"🔢 Processo: {checklist['numero_processo']}")
|
| 42 |
+
with col2:
|
| 43 |
+
if st.button("📊 Análises"):
|
| 44 |
+
st.switch_page("pages/analytics.py")
|
| 45 |
+
with col3:
|
| 46 |
+
if st.button("← Voltar"):
|
| 47 |
+
st.switch_page("app.py")
|
| 48 |
+
|
| 49 |
+
st.markdown("---")
|
| 50 |
+
|
| 51 |
+
progress = get_progress(checklist['items'])
|
| 52 |
+
|
| 53 |
+
col1, col2 = st.columns([3, 1])
|
| 54 |
+
with col1:
|
| 55 |
+
st.progress(progress / 100)
|
| 56 |
+
with col2:
|
| 57 |
+
st.metric("Progresso", f"{progress:.0f}%")
|
| 58 |
+
|
| 59 |
+
st.markdown("### Items do Checklist")
|
| 60 |
+
|
| 61 |
+
if not checklist['items']:
|
| 62 |
+
st.info("Este checklist não possui items.")
|
| 63 |
+
else:
|
| 64 |
+
for item in checklist['items']:
|
| 65 |
+
col1, col2 = st.columns([10, 1])
|
| 66 |
+
|
| 67 |
+
with col1:
|
| 68 |
+
current_state = item['is_checked']
|
| 69 |
+
new_state = st.checkbox(
|
| 70 |
+
item['text'],
|
| 71 |
+
value=current_state,
|
| 72 |
+
key=f"check_{item['id']}"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Se o estado mudou, atualizar no banco
|
| 76 |
+
if new_state != current_state:
|
| 77 |
+
try:
|
| 78 |
+
toggle_item_db(item['id'], checklist_id, new_state)
|
| 79 |
+
st.rerun()
|
| 80 |
+
except Exception as e:
|
| 81 |
+
st.error(f"Erro ao atualizar item: {str(e)}")
|
| 82 |
+
|
| 83 |
+
items_total = len(checklist['items'])
|
| 84 |
+
items_checked = sum(1 for item in checklist['items'] if item['is_checked'])
|
| 85 |
+
items_remaining = items_total - items_checked
|
| 86 |
+
|
| 87 |
+
st.markdown("---")
|
| 88 |
+
|
| 89 |
+
col1, col2, col3 = st.columns(3)
|
| 90 |
+
|
| 91 |
+
with col1:
|
| 92 |
+
st.metric("Total de Items", items_total)
|
| 93 |
+
with col2:
|
| 94 |
+
st.metric("Concluídos", items_checked, delta=f"{items_checked}/{items_total}")
|
| 95 |
+
with col3:
|
| 96 |
+
st.metric("Restantes", items_remaining)
|
| 97 |
+
|
| 98 |
+
if progress == 100:
|
| 99 |
+
st.success("🎉 Parabéns! Você completou todo o checklist!")
|
| 100 |
+
st.balloons()
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
st.error(f"Erro ao carregar checklist: {str(e)}")
|
| 104 |
+
if st.button("← Voltar para Início"):
|
| 105 |
+
st.switch_page("app.py")
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|
pages/dashboard_geral.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dashboard Geral - Análise de todos os checklists"""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
import plotly.graph_objects as go
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from utils.database import (
|
| 9 |
+
get_all_checklists_with_stats,
|
| 10 |
+
get_general_stats,
|
| 11 |
+
get_checklist_interactions_summary,
|
| 12 |
+
get_process_summary,
|
| 13 |
+
get_fastest_processes,
|
| 14 |
+
get_process_deadline_analysis,
|
| 15 |
+
test_connection
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
st.set_page_config(
|
| 19 |
+
page_title="Dashboard Geral - Checklists",
|
| 20 |
+
page_icon="📊",
|
| 21 |
+
layout="wide"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
st.title("📊 Painel Geral - Análise de Todos os Checklists")
|
| 25 |
+
|
| 26 |
+
# Função para carregar dados
|
| 27 |
+
@st.cache_data(ttl=300) # Cache por 5 minutos
|
| 28 |
+
def carregar_dados():
|
| 29 |
+
"""Carrega todos os dados necessários"""
|
| 30 |
+
try:
|
| 31 |
+
if not test_connection():
|
| 32 |
+
return None, None, None, None, None, None
|
| 33 |
+
|
| 34 |
+
checklists = get_all_checklists_with_stats()
|
| 35 |
+
stats = get_general_stats()
|
| 36 |
+
interactions = get_checklist_interactions_summary()
|
| 37 |
+
processes = get_process_summary()
|
| 38 |
+
fastest_processes = get_fastest_processes()
|
| 39 |
+
deadline_analysis = get_process_deadline_analysis()
|
| 40 |
+
|
| 41 |
+
return checklists, stats, interactions, processes, fastest_processes, deadline_analysis
|
| 42 |
+
except Exception as e:
|
| 43 |
+
st.error(f"❌ Erro ao carregar dados: {str(e)}")
|
| 44 |
+
return None, None, None, None, None, None
|
| 45 |
+
|
| 46 |
+
# Botão para atualizar dados
|
| 47 |
+
col1, col2 = st.columns([6, 1])
|
| 48 |
+
with col2:
|
| 49 |
+
if st.button("🔄 Atualizar Dados"):
|
| 50 |
+
st.cache_data.clear()
|
| 51 |
+
st.rerun()
|
| 52 |
+
|
| 53 |
+
with col1:
|
| 54 |
+
if st.button("← Voltar para Início"):
|
| 55 |
+
st.switch_page("app.py")
|
| 56 |
+
|
| 57 |
+
# Carrega os dados
|
| 58 |
+
resultado = carregar_dados()
|
| 59 |
+
if resultado is None or len(resultado) != 6:
|
| 60 |
+
st.error("❌ Erro ao conectar com o banco de dados ou dados não disponíveis!")
|
| 61 |
+
st.stop()
|
| 62 |
+
|
| 63 |
+
checklists, stats, interactions, processes, fastest_processes, deadline_analysis = resultado
|
| 64 |
+
|
| 65 |
+
if not checklists or not stats:
|
| 66 |
+
st.error("❌ Erro ao conectar com o banco de dados ou dados não disponíveis!")
|
| 67 |
+
st.stop()
|
| 68 |
+
|
| 69 |
+
# Converte para DataFrame
|
| 70 |
+
df_checklists = pd.DataFrame(checklists)
|
| 71 |
+
df_interactions = pd.DataFrame(interactions) if interactions else pd.DataFrame()
|
| 72 |
+
df_processes = pd.DataFrame(processes) if processes else pd.DataFrame()
|
| 73 |
+
df_fastest = pd.DataFrame(fastest_processes) if fastest_processes else pd.DataFrame()
|
| 74 |
+
df_deadlines = pd.DataFrame(deadline_analysis) if deadline_analysis else pd.DataFrame()
|
| 75 |
+
|
| 76 |
+
# Converter colunas numéricas para tipos corretos
|
| 77 |
+
if not df_checklists.empty:
|
| 78 |
+
df_checklists['progress_percentage'] = pd.to_numeric(df_checklists['progress_percentage'], errors='coerce').fillna(0)
|
| 79 |
+
df_checklists['total_items'] = pd.to_numeric(df_checklists['total_items'], errors='coerce').fillna(0)
|
| 80 |
+
df_checklists['completed_items'] = pd.to_numeric(df_checklists['completed_items'], errors='coerce').fillna(0)
|
| 81 |
+
|
| 82 |
+
if not df_interactions.empty:
|
| 83 |
+
df_interactions['total_interactions'] = pd.to_numeric(df_interactions['total_interactions'], errors='coerce').fillna(0)
|
| 84 |
+
|
| 85 |
+
if not df_processes.empty:
|
| 86 |
+
df_processes['avg_progress'] = pd.to_numeric(df_processes['avg_progress'], errors='coerce').fillna(0)
|
| 87 |
+
df_processes['total_checklists'] = pd.to_numeric(df_processes['total_checklists'], errors='coerce').fillna(0)
|
| 88 |
+
df_processes['total_items'] = pd.to_numeric(df_processes['total_items'], errors='coerce').fillna(0)
|
| 89 |
+
df_processes['completed_items'] = pd.to_numeric(df_processes['completed_items'], errors='coerce').fillna(0)
|
| 90 |
+
|
| 91 |
+
if not df_fastest.empty:
|
| 92 |
+
df_fastest['avg_progress'] = pd.to_numeric(df_fastest['avg_progress'], errors='coerce').fillna(0)
|
| 93 |
+
df_fastest['total_checklists'] = pd.to_numeric(df_fastest['total_checklists'], errors='coerce').fillna(0)
|
| 94 |
+
df_fastest['avg_completion_hours'] = pd.to_numeric(df_fastest['avg_completion_hours'], errors='coerce').fillna(0)
|
| 95 |
+
df_fastest['avg_start_hours'] = pd.to_numeric(df_fastest['avg_start_hours'], errors='coerce').fillna(0)
|
| 96 |
+
df_fastest['total_avg_time_hours'] = pd.to_numeric(df_fastest['total_avg_time_hours'], errors='coerce').fillna(0)
|
| 97 |
+
|
| 98 |
+
if not df_deadlines.empty:
|
| 99 |
+
df_deadlines['avg_progress'] = pd.to_numeric(df_deadlines['avg_progress'], errors='coerce').fillna(0)
|
| 100 |
+
df_deadlines['days_elapsed'] = pd.to_numeric(df_deadlines['days_elapsed'], errors='coerce').fillna(0)
|
| 101 |
+
df_deadlines['days_remaining'] = pd.to_numeric(df_deadlines['days_remaining'], errors='coerce').fillna(0)
|
| 102 |
+
df_deadlines['projected_days_to_complete'] = pd.to_numeric(df_deadlines['projected_days_to_complete'], errors='coerce').fillna(0)
|
| 103 |
+
|
| 104 |
+
if df_checklists.empty:
|
| 105 |
+
st.warning("📭 Nenhum checklist encontrado no banco de dados.")
|
| 106 |
+
st.info("💡 Clique em 'Novo Checklist' na página inicial para criar o primeiro checklist.")
|
| 107 |
+
else:
|
| 108 |
+
# =================== MÉTRICA DE PRAZO IDEAL ===================
|
| 109 |
+
st.markdown("---")
|
| 110 |
+
st.info("⏰ **Tempo Médio Ideal de Execução do Processo: 6 meses (180 dias)**")
|
| 111 |
+
|
| 112 |
+
# Estatísticas de prazo
|
| 113 |
+
if not df_deadlines.empty:
|
| 114 |
+
status_counts = df_deadlines['status_prazo'].value_counts()
|
| 115 |
+
|
| 116 |
+
col1, col2, col3, col4, col5 = st.columns(5)
|
| 117 |
+
|
| 118 |
+
with col1:
|
| 119 |
+
concluidos = status_counts.get('CONCLUIDO', 0)
|
| 120 |
+
st.metric("✅ Concluídos", concluidos)
|
| 121 |
+
|
| 122 |
+
with col2:
|
| 123 |
+
no_prazo = status_counts.get('NO_PRAZO', 0)
|
| 124 |
+
st.metric("🟢 No Prazo", no_prazo)
|
| 125 |
+
|
| 126 |
+
with col3:
|
| 127 |
+
em_risco = status_counts.get('EM_RISCO', 0)
|
| 128 |
+
st.metric("🟡 Em Risco", em_risco)
|
| 129 |
+
|
| 130 |
+
with col4:
|
| 131 |
+
risco_atraso = status_counts.get('RISCO_ATRASO', 0)
|
| 132 |
+
st.metric("🟠 Alto Risco", risco_atraso)
|
| 133 |
+
|
| 134 |
+
with col5:
|
| 135 |
+
atrasados = status_counts.get('ATRASADO', 0)
|
| 136 |
+
st.metric("🔴 Atrasados", atrasados)
|
| 137 |
+
# =================== MÉTRICAS GERAIS ===================
|
| 138 |
+
st.markdown("---")
|
| 139 |
+
st.subheader("📈 Métricas Gerais")
|
| 140 |
+
|
| 141 |
+
col1, col2, col3, col4, col5 = st.columns(5)
|
| 142 |
+
|
| 143 |
+
with col1:
|
| 144 |
+
st.metric("🗂️ Total de Checklists", stats['total_checklists'])
|
| 145 |
+
|
| 146 |
+
with col2:
|
| 147 |
+
st.metric("🔢 Processos Únicos", stats['total_processos'] or 0)
|
| 148 |
+
|
| 149 |
+
with col3:
|
| 150 |
+
completed_checklists = stats['completed_checklists'] or 0
|
| 151 |
+
completion_rate = (completed_checklists / stats['total_checklists']) * 100 if stats['total_checklists'] > 0 else 0
|
| 152 |
+
st.metric("✅ Checklists Concluídos", f"{completed_checklists} ({completion_rate:.1f}%)")
|
| 153 |
+
|
| 154 |
+
with col4:
|
| 155 |
+
total_items = stats['total_items'] or 0
|
| 156 |
+
completed_items = stats['completed_items'] or 0
|
| 157 |
+
items_completion = (completed_items / total_items) * 100 if total_items > 0 else 0
|
| 158 |
+
st.metric("📋 Items Concluídos", f"{completed_items}/{total_items} ({items_completion:.1f}%)")
|
| 159 |
+
|
| 160 |
+
with col5:
|
| 161 |
+
avg_progress = stats['avg_progress'] or 0
|
| 162 |
+
st.metric("📊 Progresso Médio", f"{avg_progress:.1f}%")
|
| 163 |
+
|
| 164 |
+
# =================== GRÁFICOS - LINHA 1 ===================
|
| 165 |
+
st.markdown("---")
|
| 166 |
+
st.subheader("📊 Análise por Status e Processo")
|
| 167 |
+
|
| 168 |
+
col1, col2 = st.columns(2)
|
| 169 |
+
|
| 170 |
+
with col1:
|
| 171 |
+
# Gráfico de progresso dos checklists
|
| 172 |
+
st.markdown("#### 📈 Distribuição de Progresso")
|
| 173 |
+
|
| 174 |
+
# Criar faixas de progresso
|
| 175 |
+
df_checklists['faixa_progresso'] = pd.cut(
|
| 176 |
+
df_checklists['progress_percentage'],
|
| 177 |
+
bins=[0, 25, 50, 75, 100],
|
| 178 |
+
labels=['0-25%', '26-50%', '51-75%', '76-100%'],
|
| 179 |
+
include_lowest=True
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
progress_counts = df_checklists['faixa_progresso'].value_counts().sort_index()
|
| 183 |
+
|
| 184 |
+
fig_progress = px.pie(
|
| 185 |
+
values=progress_counts.values,
|
| 186 |
+
names=progress_counts.index,
|
| 187 |
+
title="Distribuição de Checklists por Faixa de Progresso",
|
| 188 |
+
color_discrete_sequence=['#ff6b6b', '#feca57', '#48dbfb', '#1dd1a1']
|
| 189 |
+
)
|
| 190 |
+
st.plotly_chart(fig_progress, use_container_width=True)
|
| 191 |
+
|
| 192 |
+
with col2:
|
| 193 |
+
# Gráfico de processos mais rápidos
|
| 194 |
+
st.markdown("#### ⚡ Top 10 Processos Mais Rápidos")
|
| 195 |
+
|
| 196 |
+
if not df_fastest.empty:
|
| 197 |
+
# Função para formatar tempo em horas
|
| 198 |
+
def format_hours(hours):
|
| 199 |
+
if pd.isna(hours) or hours == 0:
|
| 200 |
+
return "0h"
|
| 201 |
+
if hours < 1:
|
| 202 |
+
minutes = int(hours * 60)
|
| 203 |
+
return f"{minutes}min"
|
| 204 |
+
elif hours < 24:
|
| 205 |
+
return f"{hours:.1f}h"
|
| 206 |
+
else:
|
| 207 |
+
days = int(hours // 24)
|
| 208 |
+
remaining_hours = hours % 24
|
| 209 |
+
return f"{days}d {remaining_hours:.1f}h"
|
| 210 |
+
|
| 211 |
+
# Preparar dados para o gráfico
|
| 212 |
+
df_fastest_display = df_fastest.head(10).copy()
|
| 213 |
+
df_fastest_display['tempo_formatado'] = df_fastest_display['total_avg_time_hours'].apply(format_hours)
|
| 214 |
+
|
| 215 |
+
fig_fastest = px.bar(
|
| 216 |
+
df_fastest_display,
|
| 217 |
+
x='numero_processo',
|
| 218 |
+
y='total_avg_time_hours',
|
| 219 |
+
title="Processos Mais Rápidos (Menor Tempo Médio)",
|
| 220 |
+
color='avg_progress',
|
| 221 |
+
color_continuous_scale="RdYlGn",
|
| 222 |
+
hover_data=['tempo_formatado', 'total_checklists']
|
| 223 |
+
)
|
| 224 |
+
fig_fastest.update_layout(
|
| 225 |
+
xaxis_tickangle=45,
|
| 226 |
+
yaxis_title="Tempo Médio (horas)"
|
| 227 |
+
)
|
| 228 |
+
st.plotly_chart(fig_fastest, use_container_width=True)
|
| 229 |
+
else:
|
| 230 |
+
st.info("Dados de tempo não disponíveis ainda. Execute alguns checklists para gerar métricas de velocidade.")
|
| 231 |
+
|
| 232 |
+
# =================== ANÁLISE DE PRAZOS ===================
|
| 233 |
+
if not df_deadlines.empty:
|
| 234 |
+
st.markdown("---")
|
| 235 |
+
st.subheader("🚨 Análise de Cumprimento de Prazos")
|
| 236 |
+
|
| 237 |
+
col1, col2 = st.columns(2)
|
| 238 |
+
|
| 239 |
+
with col1:
|
| 240 |
+
# Gráfico de status de prazos
|
| 241 |
+
st.markdown("#### 📊 Distribuição por Status de Prazo")
|
| 242 |
+
|
| 243 |
+
status_counts = df_deadlines['status_prazo'].value_counts()
|
| 244 |
+
|
| 245 |
+
# Mapeamento de cores por status
|
| 246 |
+
color_map = {
|
| 247 |
+
'CONCLUIDO': '#1dd1a1', # Verde
|
| 248 |
+
'NO_PRAZO': '#00d2d3', # Azul claro
|
| 249 |
+
'EM_RISCO': '#feca57', # Amarelo
|
| 250 |
+
'RISCO_ATRASO': '#ff9ff3', # Laranja
|
| 251 |
+
'ATRASADO': '#ff6b6b', # Vermelho
|
| 252 |
+
'SEM_DADOS': '#c7ecee' # Cinza
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
colors = [color_map.get(status, '#c7ecee') for status in status_counts.index]
|
| 256 |
+
|
| 257 |
+
fig_status = px.pie(
|
| 258 |
+
values=status_counts.values,
|
| 259 |
+
names=status_counts.index,
|
| 260 |
+
title="Status dos Processos em Relação ao Prazo de 6 Meses",
|
| 261 |
+
color_discrete_sequence=colors
|
| 262 |
+
)
|
| 263 |
+
st.plotly_chart(fig_status, use_container_width=True)
|
| 264 |
+
|
| 265 |
+
with col2:
|
| 266 |
+
# Gráfico de processos que não vão finalizar no prazo
|
| 267 |
+
st.markdown("#### 🚨 Processos em Risco ou Atrasados")
|
| 268 |
+
|
| 269 |
+
processos_problema = df_deadlines[df_deadlines['status_prazo'].isin(['RISCO_ATRASO', 'ATRASADO', 'EM_RISCO'])]
|
| 270 |
+
|
| 271 |
+
if not processos_problema.empty:
|
| 272 |
+
# Calcular dias totais previstos
|
| 273 |
+
processos_problema = processos_problema.copy()
|
| 274 |
+
processos_problema['dias_totais_previstos'] = processos_problema['days_elapsed'] + processos_problema['projected_days_to_complete']
|
| 275 |
+
|
| 276 |
+
fig_problema = px.bar(
|
| 277 |
+
processos_problema.head(10),
|
| 278 |
+
x='numero_processo',
|
| 279 |
+
y='dias_totais_previstos',
|
| 280 |
+
color='status_prazo',
|
| 281 |
+
title="Projeção de Dias Totais vs Prazo (180 dias)",
|
| 282 |
+
color_discrete_map={
|
| 283 |
+
'EM_RISCO': '#feca57',
|
| 284 |
+
'RISCO_ATRASO': '#ff9ff3',
|
| 285 |
+
'ATRASADO': '#ff6b6b'
|
| 286 |
+
},
|
| 287 |
+
hover_data=['avg_progress', 'days_remaining']
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# Adicionar linha de referência do prazo
|
| 291 |
+
fig_problema.add_hline(y=180, line_dash="dash", line_color="red",
|
| 292 |
+
annotation_text="Prazo Limite (180 dias)")
|
| 293 |
+
|
| 294 |
+
fig_problema.update_layout(xaxis_tickangle=45)
|
| 295 |
+
st.plotly_chart(fig_problema, use_container_width=True)
|
| 296 |
+
else:
|
| 297 |
+
st.success("🎉 Todos os processos estão dentro do prazo!")
|
| 298 |
+
|
| 299 |
+
# =================== GRÁFICOS - LINHA 2 ===================
|
| 300 |
+
st.markdown("---")
|
| 301 |
+
st.subheader("📅 Análise Temporal")
|
| 302 |
+
|
| 303 |
+
col1, col2 = st.columns(2)
|
| 304 |
+
|
| 305 |
+
with col1:
|
| 306 |
+
# Checklists criados ao longo do tempo
|
| 307 |
+
st.markdown("#### 📅 Criação de Checklists ao Longo do Tempo")
|
| 308 |
+
|
| 309 |
+
df_checklists['data_criacao'] = pd.to_datetime(df_checklists['created_at']).dt.date
|
| 310 |
+
checklists_diarios = df_checklists.groupby('data_criacao').size().reset_index(name='quantidade')
|
| 311 |
+
|
| 312 |
+
fig_temporal = px.line(
|
| 313 |
+
checklists_diarios,
|
| 314 |
+
x='data_criacao',
|
| 315 |
+
y='quantidade',
|
| 316 |
+
title="Checklists Criados por Dia",
|
| 317 |
+
markers=True
|
| 318 |
+
)
|
| 319 |
+
st.plotly_chart(fig_temporal, use_container_width=True)
|
| 320 |
+
|
| 321 |
+
with col2:
|
| 322 |
+
# Progresso médio por data de criação
|
| 323 |
+
st.markdown("#### 📊 Progresso Médio por Período")
|
| 324 |
+
|
| 325 |
+
progress_temporal = df_checklists.groupby('data_criacao')['progress_percentage'].mean().reset_index()
|
| 326 |
+
|
| 327 |
+
fig_progress_temporal = px.bar(
|
| 328 |
+
progress_temporal,
|
| 329 |
+
x='data_criacao',
|
| 330 |
+
y='progress_percentage',
|
| 331 |
+
title="Progresso Médio dos Checklists por Data de Criação",
|
| 332 |
+
color='progress_percentage',
|
| 333 |
+
color_continuous_scale="RdYlGn"
|
| 334 |
+
)
|
| 335 |
+
st.plotly_chart(fig_progress_temporal, use_container_width=True)
|
| 336 |
+
|
| 337 |
+
# =================== ANÁLISE DE INTERAÇÕES ===================
|
| 338 |
+
if not df_interactions.empty:
|
| 339 |
+
st.markdown("---")
|
| 340 |
+
st.subheader("🔄 Análise de Atividade")
|
| 341 |
+
|
| 342 |
+
col1, col2 = st.columns(2)
|
| 343 |
+
|
| 344 |
+
with col1:
|
| 345 |
+
# Processos mais ativos
|
| 346 |
+
st.markdown("#### 🔥 Processos Mais Ativos")
|
| 347 |
+
|
| 348 |
+
if not df_interactions.empty:
|
| 349 |
+
# Agrupar por processo e somar interações
|
| 350 |
+
process_activity = df_interactions.groupby('numero_processo').agg({
|
| 351 |
+
'total_interactions': 'sum',
|
| 352 |
+
'name': 'count' # contando quantos checklists por processo
|
| 353 |
+
}).reset_index()
|
| 354 |
+
process_activity.columns = ['numero_processo', 'total_interactions', 'total_checklists']
|
| 355 |
+
process_activity = process_activity.sort_values('total_interactions', ascending=False).head(10)
|
| 356 |
+
|
| 357 |
+
fig_active = px.bar(
|
| 358 |
+
process_activity,
|
| 359 |
+
x='total_interactions',
|
| 360 |
+
y='numero_processo',
|
| 361 |
+
orientation='h',
|
| 362 |
+
title="Top 10 Processos por Número de Interações",
|
| 363 |
+
color='total_interactions',
|
| 364 |
+
color_continuous_scale="Blues",
|
| 365 |
+
hover_data=['total_checklists']
|
| 366 |
+
)
|
| 367 |
+
st.plotly_chart(fig_active, use_container_width=True)
|
| 368 |
+
else:
|
| 369 |
+
st.info("Nenhum processo com interações disponível.")
|
| 370 |
+
|
| 371 |
+
with col2:
|
| 372 |
+
# Atividade ao longo do tempo
|
| 373 |
+
st.markdown("#### ⏰ Atividade ao Longo do Tempo")
|
| 374 |
+
|
| 375 |
+
df_interactions['data_primeira_interacao'] = pd.to_datetime(df_interactions['first_interaction']).dt.date
|
| 376 |
+
atividade_diaria = df_interactions.groupby('data_primeira_interacao').size().reset_index(name='checklists_iniciados')
|
| 377 |
+
|
| 378 |
+
fig_atividade = px.line(
|
| 379 |
+
atividade_diaria,
|
| 380 |
+
x='data_primeira_interacao',
|
| 381 |
+
y='checklists_iniciados',
|
| 382 |
+
title="Checklists com Primeira Interação por Dia",
|
| 383 |
+
markers=True
|
| 384 |
+
)
|
| 385 |
+
st.plotly_chart(fig_atividade, use_container_width=True)
|
| 386 |
+
|
| 387 |
+
# =================== RANKING DE PROCESSOS ===================
|
| 388 |
+
if not df_processes.empty:
|
| 389 |
+
st.markdown("---")
|
| 390 |
+
st.subheader("🏆 Ranking de Processos")
|
| 391 |
+
|
| 392 |
+
col1, col2 = st.columns(2)
|
| 393 |
+
|
| 394 |
+
with col1:
|
| 395 |
+
# Processos com maior progresso
|
| 396 |
+
st.markdown("#### 🥇 Processos com Maior Progresso")
|
| 397 |
+
|
| 398 |
+
top_progress = df_processes.sort_values('avg_progress', ascending=False).head(5)
|
| 399 |
+
|
| 400 |
+
for i, (idx, row) in enumerate(top_progress.iterrows()):
|
| 401 |
+
progress = row['avg_progress'] or 0
|
| 402 |
+
medal = "🥇" if i == 0 else "🥈" if i == 1 else "🥉" if i == 2 else "🏅"
|
| 403 |
+
st.write(f"{medal} **{row['numero_processo']}** - {progress:.1f}% ({row['total_checklists']} checklists)")
|
| 404 |
+
|
| 405 |
+
with col2:
|
| 406 |
+
# Processos com mais checklists
|
| 407 |
+
st.markdown("#### 📊 Processos com Mais Checklists")
|
| 408 |
+
|
| 409 |
+
top_volume = df_processes.sort_values('total_checklists', ascending=False).head(5)
|
| 410 |
+
|
| 411 |
+
for i, (idx, row) in enumerate(top_volume.iterrows()):
|
| 412 |
+
progress = row['avg_progress'] or 0
|
| 413 |
+
medal = "🥇" if i == 0 else "🥈" if i == 1 else "🥉" if i == 2 else "🏅"
|
| 414 |
+
st.write(f"{medal} **{row['numero_processo']}** - {row['total_checklists']} checklists ({progress:.1f}% progresso)")
|
| 415 |
+
|
| 416 |
+
# =================== TABELA DETALHADA ===================
|
| 417 |
+
st.markdown("---")
|
| 418 |
+
st.subheader("📋 Dados Detalhados")
|
| 419 |
+
|
| 420 |
+
# Filtros
|
| 421 |
+
col1, col2, col3 = st.columns(3)
|
| 422 |
+
|
| 423 |
+
with col1:
|
| 424 |
+
if not df_processes.empty:
|
| 425 |
+
processos_disponiveis = ['Todos'] + df_processes['numero_processo'].tolist()
|
| 426 |
+
processo_selecionado = st.selectbox(
|
| 427 |
+
"Filtrar por Processo:",
|
| 428 |
+
options=processos_disponiveis,
|
| 429 |
+
index=0
|
| 430 |
+
)
|
| 431 |
+
else:
|
| 432 |
+
processo_selecionado = 'Todos'
|
| 433 |
+
|
| 434 |
+
with col2:
|
| 435 |
+
faixa_progresso_selecionada = st.selectbox(
|
| 436 |
+
"Filtrar por Progresso:",
|
| 437 |
+
options=['Todos', '0-25%', '26-50%', '51-75%', '76-100%'],
|
| 438 |
+
index=0
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
with col3:
|
| 442 |
+
# Ordenação
|
| 443 |
+
ordenacao = st.selectbox(
|
| 444 |
+
"Ordenar por:",
|
| 445 |
+
options=['Data de Criação', 'Progresso', 'Nome', 'Número de Items'],
|
| 446 |
+
index=0
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
# Aplicar filtros
|
| 450 |
+
df_filtrado = df_checklists.copy()
|
| 451 |
+
|
| 452 |
+
if processo_selecionado != 'Todos':
|
| 453 |
+
df_filtrado = df_filtrado[df_filtrado['numero_processo'] == processo_selecionado]
|
| 454 |
+
|
| 455 |
+
if faixa_progresso_selecionada != 'Todos':
|
| 456 |
+
df_filtrado = df_filtrado[df_filtrado['faixa_progresso'] == faixa_progresso_selecionada]
|
| 457 |
+
|
| 458 |
+
# Aplicar ordenação
|
| 459 |
+
if ordenacao == 'Data de Criação':
|
| 460 |
+
df_filtrado = df_filtrado.sort_values('created_at', ascending=False)
|
| 461 |
+
elif ordenacao == 'Progresso':
|
| 462 |
+
df_filtrado = df_filtrado.sort_values('progress_percentage', ascending=False)
|
| 463 |
+
elif ordenacao == 'Nome':
|
| 464 |
+
df_filtrado = df_filtrado.sort_values('name')
|
| 465 |
+
elif ordenacao == 'Número de Items':
|
| 466 |
+
df_filtrado = df_filtrado.sort_values('total_items', ascending=False)
|
| 467 |
+
|
| 468 |
+
# Mostrar tabela filtrada
|
| 469 |
+
if not df_filtrado.empty:
|
| 470 |
+
# Preparar dados para exibição
|
| 471 |
+
df_display = df_filtrado[['name', 'numero_processo', 'total_items', 'completed_items', 'progress_percentage', 'created_at']].copy()
|
| 472 |
+
df_display.columns = ['Nome', 'Processo', 'Total Items', 'Items Concluídos', 'Progresso (%)', 'Data Criação']
|
| 473 |
+
df_display['Data Criação'] = pd.to_datetime(df_display['Data Criação']).dt.strftime('%d/%m/%Y')
|
| 474 |
+
|
| 475 |
+
st.dataframe(df_display, use_container_width=True)
|
| 476 |
+
|
| 477 |
+
# Estatísticas dos dados filtrados
|
| 478 |
+
st.markdown("#### 📊 Estatísticas dos Dados Filtrados")
|
| 479 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 480 |
+
|
| 481 |
+
with col1:
|
| 482 |
+
st.metric("Checklists Filtrados", len(df_filtrado))
|
| 483 |
+
|
| 484 |
+
with col2:
|
| 485 |
+
progresso_medio_filtrado = df_filtrado['progress_percentage'].mean()
|
| 486 |
+
st.metric("Progresso Médio", f"{progresso_medio_filtrado:.1f}%")
|
| 487 |
+
|
| 488 |
+
with col3:
|
| 489 |
+
checklists_completos_filtrados = len(df_filtrado[df_filtrado['progress_percentage'] == 100])
|
| 490 |
+
st.metric("Checklists Completos", checklists_completos_filtrados)
|
| 491 |
+
|
| 492 |
+
with col4:
|
| 493 |
+
total_items_filtrados = df_filtrado['total_items'].sum()
|
| 494 |
+
st.metric("Total de Items", total_items_filtrados)
|
| 495 |
+
else:
|
| 496 |
+
st.info("🔍 Nenhum checklist encontrado com os filtros aplicados.")
|
| 497 |
+
|
| 498 |
+
# Rodapé
|
| 499 |
+
st.markdown("---")
|
| 500 |
+
st.markdown("📊 **Dashboard Geral de Checklists** | Atualizado automaticamente a cada 5 minutos")
|
| 501 |
+
st.markdown("💡 *Dica: Use os filtros para análises mais específicas por processo ou faixa de progresso*")
|
pages/relatorio_ia.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Página de Relatórios com IA"""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import os
|
| 7 |
+
from io import BytesIO
|
| 8 |
+
import plotly.express as px
|
| 9 |
+
import plotly.graph_objects as go
|
| 10 |
+
|
| 11 |
+
# Tentar importar dependências da IA
|
| 12 |
+
try:
|
| 13 |
+
import openai
|
| 14 |
+
from reportlab.lib.pagesizes import letter, A4
|
| 15 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
| 16 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 17 |
+
from reportlab.lib.units import inch
|
| 18 |
+
from dotenv import load_dotenv
|
| 19 |
+
AI_AVAILABLE = True
|
| 20 |
+
except ImportError:
|
| 21 |
+
AI_AVAILABLE = False
|
| 22 |
+
|
| 23 |
+
from utils.database import get_comprehensive_analysis_data
|
| 24 |
+
|
| 25 |
+
def load_prompts():
|
| 26 |
+
"""Carrega os prompts dos arquivos"""
|
| 27 |
+
prompts = {}
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
with open('prompt_analise_checklists.txt', 'r', encoding='utf-8') as f:
|
| 31 |
+
prompts['analise'] = f.read()
|
| 32 |
+
except FileNotFoundError:
|
| 33 |
+
prompts['analise'] = "Analise os dados de checklists fornecidos e forneça insights sobre eficiência dos processos."
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
with open('prompt_relatorio_executivo.txt', 'r', encoding='utf-8') as f:
|
| 37 |
+
prompts['executivo'] = f.read()
|
| 38 |
+
except FileNotFoundError:
|
| 39 |
+
prompts['executivo'] = "Crie um relatório executivo baseado nos dados fornecidos."
|
| 40 |
+
|
| 41 |
+
return prompts
|
| 42 |
+
|
| 43 |
+
def setup_openai():
|
| 44 |
+
"""Configura a API da OpenAI"""
|
| 45 |
+
# Tentar carregar .env se existir (desenvolvimento local)
|
| 46 |
+
try:
|
| 47 |
+
load_dotenv()
|
| 48 |
+
except:
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
# Priorizar variáveis de ambiente do sistema (Hugging Face Spaces)
|
| 52 |
+
api_key = os.getenv('OPENAI_API_KEY')
|
| 53 |
+
|
| 54 |
+
if not api_key:
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
openai.api_key = api_key
|
| 58 |
+
return True
|
| 59 |
+
|
| 60 |
+
def format_data_for_ai(data):
|
| 61 |
+
"""Formata os dados para análise da IA"""
|
| 62 |
+
process_data = data['process_data']
|
| 63 |
+
global_stats = data['global_stats']
|
| 64 |
+
|
| 65 |
+
# Formatação dos dados de processos
|
| 66 |
+
formatted_data = "=== DADOS DOS PROCESSOS ===\n\n"
|
| 67 |
+
|
| 68 |
+
for process in process_data:
|
| 69 |
+
formatted_data += f"Processo: {process['numero_processo']}\n"
|
| 70 |
+
formatted_data += f"- Checklists: {process['total_checklists']}\n"
|
| 71 |
+
formatted_data += f"- Progresso médio: {process['avg_progress_percentage']}%\n"
|
| 72 |
+
formatted_data += f"- Total de itens: {process['total_items']}\n"
|
| 73 |
+
formatted_data += f"- Itens concluídos: {process['completed_items']}\n"
|
| 74 |
+
formatted_data += f"- Data de início: {process['process_start_date']}\n"
|
| 75 |
+
formatted_data += f"- Dias decorridos: {process['days_elapsed']}\n"
|
| 76 |
+
formatted_data += f"- Dias restantes para deadline: {process['days_remaining_to_deadline']}\n"
|
| 77 |
+
formatted_data += f"- Status do prazo: {process['status_prazo']}\n"
|
| 78 |
+
formatted_data += f"- Projeção para conclusão: {process['projected_days_to_complete']} dias\n"
|
| 79 |
+
formatted_data += f"- Total de interações: {process['total_interactions']}\n\n"
|
| 80 |
+
|
| 81 |
+
# Estatísticas globais
|
| 82 |
+
formatted_data += "=== ESTATÍSTICAS GLOBAIS ===\n\n"
|
| 83 |
+
formatted_data += f"Total de checklists: {global_stats['total_checklists_global']}\n"
|
| 84 |
+
formatted_data += f"Total de processos: {global_stats['total_processes']}\n"
|
| 85 |
+
formatted_data += f"Total de itens: {global_stats['total_items_global']}\n"
|
| 86 |
+
formatted_data += f"Itens concluídos: {global_stats['completed_items_global']}\n"
|
| 87 |
+
formatted_data += f"Total de interações: {global_stats['total_interactions_global']}\n"
|
| 88 |
+
formatted_data += f"Primeiro processo: {global_stats['earliest_process']}\n"
|
| 89 |
+
formatted_data += f"Último processo: {global_stats['latest_process']}\n"
|
| 90 |
+
|
| 91 |
+
return formatted_data
|
| 92 |
+
|
| 93 |
+
def generate_ai_analysis(data, prompt_type="analise"):
|
| 94 |
+
"""Gera análise usando OpenAI"""
|
| 95 |
+
if not AI_AVAILABLE:
|
| 96 |
+
return "❌ Bibliotecas de IA não estão instaladas. Execute: pip install openai python-dotenv"
|
| 97 |
+
|
| 98 |
+
if not setup_openai():
|
| 99 |
+
return "❌ Chave da API OpenAI não configurada. Crie um arquivo .env com OPENAI_API_KEY=sua_chave"
|
| 100 |
+
|
| 101 |
+
prompts = load_prompts()
|
| 102 |
+
system_prompt = prompts.get(prompt_type, "Analise os dados fornecidos.")
|
| 103 |
+
formatted_data = format_data_for_ai(data)
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
client = openai.OpenAI()
|
| 107 |
+
response = client.chat.completions.create(
|
| 108 |
+
model="gpt-4",
|
| 109 |
+
messages=[
|
| 110 |
+
{"role": "system", "content": system_prompt},
|
| 111 |
+
{"role": "user", "content": formatted_data}
|
| 112 |
+
],
|
| 113 |
+
temperature=0.7,
|
| 114 |
+
max_tokens=2000
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
return response.choices[0].message.content
|
| 118 |
+
|
| 119 |
+
except Exception as e:
|
| 120 |
+
return f"❌ Erro ao gerar análise: {str(e)}"
|
| 121 |
+
|
| 122 |
+
def create_pdf_report(analysis_text, report_type="Análise de Checklists"):
|
| 123 |
+
"""Cria PDF do relatório"""
|
| 124 |
+
if not AI_AVAILABLE:
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
buffer = BytesIO()
|
| 128 |
+
doc = SimpleDocTemplate(buffer, pagesize=A4)
|
| 129 |
+
styles = getSampleStyleSheet()
|
| 130 |
+
|
| 131 |
+
# Estilo customizado para título
|
| 132 |
+
title_style = ParagraphStyle(
|
| 133 |
+
'CustomTitle',
|
| 134 |
+
parent=styles['Heading1'],
|
| 135 |
+
fontSize=16,
|
| 136 |
+
alignment=1, # Centralizado
|
| 137 |
+
spaceAfter=30,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# Conteúdo do PDF
|
| 141 |
+
story = []
|
| 142 |
+
|
| 143 |
+
# Título
|
| 144 |
+
title = Paragraph(f"{report_type}", title_style)
|
| 145 |
+
story.append(title)
|
| 146 |
+
|
| 147 |
+
# Data
|
| 148 |
+
date_str = datetime.now().strftime("%d/%m/%Y às %H:%M")
|
| 149 |
+
date_para = Paragraph(f"Gerado em: {date_str}", styles['Normal'])
|
| 150 |
+
story.append(date_para)
|
| 151 |
+
story.append(Spacer(1, 20))
|
| 152 |
+
|
| 153 |
+
# Conteúdo da análise
|
| 154 |
+
# Quebrar o texto em parágrafos
|
| 155 |
+
paragraphs = analysis_text.split('\n\n')
|
| 156 |
+
for para in paragraphs:
|
| 157 |
+
if para.strip():
|
| 158 |
+
# Verificar se é um título (começar com ##)
|
| 159 |
+
if para.strip().startswith('##'):
|
| 160 |
+
para_clean = para.replace('##', '').strip()
|
| 161 |
+
p = Paragraph(para_clean, styles['Heading2'])
|
| 162 |
+
elif para.strip().startswith('- '):
|
| 163 |
+
p = Paragraph(para.strip(), styles['Normal'])
|
| 164 |
+
else:
|
| 165 |
+
p = Paragraph(para.strip(), styles['Normal'])
|
| 166 |
+
story.append(p)
|
| 167 |
+
story.append(Spacer(1, 12))
|
| 168 |
+
|
| 169 |
+
# Gerar PDF
|
| 170 |
+
doc.build(story)
|
| 171 |
+
buffer.seek(0)
|
| 172 |
+
|
| 173 |
+
return buffer
|
| 174 |
+
|
| 175 |
+
def show_data_overview(data):
|
| 176 |
+
"""Mostra visão geral dos dados"""
|
| 177 |
+
process_data = data['process_data']
|
| 178 |
+
global_stats = data['global_stats']
|
| 179 |
+
|
| 180 |
+
st.subheader("📊 Visão Geral dos Dados")
|
| 181 |
+
|
| 182 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 183 |
+
|
| 184 |
+
with col1:
|
| 185 |
+
st.metric("Total de Processos", global_stats['total_processes'])
|
| 186 |
+
|
| 187 |
+
with col2:
|
| 188 |
+
st.metric("Total de Checklists", global_stats['total_checklists_global'])
|
| 189 |
+
|
| 190 |
+
with col3:
|
| 191 |
+
st.metric("Total de Itens", global_stats['total_items_global'])
|
| 192 |
+
|
| 193 |
+
with col4:
|
| 194 |
+
if global_stats['total_items_global'] > 0:
|
| 195 |
+
completion_rate = (global_stats['completed_items_global'] / global_stats['total_items_global']) * 100
|
| 196 |
+
st.metric("Taxa de Conclusão", f"{completion_rate:.1f}%")
|
| 197 |
+
else:
|
| 198 |
+
st.metric("Taxa de Conclusão", "0%")
|
| 199 |
+
|
| 200 |
+
# Status dos processos
|
| 201 |
+
if process_data:
|
| 202 |
+
st.subheader("🎯 Status dos Processos por Prazo")
|
| 203 |
+
|
| 204 |
+
status_counts = {}
|
| 205 |
+
for process in process_data:
|
| 206 |
+
status = process['status_prazo']
|
| 207 |
+
status_counts[status] = status_counts.get(status, 0) + 1
|
| 208 |
+
|
| 209 |
+
# Gráfico de pizza para status
|
| 210 |
+
fig = px.pie(
|
| 211 |
+
values=list(status_counts.values()),
|
| 212 |
+
names=list(status_counts.keys()),
|
| 213 |
+
title="Distribuição de Status dos Processos"
|
| 214 |
+
)
|
| 215 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 216 |
+
|
| 217 |
+
# Gráfico de progresso por processo
|
| 218 |
+
st.subheader("📈 Progresso por Processo")
|
| 219 |
+
|
| 220 |
+
df_processes = pd.DataFrame(process_data)
|
| 221 |
+
if not df_processes.empty:
|
| 222 |
+
fig2 = px.bar(
|
| 223 |
+
df_processes,
|
| 224 |
+
x='numero_processo',
|
| 225 |
+
y='avg_progress_percentage',
|
| 226 |
+
title="Progresso Médio por Processo",
|
| 227 |
+
labels={'avg_progress_percentage': 'Progresso (%)', 'numero_processo': 'Número do Processo'}
|
| 228 |
+
)
|
| 229 |
+
fig2.update_layout(xaxis_tickangle=-45)
|
| 230 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 231 |
+
|
| 232 |
+
def main():
|
| 233 |
+
st.title("🤖 Relatórios com Inteligência Artificial")
|
| 234 |
+
st.markdown("---")
|
| 235 |
+
|
| 236 |
+
if not AI_AVAILABLE:
|
| 237 |
+
st.error("⚠️ **Dependências de IA não instaladas**")
|
| 238 |
+
st.markdown("""
|
| 239 |
+
Para usar esta funcionalidade, instale as dependências:
|
| 240 |
+
```bash
|
| 241 |
+
pip install openai reportlab python-dotenv
|
| 242 |
+
```
|
| 243 |
+
""")
|
| 244 |
+
return
|
| 245 |
+
|
| 246 |
+
# Verificar se tem dados
|
| 247 |
+
try:
|
| 248 |
+
data = get_comprehensive_analysis_data()
|
| 249 |
+
if not data['process_data']:
|
| 250 |
+
st.warning("📋 Nenhum processo encontrado. Crie alguns checklists primeiro!")
|
| 251 |
+
return
|
| 252 |
+
except Exception as e:
|
| 253 |
+
st.error(f"❌ Erro ao carregar dados: {str(e)}")
|
| 254 |
+
return
|
| 255 |
+
|
| 256 |
+
# Abas principais
|
| 257 |
+
tab1, tab2, tab3 = st.tabs(["📊 Dados", "🔍 Análise IA", "📋 Relatório Executivo"])
|
| 258 |
+
|
| 259 |
+
with tab1:
|
| 260 |
+
show_data_overview(data)
|
| 261 |
+
|
| 262 |
+
with tab2:
|
| 263 |
+
st.subheader("🔍 Análise Detalhada com IA")
|
| 264 |
+
st.markdown("Esta análise utiliza IA para identificar padrões, gargalos e oportunidades de melhoria.")
|
| 265 |
+
|
| 266 |
+
if st.button("🚀 Gerar Análise com IA", use_container_width=True):
|
| 267 |
+
with st.spinner("🤖 IA analisando os dados..."):
|
| 268 |
+
analysis = generate_ai_analysis(data, "analise")
|
| 269 |
+
|
| 270 |
+
st.markdown("### 📋 Resultado da Análise")
|
| 271 |
+
st.markdown(analysis)
|
| 272 |
+
|
| 273 |
+
# Botão para download PDF
|
| 274 |
+
if "❌" not in analysis:
|
| 275 |
+
pdf_buffer = create_pdf_report(analysis, "Análise Detalhada de Checklists")
|
| 276 |
+
if pdf_buffer:
|
| 277 |
+
st.download_button(
|
| 278 |
+
label="📄 Download PDF da Análise",
|
| 279 |
+
data=pdf_buffer,
|
| 280 |
+
file_name=f"analise_checklists_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
|
| 281 |
+
mime="application/pdf",
|
| 282 |
+
use_container_width=True
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
with tab3:
|
| 286 |
+
st.subheader("📋 Relatório Executivo")
|
| 287 |
+
st.markdown("Relatório estratégico para apresentação à alta gestão.")
|
| 288 |
+
|
| 289 |
+
if st.button("📊 Gerar Relatório Executivo", use_container_width=True):
|
| 290 |
+
with st.spinner("📊 Gerando relatório executivo..."):
|
| 291 |
+
executive_report = generate_ai_analysis(data, "executivo")
|
| 292 |
+
|
| 293 |
+
st.markdown("### 🎯 Relatório Executivo")
|
| 294 |
+
st.markdown(executive_report)
|
| 295 |
+
|
| 296 |
+
# Botão para download PDF
|
| 297 |
+
if "❌" not in executive_report:
|
| 298 |
+
pdf_buffer = create_pdf_report(executive_report, "Relatório Executivo - Checklists de Planejamento")
|
| 299 |
+
if pdf_buffer:
|
| 300 |
+
st.download_button(
|
| 301 |
+
label="📄 Download PDF do Relatório",
|
| 302 |
+
data=pdf_buffer,
|
| 303 |
+
file_name=f"relatorio_executivo_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
|
| 304 |
+
mime="application/pdf",
|
| 305 |
+
use_container_width=True
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Seção de configuração
|
| 309 |
+
st.markdown("---")
|
| 310 |
+
with st.expander("⚙️ Configuração da API"):
|
| 311 |
+
st.markdown("""
|
| 312 |
+
**Para usar a funcionalidade de IA:**
|
| 313 |
+
|
| 314 |
+
1. Obtenha uma chave da API OpenAI em: https://platform.openai.com/api-keys
|
| 315 |
+
2. Crie um arquivo `.env` na pasta do projeto
|
| 316 |
+
3. Adicione a linha: `OPENAI_API_KEY=sua_chave_aqui`
|
| 317 |
+
|
| 318 |
+
**Arquivo .env exemplo:**
|
| 319 |
+
```
|
| 320 |
+
OPENAI_API_KEY=sk-proj-abcd1234...
|
| 321 |
+
```
|
| 322 |
+
""")
|
| 323 |
+
|
| 324 |
+
if __name__ == "__main__":
|
| 325 |
+
main()
|
prompt_analise_checklists.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Você é um especialista em análise de processos de planejamento e gestão de projetos. Sua função é analisar dados de checklists de processos de planejamento e fornecer insights valiosos.
|
| 2 |
+
|
| 3 |
+
CONTEXTO:
|
| 4 |
+
- Os dados representam checklists de processos de planejamento
|
| 5 |
+
- Cada processo tem um número identificador
|
| 6 |
+
- Os checklists contêm items que podem estar concluídos ou pendentes
|
| 7 |
+
- Há registros de interações (quando usuários marcam/desmarcam items)
|
| 8 |
+
- O prazo ideal para conclusão de um processo é de 6 meses (180 dias)
|
| 9 |
+
|
| 10 |
+
INSTRUÇÕES PARA ANÁLISE:
|
| 11 |
+
1. Analise os padrões de conclusão dos checklists
|
| 12 |
+
2. Identifique processos com alta eficiência vs baixa eficiência
|
| 13 |
+
3. Examine o tempo médio gasto em cada processo
|
| 14 |
+
4. Identifique gargalos e pontos de melhoria
|
| 15 |
+
5. Analise a aderência aos prazos estabelecidos
|
| 16 |
+
6. Forneça recomendações específicas e acionáveis
|
| 17 |
+
|
| 18 |
+
FORMATO DA RESPOSTA:
|
| 19 |
+
- Use linguagem profissional mas acessível
|
| 20 |
+
- Organize em seções claras
|
| 21 |
+
- Inclua métricas específicas quando possível
|
| 22 |
+
- Destaque insights mais importantes
|
| 23 |
+
- Termine com recomendações práticas
|
| 24 |
+
|
| 25 |
+
FOQUE EM:
|
| 26 |
+
- Eficiência dos processos
|
| 27 |
+
- Cumprimento de prazos
|
| 28 |
+
- Padrões de comportamento dos usuários
|
| 29 |
+
- Oportunidades de melhoria
|
| 30 |
+
- Riscos identificados
|
prompt_relatorio_executivo.txt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Você é um consultor executivo especializado em processos de planejamento. Sua função é criar um relatório executivo completo e estratégico para a alta gestão.
|
| 2 |
+
|
| 3 |
+
CONTEXTO:
|
| 4 |
+
- Este relatório será apresentado para gestores seniores
|
| 5 |
+
- Os dados são sobre checklists de processos de planejamento
|
| 6 |
+
- O prazo ideal é de 6 meses por processo
|
| 7 |
+
- O foco deve ser em decisões estratégicas e ações gerenciais
|
| 8 |
+
|
| 9 |
+
INSTRUÇÕES:
|
| 10 |
+
1. Crie um relatório executivo conciso mas completo
|
| 11 |
+
2. Use linguagem executiva apropriada
|
| 12 |
+
3. Inclua análise de riscos e oportunidades
|
| 13 |
+
4. Forneça recomendações específicas com priorização
|
| 14 |
+
5. Destaque métricas chave de performance (KPIs)
|
| 15 |
+
6. Identifique necessidades de recursos ou ajustes
|
| 16 |
+
|
| 17 |
+
ESTRUTURA OBRIGATÓRIA:
|
| 18 |
+
## RESUMO EXECUTIVO
|
| 19 |
+
- Principais descobertas em 3-4 pontos
|
| 20 |
+
- Status geral dos processos
|
| 21 |
+
|
| 22 |
+
## INDICADORES CHAVE
|
| 23 |
+
- Métricas principais de performance
|
| 24 |
+
- Comparação com metas estabelecidas
|
| 25 |
+
|
| 26 |
+
## ANÁLISE DE RISCOS
|
| 27 |
+
- Processos em risco de atraso
|
| 28 |
+
- Impactos potenciais
|
| 29 |
+
|
| 30 |
+
## OPORTUNIDADES DE MELHORIA
|
| 31 |
+
- Pontos de otimização identificados
|
| 32 |
+
- Benefícios esperados
|
| 33 |
+
|
| 34 |
+
## RECOMENDAÇÕES ESTRATÉGICAS
|
| 35 |
+
- Ações imediatas (próximos 30 dias)
|
| 36 |
+
- Ações de médio prazo (próximos 3 meses)
|
| 37 |
+
- Investimentos ou recursos necessários
|
| 38 |
+
|
| 39 |
+
## CONCLUSÃO
|
| 40 |
+
- Próximos passos
|
| 41 |
+
- Expectativas de resultados
|
| 42 |
+
|
| 43 |
+
DIRETRIZES:
|
| 44 |
+
- Máximo 2 páginas quando impresso
|
| 45 |
+
- Linguagem objetiva e direta
|
| 46 |
+
- Foco em ação e resultados
|
| 47 |
+
- Inclua números e percentuais específicos
|
| 48 |
+
- Evite jargões técnicos desnecessários
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.47.1
|
| 2 |
+
psycopg2-binary==2.9.10
|
| 3 |
+
plotly==6.2.0
|
| 4 |
+
pandas==2.3.1
|
| 5 |
+
matplotlib==3.10.5
|
| 6 |
+
seaborn==0.13.2
|
| 7 |
+
openai==1.98.0
|
| 8 |
+
reportlab==4.4.3
|
| 9 |
+
python-dotenv==1.1.1
|
setup_database.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Script para configurar o banco de dados"""
|
| 2 |
+
|
| 3 |
+
from utils.database import test_connection, create_tables
|
| 4 |
+
|
| 5 |
+
def main():
|
| 6 |
+
print("Testando conexão com o banco de dados...")
|
| 7 |
+
|
| 8 |
+
if test_connection():
|
| 9 |
+
print("\nConexão bem-sucedida!")
|
| 10 |
+
print("\nCriando tabelas...")
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
create_tables()
|
| 14 |
+
print("\nBanco de dados configurado com sucesso!")
|
| 15 |
+
print("\nTabelas criadas:")
|
| 16 |
+
print("- checklists")
|
| 17 |
+
print("- checklist_items")
|
| 18 |
+
print("- item_interactions")
|
| 19 |
+
print("\nViews criadas:")
|
| 20 |
+
print("- current_item_states")
|
| 21 |
+
print("- item_time_analysis")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"\nErro ao criar tabelas: {e}")
|
| 24 |
+
else:
|
| 25 |
+
print("\nFalha na conexão com o banco de dados.")
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
main()
|
update_database.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Script para adicionar coluna numero_processo"""
|
| 2 |
+
|
| 3 |
+
from utils.database import get_db_connection
|
| 4 |
+
|
| 5 |
+
def add_numero_processo_column():
|
| 6 |
+
"""Adiciona coluna numero_processo na tabela checklists"""
|
| 7 |
+
with open('/Users/abimaeltorcate/checklist/add_numero_processo.sql', 'r') as f:
|
| 8 |
+
sql_script = f.read()
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
with get_db_connection() as conn:
|
| 12 |
+
with conn.cursor() as cur:
|
| 13 |
+
# Executar cada comando separadamente
|
| 14 |
+
commands = sql_script.split(';')
|
| 15 |
+
for command in commands:
|
| 16 |
+
command = command.strip()
|
| 17 |
+
if command:
|
| 18 |
+
print(f"Executando: {command[:50]}...")
|
| 19 |
+
cur.execute(command)
|
| 20 |
+
|
| 21 |
+
conn.commit()
|
| 22 |
+
print("✅ Coluna numero_processo adicionada com sucesso!")
|
| 23 |
+
|
| 24 |
+
# Verificar se a coluna foi adicionada
|
| 25 |
+
cur.execute("""
|
| 26 |
+
SELECT column_name, data_type, is_nullable
|
| 27 |
+
FROM information_schema.columns
|
| 28 |
+
WHERE table_name = 'checklists' AND table_schema = 'public'
|
| 29 |
+
ORDER BY ordinal_position
|
| 30 |
+
""")
|
| 31 |
+
|
| 32 |
+
columns = cur.fetchall()
|
| 33 |
+
print("\n📋 Colunas da tabela checklists:")
|
| 34 |
+
for col in columns:
|
| 35 |
+
print(f" - {col[0]} ({col[1]}) - Nullable: {col[2]}")
|
| 36 |
+
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"❌ Erro: {e}")
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
add_numero_processo_column()
|
utils/database.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Módulo de conexão e operações com o banco de dados PostgreSQL"""
|
| 2 |
+
|
| 3 |
+
import psycopg2
|
| 4 |
+
from psycopg2.extras import RealDictCursor
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
+
import streamlit as st
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# Configurações do banco de dados
|
| 12 |
+
# Prioriza variáveis de ambiente (Hugging Face Spaces) sobre valores hardcoded
|
| 13 |
+
DB_CONFIG = {
|
| 14 |
+
'host': os.getenv('DB_HOST', '77.37.43.160'),
|
| 15 |
+
'port': int(os.getenv('DB_PORT', 5432)),
|
| 16 |
+
'database': os.getenv('DB_NAME', 'checklist'),
|
| 17 |
+
'user': os.getenv('DB_USER', 'abimael'),
|
| 18 |
+
'password': os.getenv('DB_PASSWORD', 'ctweek')
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@contextmanager
|
| 22 |
+
def get_db_connection():
|
| 23 |
+
"""Context manager para conexão com o banco de dados"""
|
| 24 |
+
conn = None
|
| 25 |
+
try:
|
| 26 |
+
conn = psycopg2.connect(**DB_CONFIG)
|
| 27 |
+
yield conn
|
| 28 |
+
except Exception as e:
|
| 29 |
+
if conn:
|
| 30 |
+
conn.rollback()
|
| 31 |
+
raise e
|
| 32 |
+
finally:
|
| 33 |
+
if conn:
|
| 34 |
+
conn.close()
|
| 35 |
+
|
| 36 |
+
def create_tables():
|
| 37 |
+
"""Cria as tabelas do banco se não existirem"""
|
| 38 |
+
# SQL inline para evitar dependência de arquivo externo
|
| 39 |
+
sql_script = """
|
| 40 |
+
-- Criar tabela de checklists
|
| 41 |
+
CREATE TABLE IF NOT EXISTS checklists (
|
| 42 |
+
id VARCHAR(36) PRIMARY KEY,
|
| 43 |
+
name VARCHAR(255) NOT NULL,
|
| 44 |
+
numero_processo VARCHAR(100),
|
| 45 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
-- Criar tabela de itens do checklist
|
| 49 |
+
CREATE TABLE IF NOT EXISTS checklist_items (
|
| 50 |
+
id VARCHAR(36) PRIMARY KEY,
|
| 51 |
+
checklist_id VARCHAR(36) REFERENCES checklists(id) ON DELETE CASCADE,
|
| 52 |
+
text TEXT NOT NULL,
|
| 53 |
+
position INTEGER NOT NULL
|
| 54 |
+
);
|
| 55 |
+
|
| 56 |
+
-- Criar tabela de interações
|
| 57 |
+
CREATE TABLE IF NOT EXISTS item_interactions (
|
| 58 |
+
id SERIAL PRIMARY KEY,
|
| 59 |
+
item_id VARCHAR(36) REFERENCES checklist_items(id) ON DELETE CASCADE,
|
| 60 |
+
checklist_id VARCHAR(36) REFERENCES checklists(id) ON DELETE CASCADE,
|
| 61 |
+
action VARCHAR(20) NOT NULL,
|
| 62 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 63 |
+
);
|
| 64 |
+
|
| 65 |
+
-- Criar view para estados atuais dos itens
|
| 66 |
+
CREATE OR REPLACE VIEW current_item_states AS
|
| 67 |
+
WITH latest_interactions AS (
|
| 68 |
+
SELECT
|
| 69 |
+
item_id,
|
| 70 |
+
action,
|
| 71 |
+
timestamp,
|
| 72 |
+
ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY timestamp DESC) as rn
|
| 73 |
+
FROM item_interactions
|
| 74 |
+
)
|
| 75 |
+
SELECT
|
| 76 |
+
item_id,
|
| 77 |
+
CASE WHEN action = 'checked' THEN true ELSE false END as is_checked
|
| 78 |
+
FROM latest_interactions
|
| 79 |
+
WHERE rn = 1;
|
| 80 |
+
|
| 81 |
+
-- Criar view para análise de tempo por item
|
| 82 |
+
CREATE OR REPLACE VIEW item_time_analysis AS
|
| 83 |
+
WITH item_sessions AS (
|
| 84 |
+
SELECT
|
| 85 |
+
ii.item_id,
|
| 86 |
+
ci.text as item_text,
|
| 87 |
+
ci.position,
|
| 88 |
+
ci.checklist_id,
|
| 89 |
+
ii.timestamp,
|
| 90 |
+
ii.action,
|
| 91 |
+
LAG(ii.timestamp) OVER (
|
| 92 |
+
PARTITION BY ii.item_id
|
| 93 |
+
ORDER BY ii.timestamp
|
| 94 |
+
) as prev_timestamp,
|
| 95 |
+
LAG(ii.action) OVER (
|
| 96 |
+
PARTITION BY ii.item_id
|
| 97 |
+
ORDER BY ii.timestamp
|
| 98 |
+
) as prev_action
|
| 99 |
+
FROM item_interactions ii
|
| 100 |
+
JOIN checklist_items ci ON ii.item_id = ci.id
|
| 101 |
+
ORDER BY ii.item_id, ii.timestamp
|
| 102 |
+
),
|
| 103 |
+
work_sessions AS (
|
| 104 |
+
SELECT
|
| 105 |
+
item_id,
|
| 106 |
+
item_text,
|
| 107 |
+
position,
|
| 108 |
+
checklist_id,
|
| 109 |
+
CASE
|
| 110 |
+
WHEN prev_action = 'unchecked' AND action = 'checked' AND prev_timestamp IS NOT NULL
|
| 111 |
+
THEN EXTRACT(EPOCH FROM (timestamp - prev_timestamp))
|
| 112 |
+
ELSE 0
|
| 113 |
+
END as session_seconds
|
| 114 |
+
FROM item_sessions
|
| 115 |
+
WHERE prev_timestamp IS NOT NULL
|
| 116 |
+
)
|
| 117 |
+
SELECT
|
| 118 |
+
item_id,
|
| 119 |
+
item_text,
|
| 120 |
+
position,
|
| 121 |
+
checklist_id,
|
| 122 |
+
COUNT(CASE WHEN session_seconds > 0 THEN 1 END) as times_worked,
|
| 123 |
+
COALESCE(SUM(session_seconds), 0) as total_seconds_spent,
|
| 124 |
+
CASE
|
| 125 |
+
WHEN COUNT(CASE WHEN session_seconds > 0 THEN 1 END) > 0
|
| 126 |
+
THEN COALESCE(SUM(session_seconds), 0) / COUNT(CASE WHEN session_seconds > 0 THEN 1 END)
|
| 127 |
+
ELSE 0
|
| 128 |
+
END as avg_seconds_per_completion
|
| 129 |
+
FROM work_sessions
|
| 130 |
+
GROUP BY item_id, item_text, position, checklist_id;
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
with get_db_connection() as conn:
|
| 134 |
+
with conn.cursor() as cur:
|
| 135 |
+
cur.execute(sql_script)
|
| 136 |
+
conn.commit()
|
| 137 |
+
print("Tabelas criadas com sucesso!")
|
| 138 |
+
|
| 139 |
+
def save_checklist(name, items, numero_processo=None):
|
| 140 |
+
"""Salva um novo checklist no banco de dados"""
|
| 141 |
+
checklist_id = str(uuid.uuid4())
|
| 142 |
+
|
| 143 |
+
with get_db_connection() as conn:
|
| 144 |
+
with conn.cursor() as cur:
|
| 145 |
+
# Inserir checklist
|
| 146 |
+
cur.execute("""
|
| 147 |
+
INSERT INTO checklists (id, name, numero_processo)
|
| 148 |
+
VALUES (%s, %s, %s)
|
| 149 |
+
""", (checklist_id, name, numero_processo))
|
| 150 |
+
|
| 151 |
+
# Inserir items
|
| 152 |
+
for position, item_text in enumerate(items):
|
| 153 |
+
if item_text.strip():
|
| 154 |
+
item_id = str(uuid.uuid4())
|
| 155 |
+
cur.execute("""
|
| 156 |
+
INSERT INTO checklist_items (id, checklist_id, text, position)
|
| 157 |
+
VALUES (%s, %s, %s, %s)
|
| 158 |
+
""", (item_id, checklist_id, item_text.strip(), position))
|
| 159 |
+
|
| 160 |
+
conn.commit()
|
| 161 |
+
|
| 162 |
+
return checklist_id
|
| 163 |
+
|
| 164 |
+
def get_all_checklists():
|
| 165 |
+
"""Retorna todos os checklists do banco"""
|
| 166 |
+
with get_db_connection() as conn:
|
| 167 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 168 |
+
cur.execute("""
|
| 169 |
+
SELECT id, name, numero_processo, created_at
|
| 170 |
+
FROM checklists
|
| 171 |
+
ORDER BY created_at DESC
|
| 172 |
+
""")
|
| 173 |
+
return cur.fetchall()
|
| 174 |
+
|
| 175 |
+
def get_checklist_with_items(checklist_id):
|
| 176 |
+
"""Retorna um checklist com seus itens e estados atuais"""
|
| 177 |
+
with get_db_connection() as conn:
|
| 178 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 179 |
+
# Buscar checklist
|
| 180 |
+
cur.execute("""
|
| 181 |
+
SELECT id, name, numero_processo, created_at
|
| 182 |
+
FROM checklists
|
| 183 |
+
WHERE id = %s
|
| 184 |
+
""", (checklist_id,))
|
| 185 |
+
checklist = cur.fetchone()
|
| 186 |
+
|
| 187 |
+
if not checklist:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
# Buscar items com estado atual
|
| 191 |
+
cur.execute("""
|
| 192 |
+
SELECT
|
| 193 |
+
ci.id,
|
| 194 |
+
ci.text,
|
| 195 |
+
ci.position,
|
| 196 |
+
COALESCE(cis.is_checked, false) as is_checked
|
| 197 |
+
FROM checklist_items ci
|
| 198 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 199 |
+
WHERE ci.checklist_id = %s
|
| 200 |
+
ORDER BY ci.position
|
| 201 |
+
""", (checklist_id,))
|
| 202 |
+
|
| 203 |
+
checklist['items'] = cur.fetchall()
|
| 204 |
+
|
| 205 |
+
return checklist
|
| 206 |
+
|
| 207 |
+
def toggle_item(item_id, checklist_id, new_state):
|
| 208 |
+
"""Registra a mudança de estado de um item"""
|
| 209 |
+
action = 'checked' if new_state else 'unchecked'
|
| 210 |
+
|
| 211 |
+
with get_db_connection() as conn:
|
| 212 |
+
with conn.cursor() as cur:
|
| 213 |
+
cur.execute("""
|
| 214 |
+
INSERT INTO item_interactions (item_id, checklist_id, action)
|
| 215 |
+
VALUES (%s, %s, %s)
|
| 216 |
+
""", (item_id, checklist_id, action))
|
| 217 |
+
conn.commit()
|
| 218 |
+
|
| 219 |
+
def get_checklist_analytics(checklist_id):
|
| 220 |
+
"""Retorna análises de tempo do checklist"""
|
| 221 |
+
with get_db_connection() as conn:
|
| 222 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 223 |
+
# Estatísticas gerais
|
| 224 |
+
cur.execute("""
|
| 225 |
+
SELECT
|
| 226 |
+
COUNT(DISTINCT ci.id) as total_items,
|
| 227 |
+
COUNT(DISTINCT CASE WHEN cis.is_checked THEN ci.id END) as completed_items,
|
| 228 |
+
MIN(ii.timestamp) as first_interaction,
|
| 229 |
+
MAX(ii.timestamp) as last_interaction
|
| 230 |
+
FROM checklist_items ci
|
| 231 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 232 |
+
LEFT JOIN item_interactions ii ON ci.id = ii.item_id
|
| 233 |
+
WHERE ci.checklist_id = %s
|
| 234 |
+
""", (checklist_id,))
|
| 235 |
+
|
| 236 |
+
stats = cur.fetchone()
|
| 237 |
+
|
| 238 |
+
# Análise de tempo por item
|
| 239 |
+
cur.execute("""
|
| 240 |
+
SELECT
|
| 241 |
+
item_text,
|
| 242 |
+
position,
|
| 243 |
+
times_worked,
|
| 244 |
+
total_seconds_spent,
|
| 245 |
+
avg_seconds_per_completion
|
| 246 |
+
FROM item_time_analysis
|
| 247 |
+
WHERE checklist_id = %s
|
| 248 |
+
ORDER BY position
|
| 249 |
+
""", (checklist_id,))
|
| 250 |
+
|
| 251 |
+
time_analysis = cur.fetchall()
|
| 252 |
+
|
| 253 |
+
return {
|
| 254 |
+
'stats': stats,
|
| 255 |
+
'time_analysis': time_analysis
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
def delete_checklist(checklist_id):
|
| 259 |
+
"""Deleta um checklist e todos os seus dados relacionados"""
|
| 260 |
+
with get_db_connection() as conn:
|
| 261 |
+
with conn.cursor() as cur:
|
| 262 |
+
cur.execute("DELETE FROM checklists WHERE id = %s", (checklist_id,))
|
| 263 |
+
conn.commit()
|
| 264 |
+
|
| 265 |
+
def get_all_checklists_with_stats():
|
| 266 |
+
"""Retorna todos os checklists com estatísticas de progresso"""
|
| 267 |
+
with get_db_connection() as conn:
|
| 268 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 269 |
+
cur.execute("""
|
| 270 |
+
SELECT
|
| 271 |
+
c.id,
|
| 272 |
+
c.name,
|
| 273 |
+
c.numero_processo,
|
| 274 |
+
c.created_at,
|
| 275 |
+
COUNT(ci.id) as total_items,
|
| 276 |
+
COUNT(CASE WHEN cis.is_checked THEN 1 END) as completed_items,
|
| 277 |
+
CASE
|
| 278 |
+
WHEN COUNT(ci.id) > 0 THEN
|
| 279 |
+
ROUND((COUNT(CASE WHEN cis.is_checked THEN 1 END)::decimal / COUNT(ci.id)) * 100, 1)
|
| 280 |
+
ELSE 0
|
| 281 |
+
END as progress_percentage
|
| 282 |
+
FROM checklists c
|
| 283 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 284 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 285 |
+
GROUP BY c.id, c.name, c.numero_processo, c.created_at
|
| 286 |
+
ORDER BY c.created_at DESC
|
| 287 |
+
""")
|
| 288 |
+
return cur.fetchall()
|
| 289 |
+
|
| 290 |
+
def get_general_stats():
|
| 291 |
+
"""Retorna estatísticas gerais do sistema"""
|
| 292 |
+
with get_db_connection() as conn:
|
| 293 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 294 |
+
cur.execute("""
|
| 295 |
+
WITH checklist_progress AS (
|
| 296 |
+
SELECT
|
| 297 |
+
c.id,
|
| 298 |
+
c.numero_processo,
|
| 299 |
+
c.created_at,
|
| 300 |
+
COUNT(ci.id) as total_items,
|
| 301 |
+
COUNT(CASE WHEN cis.is_checked THEN 1 END) as completed_items,
|
| 302 |
+
CASE
|
| 303 |
+
WHEN COUNT(ci.id) > 0 THEN
|
| 304 |
+
ROUND((COUNT(CASE WHEN cis.is_checked THEN 1 END)::decimal / COUNT(ci.id)) * 100, 1)
|
| 305 |
+
ELSE 0
|
| 306 |
+
END as progress_percentage
|
| 307 |
+
FROM checklists c
|
| 308 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 309 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 310 |
+
GROUP BY c.id, c.numero_processo, c.created_at
|
| 311 |
+
)
|
| 312 |
+
SELECT
|
| 313 |
+
COUNT(*) as total_checklists,
|
| 314 |
+
COUNT(DISTINCT numero_processo) as total_processos,
|
| 315 |
+
SUM(total_items) as total_items,
|
| 316 |
+
SUM(completed_items) as completed_items,
|
| 317 |
+
COUNT(CASE WHEN progress_percentage = 100 THEN 1 END) as completed_checklists,
|
| 318 |
+
MIN(created_at) as first_checklist_date,
|
| 319 |
+
MAX(created_at) as last_checklist_date,
|
| 320 |
+
ROUND(AVG(progress_percentage), 1) as avg_progress
|
| 321 |
+
FROM checklist_progress
|
| 322 |
+
""")
|
| 323 |
+
return cur.fetchone()
|
| 324 |
+
|
| 325 |
+
def get_checklist_interactions_summary():
|
| 326 |
+
"""Retorna resumo de interações por checklist"""
|
| 327 |
+
with get_db_connection() as conn:
|
| 328 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 329 |
+
cur.execute("""
|
| 330 |
+
SELECT
|
| 331 |
+
c.id,
|
| 332 |
+
c.name,
|
| 333 |
+
c.numero_processo,
|
| 334 |
+
c.created_at,
|
| 335 |
+
COUNT(ii.id) as total_interactions,
|
| 336 |
+
COUNT(DISTINCT ii.item_id) as items_with_interactions,
|
| 337 |
+
MIN(ii.timestamp) as first_interaction,
|
| 338 |
+
MAX(ii.timestamp) as last_interaction
|
| 339 |
+
FROM checklists c
|
| 340 |
+
LEFT JOIN item_interactions ii ON c.id = ii.checklist_id
|
| 341 |
+
GROUP BY c.id, c.name, c.numero_processo, c.created_at
|
| 342 |
+
HAVING COUNT(ii.id) > 0
|
| 343 |
+
ORDER BY c.created_at DESC
|
| 344 |
+
""")
|
| 345 |
+
return cur.fetchall()
|
| 346 |
+
|
| 347 |
+
def get_process_summary():
|
| 348 |
+
"""Retorna resumo por número de processo"""
|
| 349 |
+
with get_db_connection() as conn:
|
| 350 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 351 |
+
cur.execute("""
|
| 352 |
+
WITH checklist_progress AS (
|
| 353 |
+
SELECT
|
| 354 |
+
c.id,
|
| 355 |
+
c.numero_processo,
|
| 356 |
+
COUNT(ci.id) as total_items,
|
| 357 |
+
COUNT(CASE WHEN cis.is_checked THEN 1 END) as completed_items,
|
| 358 |
+
CASE
|
| 359 |
+
WHEN COUNT(ci.id) > 0 THEN
|
| 360 |
+
ROUND((COUNT(CASE WHEN cis.is_checked THEN 1 END)::decimal / COUNT(ci.id)) * 100, 1)
|
| 361 |
+
ELSE 0
|
| 362 |
+
END as progress_percentage
|
| 363 |
+
FROM checklists c
|
| 364 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 365 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 366 |
+
WHERE c.numero_processo IS NOT NULL
|
| 367 |
+
GROUP BY c.id, c.numero_processo
|
| 368 |
+
)
|
| 369 |
+
SELECT
|
| 370 |
+
numero_processo,
|
| 371 |
+
COUNT(*) as total_checklists,
|
| 372 |
+
SUM(total_items) as total_items,
|
| 373 |
+
SUM(completed_items) as completed_items,
|
| 374 |
+
ROUND(AVG(progress_percentage), 1) as avg_progress
|
| 375 |
+
FROM checklist_progress
|
| 376 |
+
GROUP BY numero_processo
|
| 377 |
+
ORDER BY avg_progress DESC
|
| 378 |
+
""")
|
| 379 |
+
return cur.fetchall()
|
| 380 |
+
|
| 381 |
+
def get_fastest_processes():
|
| 382 |
+
"""Retorna os processos mais rápidos baseado no tempo médio de conclusão"""
|
| 383 |
+
with get_db_connection() as conn:
|
| 384 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 385 |
+
cur.execute("""
|
| 386 |
+
WITH checklist_completion_times AS (
|
| 387 |
+
SELECT
|
| 388 |
+
c.id,
|
| 389 |
+
c.numero_processo,
|
| 390 |
+
c.name,
|
| 391 |
+
c.created_at,
|
| 392 |
+
MIN(ii.timestamp) as first_interaction,
|
| 393 |
+
MAX(ii.timestamp) as last_interaction,
|
| 394 |
+
COUNT(DISTINCT ci.id) as total_items,
|
| 395 |
+
COUNT(DISTINCT CASE WHEN cis.is_checked THEN ci.id END) as completed_items,
|
| 396 |
+
CASE
|
| 397 |
+
WHEN COUNT(ci.id) > 0 THEN
|
| 398 |
+
ROUND((COUNT(CASE WHEN cis.is_checked THEN 1 END)::decimal / COUNT(ci.id)) * 100, 1)
|
| 399 |
+
ELSE 0
|
| 400 |
+
END as progress_percentage
|
| 401 |
+
FROM checklists c
|
| 402 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 403 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 404 |
+
LEFT JOIN item_interactions ii ON c.id = ii.checklist_id
|
| 405 |
+
WHERE c.numero_processo IS NOT NULL
|
| 406 |
+
GROUP BY c.id, c.numero_processo, c.name, c.created_at
|
| 407 |
+
),
|
| 408 |
+
process_times AS (
|
| 409 |
+
SELECT
|
| 410 |
+
numero_processo,
|
| 411 |
+
COUNT(*) as total_checklists,
|
| 412 |
+
COUNT(CASE WHEN progress_percentage = 100 THEN 1 END) as completed_checklists,
|
| 413 |
+
AVG(progress_percentage) as avg_progress,
|
| 414 |
+
AVG(
|
| 415 |
+
CASE
|
| 416 |
+
WHEN first_interaction IS NOT NULL AND last_interaction IS NOT NULL
|
| 417 |
+
THEN EXTRACT(EPOCH FROM (last_interaction - first_interaction)) / 3600.0 -- horas
|
| 418 |
+
ELSE NULL
|
| 419 |
+
END
|
| 420 |
+
) as avg_completion_hours,
|
| 421 |
+
AVG(
|
| 422 |
+
CASE
|
| 423 |
+
WHEN first_interaction IS NOT NULL
|
| 424 |
+
THEN EXTRACT(EPOCH FROM (first_interaction - created_at)) / 3600.0 -- horas até primeira interação
|
| 425 |
+
ELSE NULL
|
| 426 |
+
END
|
| 427 |
+
) as avg_start_hours
|
| 428 |
+
FROM checklist_completion_times
|
| 429 |
+
GROUP BY numero_processo
|
| 430 |
+
)
|
| 431 |
+
SELECT
|
| 432 |
+
numero_processo,
|
| 433 |
+
total_checklists,
|
| 434 |
+
completed_checklists,
|
| 435 |
+
ROUND(avg_progress::numeric, 1) as avg_progress,
|
| 436 |
+
ROUND(avg_completion_hours::numeric, 2) as avg_completion_hours,
|
| 437 |
+
ROUND(avg_start_hours::numeric, 2) as avg_start_hours,
|
| 438 |
+
ROUND((avg_completion_hours + avg_start_hours)::numeric, 2) as total_avg_time_hours
|
| 439 |
+
FROM process_times
|
| 440 |
+
WHERE avg_completion_hours IS NOT NULL
|
| 441 |
+
OR avg_start_hours IS NOT NULL
|
| 442 |
+
ORDER BY total_avg_time_hours ASC NULLS LAST
|
| 443 |
+
LIMIT 10
|
| 444 |
+
""")
|
| 445 |
+
return cur.fetchall()
|
| 446 |
+
|
| 447 |
+
def get_process_deadline_analysis():
|
| 448 |
+
"""Analisa prazos dos processos com deadline de 6 meses"""
|
| 449 |
+
with get_db_connection() as conn:
|
| 450 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 451 |
+
cur.execute("""
|
| 452 |
+
WITH process_progress AS (
|
| 453 |
+
SELECT
|
| 454 |
+
c.numero_processo,
|
| 455 |
+
c.created_at,
|
| 456 |
+
COUNT(ci.id) as total_items,
|
| 457 |
+
COUNT(CASE WHEN cis.is_checked THEN 1 END) as completed_items,
|
| 458 |
+
CASE
|
| 459 |
+
WHEN COUNT(ci.id) > 0 THEN
|
| 460 |
+
ROUND((COUNT(CASE WHEN cis.is_checked THEN 1 END)::decimal / COUNT(ci.id)) * 100, 1)
|
| 461 |
+
ELSE 0
|
| 462 |
+
END as progress_percentage,
|
| 463 |
+
MIN(ii.timestamp) as first_interaction,
|
| 464 |
+
MAX(ii.timestamp) as last_interaction,
|
| 465 |
+
COUNT(DISTINCT c.id) as total_checklists
|
| 466 |
+
FROM checklists c
|
| 467 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 468 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 469 |
+
LEFT JOIN item_interactions ii ON c.id = ii.checklist_id
|
| 470 |
+
WHERE c.numero_processo IS NOT NULL
|
| 471 |
+
GROUP BY c.numero_processo, c.created_at
|
| 472 |
+
),
|
| 473 |
+
process_summary AS (
|
| 474 |
+
SELECT
|
| 475 |
+
numero_processo,
|
| 476 |
+
MIN(created_at) as process_start_date,
|
| 477 |
+
MAX(created_at) as latest_checklist_date,
|
| 478 |
+
SUM(total_items) as total_items,
|
| 479 |
+
SUM(completed_items) as completed_items,
|
| 480 |
+
ROUND(AVG(progress_percentage), 1) as avg_progress,
|
| 481 |
+
MIN(first_interaction) as first_interaction,
|
| 482 |
+
MAX(last_interaction) as last_interaction,
|
| 483 |
+
SUM(total_checklists) as total_checklists
|
| 484 |
+
FROM process_progress
|
| 485 |
+
GROUP BY numero_processo
|
| 486 |
+
)
|
| 487 |
+
SELECT
|
| 488 |
+
numero_processo,
|
| 489 |
+
process_start_date,
|
| 490 |
+
latest_checklist_date,
|
| 491 |
+
total_items,
|
| 492 |
+
completed_items,
|
| 493 |
+
avg_progress,
|
| 494 |
+
first_interaction,
|
| 495 |
+
last_interaction,
|
| 496 |
+
total_checklists,
|
| 497 |
+
-- Cálculos de prazo (6 meses = 180 dias)
|
| 498 |
+
(process_start_date + INTERVAL '180 days')::date as deadline_date,
|
| 499 |
+
CURRENT_DATE - process_start_date::date as days_elapsed,
|
| 500 |
+
(process_start_date + INTERVAL '180 days')::date - CURRENT_DATE as days_remaining,
|
| 501 |
+
-- Velocidade e projeção
|
| 502 |
+
CASE
|
| 503 |
+
WHEN (CURRENT_DATE - process_start_date::date) > 0 AND avg_progress > 0 THEN
|
| 504 |
+
ROUND((avg_progress / (CURRENT_DATE - process_start_date::date)) *
|
| 505 |
+
(100 - avg_progress), 0)
|
| 506 |
+
ELSE NULL
|
| 507 |
+
END as projected_days_to_complete,
|
| 508 |
+
-- Status do prazo
|
| 509 |
+
CASE
|
| 510 |
+
WHEN avg_progress = 100 THEN 'CONCLUIDO'
|
| 511 |
+
WHEN (CURRENT_DATE - process_start_date::date) > 180 THEN 'ATRASADO'
|
| 512 |
+
WHEN (CURRENT_DATE - process_start_date::date) > 0 AND avg_progress > 0 THEN
|
| 513 |
+
CASE
|
| 514 |
+
WHEN ((avg_progress / (CURRENT_DATE - process_start_date::date)) *
|
| 515 |
+
(100 - avg_progress) + (CURRENT_DATE - process_start_date::date)) > 180
|
| 516 |
+
THEN 'RISCO_ATRASO'
|
| 517 |
+
WHEN ((avg_progress / (CURRENT_DATE - process_start_date::date)) *
|
| 518 |
+
(100 - avg_progress) + (CURRENT_DATE - process_start_date::date)) > 150
|
| 519 |
+
THEN 'EM_RISCO'
|
| 520 |
+
ELSE 'NO_PRAZO'
|
| 521 |
+
END
|
| 522 |
+
ELSE 'SEM_DADOS'
|
| 523 |
+
END as status_prazo
|
| 524 |
+
FROM process_summary
|
| 525 |
+
ORDER BY days_remaining ASC NULLS LAST
|
| 526 |
+
""")
|
| 527 |
+
return cur.fetchall()
|
| 528 |
+
|
| 529 |
+
def get_comprehensive_analysis_data():
|
| 530 |
+
"""Retorna dados completos para análise com IA"""
|
| 531 |
+
with get_db_connection() as conn:
|
| 532 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 533 |
+
# Dados gerais dos processos
|
| 534 |
+
cur.execute("""
|
| 535 |
+
WITH process_stats AS (
|
| 536 |
+
SELECT
|
| 537 |
+
c.numero_processo,
|
| 538 |
+
COUNT(DISTINCT c.id) as total_checklists,
|
| 539 |
+
MIN(c.created_at) as process_start_date,
|
| 540 |
+
MAX(c.created_at) as latest_checklist_date,
|
| 541 |
+
COUNT(ci.id) as total_items,
|
| 542 |
+
COUNT(CASE WHEN cis.is_checked THEN 1 END) as completed_items,
|
| 543 |
+
ROUND(AVG(
|
| 544 |
+
CASE
|
| 545 |
+
WHEN COUNT(ci.id) OVER (PARTITION BY c.id) > 0 THEN
|
| 546 |
+
(COUNT(CASE WHEN cis.is_checked THEN 1 END) OVER (PARTITION BY c.id)::decimal /
|
| 547 |
+
COUNT(ci.id) OVER (PARTITION BY c.id)) * 100
|
| 548 |
+
ELSE 0
|
| 549 |
+
END
|
| 550 |
+
), 1) as avg_progress_percentage,
|
| 551 |
+
COUNT(ii.id) as total_interactions,
|
| 552 |
+
MIN(ii.timestamp) as first_interaction,
|
| 553 |
+
MAX(ii.timestamp) as last_interaction,
|
| 554 |
+
CURRENT_DATE - MIN(c.created_at)::date as days_elapsed,
|
| 555 |
+
(MIN(c.created_at) + INTERVAL '180 days')::date - CURRENT_DATE as days_remaining_to_deadline
|
| 556 |
+
FROM checklists c
|
| 557 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 558 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 559 |
+
LEFT JOIN item_interactions ii ON c.id = ii.checklist_id
|
| 560 |
+
WHERE c.numero_processo IS NOT NULL
|
| 561 |
+
GROUP BY c.numero_processo
|
| 562 |
+
)
|
| 563 |
+
SELECT
|
| 564 |
+
numero_processo,
|
| 565 |
+
total_checklists,
|
| 566 |
+
process_start_date,
|
| 567 |
+
latest_checklist_date,
|
| 568 |
+
total_items,
|
| 569 |
+
completed_items,
|
| 570 |
+
avg_progress_percentage,
|
| 571 |
+
total_interactions,
|
| 572 |
+
first_interaction,
|
| 573 |
+
last_interaction,
|
| 574 |
+
days_elapsed,
|
| 575 |
+
days_remaining_to_deadline,
|
| 576 |
+
CASE
|
| 577 |
+
WHEN avg_progress_percentage = 100 THEN 'CONCLUIDO'
|
| 578 |
+
WHEN days_remaining_to_deadline < 0 THEN 'ATRASADO'
|
| 579 |
+
WHEN days_remaining_to_deadline < 30 THEN 'EM_RISCO'
|
| 580 |
+
ELSE 'NO_PRAZO'
|
| 581 |
+
END as status_prazo,
|
| 582 |
+
CASE
|
| 583 |
+
WHEN days_elapsed > 0 AND avg_progress_percentage > 0 THEN
|
| 584 |
+
ROUND((avg_progress_percentage / days_elapsed) * (100 - avg_progress_percentage), 0)
|
| 585 |
+
ELSE NULL
|
| 586 |
+
END as projected_days_to_complete
|
| 587 |
+
FROM process_stats
|
| 588 |
+
ORDER BY days_remaining_to_deadline ASC
|
| 589 |
+
""")
|
| 590 |
+
|
| 591 |
+
process_data = cur.fetchall()
|
| 592 |
+
|
| 593 |
+
# Estatísticas globais
|
| 594 |
+
cur.execute("""
|
| 595 |
+
SELECT
|
| 596 |
+
COUNT(DISTINCT c.id) as total_checklists_global,
|
| 597 |
+
COUNT(DISTINCT c.numero_processo) as total_processes,
|
| 598 |
+
COUNT(ci.id) as total_items_global,
|
| 599 |
+
COUNT(CASE WHEN cis.is_checked THEN 1 END) as completed_items_global,
|
| 600 |
+
COUNT(ii.id) as total_interactions_global,
|
| 601 |
+
MIN(c.created_at) as earliest_process,
|
| 602 |
+
MAX(c.created_at) as latest_process
|
| 603 |
+
FROM checklists c
|
| 604 |
+
LEFT JOIN checklist_items ci ON c.id = ci.checklist_id
|
| 605 |
+
LEFT JOIN current_item_states cis ON ci.id = cis.item_id
|
| 606 |
+
LEFT JOIN item_interactions ii ON c.id = ii.checklist_id
|
| 607 |
+
""")
|
| 608 |
+
|
| 609 |
+
global_stats = cur.fetchone()
|
| 610 |
+
|
| 611 |
+
return {
|
| 612 |
+
'process_data': process_data,
|
| 613 |
+
'global_stats': global_stats
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
# Função para testar conexão
|
| 617 |
+
def test_connection():
|
| 618 |
+
"""Testa a conexão com o banco de dados"""
|
| 619 |
+
try:
|
| 620 |
+
with get_db_connection() as conn:
|
| 621 |
+
with conn.cursor() as cur:
|
| 622 |
+
cur.execute("SELECT version()")
|
| 623 |
+
version = cur.fetchone()
|
| 624 |
+
print(f"Conectado ao PostgreSQL: {version[0]}")
|
| 625 |
+
return True
|
| 626 |
+
except Exception as e:
|
| 627 |
+
print(f"Erro ao conectar: {e}")
|
| 628 |
+
return False
|