Ir para o conteúdo

Frontend - Estrutura e Componentes

📁 Organização

public/
├── *.html                    # Páginas de cada ferramenta
├── js/
│   ├── [ferramenta].js       # Lógica específica
│   ├── sidebar.js            # Menu lateral
│   ├── auth-client.js        # Autenticação
│   ├── upload-helper.js      # Upload de arquivos
│   ├── logs.js               # Visualização logs
│   └── municipios_dimob.json # Dados estáticos
├── css/
│   └── styles.css            # Estilos globais
└── img/
    └── *.png, *.jpg          # Imagens/logos

🏗️ Arquivos Principais

1. HTML (Páginas)

Exemplo: public/gerador-atas.html

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Gerador de Atas - Central Utils</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="sidebar">
        <!-- Menu incluído via sidebar.js -->
    </div>

    <div class="main-content">
        <h1>Gerador de Atas</h1>
        <p>Descrição breve da ferramenta</p>

        <!-- Formulário específico -->
        <form id="form-ferramenta">
            <!-- Campos específicos -->
        </form>

        <!-- Resultado -->
        <div id="resultado" style="display:none;">
            <!-- Mostrado após processamento -->
        </div>
    </div>

    <script src="js/sidebar.js"></script>
    <script src="js/auth-client.js"></script>
    <script src="js/gerador-atas.js"></script>
</body>
</html>

2. JavaScript por Ferramenta

Padrão: public/js/[ferramenta].js

// Inicializar quando página carrega
document.addEventListener('DOMContentLoaded', () => {
    setupForm();
    setupEventListeners();
});

function setupForm() {
    // Inicializar campos
}

function setupEventListeners() {
    // Listener do formulário
    document.getElementById('form-ferramenta')
        .addEventListener('submit', handleSubmit);
}

async function handleSubmit(e) {
    e.preventDefault();

    // Coletar dados
    const data = {
        campo1: document.getElementById('campo1').value,
        campo2: document.getElementById('campo2').value
    };

    // Validar
    if (!validarDados(data)) {
        alert('Dados inválidos');
        return;
    }

    // Enviar para API
    try {
        const response = await fetch('/api/[ferramenta]/processar', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${getToken()}`
            },
            body: JSON.stringify(data)
        });

        const result = await response.json();

        if (result.success) {
            exibirResultado(result);
        } else {
            alert(`Erro: ${result.error}`);
        }
    } catch (error) {
        alert(`Erro na requisição: ${error}`);
    }
}

function exibirResultado(result) {
    document.getElementById('resultado').style.display = 'block';
    document.getElementById('resultado').innerHTML = `
        <p>✅ ${result.message}</p>
        <a href="/download/${result.output_path}" download class="btn">
            Baixar Resultado
        </a>
    `;
}

function validarDados(data) {
    return data.campo1 && data.campo2;
}

function getToken() {
    return localStorage.getItem('auth_token') || '';
}

🔑 Componentes Reutilizáveis

auth-client.js

Gerencia autenticação no lado cliente:

// Login
async function login(username, password) {
    const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });

    const data = await response.json();
    if (data.token) {
        localStorage.setItem('auth_token', data.token);
        window.location.href = '/public/home.html';
    }
}

// Logout
function logout() {
    localStorage.removeItem('auth_token');
    window.location.href = '/public/login.html';
}

// Verificar se autenticado
function isAuthenticated() {
    return !!localStorage.getItem('auth_token');
}

// Redirecionar se não autenticado
if (!isAuthenticated() && !window.location.pathname.includes('login')) {
    window.location.href = '/public/login.html';
}

upload-helper.js

Facilita upload de arquivos:

function setupFileUpload(inputId, displayId) {
    const input = document.getElementById(inputId);
    const display = document.getElementById(displayId);

    input.addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (file) {
            display.textContent = `📄 ${file.name} (${formatBytes(file.size)})`;
        }
    });
}

async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
        headers: {
            'Authorization': `Bearer ${getToken()}`
        }
    });

    return response.json();
}

function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}

sidebar.js

Menu lateral dinâmico:

const MENU_ITEMS = [
    { label: 'Home', href: '/public/home.html' },
    { label: 'Gerador de Atas', href: '/public/gerador-atas.html' },
    { label: 'Separador PDF Férias', href: '/public/separador-pdf-relatorio-de-ferias.html' },
    // ... mais itens
    { label: 'Admin', href: '/public/admin-usuarios.html', admin: true },
    { label: 'Sair', action: logout }
];

function renderSidebar() {
    const sidebar = document.querySelector('.sidebar');
    let html = '<ul>';

    MENU_ITEMS.forEach(item => {
        // Skip admin items se não admin
        if (item.admin && !isAdmin()) return;

        if (item.action) {
            html += `<li><a href="javascript:${item.label.replace(' ', '')}()">${item.label}</a></li>`;
        } else {
            html += `<li><a href="${item.href}">${item.label}</a></li>`;
        }
    });

    html += '</ul>';
    sidebar.innerHTML = html;
}

function isAdmin() {
    // Verificar flag de admin do usuário
    return localStorage.getItem('is_admin') === 'true';
}

🎨 CSS Global (styles.css)

Principais estilos:

/* Layout */
body {
    margin: 0;
    font-family: 'Lato', Arial, sans-serif;
    background: #f5f5f5;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

/* Sidebar */
.sidebar {
    position: fixed;
    left: 0;
    top: 0;
    width: 250px;
    height: 100vh;
    background: #1a3a4a;
    color: white;
    padding: 20px;
    overflow-y: auto;
}

.sidebar ul {
    list-style: none;
    padding: 0;
}

.sidebar li {
    margin: 10px 0;
}

.sidebar a {
    color: white;
    text-decoration: none;
    display: block;
    padding: 10px;
    border-radius: 4px;
    transition: background 0.3s;
}

.sidebar a:hover {
    background: #2c5aa0;
}

/* Main Content */
.main-content {
    margin-left: 250px;
    padding: 30px;
}

/* Forms */
.form-group {
    margin: 20px 0;
}

.form-group label {
    display: block;
    font-weight: bold;
    margin-bottom: 5px;
}

.form-group input,
.form-group select,
.form-group textarea {
    width: 100%;
    max-width: 500px;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-family: inherit;
}

/* Buttons */
.btn {
    background: #2c5aa0;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.3s;
}

.btn:hover {
    background: #1a3a4a;
}

/* Results */
.resultado {
    background: #e8f5e9;
    border-left: 4px solid #4caf50;
    padding: 15px;
    margin-top: 20px;
    border-radius: 4px;
}

/* Progress */
.progress-bar {
    width: 100%;
    height: 20px;
    background: #eee;
    border-radius: 10px;
    overflow: hidden;
}

.progress {
    height: 100%;
    background: #4caf50;
    transition: width 0.3s;
}

📡 Padrão de Integração com API

Fluxo Típico

// 1. Fazer upload (se arquivo)
const fileResult = await uploadFile(fileInput.files[0]);

// 2. Processar
const processResult = await fetch('/api/ferramenta/processar', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`
    },
    body: JSON.stringify({
        input_path: fileResult.file_path,
        param1: formData.param1
    })
});

// 3. Exibir resultado
const result = await processResult.json();
if (result.success) {
    showDownloadLink(result.output_path);
} else {
    showError(result.error);
}

🔄 Comunicação com Socket.io (Tempo Real)

// Conectar
const socket = io();

// Receber progresso
socket.on('job:progress', (data) => {
    updateProgressBar(data.percentage);
    updateStatusText(data.message);
});

// Job concluído
socket.on('job:completed', (data) => {
    showResult(data);
    enableDownloadButton();
});

// Erro
socket.on('job:error', (data) => {
    showError(data.error);
});

📋 Checklist para Nova Página

  • [ ] Criar public/[ferramenta].html
  • [ ] Criar public/js/[ferramenta].js
  • [ ] Formulário com validação
  • [ ] Upload se necessário
  • [ ] Requisição à API
  • [ ] Exibição de resultado
  • [ ] Link de download
  • [ ] Tratamento de erros
  • [ ] Adicionar ao menu sidebar
  • [ ] Testar responsividade

Última atualização: Fevereiro 2026