Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/static/css/familias_atendidas_30_dias.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
gap: 2rem;
}

.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}

.header-actions .btn {
white-space: nowrap;
}

.header-text h1 {
color: white;
font-weight: 700;
Expand Down
34 changes: 34 additions & 0 deletions app/static/js/familias_atendidas_30_dias.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
document.addEventListener('DOMContentLoaded', function () {
const downloadBtn = document.getElementById('downloadBtn');
if (downloadBtn) {
const downloadUrl = downloadBtn.dataset.url;
downloadBtn.addEventListener('click', () => {
fetch(downloadUrl, {
method: 'GET',
headers: {
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
})
.then(response => {
if (!response.ok) {
throw new Error('Erro ao gerar arquivo');
}
return response.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
const now = new Date();
const dateStr = now.getFullYear() + '_' + String(now.getMonth() + 1).padStart(2, '0') + '_' + String(now.getDate()).padStart(2, '0');
link.href = url;
link.download = `familias_atendidas_30_dias_${dateStr}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
})
.catch(error => {
console.error('Erro no download:', error);
});
});
}

const dados = window.familiasAtendidas || [];
const tbody = document.querySelector('#tabelaFamiliasAtendidas tbody');
const paginacao = document.getElementById('paginacaoFamiliasAtendidas');
Expand Down
14 changes: 10 additions & 4 deletions app/templates/dashboards/familias_atendidas_30_dias.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ <h1>
</h1>
<p>Listagem de famílias que receberam atendimento no período</p>
</div>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
Voltar ao Dashboard
</a>
<div class="header-actions">
<button id="downloadBtn" class="btn btn-primary" data-url="{{ url_for('download_familias_atendidas_30_dias') }}">
<i class="fas fa-file-excel"></i>
Baixar Excel
</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
Voltar ao Dashboard
</a>
</div>
</div>
</div>
<div class="table-responsive">
Expand Down
128 changes: 128 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,5 +603,133 @@ def download_familias_cadastradas():
}), 500


@app.route("/dashboard/familias-atendidas-30-dias/download")
@login_required
@admin_required
def download_familias_atendidas_30_dias():
"""Download de arquivo Excel com dados completos das famílias atendidas nos últimos 30 dias."""
try:
sql_query_string = """
SELECT
f.*,
e.*,
c.*,
cf.*,
cm.*,
ed.*,
ep.*,
rf.*,
sf.*,
demandas_json.demandas,
atendimentos_json.atendimentos
FROM familias f
LEFT JOIN enderecos e ON f.familia_id = e.familia_id
LEFT JOIN contatos c ON f.familia_id = c.familia_id
LEFT JOIN composicao_familiar cf ON f.familia_id = cf.familia_id
LEFT JOIN condicoes_moradia cm ON f.familia_id = cm.familia_id
LEFT JOIN educacao_entrevistado ed ON f.familia_id = ed.familia_id
LEFT JOIN emprego_provedor ep ON f.familia_id = ep.familia_id
LEFT JOIN renda_familiar rf ON f.familia_id = rf.familia_id
LEFT JOIN saude_familiar sf ON f.familia_id = sf.familia_id
OUTER APPLY (
SELECT
df.demanda_id,
df.familia_id,
df.demanda_tipo_id,
dt.demanda_tipo_nome,
df.status,
df.descricao,
df.data_identificacao,
df.prioridade,
de.data_atualizacao,
de.status_atual,
de.observacao,
de.usuario_atualizacao
FROM demanda_familia df
INNER JOIN demanda_tipo dt ON df.demanda_tipo_id = dt.demanda_tipo_id
INNER JOIN demanda_etapa de ON df.demanda_id = de.demanda_id
WHERE df.familia_id = f.familia_id
FOR JSON PATH
) AS demandas_json(demandas)
OUTER APPLY (
SELECT
a.*
FROM atendimentos a
WHERE a.familia_id = f.familia_id
FOR JSON PATH
) AS atendimentos_json(atendimentos)
WHERE EXISTS (
SELECT 1 FROM atendimentos a2
WHERE a2.familia_id = f.familia_id
AND a2.data_hora_atendimento >= DATEADD(DAY, -30, GETDATE())
)
"""

sql_query = text(sql_query_string)
resultados = db.session.execute(sql_query).mappings().all()
if not resultados:
return jsonify({"error": "Nenhum dado encontrado para exportação"}), 404

dados = [dict(r) for r in resultados]
df = pd.DataFrame(dados)

for column in df.columns:
if df[column].dtype == 'object':
sample_values = df[column].dropna().head(5)
if len(sample_values) > 0:
first_value = sample_values.iloc[0]
if hasattr(first_value, 'tzinfo') and first_value.tzinfo is not None:
df[column] = pd.to_datetime(df[column], errors='ignore').dt.tz_localize(None)
elif 'datetime64[ns, ' in str(df[column].dtype):
df[column] = df[column].dt.tz_localize(None)

colunas_pt = {
'familia_id': 'ID Família',
'nome_responsavel': 'Nome do Responsável',
'cpf': 'CPF',
'data_nascimento': 'Data de Nascimento',
'telefone_principal': 'Telefone Principal',
'email': 'Email',
'data_cadastro': 'Data de Cadastro',
'logradouro': 'Logradouro',
'numero': 'Número',
'complemento': 'Complemento',
'bairro': 'Bairro',
'cidade': 'Cidade',
'estado': 'Estado',
'cep': 'CEP'
}
colunas_existentes = {k: v for k, v in colunas_pt.items() if k in df.columns}
df = df.rename(columns=colunas_existentes)

output = BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Dados_Familias', index=False)
workbook = writer.book
worksheet = writer.sheets['Dados_Familias']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if cell.value and len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max(max_length + 2, 10), 50)
worksheet.column_dimensions[column_letter].width = adjusted_width
output.seek(0)

data_atual = datetime.now().strftime("%Y_%m_%d")
nome_arquivo = f"familias_atendidas_30_dias_{data_atual}.xlsx"
return send_file(
output,
as_attachment=True,
download_name=nome_arquivo,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
except Exception as e:
return jsonify({"error": f"Erro ao gerar arquivo: {str(e)}"}), 500

if __name__ == "__main__":
app.run(debug=True)
83 changes: 64 additions & 19 deletions reference_inputs/docs/guia_implementacao_relatorios_dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ app/
│ └── js/
│ ├── dashboard_demandas.js # JS específico de demandas
│ └── [novo_relatorio].js # JS específico do novo relatório
main.py # Rotas do backend
main.py # Rotas do back-end
```

## Padrão de Implementação

### 1. Rota no Backend (main.py)
### 1. Rota no Back-end (main.py)

#### Padrão de Nomenclatura
- URL: `/dashboard/[nome-relatorio-kebab-case]`
Expand Down Expand Up @@ -142,7 +142,7 @@ def dashboard_familias_atendidas_30_dias():
#### Padrões de Estilo

```css
/* Header Styles - Padrão para todos os relatórios */
/* Estilos de cabeçalho - padrão para todos os relatórios */
.header-content {
display: flex;
justify-content: space-between;
Expand All @@ -166,7 +166,7 @@ def dashboard_familias_atendidas_30_dias():
font-size: 1.1rem;
}

/* Responsive Header */
/* Cabeçalho responsivo */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
Expand Down Expand Up @@ -453,7 +453,7 @@ No arquivo `dashboard.html`, localizar o script de clique dos cards e atualizar:

#### Padrão de Implementação

No arquivo `main.py`, na função `dashboard()`, adicionar uma query SQL para calcular dinamicamente o valor do card:
No arquivo `main.py`, na função `dashboard()`, adicionar uma consulta SQL para calcular dinamicamente o valor do card:

```python
# Exemplo para famílias atendidas nos últimos 30 dias
Expand All @@ -477,7 +477,7 @@ dados_dashboard = {
}
```

#### Template de Query para Cards Dinâmicos
#### Template de consulta para cards dinâmicos

```python
# Template genérico para contagem de registros
Expand All @@ -494,7 +494,7 @@ resultado_[nome_metrica] = db.session.execute(sql_[nome_metrica]).mappings().fir
total_[nome_metrica] = resultado_[nome_metrica]['total_[nome_metrica]'] if resultado_[nome_metrica] else 0
```

#### Exemplos de Queries por Tipo de Card
#### Exemplos de consultas por tipo de card

**Contagem de Famílias por Período:**
```sql
Expand Down Expand Up @@ -532,10 +532,10 @@ WHERE a.percepcao_necessidade = 'Alta'
AND a.data_hora_atendimento >= DATEADD(DAY, -30, GETDATE())
```

#### Checklist para Atualização de Cards
#### Lista de verificação para atualização de cards

- [ ] Implementar query SQL específica para a métrica do card
- [ ] Executar a query na função `dashboard()` em `main.py`
- [ ] Implementar consulta SQL específica para a métrica do card
- [ ] Executar a consulta na função `dashboard()` em `main.py`
- [ ] Substituir o valor mock no dicionário `dados_dashboard`
- [ ] Testar se o card exibe o valor correto no dashboard
- [ ] Verificar se o valor é atualizado quando novos dados são inseridos
Expand All @@ -551,9 +551,54 @@ AND a.data_hora_atendimento >= DATEADD(DAY, -30, GETDATE())
- **Download**: `fas fa-download`
- **Relatórios**: `fas fa-chart-line`

## Exportação de dados em Excel

### 1. Rota no back-end (`main.py`)
- **URL**: `/dashboard/[nome-relatorio-kebab-case]/download`
- **Função**: `download_[nome_relatorio_snake_case]()`
- **Requisitos**
- Incluir `@login_required` e `@admin_required`.
- Buscar **todas** as colunas relevantes (família, endereços, contatos etc.), mesmo as não exibidas na tabela do relatório.
- Transformar o resultado em `pandas.DataFrame`, remover fuso horário (`tzinfo`) quando existir e renomear colunas para PT-BR.
- Retornar o arquivo usando `send_file(..., as_attachment=True, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')`.
- **Exemplo**
`download_familias_atendidas_30_dias()` em `main.py`.

### 2. Template HTML (`app/templates/dashboards/[novo_relatorio].html`)
- Adicionar botão de download dentro de `.header-actions`:
```html
<button id="downloadBtn" class="btn btn-primary"
data-url="{{ url_for('download_[nome_relatorio]') }}">
<i class="fas fa-file-excel"></i>
Baixar Excel
</button>
```
- Manter o link “Voltar ao Dashboard” ao lado do botão, conforme padrão das páginas existentes.

### 3. JavaScript (`app/static/js/[novo_relatorio].js`)
- No `DOMContentLoaded`, capturar clique em `#downloadBtn`.
- Efetuar `fetch` `GET` para `downloadBtn.dataset.url`.
- Converter a resposta para `blob`, criar `URL.createObjectURL`, definir o nome do arquivo `"[nome_relatorio]_AAAA_MM_DD.xlsx"` e disparar o download.
- Exemplo de uso: `familias_atendidas_30_dias.js`.

### 4. CSS (`app/static/css/[novo_relatorio].css`)
- Utilizar layout flexível do cabeçalho:
```css
.header-content { display: flex; justify-content: space-between; align-items: center; gap: 2rem; }
.header-actions { display: flex; align-items: center; gap: 1rem; }
```
- Garantir responsividade via consultas de mídia (`media queries`) semelhantes às usadas em `familias_atendidas_30_dias.css`.

### 5. Lista de verificação adicional
- [ ] Rota de download criada com `@login_required` e `@admin_required`.
- [ ] Botão “Baixar Excel” adicionado ao template.
- [ ] Lógica de download implementada em JS.
- [ ] Arquivo CSS do relatório contém estilos do cabeçalho e botão.
- [ ] Planilha contém todas as colunas necessárias com nomenclatura em PT-BR.

## Responsividade

### Breakpoints Padrão
### Pontos de quebra padrão

```css
/* Tablet */
Expand All @@ -570,15 +615,15 @@ AND a.data_hora_atendimento >= DATEADD(DAY, -30, GETDATE())
}
```

## Checklist de Implementação
## Lista de verificação de implementação

### ✅ Backend
### ✅ Back-end
- [ ] Criar rota em `main.py` com decorators `@login_required` e `@admin_required`
- [ ] Implementar query SQL com JOINs apropriados
- [ ] Implementar consulta SQL com JOINs apropriados
- [ ] Converter resultados para lista de dicionários
- [ ] Passar dados para template via `render_template`

### ✅ Frontend
### ✅ Front-end
- [ ] Criar template HTML seguindo estrutura padrão
- [ ] Implementar cabeçalhos e filtros de tabela
- [ ] Criar CSS específico com ID único da tabela
Expand All @@ -593,7 +638,7 @@ AND a.data_hora_atendimento >= DATEADD(DAY, -30, GETDATE())
- [ ] Validar formatação de dados e badges
- [ ] **Verificar se o card exibe dados reais (não mock) no dashboard principal**

## Exemplos de Queries por Tipo de Relatório
## Exemplos de consultas por tipo de relatório

### Relatórios Temporais (30 dias, 90 dias, etc.)
```sql
Expand All @@ -620,7 +665,7 @@ GROUP BY e.bairro
ORDER BY total_familias DESC
```

## Considerações de Performance
## Considerações de desempenho

### Índices Recomendados
- `familia_id` em todas as tabelas relacionadas
Expand All @@ -630,13 +675,13 @@ ORDER BY total_familias DESC

### Paginação
- Manter padrão de 15 registros por página
- Implementar paginação no frontend (não no backend)
- Implementar paginação no front-end (não no back-end)
- Mostrar indicador de "Nenhum registro encontrado"

## Manutenção e Evolução

### Versionamento
- Manter comentários nas queries SQL explicando a lógica
- Manter comentários nas consultas SQL explicando a lógica
- Documentar formatações específicas em comentários no CSS
- Usar nomes descritivos para funções JavaScript

Expand Down