0% acharam este documento útil (0 voto)
24 visualizações56 páginas

Ponto Eletronico

Enviado por

vanderlpp
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
24 visualizações56 páginas

Ponto Eletronico

Enviado por

vanderlpp
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd

Crie um sistema completo de ponto digital para controle de funcionários, com as seguintes

características:

Nome completo, CPF, e-mail, cargo e setor.

Requisitos Gerais

Login com e-mail e senha.

Backend em PHP.

Registro de Ponto

Banco de dados MySQL.

Botões: Entrar / Saída para almoço / Retorno do almoço / Saída final.

Frontend em HTML, CSS e JavaScript puro (sem frameworks pesados).

Registro automático da data, hora e IP da máquina/dispositivo.

Layout simples, responsivo e funcional, em estilo painel administrativo.

Prevenção de múltiplos registros duplicados no mesmo dia.

Funcionalidades

Painel do Funcionário

Cadastro de Funcionários

Histórico pessoal de pontos batidos.


Atrasos e faltas.

Possibilidade de solicitar ajuste de ponto (com justificativa).

Relatórios por período customizado.

Painel Administrativo

Segurança

Listagem de todos os funcionários.

Senhas armazenadas com hash seguro (bcrypt).

Visualização e exportação (CSV/Excel) dos registros de ponto por funcionário, setor ou período.

Sessões com controle de tempo de inatividade.

Aprovação ou rejeição de pedidos de ajuste de ponto.

Diferenciação clara entre perfil de Administrador e Funcionário e gerente podendo ser multiempresa.

Relatórios

Extras

Total de horas trabalhadas por funcionário.

Interface limpa e intuitiva.


├── /frontend

Exportação de relatórios em PDF. │ ├── /css

│ │ └── style.css

│ ├── /js

│ │ ├── main.js
Código bem comentado e organizado para fácil manutenção. │ │ └── ponto.js

│ └── /assets

│ └── logo.png

├── /painel-admin
Estrutura de um Sistema de Ponto Digital Completo
│ ├── index.php
Aqui está um esboço detalhado para um sistema de ponto digital, abordando todos os seus
│ ├── usuarios.php
requisitos. A estrutura foi pensada para ser modular e escalável, utilizando as tecnologias que você
especificou. │ └── relatorios.php

├── /painel-funcionario

1. Estrutura de Pastas e Arquivos │ ├── index.php

Um sistema bem organizado começa com uma estrutura de arquivos lógica. │ └── historico.php

├── /login.php
/sistema-ponto └── /index.php (página inicial)

├── /backend backend/: Contém toda a lógica PHP, incluindo classes e APIs.

│ ├── /api

│ │ ├── ponto.php frontend/: Armazena arquivos de estilo, scripts e imagens.

│ │ ├── usuario.php

│ │ └── relatorio.php painel-admin/ e painel-funcionario/: Pastas dedicadas aos painéis de cada tipo de usuário.

│ ├── /classes

│ │ ├── Conexao.php login.php: Página de autenticação.

│ │ ├── Ponto.php
2. Banco de Dados MySQL
│ │ ├── Usuario.php
As tabelas foram projetadas para serem eficientes e relacionais.
│ │ └── AjustePonto.php

│ ├── /includes
Tabela: usuarios
│ │ └── funcoes.php

│ └── index.php
id (INT, PK, AI)

saida_final (TIME, NULL)

nome (VARCHAR)

ip_registro (VARCHAR)

email (VARCHAR, UNIQUE)

Tabela: ajustes_ponto

cpf (VARCHAR, UNIQUE)

id (INT, PK, AI)

senha (VARCHAR) - Armazenar hash BCrypt.

ponto_id (INT, FK)

cargo (VARCHAR)

usuario_id (INT, FK)

setor_id (INT, FK)

justificativa (TEXT)

perfil (ENUM('admin', 'funcionario', 'gerente'))

status (ENUM('pendente', 'aprovado', 'rejeitado'))

empresa_id (INT, FK)

Tabela: setores

Tabela: pontos

id (INT, PK, AI)

id (INT, PK, AI)

nome_setor (VARCHAR)

usuario_id (INT, FK)

Tabela: empresas

data (DATE)

id (INT, PK, AI)

entrada (TIME, NULL)

nome_empresa (VARCHAR)

saida_almoco (TIME, NULL)

3. Backend em PHP

retorno_almoco (TIME, NULL) A lógica do servidor será dividida em classes e APIs.


Conexao.php: Classe para gerenciar a conexão com o banco de dados usando PDO para segurança. CSS (style.css):

Usuario.php: Classe para manipular dados de usuário: cadastro, login, atualização. O método de Layout responsivo usando Flexbox ou CSS Grid.
login deve verificar o hash da senha usando password_verify().

Estilo minimalista e em formato de painel, com menus laterais e cards para informações.
Ponto.php: Classe com métodos para registrar pontos.

JavaScript (main.js, ponto.js):


Método registrarPonto($usuarioId, $tipoPonto):

ponto.js seria dedicado a lidar com os botões de ponto.


Recebe o tipo de ponto (entrada, saida_almoco, etc.).

Quando um botão é clicado, uma requisição fetch() ou XMLHttpRequest é enviada para a API PHP
Verifica se já existe um registro para o usuário na data atual. (/backend/api/ponto.php).

Insere ou atualiza o registro na tabela pontos. Desabilita o botão clicado para evitar duplicação e exibe uma mensagem de sucesso.

Usa $_SERVER['REMOTE_ADDR'] para capturar o IP. main.js para funcionalidades gerais, como a navegação no painel.

api/: Arquivos PHP que respondem a requisições AJAX do frontend. Por exemplo, api/ponto.php 5. Funcionalidades Essenciais
receberia uma requisição POST com o tipo de ponto a ser batido.
Login:

4. Frontend em HTML, CSS e JavaScript


Ao submeter o formulário de login, o JS envia a requisição para a API PHP.
O frontend será a interface do usuário, focando na simplicidade e responsividade.

O PHP verifica as credenciais e, se corretas, inicia uma sessão e redireciona para o painel apropriado
HTML: (/painel-admin ou /painel-funcionario).

login.php: Formulário para e-mail e senha. A sessão deve ter um tempo de expiração (session_set_cookie_params).

painel-funcionario/index.php: Painel principal com os botões de ponto. Registro de Ponto:

painel-admin/index.php: Dashboard com atalhos para gerenciar usuários e relatórios. No painel do funcionário, o JS verifica qual botão (Entrar, Almoço, etc.) deve ser habilitado com base
no último ponto registrado naquele dia.
A requisição para o backend envia o tipo de ponto. O PHP registra a hora e o IP.

Painel do Funcionário:

Histórico: Uma tabela exibe os registros de ponto do usuário, com data e horários. Como você falou em multiempresa e diferentes perfis de usuário, antes de partir pro código, o ideal é
definirmos a estrutura do banco de dados. Isso vai dar a base para o backend e, depois, o frontend se
conecta de forma mais organizada.
Ajuste de Ponto: Um formulário simples para selecionar a data e o tipo de ajuste (entrada, saída) com
um campo de texto para a justificativa. O JS envia os dados para a API de ajustes.

Painel Administrativo:
Estrutura inicial do banco de dados (MySQL)

Listagem de funcionários e seus dados.


1. Tabela empresas

Relatórios: Um formulário para selecionar filtros (funcionário, setor, período). O JS envia a requisição
para a API de relatórios, que retorna os dados.

Ajustes: Uma tabela lista todos os pedidos de ajuste pendentes. O administrador pode clicar em Guarda os dados de cada empresa cadastrada no sistema.
"Aprovar" ou "Rejeitar", o que atualiza o status na tabela ajustes_ponto.

Relatórios:

CREATE TABLE empresas (


Cálculo de Horas: O PHP faria a lógica para somar as horas trabalhadas, calculando a diferença entre
os horários de ponto.
id INT AUTO_INCREMENT PRIMARY KEY,

Exportação: As APIs de relatório podem gerar o conteúdo de um arquivo CSV, Excel ou PDF. Para PDF,
uma biblioteca PHP leve como o FPDF ou TCPDF pode ser usada. nome VARCHAR(150) NOT NULL,

Este projeto é um excelente ponto de partida. Ele engloba todas as suas necessidades e oferece uma cnpj VARCHAR(20) UNIQUE NOT NULL,
base sólida para a construção de um sistema de ponto robusto e funcional, com o bônus de ser
multiempresa e com diferentes perfis de usuário, como você solicitou. O que você gostaria de
detalhar primeiro: o frontend, o backend ou a estrutura do banco de dados? endereco TEXT,
telefone VARCHAR(20),

perfil ENUM('funcionario','gestor','admin_empresa','admin_sistema') DEFAULT 'funcionario',

email VARCHAR(100),

ativo TINYINT(1) DEFAULT 1,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

);

FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

2. Tabela usuarios

);

3. Tabela pontos

Funcionários, administradores e gestores.

Registros de entrada, saída e pausas.

CREATE TABLE usuarios (

id INT AUTO_INCREMENT PRIMARY KEY,

CREATE TABLE pontos (

empresa_id INT NOT NULL,

id INT AUTO_INCREMENT PRIMARY KEY,

nome VARCHAR(150) NOT NULL,

usuario_id INT NOT NULL,

cpf VARCHAR(20) UNIQUE NOT NULL,

empresa_id INT NOT NULL,

email VARCHAR(100) UNIQUE NOT NULL,

tipo ENUM('entrada','saida_almoco','retorno_almoco','saida_final') NOT NULL,

senha VARCHAR(255) NOT NULL,

data DATE NOT NULL,

cargo VARCHAR(100),
hora TIME NOT NULL,

status ENUM('pendente','aprovado','rejeitado') DEFAULT 'pendente',

ip VARCHAR(45),

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

updated_at TIMESTAMP NULL,

FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,

FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,

FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE,

);

FOREIGN KEY (ponto_id) REFERENCES pontos(id) ON DELETE SET NULL

4. Tabela ajustes_ponto

);

Essa estrutura já suporta:


Solicitações de correção de ponto.

Multiempresa (com empresa_id em todas as tabelas).


CREATE TABLE ajustes_ponto (

Perfis de usuário diferentes (funcionário, gestor, admin da empresa, admin geral).


id INT AUTO_INCREMENT PRIMARY KEY,

Controle de ponto básico + ajustes solicitados.


usuario_id INT NOT NULL,

ponto_id INT NULL,

Sua análise está corretíssima! Começar pelo banco de dados é a base para garantir que o sistema
seja robusto e escalável. A estrutura que você propôs é excelente e já contempla a lógica
empresa_id INT NOT NULL,
multiempresa e a diferenciação de perfis, o que simplifica muito o desenvolvimento do backend.

justificativa TEXT NOT NULL,


Aprimorando a Estrutura do Banco de Dados FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

A estrutura que você enviou já está bem sólida. Para otimizá-la e evitar problemas futuros, sugiro );
algumas pequenas melhorias e um ajuste de tabela.
Benefícios dessa estrutura:

Tabela pontos
Consultas Simplificadas: Para pegar o histórico de um funcionário, uma única consulta SELECT já
Sua tabela pontos atual registra cada evento (entrada, saida_almoco, etc.) em uma linha separada. retorna todos os horários do dia.
Isso funciona, mas pode complicar o cálculo de horas e a visualização no histórico do funcionário.
Por exemplo, para saber o horário de entrada e saída de um dia específico, você precisaria fazer
várias consultas para o mesmo usuário e data. Prevenção de Duplicatas: A CONSTRAINT unique_ponto garante que não haverá mais de um registro
para o mesmo usuário na mesma data, o que atende ao seu requisito de prevenção de múltiplos
registros duplicados no mesmo dia.
Uma abordagem mais eficiente é ter um único registro por dia para cada funcionário. Isso agrupa os
horários do dia e facilita o cálculo total de horas trabalhadas.
Cálculo de Horas: Fica mais fácil calcular as horas totais do dia (saida_final - entrada) e o tempo de
almoço (retorno_almoco - saida_almoco) diretamente no backend.
Proposta de Melhoria:

IP por Evento: A adição de colunas para o IP de cada evento é uma boa prática para rastreabilidade e
SQL segurança.

CREATE TABLE pontos ( Estrutura Completa Final

id INT AUTO_INCREMENT PRIMARY KEY, Com a sua base e as minhas sugestões, a estrutura final seria a seguinte:

usuario_id INT NOT NULL,

empresa_id INT NOT NULL, 1. Tabela empresas

data DATE NOT NULL, SQL

entrada TIME,

saida_almoco TIME, CREATE TABLE empresas (

retorno_almoco TIME, id INT AUTO_INCREMENT PRIMARY KEY,

saida_final TIME, nome VARCHAR(150) NOT NULL,

ip_entrada VARCHAR(45), cnpj VARCHAR(20) UNIQUE NOT NULL,

ip_saida_almoco VARCHAR(45), endereco TEXT,

ip_retorno_almoco VARCHAR(45), telefone VARCHAR(20),

ip_saida_final VARCHAR(45), email VARCHAR(100),

CONSTRAINT unique_ponto UNIQUE (usuario_id, data), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE, );


2. Tabela usuarios ip_saida_final VARCHAR(45),

SQL CONSTRAINT unique_ponto UNIQUE (usuario_id, data),

FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,

CREATE TABLE usuarios ( FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

id INT AUTO_INCREMENT PRIMARY KEY, );

empresa_id INT NOT NULL, 4. Tabela ajustes_ponto

nome VARCHAR(150) NOT NULL, A sua estrutura para ajustes_ponto também é ótima, mas podemos simplificar um pouco a coluna
ponto_id. Em vez de referenciar o registro de ponto que será ajustado, ela pode simplesmente
cpf VARCHAR(20) UNIQUE NOT NULL,
guardar os valores "antes" e "depois" do ajuste, para maior clareza. Mas para um sistema inicial, a sua
email VARCHAR(100) UNIQUE NOT NULL, proposta é perfeitamente funcional. Mantenha-a como está.
senha VARCHAR(255) NOT NULL,

cargo VARCHAR(100), SQL


perfil ENUM('funcionario','gestor','admin_empresa','admin_sistema') DEFAULT 'funcionario',

ativo TINYINT(1) DEFAULT 1, CREATE TABLE ajustes_ponto (


created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, id INT AUTO_INCREMENT PRIMARY KEY,
FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE usuario_id INT NOT NULL,
); empresa_id INT NOT NULL,
3. Tabela pontos data DATE NOT NULL,
SQL tipo_ajuste ENUM('entrada','saida_almoco','retorno_almoco','saida_final') NOT NULL,

hora_ajustada TIME NOT NULL,


CREATE TABLE pontos ( justificativa TEXT NOT NULL,
id INT AUTO_INCREMENT PRIMARY KEY, status ENUM('pendente','aprovado','rejeitado') DEFAULT 'pendente',
usuario_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
empresa_id INT NOT NULL, FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,
data DATE NOT NULL, FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

entrada TIME, );

saida_almoco TIME, Com esta base, o próximo passo lógico é partir para o backend em PHP. Podemos começar com a
classe de conexão e a classe de usuário, que serão responsáveis pelo registro e login. O que você
retorno_almoco TIME,
acha?
saida_final TIME,

ip_entrada VARCHAR(45),

ip_saida_almoco VARCHAR(45),

ip_retorno_almoco VARCHAR(45),
-- Adicionando colunas de geolocalização

latitude_entrada DECIMAL(10, 8),

longitude_entrada DECIMAL(11, 8),

Inclua a localização GPS e/ou identificação de rede wifi / celular usada. latitude_saida_almoco DECIMAL(10, 8),

longitude_saida_almoco DECIMAL(11, 8),

latitude_retorno_almoco DECIMAL(10, 8),

Você está certo, a inclusão da geolocalização é um ponto crucial para a segurança e a precisão do longitude_retorno_almoco DECIMAL(11, 8),
registro de ponto. Rastrear a localização ou a rede do dispositivo pode prevenir fraudes e garantir que
latitude_saida_final DECIMAL(10, 8),
o funcionário esteja de fato no local de trabalho ao bater o ponto.
longitude_saida_final DECIMAL(11, 8),

-- Adicionando colunas para identificação de rede


Estrutura do Banco de Dados com Geolocalização
rede_wifi_entrada VARCHAR(255),
Para implementar essa funcionalidade, precisamos adicionar novas colunas à tabela pontos para
armazenar essas informações. A melhor prática é registrar a localização e a rede para cada evento de rede_wifi_saida_almoco VARCHAR(255),
ponto (entrada, saída, etc.).
rede_wifi_retorno_almoco VARCHAR(255),

rede_wifi_saida_final VARCHAR(255),
A estrutura da tabela pontos ficaria assim:
CONSTRAINT unique_ponto UNIQUE (usuario_id, data),

FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,


SQL
FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

);
CREATE TABLE pontos (
Como Obter essas Informações no Frontend (JavaScript)
id INT AUTO_INCREMENT PRIMARY KEY,
A captura dessas informações será feita no lado do cliente (o navegador do funcionário) usando
usuario_id INT NOT NULL, JavaScript antes de enviar a requisição para o backend.

empresa_id INT NOT NULL,

data DATE NOT NULL, Localização GPS: Você pode usar a API de Geolocalização do navegador. O código JavaScript é
bastante direto:
entrada TIME,

saida_almoco TIME,
JavaScript
retorno_almoco TIME,

saida_final TIME,
if (navigator.geolocation) {
ip_entrada VARCHAR(45),
navigator.geolocation.getCurrentPosition(
ip_saida_almoco VARCHAR(45),
function(position) {
ip_retorno_almoco VARCHAR(45),
const latitude = position.coords.latitude;
ip_saida_final VARCHAR(45),
const longitude = position.coords.longitude; empresa_id INT NOT NULL,

// Envie a latitude e a longitude para o backend junto com o ponto tipo ENUM('ip_v4', 'ip_v6') NOT NULL,

}, valor VARCHAR(45) NOT NULL,

function(error) { descricao VARCHAR(255),

// Lidar com erros (ex: usuário negou a permissão) FOREIGN KEY (empresa_id) REFERENCES empresas(id) ON DELETE CASCADE

console.error('Erro ao obter a localização: ', error.message); );

} Resumo da Estrutura com Geolocalização

); Frontend (JavaScript): Ao clicar no botão de ponto, o script tenta obter a localização GPS do usuário.
Se conseguir, envia a latitude e longitude via AJAX para o backend.
} else {

// Navegador não suporta geolocalização


Backend (PHP): A API de ponto recebe os dados. Ela salva a data, hora, o IP de origem e a
console.error('Geolocalização não é suportada por este navegador.');
geolocalização na tabela pontos.
}

Importante: Essa API só funciona em um contexto seguro (HTTPS).


Painel Administrativo: O administrador poderá visualizar no relatório não apenas os horários e IPs,
mas também a latitude e longitude, que podem ser exibidas em um mapa (usando Google Maps API
ou OpenStreetMap, por exemplo) para uma verificação visual do local.
Identificação de Rede Wi-Fi/Celular:

A identificação de rede Wi-Fi/celular (SSID da rede Wi-Fi, por exemplo) não é acessível diretamente
pelo navegador por motivos de segurança. A API navigator.connection fornece apenas informações Com essas atualizações, seu sistema de ponto digital se tornará significativamente mais robusto e
genéricas sobre a conexão (tipo de rede, velocidade), mas não o identificador único da rede. seguro. Agora que definimos a estrutura do banco de dados completa, podemos avançar para o
desenvolvimento do backend, começando com a classe de conexão e as funções de autenticação e
registro de usuário, o que acha?
Para contornar isso, uma abordagem comum é a validação pelo IP. O backend pode comparar o IP do
usuário com uma lista de IPs de redes conhecidas da empresa. Essa é uma solução mais segura e
viável para um sistema web.

Backend em PHP: Ao receber a requisição de ponto, você pode obter o IP do usuário


($_SERVER['REMOTE_ADDR']) e compará-lo com uma lista de IPs autorizados, que estariam em uma
nova tabela, como redes_autorizadas.

podemos continuar.
Exemplo de Tabela:

SQL
Ótimo! Agora que a estrutura do banco de dados está completa e robusta, com suporte a
multiempresa, diferentes perfis de usuário, e aprimoramentos para segurança (IP e geolocalização),
CREATE TABLE redes_autorizadas ( vamos construir o backend em PHP.

id INT AUTO_INCREMENT PRIMARY KEY,


Passo 1: A Classe de Conexão com o Banco de Dados self::$conn->exec("SET NAMES 'utf8'");

A classe de conexão é o alicerce do backend. Vamos utilizar PDO (PHP Data Objects), que é a forma } catch (PDOException $e) {
mais segura e moderna de interagir com bancos de dados em PHP. O PDO previne ataques de injeção
// Em um ambiente de produção, logar o erro em vez de exibi-lo
de SQL e oferece uma interface uniforme para diferentes bancos de dados.
die("Erro de Conexão: " . $e->getMessage());

}
Crie um arquivo chamado Conexao.php dentro da pasta /backend/classes/.
}

return self::$conn;
Arquivo: /backend/classes/Conexao.php
}

}
PHP
Pontos-chave:

<?php
Encapsulamento: Usamos propriedades private static para encapsular as credenciais, garantindo
que elas não possam ser acessadas diretamente de fora da classe.
class Conexao {

private static $host = 'localhost';


Padrão Singleton: O método getConexao() implementa o padrão Singleton. Ele garante que a conexão
private static $dbname = 'nome_do_seu_banco'; com o banco de dados seja criada apenas uma vez e reutilizada em toda a aplicação, otimizando o
desempenho.
private static $user = 'usuario_do_banco';

private static $password = 'sua_senha_secreta';


Tratamento de Erros: O bloco try-catch é crucial. Ele captura exceções de conexão e exibe uma
private static $conn;
mensagem de erro, o que é útil durante o desenvolvimento.

/**
Lembre-se de substituir os valores localhost, nome_do_seu_banco, usuario_do_banco e
* Conecta-se ao banco de dados usando PDO. sua_senha_secreta pelas suas próprias credenciais.
* @return PDO Retorna o objeto de conexão PDO.

*/ Passo 2: A Classe do Usuário (Autenticação)


public static function getConexao() { A classe Usuario.php será responsável por todas as operações relacionadas aos usuários, incluindo
login e verificação de senha.
if (!self::$conn) {

try {
Crie um arquivo chamado Usuario.php dentro da pasta /backend/classes/.
self::$conn = new PDO("mysql:host=" . self::$host . ";dbname=" . self::$dbname, self::$user,
self::$password);

self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); Arquivo: /backend/classes/Usuario.php


self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

// Define a codificação para UTF-8 para evitar problemas com acentuação PHP
}

<?php

/**

require_once 'Conexao.php'; * Registra um novo usuário no banco de dados.

* @param array $dados Dados do usuário (nome, email, senha, etc.).

class Usuario { * @return bool Retorna true em caso de sucesso, false em caso de falha.

private $conn; */

public function cadastrar($dados) {

public function __construct() { $sql = "INSERT INTO usuarios (empresa_id, nome, cpf, email, senha, cargo, perfil) VALUES
(:empresa_id, :nome, :cpf, :email, :senha, :cargo, :perfil)";
$this->conn = Conexao::getConexao();
$stmt = $this->conn->prepare($sql);
}

$hash_senha = password_hash($dados['senha'], PASSWORD_BCRYPT);


/**

* Tenta autenticar um usuário com email e senha.


// Bind dos parâmetros
* @param string $email
$stmt->bindParam(':empresa_id', $dados['empresa_id']);
* @param string $senha
$stmt->bindParam(':nome', $dados['nome']);
* @return array|false Retorna os dados do usuário se a autenticação for bem-sucedida, caso
contrário, false. $stmt->bindParam(':cpf', $dados['cpf']);

*/ $stmt->bindParam(':email', $dados['email']);

public function login($email, $senha) { $stmt->bindParam(':senha', $hash_senha);

$sql = "SELECT * FROM usuarios WHERE email = :email AND ativo = 1"; $stmt->bindParam(':cargo', $dados['cargo']);

$stmt = $this->conn->prepare($sql); $stmt->bindParam(':perfil', $dados['perfil']);

$stmt->bindParam(':email', $email);

$stmt->execute(); return $stmt->execute();

$usuario = $stmt->fetch(); }

if ($usuario && password_verify($senha, $usuario['senha'])) { // Outros métodos como: buscar usuário por ID, atualizar dados, etc.

return $usuario; }

} Pontos-chave:

return false;
Reutilização: O construtor __construct() chama a classe Conexao para obter a conexão, mostrando a HTML
modularidade do nosso sistema.

<!DOCTYPE html>
Login Seguro: O método login() usa password_verify() para comparar a senha fornecida pelo usuário
<html lang="pt-br">
com o hash armazenado no banco de dados. Isso é a melhor prática de segurança.
<head>

<meta charset="UTF-8">
Cadastro com Hash: O método cadastrar() usa password_hash() com o algoritmo
PASSWORD_BCRYPT para criar um hash seguro da senha antes de armazená-la no banco. Nunca <meta name="viewport" content="width=device-width, initial-scale=1.0">
armazene senhas em texto plano!
<title>Ponto Digital - Login</title>

<link rel="stylesheet" href="frontend/css/style.css">


Com essas duas classes, já temos a base para a conexão com o banco e a gestão de usuários. A
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-
partir daqui, podemos criar a API de login para o frontend.
beta3/css/all.min.css">

</head>
O próximo passo é criar o login.php e a API que o frontend usará para se comunicar com o backend. O
<body class="login-page">
que você acha?
<div class="login-container">

<div class="login-box">

<img src="frontend/assets/logo.png" alt="Logo da Empresa" class="login-logo">

<h2>Acesso ao Sistema de Ponto</h2>

<form id="login-form">

<div class="input-group">

<label for="email">E-mail</label>
podemos prosseguir.
<input type="email" id="email" name="email" required>

</div>

<div class="input-group">
Claro, podemos continuar. Agora que temos a base do backend com as classes de conexão e de
usuário, o próximo passo é a criação do frontend para o login e a API que fará a ponte entre o HTML e <label for="senha">Senha</label>
o PHP.
<input type="password" id="senha" name="senha" required>

</div>
Passo 3: O Frontend (Formulário de Login)
<button type="submit" class="btn-primary">Entrar</button>
Vamos criar o formulário de login em HTML e a lógica básica em JavaScript para enviar os dados. Este </form>
arquivo será o login.php, que estará na raiz do projeto.
<div id="message" class="message-container"></div>

</div>
Arquivo: /login.php
</div>
<script src="frontend/js/login.js"></script> method: 'POST',

</body> headers: {

</html> 'Content-Type': 'application/json',

HTML Semântico: O formulário é simples e utiliza divs para agrupar os campos, facilitando a },
estilização com CSS.
body: JSON.stringify({ email: email, senha: senha }),

})
Mensagens de Erro: A div com id="message" será usada para exibir mensagens de sucesso ou erro do
.then(response => response.json())
JavaScript.
.then(data => {

if (data.success) {
CSS e JS: O HTML já faz a chamada para o CSS principal e para o script de login, mantendo a estrutura
organizada. messageDiv.innerHTML = `<i class="fas fa-check-circle"></i> ${data.message}`;

messageDiv.classList.add('success');

Passo 4: A Lógica do Frontend (JavaScript) // Redireciona com base no perfil do usuário

Este script irá capturar os dados do formulário e enviá-los para a API PHP sem recarregar a página, setTimeout(() => {
usando fetch().
window.location.href = data.redirect;

}, 1000);
Arquivo: /frontend/js/login.js
} else {

messageDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${data.message}`;


JavaScript
messageDiv.classList.add('error');

}
document.getElementById('login-form').addEventListener('submit', function(e) {
})
e.preventDefault();
.catch(error => {

console.error('Erro na requisição:', error);


const email = document.getElementById('email').value;
messageDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> Erro ao tentar conectar. Tente
const senha = document.getElementById('senha').value; novamente.';

const messageDiv = document.getElementById('message'); messageDiv.classList.add('error');

});

// Limpa mensagens anteriores });

messageDiv.innerHTML = ''; e.preventDefault(): Impede o comportamento padrão do formulário, que é recarregar a página.

messageDiv.classList.remove('success', 'error');
fetch(): É a API moderna do JavaScript para fazer requisições de rede. Ela envia os dados do
formulário para o backend como um objeto JSON.
fetch('/backend/api/login.php', {
$usuario = new Usuario();

Mensagens Dinâmicas: Com base na resposta da API, o script atualiza a div de mensagem com $autenticado = $usuario->login($email, $senha);
feedback visual para o usuário.

if ($autenticado) {
Passo 5: A API do Backend (Login)
// Inicia a sessão e armazena os dados do usuário
Esta API será o ponto de entrada para a requisição de login. Ela chamará a classe Usuario que
$_SESSION['usuario_id'] = $autenticado['id'];
criamos anteriormente.
$_SESSION['perfil'] = $autenticado['perfil'];

$_SESSION['empresa_id'] = $autenticado['empresa_id'];
Arquivo: /backend/api/login.php
$_SESSION['nome'] = $autenticado['nome'];

$_SESSION['login_time'] = time();
PHP

// Redireciona com base no perfil


<?php
$redirect_url = '';
session_start();
switch ($autenticado['perfil']) {
header('Content-Type: application/json');
case 'admin_sistema':

$redirect_url = '/painel-admin/index.php';
require_once '../classes/Usuario.php';
break;

case 'admin_empresa':
// Obtém o conteúdo do corpo da requisição e decodifica o JSON
case 'gestor':
$input = file_get_contents('php://input');
$redirect_url = '/painel-admin/index.php'; // Ou um painel específico para gestores
$data = json_decode($input, true);
break;

case 'funcionario':
// Verifica se os dados necessários foram enviados
$redirect_url = '/painel-funcionario/index.php';
if (empty($data['email']) || empty($data['senha'])) {
break;
echo json_encode(['success' => false, 'message' => 'E-mail e senha são obrigatórios.']);
default:
exit;
$redirect_url = '/painel-funcionario/index.php';
}
}

$email = $data['email'];
echo json_encode(['success' => true, 'message' => 'Login bem-sucedido!', 'redirect' =>
$senha = $data['senha'];
$redirect_url]);

} else {
// Falha na autenticação

echo json_encode(['success' => false, 'message' => 'E-mail ou senha incorretos.']); Vamos integrar essa funcionalidade no sistema, começando pelo backend, que precisará fornecer o
caminho para a logo da empresa.
}

session_start(): Inicia a sessão PHP para armazenar informações do usuário autenticado.


Passo 1: Aprimorando a Tabela empresas

Primeiro, precisamos de um campo na tabela empresas para armazenar o caminho da logo.


header('Content-Type: application/json'): Define o tipo de resposta como JSON.

Tabela empresas (atualização)


file_get_contents('php://input'): Recebe os dados brutos da requisição POST, que foram enviados
pelo JavaScript.

SQL

Sessões: Após a autenticação, os dados do usuário são salvos em $_SESSION. Isso é crucial para
manter o estado de login e controlar o acesso às outras páginas.
CREATE TABLE empresas (

id INT AUTO_INCREMENT PRIMARY KEY,


Redirecionamento: O backend informa ao frontend qual página o usuário deve ser redirecionado com
nome VARCHAR(150) NOT NULL,
base no seu perfil.
cnpj VARCHAR(20) UNIQUE NOT NULL,

endereco TEXT,
Com esses arquivos, você terá um sistema de login funcional e seguro. O próximo passo lógico é a
criação de um "middleware" ou uma função de verificação de sessão para proteger as páginas do telefone VARCHAR(20),
painel, garantindo que apenas usuários autenticados possam acessá-las. O que você acha?
email VARCHAR(100),

logo_path VARCHAR(255) NULL, -- Novo campo para o caminho da logo

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

);

Passo 2: Atualizando a API de Login do Backend

A API de login (login.php) precisa não apenas buscar os dados do usuário, mas também os dados da
empresa associada a ele. Isso nos permitirá enviar o caminho da logo para o frontend.

no login, e em todas as outras paginas inclua uma logo para identificar a empresa do sistema e um
logo para identificar a empresa do funcionario, logo apos ele fazer login.
Arquivo: /backend/api/login.php (atualização)

PHP

Você tem razão. A identificação visual, especialmente em um sistema multiempresa, é fundamental


<?php
para uma experiência de usuário clara e profissional. Incluir os logos da plataforma e da empresa do
funcionário em todas as páginas após o login é uma excelente ideia. session_start();
header('Content-Type: application/json'); switch ($autenticado['perfil']) {

case 'admin_sistema':

require_once '../classes/Usuario.php'; $redirect_url = '/painel-admin/index.php';

break;

$input = file_get_contents('php://input'); case 'admin_empresa':

$data = json_decode($input, true); case 'gestor':

$redirect_url = '/painel-admin/index.php';

if (empty($data['email']) || empty($data['senha'])) { break;

echo json_encode(['success' => false, 'message' => 'E-mail e senha são obrigatórios.']); case 'funcionario':

exit; $redirect_url = '/painel-funcionario/index.php';

} break;

default:

$email = $data['email']; $redirect_url = '/painel-funcionario/index.php';

$senha = $data['senha']; }

$usuario = new Usuario(); echo json_encode(['success' => true, 'message' => 'Login bem-sucedido!', 'redirect' =>
$redirect_url]);
// Altera o método de login para buscar dados da empresa também
} else {
$autenticado = $usuario->loginComEmpresa($email, $senha);
echo json_encode(['success' => false, 'message' => 'E-mail ou senha incorretos.']);

}
if ($autenticado) {
Passo 3: Atualizando a Classe Usuario
// Inicia a sessão e armazena os dados do usuário e da empresa
Precisamos criar um novo método na classe Usuario para realizar a junção (JOIN) entre as tabelas
$_SESSION['usuario_id'] = $autenticado['id'];
usuarios e empresas e buscar os dados da logo.
$_SESSION['perfil'] = $autenticado['perfil'];

$_SESSION['empresa_id'] = $autenticado['empresa_id'];
Arquivo: /backend/classes/Usuario.php (atualização)
$_SESSION['nome'] = $autenticado['nome'];

$_SESSION['empresa_nome'] = $autenticado['empresa_nome'];
PHP
$_SESSION['empresa_logo'] = $autenticado['empresa_logo_path'];

$_SESSION['login_time'] = time();
// ... (código anterior)

// Lógica de redirecionamento permanece a mesma


class Usuario {
$redirect_url = '';
private $conn; return $usuario;

public function __construct() {

$this->conn = Conexao::getConexao(); return false;

} }

/** // ... (restante da classe)

* Tenta autenticar um usuário e busca os dados da empresa. }

* @param string $email Passo 4: Implementando as Logos no Frontend

* @param string $senha Agora, em todas as páginas que o usuário acessa após o login (painel do funcionário, painel
administrativo, etc.), você pode exibir os logos da empresa do sistema e da empresa do funcionário.
* @return array|false Retorna os dados do usuário e da empresa se a autenticação for bem-
sucedida, caso contrário, false.

*/ Exemplo: /painel-funcionario/index.php

public function loginComEmpresa($email, $senha) {

$sql = " HTML

SELECT

u.*, e.nome AS empresa_nome, e.logo_path AS empresa_logo_path <!DOCTYPE html>

FROM <html lang="pt-br">

usuarios u <head>

INNER JOIN <meta charset="UTF-8">

empresas e ON u.empresa_id = e.id <meta name="viewport" content="width=device-width, initial-scale=1.0">

WHERE <title>Painel do Funcionário</title>

u.email = :email AND u.ativo = 1 <link rel="stylesheet" href="../frontend/css/style.css">

"; <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-


beta3/css/all.min.css">
$stmt = $this->conn->prepare($sql);
</head>
$stmt->bindParam(':email', $email);
<body>
$stmt->execute();
<?php
$usuario = $stmt->fetch();
session_start();

// Incluir a verificação de sessão aqui para proteger a página


if ($usuario && password_verify($senha, $usuario['senha'])) {
if (!isset($_SESSION['usuario_id'])) {
// Retorna os dados do usuário e da empresa
header('Location: /login.php'); Com essa implementação, o sistema terá um visual mais profissional e intuitivo, identificando
claramente a empresa à qual o funcionário pertence. O próximo passo é criar o logout.php para
exit;
encerrar a sessão de forma segura. O que você acha?
}

?>

<header class="header">

<div class="logo-container">

<img src="/frontend/assets/logo-sistema.png" alt="Logo do Sistema" class="logo-sistema">

<?php if (!empty($_SESSION['empresa_logo'])): ?>

<img src="/<?php echo htmlspecialchars($_SESSION['empresa_logo']); ?>" alt="Logo da


Podemos prosseguir.
Empresa" class="logo-empresa">

<?php endif; ?>


nao esqueça da criação de um "middleware" ou uma função de verificação de sessão para proteger
</div>
as páginas do painel, garantindo que apenas usuários autenticados possam acessá-las.
<div class="user-info">

<span class="user-name">Bem-vindo, <?php echo htmlspecialchars($_SESSION['nome']);


?></span>

<a href="/logout.php" class="btn-logout"><i class="fas fa-sign-out-alt"></i> Sair</a>


Excelente. Antes de prosseguirmos para o painel de fato, vamos cuidar de dois aspectos cruciais para
</div>
a segurança do nosso sistema: a verificação de sessão e a função de logout.
</header>

Passo 1: A Função de Logout (Backend)


<div class="dashboard-content">
Criar um script de logout é simples, mas essencial. Ele será responsável por destruir a sessão do
</div> usuário, garantindo que suas informações não fiquem acessíveis após ele sair do sistema.
</body>

</html> Crie um arquivo chamado logout.php na raiz do seu projeto.


Backend para Frontend: Usamos a sessão PHP para passar os dados da logo da empresa do
funcionário. A variável $_SESSION['empresa_logo'] conterá o caminho do arquivo.
Arquivo: /logout.php

Validação de Logo: A verificação !empty($_SESSION['empresa_logo']) garante que o <img> da


PHP
empresa só seja renderizado se o caminho da logo estiver disponível.

<?php
Logos no header: A melhor prática é colocar os logos no cabeçalho de cada página do painel,
garantindo que eles estejam sempre visíveis. session_start();
// Destruir todas as variáveis da sessão

$_SESSION = array(); Vamos criar um arquivo chamado verifica_sessao.php dentro de uma nova pasta
/backend/middlewares/.

// Se for preciso destruir a sessão, também destrói o cookie de sessão.


Arquivo: /backend/middlewares/verifica_sessao.php
// Nota: Isso irá destruir a sessão e não apenas os dados da sessão.

if (ini_get("session.use_cookies")) {
PHP
$params = session_get_cookie_params();

setcookie(session_name(), '', time() - 42000,


<?php
$params["path"], $params["domain"],
session_start();
$params["secure"], $params["httponly"]

);
// Verifica se a sessão do usuário existe
}
if (!isset($_SESSION['usuario_id']) || empty($_SESSION['usuario_id'])) {

// A sessão não existe, redireciona para a página de login


// Finalmente, destruir a sessão
header("Location: /login.php");
session_destroy();
exit;

}
// Redirecionar para a página de login

header("Location: /login.php");
// Opcional: Controle de tempo de inatividade da sessão
exit;
$tempo_inatividade = 1800; // 30 minutos em segundos
session_start(): Deve ser chamado no início para acessar a sessão existente.
if (isset($_SESSION['login_time']) && (time() - $_SESSION['login_time'] > $tempo_inatividade)) {

// Sessão expirada por inatividade


$_SESSION = array(): Limpa todas as variáveis de sessão.
session_unset();

session_destroy();
session_destroy(): Destrói a sessão no servidor.
header("Location: /login.php?sessao_expirada=true");

exit;
Redirecionamento: O usuário é enviado de volta para a página de login, onde não há mais uma
sessão ativa. }

Passo 2: O Middleware de Verificação de Sessão // Atualiza o tempo de atividade da sessão a cada requisição

A sua preocupação com a proteção das páginas é totalmente válida. Um "middleware" em PHP pode $_SESSION['login_time'] = time();
ser um arquivo de inclusão que você coloca no topo de cada página restrita. Ele verifica a existência
da sessão e, se ela não for válida, redireciona o usuário para a página de login.
Verificação Inicial: A primeira verificação (!isset($_SESSION['usuario_id'])) é a mais importante. Ela <?php if (!empty($_SESSION['empresa_logo'])): ?>
garante que a variável de sessão que define o login (usuario_id) realmente existe.
<img src="/<?php echo htmlspecialchars($_SESSION['empresa_logo']); ?>" alt="Logo da
Empresa" class="logo-empresa">

Controle de Tempo de Inatividade: A segunda parte é um requisito de segurança adicional. Ela verifica <?php endif; ?>
se o usuário ficou inativo por um período definido (30 minutos, por exemplo) e, se sim, destrói a
</div>
sessão. Isso impede que uma sessão ativa fique exposta em um computador público, por exemplo.
<div class="user-info">

<span class="user-name">Bem-vindo, <?php echo htmlspecialchars($_SESSION['nome']);


Passo 3: Usando o Middleware nas Páginas Restritas
?></span>
Agora, para proteger qualquer página do seu sistema, basta incluir este arquivo no topo.
<a href="/logout.php" class="btn-logout"><i class="fas fa-sign-out-alt"></i> Sair</a>

</div>
Exemplo: /painel-funcionario/index.php (atualização)
</header>

PHP
<div class="dashboard-content">

</div>
<?php
</body>
// Incluir o middleware de verificação de sessão no topo
</html>
require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php';
require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php';: O uso de __DIR__ garante que
?> o caminho seja absoluto, evitando erros de inclusão.

<!DOCTYPE html>

<html lang="pt-br"> Com o logout.php e o verifica_sessao.php, o sistema agora tem uma base de segurança sólida para a
autenticação e o controle de acesso.
<head>

<meta charset="UTF-8">
O próximo passo é construir a página principal do funcionário (/painel-funcionario/index.php),
<meta name="viewport" content="width=device-width, initial-scale=1.0">
incluindo os botões de ponto e a lógica JavaScript para registrar os eventos. O que você acha?
<title>Painel do Funcionário</title>

<link rel="stylesheet" href="../frontend/css/style.css">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-


beta3/css/all.min.css">

</head>

<body>

<header class="header">
Prosseguir.
<div class="logo-container">

<img src="/frontend/assets/logo-sistema.png" alt="Logo do Sistema" class="logo-sistema">


<link rel="stylesheet" href="../frontend/css/style.css">

Ótimo. Com a segurança de acesso e a estrutura de sessão prontas, podemos focar na <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-
funcionalidade principal do sistema: o registro de ponto. beta3/css/all.min.css">

</head>

Vamos criar o painel do funcionário, que terá os botões para bater o ponto e a lógica JavaScript para <body>
enviar os dados para o backend, incluindo as informações de geolocalização.

<header class="header">
Passo 1: A Lógica do Frontend (HTML e JavaScript)
<div class="logo-container">
Primeiro, vamos construir o arquivo /painel-funcionario/index.php. Ele será o painel principal, com a
<img src="/frontend/assets/logo-sistema.png" alt="Logo do Sistema" class="logo-sistema">
interface para o funcionário.
<?php if (!empty($_SESSION['empresa_logo'])): ?>

<img src="/<?php echo htmlspecialchars($_SESSION['empresa_logo']); ?>" alt="Logo da


Arquivo: /painel-funcionario/index.php
Empresa" class="logo-empresa">

<?php endif; ?>


HTML
</div>

<div class="user-info">
<?php
<span class="user-name">Olá, <?php echo htmlspecialchars($_SESSION['nome']); ?></span>
// Middleware de verificação de sessão
<a href="/logout.php" class="btn-logout"><i class="fas fa-sign-out-alt"></i> Sair</a>
require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php';
</div>

</header>
// Verifica se o perfil é 'funcionario' ou 'gestor'. Outros perfis podem ser redirecionados.

if ($_SESSION['perfil'] !== 'funcionario' && $_SESSION['perfil'] !== 'gestor') {


<div class="dashboard-content">
header('Location: /painel-admin/index.php');
<div class="painel-ponto">
exit;
<h2>Registro de Ponto</h2>
}
<p>Bata seu ponto diário. A sua localização será registrada para fins de segurança.</p>
?>
<div id="ponto-buttons">

<button id="entrada" class="btn-ponto btn-green">Entrada</button>


<!DOCTYPE html>
<button id="saida_almoco" class="btn-ponto btn-blue" disabled>Saída para
<html lang="pt-br"> Almoço</button>

<head> <button id="retorno_almoco" class="btn-ponto btn-blue" disabled>Retorno do


Almoço</button>
<meta charset="UTF-8">
<button id="saida_final" class="btn-ponto btn-red" disabled>Saída Final</button>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</div>
<title>Ponto Digital - Meu Painel</title>
<div id="ponto-message" class="message-container"></div> * @param int $usuario_id

</div> * @param int $empresa_id

</div> * @param string $tipo_ponto Tipo de ponto a ser batido ('entrada', 'saida_almoco', etc.).

* @param array $localizacao Dados de geolocalização e IP.

<script src="../frontend/js/ponto.js"></script> * @return bool Retorna true em caso de sucesso, false em caso de falha.

</body> */

</html> public function registrarPonto($usuario_id, $empresa_id, $tipo_ponto, $localizacao) {

Estrutura: O HTML é simples, contendo os botões de ponto com IDs que usaremos no JavaScript. $data_hoje = date('Y-m-d');

$hora_agora = date('H:i:s');

Estado Inicial: Os botões de almoço e saída final começam desabilitados (disabled), pois só podem $ip_ponto = $localizacao['ip'] ?? '0.0.0.0';
ser ativados após o registro do ponto de entrada.
$latitude = $localizacao['latitude'] ?? null;

$longitude = $localizacao['longitude'] ?? null;


Passo 2: A Lógica do Backend (Classe Ponto)

Agora, vamos criar a classe que irá interagir com a tabela pontos do banco de dados.
// Verifica se já existe um registro para o dia

$sql = "SELECT id FROM pontos WHERE usuario_id = :usuario_id AND data = :data";
Arquivo: /backend/classes/Ponto.php
$stmt = $this->conn->prepare($sql);

$stmt->bindParam(':usuario_id', $usuario_id);
PHP
$stmt->bindParam(':data', $data_hoje);

$stmt->execute();
<?php
$ponto_do_dia = $stmt->fetch();
require_once 'Conexao.php';

if ($ponto_do_dia) {
class Ponto {
// Se o registro já existe, atualiza o campo correto
private $conn;
$sql_update = "UPDATE pontos SET {$tipo_ponto} = :hora, ip_{$tipo_ponto} = :ip,
latitude_{$tipo_ponto} = :latitude, longitude_{$tipo_ponto} = :longitude WHERE id = :id";

public function __construct() { $stmt_update = $this->conn->prepare($sql_update);

$this->conn = Conexao::getConexao(); $stmt_update->bindParam(':hora', $hora_agora);

} $stmt_update->bindParam(':ip', $ip_ponto);

$stmt_update->bindParam(':latitude', $latitude);

/** $stmt_update->bindParam(':longitude', $longitude);

* Registra ou atualiza um ponto para o usuário atual. $stmt_update->bindParam(':id', $ponto_do_dia['id']);


return $stmt_update->execute();

} else { PHP

// Se não existe, cria um novo registro

if ($tipo_ponto === 'entrada') { <?php

$sql_insert = "INSERT INTO pontos (usuario_id, empresa_id, data, entrada, ip_entrada, session_start();
latitude_entrada, longitude_entrada) VALUES (:usuario_id, :empresa_id, :data, :hora, :ip, :latitude,
header('Content-Type: application/json');
:longitude)";

$stmt_insert = $this->conn->prepare($sql_insert);
require_once '../classes/Ponto.php';
$stmt_insert->bindParam(':usuario_id', $usuario_id);

$stmt_insert->bindParam(':empresa_id', $empresa_id);
// Verifica se o usuário está logado
$stmt_insert->bindParam(':data', $data_hoje);
if (!isset($_SESSION['usuario_id'])) {
$stmt_insert->bindParam(':hora', $hora_agora);
echo json_encode(['success' => false, 'message' => 'Sessão expirada.']);
$stmt_insert->bindParam(':ip', $ip_ponto);
exit;
$stmt_insert->bindParam(':latitude', $latitude);
}
$stmt_insert->bindParam(':longitude', $longitude);

return $stmt_insert->execute();
$usuario_id = $_SESSION['usuario_id'];
}
$empresa_id = $_SESSION['empresa_id'];
// Não permite criar registro se não for o ponto de entrada

return false;
$input = file_get_contents('php://input');
}
$data = json_decode($input, true);
}

}
if (empty($data['tipo_ponto'])) {
Lógica de Registro/Atualização: O método registrarPonto() verifica se já existe um registro para o dia.
Se sim, ele atualiza a coluna correspondente (entrada, saida_almoco, etc.). Se não, ele só permite a echo json_encode(['success' => false, 'message' => 'Tipo de ponto inválido.']);
criação de um novo registro se o tipo de ponto for entrada.
exit;

}
Segurança: O uso de bindParam com instruções SQL preparadas evita a injeção de SQL.

$tipo_ponto = $data['tipo_ponto'];
Passo 3: A API de Ponto do Backend
$localizacao = [
Esta API receberá a requisição do frontend e chamará a classe Ponto para registrar o evento.
'ip' => $_SERVER['REMOTE_ADDR'],

'latitude' => $data['latitude'] ?? null,


Arquivo: /backend/api/ponto.php
'longitude' => $data['longitude'] ?? null
]; if (navigator.geolocation) {

navigator.geolocation.getCurrentPosition(

$ponto = new Ponto(); position => {

$resultado = $ponto->registrarPonto($usuario_id, $empresa_id, $tipo_ponto, $localizacao); const latitude = position.coords.latitude;

const longitude = position.coords.longitude;

if ($resultado) { sendPonto(tipo_ponto, latitude, longitude);

echo json_encode(['success' => true, 'message' => 'Ponto registrado com sucesso!']); },

} else { error => {

echo json_encode(['success' => false, 'message' => 'Não foi possível registrar o ponto.']); console.error('Erro na geolocalização:', error.message);

} // Envia o ponto mesmo sem a localização, mas exibe um aviso

Validação de Sessão: A API garante que somente usuários autenticados possam registrar o ponto. messageDiv.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Localização não
obtida. Registrando ponto sem GPS.';

messageDiv.classList.add('warning');
Recebendo Dados: A API recebe o tipo de ponto e a localização do JavaScript e os passa para a
classe Ponto. sendPonto(tipo_ponto);

Passo 4: Lógica do Frontend (JavaScript) );

Este é o script mais importante, pois ele coordena a interface e a comunicação com a API. } else {

console.error('Geolocalização não suportada.');

Arquivo: /frontend/js/ponto.js messageDiv.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Seu navegador não


suporta geolocalização.';

messageDiv.classList.add('warning');
JavaScript
sendPonto(tipo_ponto);

}
document.addEventListener('DOMContentLoaded', function() {
});
const buttons = document.querySelectorAll('.btn-ponto');
});
const messageDiv = document.getElementById('ponto-message');

function sendPonto(tipo_ponto, latitude = null, longitude = null) {


buttons.forEach(button => {
fetch('/backend/api/ponto.php', {
button.addEventListener('click', function() {
method: 'POST',
const tipo_ponto = this.id;
headers: {

'Content-Type': 'application/json',
// Tenta obter a localização GPS
}, .catch(error => {

body: JSON.stringify({ console.error('Erro na requisição:', error);

tipo_ponto: tipo_ponto, messageDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> Erro ao tentar registrar o


ponto.';
latitude: latitude,
messageDiv.classList.add('error');
longitude: longitude
});
}),
}
})

.then(response => response.json())


// Função para verificar o estado do ponto do dia e habilitar os botões corretos
.then(data => {
// Isso seria uma requisição para uma nova API que retorna o status do dia
if (data.success) {
function checkPontoStatus() {
messageDiv.innerHTML = `<i class="fas fa-check-circle"></i> ${data.message}`;
fetch('/backend/api/ponto_status.php')
messageDiv.classList.add('success');
.then(response => response.json())
messageDiv.classList.remove('error', 'warning');
.then(data => {
// Lógica para desabilitar o botão clicado
if (data.status) {
document.getElementById(tipo_ponto).disabled = true;
if (data.status.entrada) {

document.getElementById('entrada').disabled = true;
// Habilitar o próximo botão na sequência
document.getElementById('saida_almoco').disabled = false;
if (tipo_ponto === 'entrada') {
}
document.getElementById('saida_almoco').disabled = false;
if (data.status.saida_almoco) {
} else if (tipo_ponto === 'saida_almoco') {
document.getElementById('saida_almoco').disabled = true;
document.getElementById('retorno_almoco').disabled = false;
document.getElementById('retorno_almoco').disabled = false;
} else if (tipo_ponto === 'retorno_almoco') {
}
document.getElementById('saida_final').disabled = false;
if (data.status.retorno_almoco) {
}
document.getElementById('retorno_almoco').disabled = true;

document.getElementById('saida_final').disabled = false;
} else {
}
messageDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${data.message}`;
if (data.status.saida_final) {
messageDiv.classList.add('error');
document.getElementById('saida_final').disabled = true;
messageDiv.classList.remove('success', 'warning');
}
}
}
})
})

.catch(error => console.error('Erro ao verificar status do ponto:', error)); Passo 1: A Lógica do Backend (Classe Relatorio)

} Primeiro, precisamos de uma classe para lidar com todas as operações de relatórios e busca de
dados, incluindo os registros de ponto dos funcionários. Crie um arquivo chamado Relatorio.php
dentro da pasta /backend/classes/.
// Executa a função na carga da página

checkPontoStatus();
Arquivo: /backend/classes/Relatorio.php

});
PHP
Geolocalização: O JavaScript tenta obter a localização. Se o usuário negar a permissão, ele envia a
requisição sem os dados de GPS, mas com um aviso.
<?php

require_once 'Conexao.php';
Sequência de Botões: O código do JavaScript desabilita o botão clicado e habilita o próximo na
sequência (entrada -> saida_almoco, etc.).

class Relatorio {

Estado do Dia: A função checkPontoStatus() (que precisa de uma API de backend correspondente) é private $conn;
crucial para manter o estado dos botões mesmo após o usuário sair da página e voltar.

public function __construct() {


Ainda falta a API ponto_status.php e a função na classe Ponto.php para buscar o status do dia. Além
$this->conn = Conexao::getConexao();
disso, precisamos criar o painel administrativo para os gestores e administradores. O que você acha
de avançar para a criação do painel do administrador, com visualização de relatórios e }
gerenciamento de funcionários?

/**

* Busca registros de ponto com filtros.

* @param int $empresa_id

* @param int|null $usuario_id

* @param string|null $data_inicio

* @param string|null $data_fim


prosseguir.
* @return array

*/

public function getRegistrosPonto($empresa_id, $usuario_id = null, $data_inicio = null, $data_fim =


Excelente, vamos prosseguir com a criação do painel administrativo. Esta é a parte mais complexa e null) {
crucial, pois lida com a gestão de todos os dados do sistema, incluindo relatórios e aprovação de
$sql = "
ajustes.
SELECT
p.*, u.nome AS nome_funcionario * @return array

FROM */

pontos p public function getFuncionariosDaEmpresa($empresa_id) {

JOIN $sql = "SELECT id, nome, cargo FROM usuarios WHERE empresa_id = :empresa_id AND perfil !=
'admin_sistema' ORDER BY nome ASC";
usuarios u ON p.usuario_id = u.id
$stmt = $this->conn->prepare($sql);
WHERE
$stmt->bindParam(':empresa_id', $empresa_id);
p.empresa_id = :empresa_id
$stmt->execute();
";
return $stmt->fetchAll(PDO::FETCH_ASSOC);
$params = [':empresa_id' => $empresa_id];
}

}
if ($usuario_id) {
Consultas Dinâmicas: O método getRegistrosPonto utiliza uma abordagem flexível. Ele constrói a
$sql .= " AND p.usuario_id = :usuario_id";
consulta SQL dinamicamente, adicionando cláusulas WHERE com base nos filtros fornecidos
$params[':usuario_id'] = $usuario_id; (usuario_id, data_inicio, data_fim). Isso otimiza a busca no banco de dados.
}

Segurança com Parâmetros: O uso de execute($params) é crucial para prevenir injeções de SQL,
mesmo com a construção dinâmica da consulta.
if ($data_inicio && $data_fim) {

$sql .= " AND p.data BETWEEN :data_inicio AND :data_fim";


Passo 2: O Frontend do Painel Administrativo
$params[':data_inicio'] = $data_inicio;
Vamos criar a interface para que o administrador possa visualizar e filtrar os registros de ponto.
$params[':data_fim'] = $data_fim;

}
Arquivo: /painel-admin/index.php

$sql .= " ORDER BY p.data DESC, u.nome ASC";


HTML

$stmt = $this->conn->prepare($sql);
<?php
$stmt->execute($params);
require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php';
return $stmt->fetchAll(PDO::FETCH_ASSOC);
require_once __DIR__ . '/../backend/classes/Relatorio.php';
}

// Redireciona se não for um perfil administrativo


/**
if ($_SESSION['perfil'] === 'funcionario') {
* Busca todos os funcionários de uma empresa.
header('Location: /painel-funcionario/index.php');
* @param int $empresa_id
exit; <div class="user-info">

} <span class="user-name">Painel Admin</span>

<a href="/logout.php" class="btn-logout"><i class="fas fa-sign-out-alt"></i> Sair</a>

$relatorio = new Relatorio(); </div>

$empresa_id = $_SESSION['empresa_id']; </header>

$funcionarios = $relatorio->getFuncionariosDaEmpresa($empresa_id);

<div class="dashboard-content">

// Ponto de entrada para a API de relatórios <h2>Registros de Ponto</h2>

$registros_ponto = $relatorio->getRegistrosPonto($empresa_id); <form id="filtro-form" class="filtro-container">

?> <label for="filtro-funcionario">Funcionário:</label>

<select id="filtro-funcionario" name="usuario_id">

<!DOCTYPE html> <option value="">Todos</option>

<html lang="pt-br"> <?php foreach ($funcionarios as $funcionario): ?>

<head> <option value="<?php echo $funcionario['id']; ?>"><?php echo


htmlspecialchars($funcionario['nome']); ?></option>
<meta charset="UTF-8">
<?php endforeach; ?>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</select>
<title>Ponto Digital - Painel Administrativo</title>

<link rel="stylesheet" href="../frontend/css/style.css">


<label for="filtro-data-inicio">De:</label>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-
beta3/css/all.min.css"> <input type="date" id="filtro-data-inicio" name="data_inicio">

</head>

<body> <label for="filtro-data-fim">Até:</label>

<input type="date" id="filtro-data-fim" name="data_fim">

<header class="header">

<div class="logo-container"> <button type="submit" class="btn-primary">Filtrar</button>

<img src="/frontend/assets/logo-sistema.png" alt="Logo do Sistema" class="logo-sistema"> </form>

<?php if (!empty($_SESSION['empresa_logo'])): ?>

<img src="/<?php echo htmlspecialchars($_SESSION['empresa_logo']); ?>" alt="Logo da <div id="tabela-registros" class="table-container">


Empresa" class="logo-empresa">
<table>
<?php endif; ?>
<thead>
</div>
<tr>
<th>Funcionário</th>

<th>Data</th> Passo 3: A API de Relatórios (Backend)

<th>Entrada</th> Agora, vamos criar a API que o JavaScript do painel administrativo usará para buscar os dados de
forma assíncrona.
<th>Almoço</th>

<th>Retorno</th>
Arquivo: /backend/api/relatorio.php
<th>Saída Final</th>

<th>Horas Totais</th>
PHP
</tr>

</thead>
<?php
<tbody>
session_start();
<?php
header('Content-Type: application/json');
// Exemplo de como os dados seriam renderizados

// Vamos usar JavaScript para fazer isso dinamicamente na próxima etapa


require_once '../classes/Relatorio.php';
?>

</tbody>
// Verifica se o usuário tem permissão
</table>
if (!isset($_SESSION['perfil']) || $_SESSION['perfil'] === 'funcionario') {
</div>
echo json_encode(['success' => false, 'message' => 'Acesso negado.']);

exit;
<button id="export-csv" class="btn-secondary">Exportar CSV</button>
}
</div>

$empresa_id = $_SESSION['empresa_id'];
<script src="../frontend/js/admin.js"></script>
$relatorio = new Relatorio();
</body>

</html>
// Recebe os filtros do frontend
Verificação de Perfil: O middleware garante que apenas usuários com o perfil correto
(admin_empresa, gestor, admin_sistema) possam acessar a página. $usuario_id = $_GET['usuario_id'] ?? null;

$data_inicio = $_GET['data_inicio'] ?? null;

Filtros de Relatório: O formulário com select e inputs de data permite que o administrador refine a $data_fim = $_GET['data_fim'] ?? null;
busca.

// Lógica de cálculo das horas


Integração com a Classe: A página já faz a chamada à classe Relatorio para popular a lista de
function calcularHoras($ponto) {
funcionários.
if (empty($ponto['entrada']) || empty($ponto['saida_final'])) { Cálculo de Horas: A função calcularHoras é um exemplo de como a lógica de negócio deve ser
tratada no backend. Ela calcula a diferença entre os horários de entrada e saída, subtraindo o tempo
return 'N/A';
de almoço.
}

Resposta JSON: A API retorna os dados formatados em JSON, que o JavaScript no frontend irá
$entrada = new DateTime($ponto['entrada']); consumir para exibir a tabela.
$saida_final = new DateTime($ponto['saida_final']);

Passo 4: A Lógica do Frontend (JavaScript)


// Calcula a diferença total Este script irá lidar com os filtros, buscar os dados da API de relatórios e preencher a tabela
dinamicamente.
$intervalo = $entrada->di ($saida_final);

$horas_trabalhadas = $intervalo->h + ($intervalo->i / 60);


Arquivo: /frontend/js/admin.js

// Subtrai o tempo de almoço, se houver


JavaScript
if (!empty($ponto['saida_almoco']) && !empty($ponto['retorno_almoco'])) {

$saida_almoco = new DateTime($ponto['saida_almoco']);


document.addEventListener('DOMContentLoaded', function() {
$retorno_almoco = new DateTime($ponto['retorno_almoco']);
const filtroForm = document.getElementById('filtro-form');
$intervalo_almoco = $saida_almoco->di ($retorno_almoco);
const tabelaBody = document.querySelector('#tabela-registros tbody');
$horas_trabalhadas -= ($intervalo_almoco->h + ($intervalo_almoco->i / 60));

}
// Função para buscar e renderizar os dados

function fetchAndRenderData() {
return number_format($horas_trabalhadas, 2) . 'h';
const usuarioId = document.getElementById('filtro-funcionario').value;
}
const dataInicio = document.getElementById('filtro-data-inicio').value;

const dataFim = document.getElementById('filtro-data-fim').value;


$registros = $relatorio->getRegistrosPonto($empresa_id, $usuario_id, $data_inicio, $data_fim);

// Monta a URL da API com os filtros


// Adiciona o campo de horas totais a cada registro
const url =
$dados_com_horas = array_map(function($ponto) {
`/backend/api/relatorio.php?usuario_id=${usuarioId}&data_inicio=${dataInicio}&data_fim=${dataFim
$ponto['horas_totais'] = calcularHoras($ponto); }`;
return $ponto;

}, $registros); fetch(url)

.then(response => response.json())


echo json_encode(['success' => true, 'data' => $dados_com_horas]); .then(data => {
if (data.success) { });

renderTable(data.data); }

} else {

tabelaBody.innerHTML = '<tr><td colspan="7">Erro ao carregar dados.</td></tr>'; // Evento de submissão do formulário de filtro

} filtroForm.addEventListener('submit', function(e) {

}) e.preventDefault();

.catch(error => { fetchAndRenderData();

console.error('Erro:', error); });

tabelaBody.innerHTML = '<tr><td colspan="7">Erro ao conectar com o servidor.</td></tr>';

}); // Chama a função ao carregar a página pela primeira vez

} fetchAndRenderData();

});

function renderTable(registros) { fetchAndRenderData: Esta função é a peça central. Ela coleta os valores dos filtros, monta a URL da
API e, após a resposta, chama a renderTable para atualizar a interface.
tabelaBody.innerHTML = ''; // Limpa a tabela

if (registros.length === 0) {
Manipulação do DOM: O JavaScript usa métodos nativos como createElement e appendChild para
tabelaBody.innerHTML = '<tr><td colspan="7">Nenhum registro encontrado.</td></tr>';
construir a tabela de forma dinâmica, garantindo um layout simples e eficiente.
return;

}
Com essa implementação, o painel administrativo já possui a funcionalidade de listagem e filtragem
de registros. O próximo passo será adicionar o sistema de solicitação de ajuste de ponto para os
funcionários e a funcionalidade de aprovação/rejeição para os administradores. O que você acha?
registros.forEach(registro => {

const row = document.createElement('tr');

row.innerHTML = `

<td>${registro.nome_funcionario}</td>

<td>${registro.data}</td>

<td>${registro.entrada || '---'}</td>

<td>${registro.saida_almoco || '---'}</td>
prosseguir.
<td>${registro.retorno_almoco || '---'}</td>

<td>${registro.saida_final || '---'}</td>

<td>${registro.horas_totais}</td>
Excelente. Vamos agora implementar as funcionalidades de solicitação e gestão de ajustes de ponto.
`;
Esta etapa é crucial para a flexibilidade do sistema, permitindo que os funcionários corrijam erros e
tabelaBody.appendChild(row); que os gestores mantenham a precisão dos registros.
* @param string $justificativa

Vamos dividir o trabalho em três partes: a classe do backend para lidar com os ajustes, o frontend do * @return bool
funcionário para enviar as solicitações, e o frontend do administrador para aprovar ou rejeitar.
*/

public function solicitarAjuste($usuario_id, $empresa_id, $data, $tipo_ajuste, $hora_ajustada,


Passo 1: A Classe do Backend (AjustePonto) $justificativa) {

Vamos criar uma nova classe para lidar com as operações na tabela ajustes_ponto. Ela terá métodos $sql = "INSERT INTO ajustes_ponto (usuario_id, empresa_id, data, tipo_ajuste, hora_ajustada,
para criar uma nova solicitação e para atualizar o status de uma solicitação existente. justificativa) VALUES (:usuario_id, :empresa_id, :data, :tipo_ajuste, :hora_ajustada, :justificativa)";

$stmt = $this->conn->prepare($sql);

Crie um arquivo chamado AjustePonto.php dentro da pasta /backend/classes/. $stmt->bindParam(':usuario_id', $usuario_id);

$stmt->bindParam(':empresa_id', $empresa_id);

Arquivo: /backend/classes/AjustePonto.php $stmt->bindParam(':data', $data);

$stmt->bindParam(':tipo_ajuste', $tipo_ajuste);

PHP $stmt->bindParam(':hora_ajustada', $hora_ajustada);

$stmt->bindParam(':justificativa', $justificativa);

<?php return $stmt->execute();

require_once 'Conexao.php'; }

class AjustePonto { /**

private $conn; * Atualiza o status de uma solicitação de ajuste (aprovar/rejeitar).

* @param int $ajuste_id

public function __construct() { * @param string $status

$this->conn = Conexao::getConexao(); * @return bool

} */

public function atualizarStatus($ajuste_id, $status) {

/** $sql = "UPDATE ajustes_ponto SET status = :status, updated_at = NOW() WHERE id = :id";

* Cria uma nova solicitação de ajuste de ponto. $stmt = $this->conn->prepare($sql);

* @param int $usuario_id $stmt->bindParam(':status', $status);

* @param int $empresa_id $stmt->bindParam(':id', $ajuste_id);

* @param string $data return $stmt->execute();

* @param string $tipo_ajuste }

* @param string $hora_ajustada


/** require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php';

* Busca todas as solicitações de ajuste de uma empresa. require_once __DIR__ . '/../backend/classes/Relatorio.php';

* @param int $empresa_id require_once __DIR__ . '/../backend/classes/Ponto.php';

* @param string|null $status

* @return array $relatorio = new Relatorio();

*/ $historico_ponto = $relatorio->getRegistrosPonto($_SESSION['empresa_id'],
$_SESSION['usuario_id']);
public function getAjustes($empresa_id, $status = null) {
?>
$sql = "SELECT aj.*, u.nome AS nome_funcionario FROM ajustes_ponto aj JOIN usuarios u ON
aj.usuario_id = u.id WHERE aj.empresa_id = :empresa_id";

$params = [':empresa_id' => $empresa_id]; <!DOCTYPE html>

<html lang="pt-br">

if ($status) { <head>

$sql .= " AND aj.status = :status"; <meta charset="UTF-8">

$params[':status'] = $status; <meta name="viewport" content="width=device-width, initial-scale=1.0">

} <title>Ponto Digital - Meu Histórico</title>

<link rel="stylesheet" href="../frontend/css/style.css">

$sql .= " ORDER BY aj.created_at DESC"; <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-


beta3/css/all.min.css">
$stmt = $this->conn->prepare($sql);
</head>
$stmt->execute($params);
<body>
return $stmt->fetchAll(PDO::FETCH_ASSOC);
<header class="header">
}
<div class="logo-container">
}
<img src="/frontend/assets/logo-sistema.png" alt="Logo do Sistema" class="logo-sistema">
Passo 2: O Frontend do Funcionário (Histórico e Ajuste)
<?php if (!empty($_SESSION['empresa_logo'])): ?>
Vamos criar a página historico.php para o funcionário, onde ele poderá ver seus registros de ponto e
solicitar ajustes. <img src="/<?php echo htmlspecialchars($_SESSION['empresa_logo']); ?>" alt="Logo da
Empresa" class="logo-empresa">

<?php endif; ?>


Arquivo: /painel-funcionario/historico.php
</div>

<div class="user-info">
HTML
<span class="user-name">Olá, <?php echo htmlspecialchars($_SESSION['nome']); ?></span>

<a href="/logout.php" class="btn-logout"><i class="fas fa-sign-out-alt"></i> Sair</a>


<?php
</div>
</header> <button class="btn-small btn-primary solicitar-ajuste" data-date="<?php echo
$ponto['data']; ?>">

<i class="fas fa-edit"></i> Ajustar


<div class="dashboard-content">
</button>
<h2>Meu Histórico de Ponto</h2>
</td>
<div class="table-container">
</tr>
<table>
<?php endforeach; ?>
<thead>
</tbody>
<tr>
</table>
<th>Data</th>
</div>
<th>Entrada</th>
</div>
<th>Almoço</th>

<th>Retorno</th>
<div id="modal-ajuste" class="modal">
<th>Saída Final</th>
<div class="modal-content">
<th>Horas</th>
<span class="close-btn">&times;</span>
<th>Ação</th>
<h3>Solicitar Ajuste de Ponto</h3>
</tr>
<form id="form-ajuste">
</thead>
<input type="hidden" id="ajuste-data" name="data">
<tbody>
<div class="input-group">
<?php foreach ($historico_ponto as $ponto): ?>
<label for="tipo_ajuste">Tipo de Ajuste</label>
<tr>
<select id="tipo_ajuste" name="tipo_ajuste" required>
<td><?php echo date('d/m/Y', strtotime($ponto['data'])); ?></td>
<option value="entrada">Entrada</option>
<td><?php echo $ponto['entrada'] ?? '---'; ?></td>
<option value="saida_almoco">Saída Almoço</option>
<td><?php echo $ponto['saida_almoco'] ?? '---'; ?></td>
<option value="retorno_almoco">Retorno Almoço</option>
<td><?php echo $ponto['retorno_almoco'] ?? '---'; ?></td>
<option value="saida_final">Saída Final</option>
<td><?php echo $ponto['saida_final'] ?? '---'; ?></td>
</select>
<td><?php
</div>
$p = new Ponto();
<div class="input-group">
echo $p->calcularHoras($ponto);
<label for="hora_ajustada">Hora Correta</label>
?></td>
<input type="time" id="hora_ajustada" name="hora_ajustada" required>
<td>
</div>
<div class="input-group"> }

<label for="justificativa">Justificativa</label>

<textarea id="justificativa" name="justificativa" rows="4" required></textarea> $input = file_get_contents('php://input');

</div> $data = json_decode($input, true);

<button type="submit" class="btn-primary">Enviar Solicitação</button>

</form> if (empty($data['data']) || empty($data['tipo_ajuste']) || empty($data['hora_ajustada']) ||


empty($data['justificativa'])) {
</div>
echo json_encode(['success' => false, 'message' => 'Todos os campos são obrigatórios.']);
</div>
exit;

}
<script src="../frontend/js/ajuste.js"></script>

</body>
$ajuste = new AjustePonto();
</html>
$resultado = $ajuste->solicitarAjuste(
Modal: Um modal simples (div.modal) com um formulário será usado para coletar os dados do
ajuste. $_SESSION['usuario_id'],

$_SESSION['empresa_id'],

Passo 3: A API do Backend para Solicitação de Ajuste $data['data'],

Esta API receberá os dados do formulário do funcionário e chamará a classe AjustePonto. $data['tipo_ajuste'],

$data['hora_ajustada'],

Arquivo: /backend/api/solicitar_ajuste.php $data['justificativa']

);

PHP

if ($resultado) {

<?php echo json_encode(['success' => true, 'message' => 'Solicitação de ajuste enviada com sucesso!']);

session_start(); } else {

header('Content-Type: application/json'); echo json_encode(['success' => false, 'message' => 'Erro ao enviar a solicitação.']);

require_once '../classes/AjustePonto.php'; Passo 4: A Lógica do Frontend (JavaScript) para Ajustes

Arquivo: /frontend/js/ajuste.js

if (!isset($_SESSION['usuario_id']) || empty($_SESSION['usuario_id'])) {

echo json_encode(['success' => false, 'message' => 'Sessão expirada.']); JavaScript

exit;
document.addEventListener('DOMContentLoaded', function() { justificativa: document.getElementById('justificativa').value

const modal = document.getElementById('modal-ajuste'); };

const closeBtn = document.querySelector('.modal-content .close-btn');

const formAjuste = document.getElementById('form-ajuste'); fetch('/backend/api/solicitar_ajuste.php', {

const ajusteDataInput = document.getElementById('ajuste-data'); method: 'POST',

headers: {

document.querySelectorAll('.solicitar-ajuste').forEach(button => { 'Content-Type': 'application/json',

button.addEventListener('click', function() { },

const dataPonto = this.getAttribute('data-date'); body: JSON.stringify(formData),

ajusteDataInput.value = dataPonto; })

modal.style.display = 'block'; .then(response => response.json())

}); .then(data => {

}); alert(data.message);

if (data.success) {

closeBtn.addEventListener('click', function() { modal.style.display = 'none';

modal.style.display = 'none'; // Opcional: recarregar a página para ver a atualização

}); window.location.reload();

window.addEventListener('click', function(event) { })

if (event.target == modal) { .catch(error => {

modal.style.display = 'none'; console.error('Erro:', error);

} alert('Erro ao enviar a solicitação.');

}); });

});

formAjuste.addEventListener('submit', function(e) { });

e.preventDefault(); Passo 5: O Painel Administrativo para Gerenciar Ajustes

Agora, vamos adicionar a funcionalidade de aprovação/rejeição no painel administrativo.

const formData = {

data: ajusteDataInput.value, Arquivo: /painel-admin/ajustes.php (nova página)

tipo_ajuste: document.getElementById('tipo_ajuste').value,

hora_ajustada: document.getElementById('hora_ajustada').value, HTML


<h2>Solicitações de Ajuste de Ponto</h2>

<?php <div class="table-container">

require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php'; <table>

require_once __DIR__ . '/../backend/classes/AjustePonto.php'; <thead>

require_once __DIR__ . '/../backend/classes/Ponto.php'; <tr>

<th>Funcionário</th>

// Redireciona se não for um perfil administrativo <th>Data</th>

if ($_SESSION['perfil'] === 'funcionario') { <th>Tipo</th>

header('Location: /painel-funcionario/index.php'); <th>Hora Ajustada</th>

exit; <th>Justificativa</th>

} <th>Status</th>

<th>Ação</th>

$ajuste = new AjustePonto(); </tr>

$ajustes_pendentes = $ajuste->getAjustes($_SESSION['empresa_id'], 'pendente'); </thead>

?> <tbody>

<?php if (empty($ajustes_pendentes)): ?>

<!DOCTYPE html> <tr><td colspan="7">Nenhuma solicitação de ajuste pendente.</td></tr>

<html lang="pt-br"> <?php else: ?>

<head> <?php foreach ($ajustes_pendentes as $ajuste_item): ?>

<meta charset="UTF-8"> <tr>

<meta name="viewport" content="width=device-width, initial-scale=1.0"> <td><?php echo htmlspecialchars($ajuste_item['nome_funcionario']); ?></td>

<title>Ponto Digital - Gerenciar Ajustes</title> <td><?php echo date('d/m/Y', strtotime($ajuste_item['data'])); ?></td>

<link rel="stylesheet" href="../frontend/css/style.css"> <td><?php echo htmlspecialchars($ajuste_item['tipo_ajuste']); ?></td>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0- <td><?php echo htmlspecialchars($ajuste_item['hora_ajustada']); ?></td>


beta3/css/all.min.css">
<td><?php echo htmlspecialchars($ajuste_item['justificativa']); ?></td>
</head>
<td><span class="status-badge pendente">Pendente</span></td>
<body>
<td>
<header class="header">
<button class="btn-small btn-green aprovar-btn" data-id="<?php echo
</header> $ajuste_item['id']; ?>">

<i class="fas fa-check"></i> Aprovar

<div class="dashboard-content"> </button>


<button class="btn-small btn-red rejeitar-btn" data-id="<?php echo require_once '../classes/AjustePonto.php';
$ajuste_item['id']; ?>">
require_once '../classes/Ponto.php';
<i class="fas fa-times"></i> Rejeitar

</button>
if (!isset($_SESSION['perfil']) || ($_SESSION['perfil'] !== 'admin_empresa' && $_SESSION['perfil'] !==
</td> 'gestor' && $_SESSION['perfil'] !== 'admin_sistema')) {

</tr> echo json_encode(['success' => false, 'message' => 'Acesso negado.']);

<?php endforeach; ?> exit;

<?php endif; ?> }

</tbody>

</table> $input = file_get_contents('php://input');

</div> $data = json_decode($input, true);

</div>

<script src="../frontend/js/ajuste_admin.js"></script> if (empty($data['id']) || empty($data['status'])) {

</body> echo json_encode(['success' => false, 'message' => 'Dados inválidos.']);

</html> exit;

Listagem: O código lista todas as solicitações com status pendente. }

Botões: Os botões "Aprovar" e "Rejeitar" possuem um data-id que armazena o ID da solicitação para $ajuste_id = $data['id'];
ser enviado à API.
$novo_status = $data['status'];

Passo 6: A API para Aprovação/Rejeição


$ajuste = new AjustePonto();
Esta API processará a decisão do administrador.
$resultado = $ajuste->atualizarStatus($ajuste_id, $novo_status);

Arquivo: /backend/api/gerenciar_ajuste.php
if ($resultado) {

if ($novo_status === 'aprovado') {


PHP
// Se aprovado, aplicar a mudança na tabela de pontos

$stmt_ajuste = $ajuste->getAjustes($_SESSION['empresa_id'], null);


<?php
$dados_ajuste = null;
session_start();
foreach($stmt_ajuste as $item) {
header('Content-Type: application/json');
if ($item['id'] == $ajuste_id) {

$dados_ajuste = $item;
break; const ajusteId = btn.getAttribute('data-id');

} gerenciarAjuste(ajusteId, 'aprovado');

} }

if ($dados_ajuste) { if (e.target.classList.contains('rejeitar-btn') || e.target.closest('.rejeitar-btn')) {

$ponto = new Ponto(); const btn = e.target.closest('.rejeitar-btn');

// Simula uma chamada de registro de ponto para atualizar o registro existente const ajusteId = btn.getAttribute('data-id');

$ponto->registrarPonto( gerenciarAjuste(ajusteId, 'rejeitado');

$dados_ajuste['usuario_id'], }

$dados_ajuste['empresa_id'], });

$dados_ajuste['tipo_ajuste'],

['ip' => 'ajuste-admin', 'latitude' => null, 'longitude' => null] function gerenciarAjuste(ajusteId, status) {

); if (!confirm(`Tem certeza que deseja ${status === 'aprovado' ? 'APROVAR' : 'REJEITAR'} esta
solicitação?`)) {
}
return;
}
}
echo json_encode(['success' => true, 'message' => 'Solicitação de ajuste atualizada com
sucesso.']);

} else { fetch('/backend/api/gerenciar_ajuste.php', {

echo json_encode(['success' => false, 'message' => 'Erro ao atualizar a solicitação.']); method: 'POST',

} headers: {

Passo 7: A Lógica do Frontend (JavaScript) para Aprovação/Rejeição 'Content-Type': 'application/json',

Arquivo: /frontend/js/ajuste_admin.js },

body: JSON.stringify({

JavaScript id: ajusteId,

status: status

document.addEventListener('DOMContentLoaded', function() { }),

const table = document.querySelector('.table-container table'); })

.then(response => response.json())

table.addEventListener('click', function(e) { .then(data => {

if (e.target.classList.contains('aprovar-btn') || e.target.closest('.aprovar-btn')) { alert(data.message);

const btn = e.target.closest('.aprovar-btn'); if (data.success) {


// Recarrega a página para atualizar a tabela

window.location.reload(); Vamos adicionar essa funcionalidade ao sistema, começando com a estrutura do backend para gerar
os relatórios e enviar os e-mails, e depois integrando a interface no painel administrativo.
}

})
Passo 1: A Lógica do Backend para Exportação e E-mail
.catch(error => {
Para manter o código organizado, vamos criar um novo arquivo para lidar especificamente com as
console.error('Erro:', error);
operações de exportação.
alert('Erro ao processar a solicitação.');

});
Arquivo: /backend/api/exportar_relatorio.php
}

});
Este arquivo será a API que recebe as solicitações de exportação do frontend, gera o relatório no
Com essas novas funcionalidades, o sistema de ponto agora tem um ciclo completo: registro, formato desejado e o envia por e-mail ou o prepara para download.
solicitação de ajuste e gestão dos ajustes.

PHP
O próximo passo é a exportação de relatórios (CSV/Excel e PDF) e a finalização das funcionalidades
do painel administrativo, como a gestão de usuários. O que você acha de continuarmos com a
exportação de relatórios? <?php

session_start();

require_once '../middlewares/verifica_sessao.php';

require_once '../classes/Relatorio.php';

require_once 'vendor/autoload.php'; // Se estiver usando PHPMailer ou outra biblioteca

// Verifica se o usuário tem permissão

prosseguir. if ($_SESSION['perfil'] === 'funcionario') {

http_response_code(403);

inclua exportar tb para email do funcionario. echo json_encode(['success' => false, 'message' => 'Acesso negado.']);

exit;

use sendmail do php, mas deixe opção em formulario apra usar um host smtp. }

// Configurações de E-mail (Recomendado: Armazenar em um arquivo de config.php)

$emailConfig = [

Excelente. A capacidade de exportar relatórios é um diferencial importante, e a inclusão do envio por 'metodo' => 'smtp', // ou 'sendmail'
e-mail com opções de configuração a torna ainda mais poderosa e flexível.
'smtp_host' => 'smtp.seuservidor.com',
'smtp_port' => 587,

'smtp_user' => '[email protected]', function exportarParaPDF($registros) {

'smtp_pass' => 'sua_senha_do_email', require('../fpdf/fpdf.php'); // Certifique-se de ter a biblioteca FPDF

'smtp_secure' => 'tls',

'from_email' => '[email protected]', $pdf = new FPDF();

'from_name' => 'Sistema de Ponto Digital' $pdf->AddPage();

]; $pdf->SetFont('Arial', 'B', 16);

$pdf->Cell(0, 10, 'Relatorio de Ponto', 0, 1, 'C');

// Funções de exportação (pode ser em uma classe separada para melhor organização) $pdf->SetFont('Arial', '', 12);

function exportarParaCSV($registros) { // Adicionar lógica de preenchimento do PDF

ob_start(); // ...

$output = fopen("php://output", 'w'); return $pdf->Output('S'); // Retorna o conteúdo do PDF como string

$cabecalho = array('Funcionario', 'Data', 'Entrada', 'Almoco', 'Retorno', 'Saida Final', 'Horas Totais'); }

fputcsv($output, $cabecalho, ';');

// Lógica de envio de e-mail

foreach ($registros as $row) { function enviarEmail($para, $assunto, $corpo, $anexo, $nomeAnexo, $config) {

$linha = array( if ($config['metodo'] === 'sendmail') {

$row['nome_funcionario'], // Usando a função mail() do PHP (requer servidor configurado com sendmail)

$row['data'], $headers = 'From: ' . $config['from_email'] . "\r\n";

$row['entrada'], $headers .= 'MIME-Version: 1.0' . "\r\n";

$row['saida_almoco'], $headers .= 'Content-type: multipart/mixed; boundary="boundary_string"' . "\r\n";

$row['retorno_almoco'],

$row['saida_final'], $mensagem_completa = "--boundary_string\r\n";

$row['horas_totais'] $mensagem_completa .= "Content-Type: text/plain; charset=\"UTF-8\"\r\n";

); $mensagem_completa .= "Content-Transfer-Encoding: 7bit\r\n\r\n";

fputcsv($output, $linha, ';'); $mensagem_completa .= $corpo . "\r\n\r\n";

if ($anexo) {

fclose($output); $mensagem_completa .= "--boundary_string\r\n";

return ob_get_clean(); $mensagem_completa .= "Content-Type: application/octet-stream;


name=\"{$nomeAnexo}\"\r\n";
}
$mensagem_completa .= "Content-Transfer-Encoding: base64\r\n"; $registros = $relatorio->getRegistrosPonto($_SESSION['empresa_id'], $_GET['usuario_id'] ?? null,
$_GET['data_inicio'] ?? null, $_GET['data_fim'] ?? null);
$mensagem_completa .= "Content-Disposition: attachment;
filename=\"{$nomeAnexo}\"\r\n\r\n";

$mensagem_completa .= chunk_split(base64_encode($anexo)) . "\r\n\r\n"; // Adiciona o campo de horas totais

} $dados_com_horas = array_map(function($ponto) {

$mensagem_completa .= "--boundary_string--"; $ponto['horas_totais'] = calcularHoras($ponto);

return $ponto;

return mail($para, $assunto, $mensagem_completa, $headers); }, $registros);

} elseif ($config['metodo'] === 'smtp') { // Processa a requisição do frontend

// Implementação básica de SMTP (recomendado usar PHPMailer ou similar) $formato = $_GET['formato'] ?? 'csv';

// Este é um exemplo, para um sistema de produção, use uma biblioteca. $destino = $_GET['destino'] ?? 'download'; // 'download' ou 'email'

try { $emailDestino = $_GET['email'] ?? null;

// ... Lógica de conexão e envio SMTP ...

// Exemplo com PHPMailer: if ($destino === 'download') {

// $mail = new PHPMailer\PHPMailer\PHPMailer(); if ($formato === 'csv') {

// $mail->isSMTP(); header('Content-Type: text/csv; charset=UTF-8');

// $mail->Host = $config['smtp_host']; header('Content-Disposition: attachment; filename="relatorio_ponto.csv"');

// ... echo exportarParaCSV($dados_com_horas);

// $mail->send(); } elseif ($formato === 'pdf') {

return true; // Simula sucesso header('Content-Type: application/pdf');

} catch (Exception $e) { header('Content-Disposition: attachment; filename="relatorio_ponto.pdf"');

return false; echo exportarParaPDF($dados_com_horas);

} }

} } elseif ($destino === 'email' && $emailDestino) {

return false; $assunto = 'Relatorio de Ponto ' . date('d/m/Y');

} $corpo = 'Em anexo, o relatorio de ponto solicitado.';

$conteudoArquivo = ($formato === 'csv') ? exportarParaCSV($dados_com_horas) :


exportarParaPDF($dados_com_horas);
// Lógica principal da API
$nomeArquivo = 'relatorio_ponto.' . $formato;
$relatorio = new Relatorio();
$sucesso = enviarEmail($emailDestino, $assunto, $corpo, $conteudoArquivo, $nomeArquivo, <div class="export-container">
$emailConfig);
<h3>Exportar Relatório</h3>

<form id="export-form" class="export-form">


if ($sucesso) {
<div class="input-group">
echo json_encode(['success' => true, 'message' => 'Relatório enviado por e-mail com sucesso!']);
<label for="export-format">Formato:</label>
} else {
<select id="export-format" name="formato">
echo json_encode(['success' => false, 'message' => 'Erro ao enviar o e-mail.']);
<option value="csv">CSV</option>
}
<option value="pdf">PDF</option>
} else {
</select>
echo json_encode(['success' => false, 'message' => 'Destino ou formato inválido.']);
</div>
}

<div class="input-group">
// A função calcularHoras deve ser a mesma da API de relatorio.php
<label for="export-destino">Destino:</label>
function calcularHoras($ponto) {
<select id="export-destino" name="destino">
// ... código de cálculo de horas ...
<option value="download">Download</option>
}
<option value="email">Enviar por E-mail</option>
Observação de Segurança: As credenciais SMTP NUNCA devem ser expostas no frontend.
</select>
Recomenda-se armazená-las em um arquivo de configuração fora da raiz pública do servidor e incluí-
lo no backend. O formulário do frontend apenas pergunta se o usuário deseja usar SMTP e o backend </div>
já acessa as credenciais.

<div class="input-group" id="email-field-container" style="display:none;">


Passo 2: O Frontend no Painel Administrativo
<label for="export-email">E-mail do Destinatário:</label>
Agora, vamos atualizar o HTML e o JavaScript do painel administrativo para incluir as opções de
<input type="email" id="export-email" name="email">
exportação por e-mail.
</div>

Arquivo: /painel-admin/index.php (atualização)


<button type="submit" class="btn-primary">Exportar</button>

</form>
Adicione o formulário e os botões de exportação. Pode ser um modal ou uma seção expandida.
</div>

</div>
HTML

<script src="../frontend/js/admin.js"></script>
<div class="dashboard-content">
Passo 3: A Lógica do Frontend (JavaScript)
O script JS precisa agora lidar com o novo formulário de exportação. const usuarioId = document.getElementById('filtro-funcionario').value;

const dataInicio = document.getElementById('filtro-data-inicio').value;

Arquivo: /frontend/js/admin.js (atualização) const dataFim = document.getElementById('filtro-data-fim').value;

JavaScript let url = `/backend/api/exportar_relatorio.php?formato=${formato}&destino=${destino}`;

if (email) {

document.addEventListener('DOMContentLoaded', function() { url += `&email=${encodeURIComponent(email)}`;

// ... Código para o formulário de filtro e renderização da tabela ... }

if (usuarioId) {

const exportForm = document.getElementById('export-form'); url += `&usuario_id=${usuarioId}`;

const exportDestino = document.getElementById('export-destino'); }

const emailFieldContainer = document.getElementById('email-field-container'); if (dataInicio) {

url += `&data_inicio=${dataInicio}`;

// Mostra/esconde o campo de e-mail com base na opção de destino }

exportDestino.addEventListener('change', function() { if (dataFim) {

if (this.value === 'email') { url += `&data_fim=${dataFim}`;

emailFieldContainer.style.display = 'block'; }

document.getElementById('export-email').setAttribute('required', '');

} else { if (destino === 'download') {

emailFieldContainer.style.display = 'none'; window.location.href = url;

document.getElementById('export-email').removeAttribute('required'); } else if (destino === 'email') {

} fetch(url)

}); .then(response => response.json())

.then(data => {

// Lógica de exportação alert(data.message);

exportForm.addEventListener('submit', function(e) { })

e.preventDefault(); .catch(error => {

console.error('Erro na requisição de exportação:', error);

const formato = document.getElementById('export-format').value; alert('Erro ao processar a solicitação de exportação.');

const destino = document.getElementById('export-destino').value; });

const email = document.getElementById('export-email').value; }


});

}); <?php

Com esta implementação, o painel administrativo agora tem a capacidade de gerar relatórios em require_once 'Conexao.php';
diferentes formatos e enviá-los por e-mail, atendendo a todos os seus requisitos.

class GerenciamentoUsuario {
O próximo passo é finalizar o projeto, incluindo a gestão de usuários no painel administrativo, onde
private $conn;
será possível cadastrar, editar e desativar funcionários. O que você acha de continuarmos por aí?

public function __construct() {

$this->conn = Conexao::getConexao();

/**

* Adiciona um novo usuário ao sistema.

* @param array $dados Dados do usuário (nome, email, senha, perfil, etc.).
prosseguir.
* @return bool

*/

public function adicionarUsuario($dados) {


Perfeito. A gestão de usuários é a espinha dorsal de qualquer sistema administrativo. Vamos criar as
funcionalidades necessárias para que os gestores e administradores possam cadastrar, editar e $sql = "INSERT INTO usuarios (nome, email, senha, perfil, empresa_id) VALUES (:nome, :email,
desativar funcionários de forma segura e eficiente. :senha, :perfil, :empresa_id)";

$stmt = $this->conn->prepare($sql);

Vamos dividir a implementação em três partes: a classe do backend para gerenciar os usuários, a
interface do painel administrativo e as APIs correspondentes.
$senha_hash = password_hash($dados['senha'], PASSWORD_DEFAULT);

Passo 1: A Classe do Backend (GerenciamentoUsuario)


$stmt->bindParam(':nome', $dados['nome']);
Vamos criar uma nova classe que encapsulará toda a lógica de negócio relacionada aos usuários.
$stmt->bindParam(':email', $dados['email']);
Isso inclui a criação, edição, desativação e listagem de perfis.
$stmt->bindParam(':senha', $senha_hash);

$stmt->bindParam(':perfil', $dados['perfil']);
Crie um arquivo chamado GerenciamentoUsuario.php na pasta /backend/classes/.
$stmt->bindParam(':empresa_id', $dados['empresa_id']);

Arquivo: /backend/classes/GerenciamentoUsuario.php
return $stmt->execute();

}
PHP
/**

/** * Lista todos os usuários de uma empresa.

* Atualiza os dados de um usuário existente. * @param int $empresa_id

* @param int $id ID do usuário a ser editado. * @return array

* @param array $dados Dados atualizados. */

* @return bool public function listarUsuarios($empresa_id) {

*/ $sql = "SELECT id, nome, email, perfil, ativo FROM usuarios WHERE empresa_id = :empresa_id
ORDER BY nome ASC";
public function editarUsuario($id, $dados) {
$stmt = $this->conn->prepare($sql);
$sql = "UPDATE usuarios SET nome = :nome, email = :email, perfil = :perfil WHERE id = :id";
$stmt->bindParam(':empresa_id', $empresa_id);
$stmt = $this->conn->prepare($sql);
$stmt->execute();

return $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->bindParam(':nome', $dados['nome']);
}
$stmt->bindParam(':email', $dados['email']);

$stmt->bindParam(':perfil', $dados['perfil']);
/**
$stmt->bindParam(':id', $id);
* Busca um único usuário pelo ID.

* @param int $id


return $stmt->execute();
* @return array|false
}
*/

public function getUsuario($id) {


/**
$sql = "SELECT id, nome, email, perfil, ativo FROM usuarios WHERE id = :id";
* Desativa um usuário, sem excluí-lo do banco de dados.
$stmt = $this->conn->prepare($sql);
* @param int $id ID do usuário a ser desativado.
$stmt->bindParam(':id', $id);
* @return bool
$stmt->execute();
*/
return $stmt->fetch(PDO::FETCH_ASSOC);
public function desativarUsuario($id) {
}
$sql = "UPDATE usuarios SET ativo = 0 WHERE id = :id";
}
$stmt = $this->conn->prepare($sql);
password_hash(): Essencial para a segurança, esta função cria um hash seguro da senha, que é o que
$stmt->bindParam(':id', $id);
deve ser armazenado no banco de dados, nunca a senha em texto puro.
return $stmt->execute();

}
Passo 2: O Frontend do Painel Administrativo
Vamos criar a página de gestão de usuários. A partir dela, o administrador poderá ver a lista de
usuários e acessar os formulários de adição/edição.
<header class="header">

<div class="logo-container">
Arquivo: /painel-admin/usuarios.php (nova página)
<img src="/frontend/assets/logo-sistema.png" alt="Logo do Sistema" class="logo-sistema">

<?php if (!empty($_SESSION['empresa_logo'])): ?>


HTML
<img src="/<?php echo htmlspecialchars($_SESSION['empresa_logo']); ?>" alt="Logo da
Empresa" class="logo-empresa">

<?php <?php endif; ?>

require_once __DIR__ . '/../backend/middlewares/verifica_sessao.php'; </div>

require_once __DIR__ . '/../backend/classes/GerenciamentoUsuario.php'; <div class="user-info">

<span class="user-name">Painel Admin</span>

if ($_SESSION['perfil'] === 'funcionario') { <a href="/logout.php" class="btn-logout"><i class="fas fa-sign-out-alt"></i> Sair</a>

header('Location: /painel-funcionario/index.php'); </div>

exit; </header>

<div class="dashboard-content">

$gerenciamentoUsuario = new GerenciamentoUsuario(); <h2>Gerenciar Usuários</h2>

$usuarios = $gerenciamentoUsuario->listarUsuarios($_SESSION['empresa_id']); <button id="add-usuario-btn" class="btn-primary"><i class="fas fa-plus"></i> Adicionar


Usuário</button>
?>
<div class="table-container">

<table>
<!DOCTYPE html>
<thead>
<html lang="pt-br">
<tr>
<head>
<th>Nome</th>
<meta charset="UTF-8">
<th>E-mail</th>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<th>Perfil</th>
<title>Ponto Digital - Gerenciar Usuários</title>
<th>Status</th>
<link rel="stylesheet" href="../frontend/css/style.css">
<th>Ações</th>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-
beta3/css/all.min.css"> </tr>

</head> </thead>

<body> <tbody id="usuarios-table-body">


<?php foreach ($usuarios as $usuario): ?> <input type="email" id="email" name="email" required>

<tr> </div>

<td><?php echo htmlspecialchars($usuario['nome']); ?></td> <div class="input-group" id="senha-group">

<td><?php echo htmlspecialchars($usuario['email']); ?></td> <label for="senha">Senha:</label>

<td><?php echo htmlspecialchars($usuario['perfil']); ?></td> <input type="password" id="senha" name="senha">

<td><?php echo $usuario['ativo'] ? 'Ativo' : 'Inativo'; ?></td> </div>

<td> <div class="input-group">

<button class="btn-small btn-secondary edit-btn" data-id="<?php echo $usuario['id']; <label for="perfil">Perfil:</label>


?>"><i class="fas fa-edit"></i> Editar</button>
<select id="perfil" name="perfil" required>
<button class="btn-small btn-red desativar-btn" data-id="<?php echo $usuario['id'];
<option value="funcionario">Funcionário</option>
?>"><i class="fas fa-user-slash"></i> Desativar</button>
<option value="gestor">Gestor</option>
</td>
<option value="admin_empresa">Administrador da Empresa</option>
</tr>
</select>
<?php endforeach; ?>
</div>
</tbody>
<button type="submit" class="btn-primary">Salvar</button>
</table>
</form>
</div>
</div>
</div>
</div>

<div id="usuario-modal" class="modal">


<script src="../frontend/js/usuarios_admin.js"></script>
<div class="modal-content">
</body>
<span class="close-btn">&times;</span>
</html>
<h3 id="modal-title">Adicionar Usuário</h3>
Passo 3: As APIs do Backend para Gerenciar Usuários
<form id="usuario-form">
Vamos criar um único arquivo de API que lide com todas as operações de CRUD (Create, Read,
<input type="hidden" id="usuario-id" name="id">
Update, Delete).
<div class="input-group">

<label for="nome">Nome:</label>
Arquivo: /backend/api/gerenciar_usuarios.php (nova página)
<input type="text" id="nome" name="nome" required>

</div>
PHP
<div class="input-group">

<label for="email">E-mail:</label>
<?php
session_start(); }

header('Content-Type: application/json'); break;

require_once '../middlewares/verifica_sessao.php'; case 'editar':

require_once '../classes/GerenciamentoUsuario.php'; $dados = $_POST;

$id = $_POST['id'] ?? null;

if ($_SESSION['perfil'] !== 'admin_empresa' && $_SESSION['perfil'] !== 'gestor') { if ($id) {

http_response_code(403); $resultado = $gerenciamentoUsuario->editarUsuario($id, $dados);

echo json_encode(['success' => false, 'message' => 'Acesso negado.']); if ($resultado) {

exit; echo json_encode(['success' => true, 'message' => 'Usuário editado com sucesso.']);

} } else {

echo json_encode(['success' => false, 'message' => 'Erro ao editar usuário.']);

$gerenciamentoUsuario = new GerenciamentoUsuario(); }

} else {

// Determina a ação com base no método da requisição http_response_code(400);

$acao = $_POST['acao'] ?? $_GET['acao'] ?? ''; echo json_encode(['success' => false, 'message' => 'ID de usuário inválido.']);

switch ($acao) { break;

case 'listar':

$usuarios = $gerenciamentoUsuario->listarUsuarios($_SESSION['empresa_id']); case 'desativar':

echo json_encode(['success' => true, 'data' => $usuarios]); $id = $_POST['id'] ?? null;

break; if ($id) {

$resultado = $gerenciamentoUsuario->desativarUsuario($id);

case 'adicionar': if ($resultado) {

$dados = $_POST; echo json_encode(['success' => true, 'message' => 'Usuário desativado com sucesso.']);

$dados['empresa_id'] = $_SESSION['empresa_id']; } else {

$resultado = $gerenciamentoUsuario->adicionarUsuario($dados); echo json_encode(['success' => false, 'message' => 'Erro ao desativar usuário.']);

if ($resultado) { }

echo json_encode(['success' => true, 'message' => 'Usuário adicionado com sucesso.']); } else {

} else { http_response_code(400);

echo json_encode(['success' => false, 'message' => 'Erro ao adicionar usuário.']); echo json_encode(['success' => false, 'message' => 'ID de usuário inválido.']);
} document.addEventListener('DOMContentLoaded', function() {

break; const usuarioModal = document.getElementById('usuario-modal');

const closeBtn = document.querySelector('.modal-content .close-btn');

case 'get': const addUsuarioBtn = document.getElementById('add-usuario-btn');

$id = $_GET['id'] ?? null; const usuarioForm = document.getElementById('usuario-form');

if ($id) { const modalTitle = document.getElementById('modal-title');

$usuario = $gerenciamentoUsuario->getUsuario($id); const usuarioIdInput = document.getElementById('usuario-id');

if ($usuario) { const senhaGroup = document.getElementById('senha-group');

echo json_encode(['success' => true, 'data' => $usuario]); const tabelaBody = document.getElementById('usuarios-table-body');

} else {

echo json_encode(['success' => false, 'message' => 'Usuário não encontrado.']); // Abre o modal para adicionar usuário

} addUsuarioBtn.addEventListener('click', function() {

} else { modalTitle.textContent = 'Adicionar Usuário';

http_response_code(400); usuarioForm.reset();

echo json_encode(['success' => false, 'message' => 'ID de usuário inválido.']); usuarioIdInput.value = '';

} senhaGroup.style.display = 'block';

break; document.getElementById('senha').setAttribute('required', '');

usuarioModal.style.display = 'block';

default: });

http_response_code(400);

echo json_encode(['success' => false, 'message' => 'Ação inválida.']); // Fecha o modal

break; closeBtn.addEventListener('click', function() {

} usuarioModal.style.display = 'none';

Passo 4: A Lógica do Frontend (JavaScript) });

Este script irá controlar a interface e interagir com a nova API para gerenciar os usuários de forma
dinâmica.
window.addEventListener('click', function(event) {

if (event.target == usuarioModal) {
Arquivo: /frontend/js/usuarios_admin.js
usuarioModal.style.display = 'none';

}
JavaScript
});
// Submissão do formulário const id = target.getAttribute('data-id');

usuarioForm.addEventListener('submit', function(e) {

e.preventDefault(); if (target.classList.contains('edit-btn')) {

fetch(`/backend/api/gerenciar_usuarios.php?acao=get&id=${id}`)

const formData = new FormData(usuarioForm); .then(response => response.json())

const acao = usuarioIdInput.value ? 'editar' : 'adicionar'; .then(data => {

formData.append('acao', acao); if (data.success) {

const user = data.data;

fetch('/backend/api/gerenciar_usuarios.php', { modalTitle.textContent = 'Editar Usuário';

method: 'POST', usuarioIdInput.value = user.id;

body: formData, document.getElementById('nome').value = user.nome;

}) document.getElementById('email').value = user.email;

.then(response => response.json()) document.getElementById('perfil').value = user.perfil;

.then(data => {

alert(data.message); senhaGroup.style.display = 'none';

if (data.success) { document.getElementById('senha').removeAttribute('required');

usuarioModal.style.display = 'none'; usuarioModal.style.display = 'block';

window.location.reload(); } else {

} alert(data.message);

}) }

.catch(error => { });

console.error('Erro:', error); }

alert('Erro ao salvar usuário.');

}); if (target.classList.contains('desativar-btn')) {

}); if (confirm('Tem certeza que deseja desativar este usuário?')) {

const formData = new FormData();

// Delegação de eventos para botões de editar e desativar formData.append('acao', 'desativar');

tabelaBody.addEventListener('click', function(e) { formData.append('id', id);

const target = e.target.closest('button');

if (!target) return; fetch('/backend/api/gerenciar_usuarios.php', {

method: 'POST',
body: formData,

}) Gerenciamento completo de usuários.

.then(response => response.json())

.then(data => { O sistema parece estar finalizado e pronto para ser implementado. O que você acha? Podemos
considerar o projeto concluíd
alert(data.message);

if (data.success) {

window.location.reload();

})

.catch(error => {

console.error('Erro:', error);

alert('Erro ao desativar usuário.');

});

});

});

Com esta etapa, o sistema de gestão de usuários está completo. Os administradores podem
adicionar, editar e desativar contas de forma segura.

O projeto agora conta com as seguintes funcionalidades:

Login com verificação de perfil e logos de empresa.

Middleware de segurança para todas as páginas protegidas.

Registro de ponto com geolocalização.

Sistema de solicitação e gestão de ajustes de ponto.

Relatórios filtráveis com exportação para CSV/PDF e envio por e-mail.

Você também pode gostar