🔍
Digite para buscar na documentação…
Documentação Oficial

TaskFlow Pro

Sistema completo de gerenciamento de projetos e tarefas construído sobre uma arquitetura MVC em PHP puro, sem dependências de frameworks externos.

O que é o TaskFlow Pro?

O TaskFlow Pro é uma aplicação web profissional para gerenciamento de tarefas com board Kanban, comentários em tempo real, upload de anexos, sistema de convites por e-mail, relatórios e muito mais. Toda a base do framework é implementada do zero em PHP 8.1+.

PHP 8.1+ MySQL 5.7+ MVC Puro Sem Framework Dark/Light Mode Responsivo API JSON CSRF Protection

Funcionalidades do Sistema

📁
Projetos
Crie projetos com cor, descrição e controle de membros. Arquive ou exclua quando necessário.
🗂
Kanban Board
Board visual com drag-and-drop entre colunas: A Fazer, Em Progresso, Em Revisão, Concluído.
Tarefas Detalhadas
Tarefas com checklist, comentários, anexos, responsáveis múltiplos, tags e prioridades.
💬
Comentários em Tempo Real
Polling automático a cada 20 segundos com suporte a respostas encadeadas (reply).
👥
Membros & Convites
Convide colaboradores por e-mail com link de aceitação e papéis editor/visualizador.
📊
Relatórios
Gráficos de progresso por status, prioridade, projetos e atividade dos últimos 7 dias.

Stack Tecnológico

Camada Tecnologia Versão
Backend PHP (MVC puro, sem Laravel/Symfony) 8.1+
Banco de Dados MySQL / MariaDB com PDO 5.7+ / 10.3+
Gerenciador de Pacotes Composer (apenas vlucas/phpdotenv) ^5.5
Frontend CSS CSS puro com variáveis (sem Bootstrap)
Frontend JS JavaScript vanilla (sem jQuery/React) ES2022+
Ícones Font Awesome 6.5
Fontes Plus Jakarta Sans (Google Fonts)
Servidor Apache com mod_rewrite 2.4+
Configuração

Instalação Rápida

Em menos de 5 minutos o TaskFlow Pro está rodando localmente com XAMPP, WAMP ou qualquer servidor Apache.

Pré-requisitos

  • PHP 8.1+ com extensões: pdo_mysql, mbstring, fileinfo, openssl
  • MySQL 5.7+ ou MariaDB 10.3+
  • Apache com mod_rewrite habilitado
  • Composer (opcional se usar o ZIP com vendor incluso)

Passo a Passo

1

Copiar o projeto para o servidor

Extraia o arquivo taskflow-mvc.zip dentro da pasta raiz do seu servidor:

Terminalbash
# XAMPP (Windows)
C:\xampp\htdocs\taskflow-mvc\

# WAMP (Windows)
C:\wamp64\www\taskflow-mvc\

# Linux/Mac
/var/www/html/taskflow-mvc/
2

Configurar o arquivo .env

Copie o arquivo de exemplo e edite com suas configurações locais:

Terminalbash
cp .env.example .env
.env
APP_NAME="TaskFlow Pro"
APP_ENV=development
APP_URL=http://localhost/taskflow-mvc/public

DB_HOST=localhost
DB_DATABASE=taskflow_db
DB_USERNAME=root
DB_PASSWORD=
3

Criar o banco de dados

Importe o arquivo SQL no phpMyAdmin ou via terminal:

Terminalbash
mysql -u root -p < database/migrations/001_taskflow_schema.sql
ℹ️

phpMyAdmin

Acesse phpMyAdmin → clique em "Importar" → selecione o arquivo database/migrations/001_taskflow_schema.sql → clique em "Executar".

4

Instalar dependências (se necessário)

O ZIP já inclui o vendor/, portanto este passo é opcional. Se precisar reinstalar:

Terminalbash
composer install
5

Acessar o sistema

Abra o navegador e acesse:

URL
http://localhost/taskflow-mvc/public/auth/register

Crie sua conta na tela de registro. O primeiro usuário registrado recebe o papel de admin.

Configurar Permissões (Linux)

Terminalbash
chmod -R 755 storage/
chmod -R 755 public/uploads/
chown -R www-data:www-data storage/ public/uploads/
Arquitetura

Estrutura de Pastas

O projeto segue a convenção MVC com separação clara de responsabilidades. Cada diretório tem um único propósito.

taskflow-mvc/
app/ ← núcleo da aplicação
Controllers/ ← lógica de cada módulo
AuthController.php
DashboardController.php
ProjectController.php
TaskController.php
UserController.php
... (10 controllers)
Core/ ← framework interno
Application.php ← bootstrap
Router.php ← roteamento
Controller.php ← base
Model.php ← base CRUD
Database.php ← PDO Singleton
Session.php ← sessões seguras
View.php ← renderizador
Request.php, Logger.php, Validator.php
Helpers/ ← funções utilitárias
functions.php ← autoload Composer
TaskflowHelper.php
SecurityHelper.php
Middlewares/
AuthMiddleware.php
CsrfMiddleware.php
GuestMiddleware.php
Models/
User.php, Project.php, Task.php
Report.php, PasswordReset.php
Views/
layouts/ ← app.php, auth.php
auth/ ← login, register, etc.
dashboard/, projects/, kanban/
tasks/, my_tasks/, reports/
profile/, settings/, users/

config/
app.php ← constantes globais
database.php ← conexão BD
mail.php ← configuração de e-mail

database/migrations/
001_taskflow_schema.sql ← schema completo

public/ ← document root do Apache
index.php ← front controller
.htaccess ← rewrite rules
assets/css/ ← taskflow.css
assets/js/ ← taskflow.js
uploads/ ← arquivos enviados

routes/
web.php ← todas as rotas da aplicação

storage/
logs/ ← error-YYYY-MM-DD.log
sessions/ ← arquivos de sessão PHP

.env ← variáveis de ambiente (privado)
.env.example ← modelo público
composer.json
⚠️

Document Root

O Apache deve apontar para a pasta public/, não para a raiz do projeto. O .env e todo o código PHP ficam acima do public/, inacessíveis ao browser.

Configuração

Variáveis de Ambiente

Toda configuração sensível fica no arquivo .env na raiz do projeto, carregado via vlucas/phpdotenv antes de qualquer código da aplicação.

.env — todas as opções
# ── Aplicação ────────────────────────────────────────────
APP_NAME="TaskFlow Pro"       # Nome exibido na interface
APP_ENV=development           # development | production
APP_DEBUG=true                # true exibe erros detalhados
APP_URL=http://localhost/taskflow-mvc/public  # SEM barra final
APP_KEY=base64:SuaChaveAqui  # Chave de segurança (qualquer string longa)

# ── Banco de Dados ────────────────────────────────────────
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=taskflow_db
DB_USERNAME=root
DB_PASSWORD=

# ── Sessão ───────────────────────────────────────────────
SESSION_LIFETIME=120          # Minutos de inatividade
SESSION_SECURE=false          # true apenas com HTTPS

# ── E-mail ───────────────────────────────────────────────
MAIL_DRIVER=dev               # dev (mostra link) | smtp (envia)
MAIL_SMTP_HOST=smtp.gmail.com
MAIL_SMTP_PORT=587
MAIL_SMTP_USER=seu@email.com
MAIL_SMTP_PASS=suasenha
MAIL_SMTP_SECURE=tls

Acessando Variáveis no Código

As variáveis do .env ficam disponíveis via $_ENV e são lidas nos arquivos de configuração em config/:

config/app.phpphp
// As variáveis .env são lidas aqui e viram constantes PHP globais
define('APP_NAME',  $_ENV['APP_NAME']  ?? 'TaskFlow Pro');
define('APP_URL',   $_ENV['APP_URL']   ?? 'http://localhost');
define('APP_DEBUG', $_ENV['APP_DEBUG'] ?? false);

// Acesso em qualquer lugar do código:
echo APP_URL;      // http://localhost/taskflow-mvc/public
echo APP_DEBUG;    // true
🔴

Nunca commite o .env

O arquivo .env contém senhas e chaves. Ele já está no .gitignore. Use sempre o .env.example como modelo.

Dados

Banco de Dados

O TaskFlow usa MySQL com PDO. A classe Database implementa o padrão Singleton e oferece uma interface fluente e segura contra SQL Injection.

Diagrama de Tabelas

Tabela Propósito Chaves Relacionadas
users Contas de usuário com tema, bio e papel — (raiz)
password_resets Tokens de recuperação de senha (1h) email → users.email
projects Projetos com cor, ícone e status owner_id → users.id
project_members Membros do projeto com papel editor/viewer project_id, user_id
project_invites Convites por e-mail com token único (7 dias) project_id, invited_by
tasks Tarefas com status, prioridade e prazo project_id, assigned_to, created_by
task_assignees Múltiplos responsáveis por tarefa task_id, user_id
task_checklist Itens de checklist por tarefa task_id
task_attachments Anexos com nome real, nome salvo e tipo MIME task_id, user_id
task_external_notes Observações sobre responsáveis externos task_id, created_by
comments Comentários com reply encadeado task_id, user_id, parent_id

Usando a Classe Database

A classe Database oferece uma interface fluente (method chaining). Todos os parâmetros são vinculados com bind(), prevenindo SQL Injection:

Exemplos de uso da Databasephp
use App\Core\Database;

$db = Database::getInstance(); // Singleton — mesma conexão em toda a requisição

// ── SELECT simples ────────────────────────────────────────────────
$user = $db->query("SELECT * FROM users WHERE id = :id LIMIT 1")
           ->bind(':id', 5)
           ->fetch();

echo $user->name; // acessa como objeto

// ── SELECT múltiplos ─────────────────────────────────────────────
$tasks = $db->query("SELECT * FROM tasks WHERE project_id = :pid AND status = :s")
            ->bind(':pid', 12)
            ->bind(':s', 'todo')
            ->fetchAll();

foreach ($tasks as $task) {
    echo $task->title;
}

// ── INSERT ───────────────────────────────────────────────────────
$db->query("INSERT INTO projects (owner_id, name, color) VALUES (:uid, :name, :color)")
   ->bind(':uid', 1)
   ->bind(':name', 'Novo Projeto')
   ->bind(':color', '#6366f1')
   ->execute();

$newId = $db->lastInsertId(); // ID do registro inserido

// ── UPDATE ───────────────────────────────────────────────────────
$db->query("UPDATE tasks SET status = :s WHERE id = :id")
   ->bind(':s', 'done')
   ->bind(':id', 42)
   ->execute();

$affected = $db->rowCount(); // Linhas afetadas

// ── TRANSAÇÃO ────────────────────────────────────────────────────
$db->beginTransaction();
try {
    $db->query("DELETE FROM task_checklist WHERE task_id = :id")->bind(':id', 5)->execute();
    $db->query("DELETE FROM tasks WHERE id = :id")->bind(':id', 5)->execute();
    $db->commit();
} catch (Exception $e) {
    $db->rollback();
    throw $e;
}

Adicionar Nova Tabela

Crie um arquivo SQL em database/migrations/ com o próximo número sequencial:

database/migrations/002_add_labels.sqlsql
USE taskflow_db;

CREATE TABLE IF NOT EXISTS labels (
    id         INT PRIMARY KEY AUTO_INCREMENT,
    project_id INT NOT NULL,
    name       VARCHAR(80) NOT NULL,
    color      VARCHAR(7) DEFAULT '#6366f1',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (project_id) REFERENCES projects(id)
) ENGINE=InnoDB;

-- Tabela pivot tarefa-label
CREATE TABLE IF NOT EXISTS task_labels (
    task_id  INT NOT NULL,
    label_id INT NOT NULL,
    PRIMARY KEY (task_id, label_id),
    FOREIGN KEY (task_id)  REFERENCES tasks(id),
    FOREIGN KEY (label_id) REFERENCES labels(id)
) ENGINE=InnoDB;
ℹ️

Depois de criar a migration SQL, importe-a no banco e crie o Model correspondente em app/Models/Label.php seguindo o padrão existente.

Arquitetura

Ciclo de Vida

Cada requisição HTTP passa por uma cadeia bem definida de componentes antes de chegar à view. Entender este fluxo é fundamental para depurar e estender o sistema.

Browser
Apache .htaccess
public/index.php
Application::run()
Router::dispatch()
Middlewares
Controller::método()
Model (BD)
View::make()
HTML → Browser

Detalhamento do Fluxo

# Etapa Arquivo O que acontece
1 HTTP Request .htaccess Reescreve toda URL para index.php?url=path
2 Bootstrap public/index.php Define constantes, carrega Composer, sessão, config
3 Application Core/Application.php Cria Request e Router; carrega routes/web.php
4 Roteamento Core/Router.php Compara URI com rotas registradas, extrai parâmetros
5 Middlewares Middlewares/*.php Executa guards (Auth, CSRF, Guest) antes do controller
6 Controller Controllers/*.php Chama Models, processa dados, chama $this->view()
7 Model Models/*.php Executa queries via Database, retorna objetos
8 View Views/*.php Renderiza HTML com dados, dentro do layout escolhido
9 Response HTML final enviado ao browser

O front controller (index.php)

public/index.phpphp
<?php
// 1. Constantes de path
define('ROOT_PATH',    dirname(__DIR__));
define('APP_PATH',     ROOT_PATH . '/app');
define('PUBLIC_PATH',  ROOT_PATH . '/public');
define('VIEW_PATH',    APP_PATH  . '/Views');
define('STORAGE_PATH', ROOT_PATH . '/storage');
define('CONFIG_PATH',  ROOT_PATH . '/config');

// 2. Autoload do Composer (PSR-4 + functions.php)
require ROOT_PATH . '/vendor/autoload.php';

// 3. Carrega variáveis do .env
$dotenv = Dotenv\Dotenv::createImmutable(ROOT_PATH);
$dotenv->load();

// 4. Configurações globais (constantes APP_*, display_errors, timezone...)
require CONFIG_PATH . '/app.php';

// 5. Inicia sessão segura
\App\Core\Session::start();

// 6. Inicializa e executa a aplicação
$app = new \App\Core\Application();
$app->run();
MVC

Sistema de Rotas

Todas as rotas ficam em routes/web.php. O Router suporta GET/POST, parâmetros dinâmicos, grupos com prefixo e middlewares por rota ou grupo.

Anatomia de uma Rota

routes/web.phpphp
<?php
// Sintaxe básica:
// $router->MÉTODO('/caminho', [Controller::class, 'método'], ['Middleware']);

// Rota GET simples
$router->get('/app/dashboard', [DashboardController::class, 'index']);

// Rota POST com CSRF obrigatório
$router->post('/app/projects/store', [ProjectController::class, 'store'], ['CsrfMiddleware']);

// Rota com parâmetro dinâmico {id}
$router->get('/app/projects/{id}/kanban', [ProjectController::class, 'kanban']);
// ↑ O valor de {id} é passado como $id para o método kanban(int $id)

// Grupo com prefixo e middleware compartilhado
$router->group(['prefix' => '/auth', 'middleware' => ['GuestMiddleware']], function ($router) {
    $router->get('/login',    [AuthController::class, 'loginForm']);
    $router->post('/login',   [AuthController::class, 'login'], ['CsrfMiddleware']);
    $router->get('/register', [AuthController::class, 'registerForm']);
    // Todas as rotas deste grupo herdam o GuestMiddleware
});

Tabela Completa de Rotas

Auth (visitantes apenas — GuestMiddleware)
GET/auth/loginAuthControllerloginForm
POST/auth/loginAuthControllerlogin
GET/auth/registerAuthControllerregisterForm
POST/auth/registerAuthControllerregister
GET/auth/forgot-passwordAuthControllerforgotForm
GET/auth/reset-passwordAuthControllerresetForm
GET/auth/logoutAuthControllerlogout
App (login obrigatório — AuthMiddleware)
GET/app/dashboardDashboardControllerindex
GET/app/projectsProjectControllerindex
POST/app/projects/storeProjectControllerstore
POST/app/projects/{id}/updateProjectControllerupdate
GET/app/projects/{id}/kanbanProjectControllerkanban
POST/app/projects/{id}/tasks/createProjectControllercreateTask
GET/app/tasks/{id}TaskControllershow
POST/app/tasks/{id}/updateTaskControllerupdate
GET/app/my-tasksTaskControllermyTasks
GET/app/reportsReportControllerindex
GET/app/profileProfileControllerindex
GET/app/settingsSettingsControllerindex
GET/app/usersUserControllerindex
API JSON (login obrigatório — sem CSRF)
POST/api/update-statusTaskControllerapiUpdateStatus
POST/api/checklist/addTaskControllerapiAddChecklist
POST/api/comments/addTaskControllerapiAddComment
GET/api/comments?task_id={id}&after={id}TaskControllerapiGetComments
POST/api/attachments/uploadTaskControllerapiUploadAttachment
POST/api/set-themeTaskControllerapiSetTheme
POST/api/invite-memberUserControllerapiInviteMember
GET/api/project-members?project_id={id}UserControllerapiGetProjectMembers

Adicionar Nova Rota

routes/web.phpphp
// 1. Adicionar o use no topo do arquivo
use App\Controllers\LabelController;

// 2. Dentro do grupo /app (já tem AuthMiddleware):
$router->group(['prefix' => '/app', 'middleware' => ['AuthMiddleware']], function ($router) {
    // ... rotas existentes ...

    // Novas rotas para labels
    $router->get('/labels',           [LabelController::class, 'index']);
    $router->post('/labels/store',    [LabelController::class, 'store'],  ['CsrfMiddleware']);
    $router->post('/labels/{id}/delete', [LabelController::class, 'delete'], ['CsrfMiddleware']);
});
MVC

Controllers

Controllers orquestram a lógica da aplicação: recebem dados da requisição, chamam Models, e retornam Views ou JSON. Herdam de App\Core\Controller.

Métodos da Classe Base

Método Descrição
$this->view('modulo.arquivo', $data, 'layout') Renderiza view com layout
$this->redirect('app/dashboard') Redireciona para URL relativa
$this->back() Redireciona para a página anterior
$this->json($data, 200) Responde com JSON e código HTTP
$this->isAuthenticated() Verifica se usuário está logado

Criando um Controller

app/Controllers/LabelController.phpphp
<?php
namespace App\Controllers;

use App\Core\Controller;
use App\Core\Session;
use App\Models\Label; // Model que você vai criar

class LabelController extends Controller
{
    private Label $labelModel;

    public function __construct()
    {
        $this->labelModel = new Label();
    }

    // GET /app/labels
    public function index(): void
    {
        $userId = currentUserId();
        $labels = $this->labelModel->getByUser($userId);

        $this->view('labels.index', [
            'title'   => 'Labels',
            'labels'  => $labels,
            'message' => Session::getFlash('label_msg'),
        ], 'app'); // 'app' = layout app.php
    }

    // POST /app/labels/store
    public function store(): void
    {
        $userId = currentUserId();
        $name   = htmlspecialchars(strip_tags(trim($_POST['name'] ?? '')));
        $color  = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['color'] ?? '') ? $_POST['color'] : '#6366f1';

        if (!$name) {
            Session::flash('label_msg', 'Nome é obrigatório.');
        } else {
            $this->labelModel->create(['user_id' => $userId, 'name' => $name, 'color' => $color]);
            Session::flash('label_msg', 'Label criado com sucesso!');
        }

        $this->redirect('app/labels');
    }
}

Padrão de Resposta JSON (endpoints de API)

Endpoint API no Controllerphp
public function apiUpdateStatus(): void
{
    $userId = currentUserId();
    if (!$userId) jsonResponse(false, 'Não autenticado.'); // encerra execução

    $input  = json_decode(file_get_contents('php://input'), true);
    $taskId = (int)($input['task_id'] ?? 0);
    $status = $input['status'] ?? '';

    $valid = ['todo', 'in_progress', 'review', 'done'];
    if (!$taskId || !in_array($status, $valid)) {
        jsonResponse(false, 'Dados inválidos.');
    }

    // ... lógica ...

    // Resposta de sucesso com dados extras
    jsonResponse(true, 'Status atualizado.', ['new_status' => $status]);
    // Gera: {"success":true,"message":"Status atualizado.","new_status":"done"}
}
MVC

Models

Models encapsulam toda a interação com o banco de dados. Herdam de App\Core\Model que fornece CRUD genérico. Métodos específicos são adicionados em cada Model filho.

Métodos da Classe Base (Model)

Métodos herdados por todo Modelphp
$model = new User();

$model->all('name', 'ASC');         // SELECT * FROM users ORDER BY name ASC
$model->find(5);                    // SELECT * FROM users WHERE id = 5
$model->findBy('email', 'a@b.com'); // SELECT * FROM users WHERE email = ?
$model->where('role', 'admin');     // SELECT * FROM users WHERE role = 'admin'
$model->create(['name'=>'João','email'=>'j@ex.com']); // INSERT (respeita fillable)
$model->update(5, ['name'=>'João Silva']);             // UPDATE WHERE id = 5
$model->delete(5);                  // DELETE WHERE id = 5

// Acesso direto ao Database para queries complexas:
$model->db->query("SELECT ...")->bind(':p', $v)->fetchAll();

Criando um Model

app/Models/Label.phpphp
<?php
namespace App\Models;

use App\Core\Model;

class Label extends Model
{
    // Nome da tabela no banco
    protected string $table = 'labels';

    // Campos que podem ser inseridos/atualizados (whitelist de segurança)
    protected array $fillable = ['project_id', 'name', 'color'];

    // Método customizado — busca labels de um projeto
    public function getByProject(int $projectId): array
    {
        return $this->db
            ->query("SELECT * FROM {$this->table} WHERE project_id = :pid ORDER BY name ASC")
            ->bind(':pid', $projectId)
            ->fetchAll();
    }

    // Query mais complexa com JOIN
    public function getWithTaskCount(int $projectId): array
    {
        return $this->db->query(
            'SELECT l.*, COUNT(tl.task_id) AS task_count
             FROM labels l
             LEFT JOIN task_labels tl ON tl.label_id = l.id
             WHERE l.project_id = :pid
             GROUP BY l.id
             ORDER BY l.name ASC'
        )->bind(':pid', $projectId)->fetchAll();
    }
}

Propriedade fillable — Segurança

⚠️

Sempre defina o fillable

O array $fillable é uma whitelist. Apenas os campos listados podem ser inseridos/atualizados via create() e update(). Isso evita que um atacante injete campos como role ou is_admin via formulário malicioso.

MVC

Views & Layouts

Views são arquivos PHP que produzem HTML. O sistema usa dois layouts: app (com sidebar) para área logada e auth (página centralizada) para login/registro.

Como Chamar uma View

No Controllerphp
// Sintaxe: $this->view('pasta.arquivo', $dados, 'layout')
// Os dots (.) são convertidos para / na resolução do path

$this->view('dashboard.index', [
    'title'  => 'Dashboard',
    'stats'  => $stats,
    'tasks'  => $tasks,
], 'app'); // usa Views/layouts/app.php

// Layout auth (login, registro):
$this->view('auth.login', ['title' => 'Login'], 'auth');

// Sem layout (para componentes ou emails):
$this->view('emails.welcome', ['user' => $user], '');
// equivale a: Views/emails/welcome.php renderizado puro

Estrutura de uma View

app/Views/labels/index.phpphp
<?php
// Variáveis injetadas pelo controller via extract($data)
// $title, $labels, $message estão disponíveis diretamente
?>
<div class="page-header">
    <h1 class="page-title">Labels</h1>
</div>

<!-- Mensagem flash (sucesso/erro após redirect) -->
<?php if (!empty($message)): ?>
    <div class="alert alert-success"><?= e($message) ?></div>
<?php endif; ?>

<!-- Formulário com CSRF obrigatório em todo POST -->
<form method="POST" action="<?= url('app/labels/store') ?>">
    <?= csrf_field() ?>  <!-- gera <input type="hidden" name="_csrf_token" ...> -->
    <input type="text" name="name" required>
    <input type="color" name="color" value="#6366f1">
    <button type="submit">Criar</button>
</form>

<!-- Loop de dados -->
<?php foreach ($labels as $label): ?>
    <div style="background:<?= e($label->color) ?>">
        <?= e($label->name) ?>  <!-- e() escapa HTML — sempre use em dados do banco -->
    </div>
<?php endforeach; ?>

Funções Disponíveis nas Views

Função Retorno Uso
e($string) string Escapa HTML — use sempre em dados do BD
url('app/dashboard') string URL absoluta a partir do APP_URL
asset('css/taskflow.css') string URL para arquivo em public/assets/
csrf_field() HTML Campo hidden com token CSRF
flash('chave') string|null Lê e remove mensagem flash da sessão
currentUserId() int ID do usuário logado
currentUserName() string Nome do usuário logado (escapado)
currentTheme() string 'light' ou 'dark'
priorityLabel('high') string Label traduzido: "Alta"
statusLabel('in_progress') string Label traduzido: "Em Progresso"
tfFormatDate('2024-01-15') string Formata para "15/01/2024"
isOverdue($date, $status) bool True se prazo passou e status != done
avatarInitials('João Silva') string "JS"
oldInput('email') string Recupera valor de formulário após erro

Editar o Layout Principal (app.php)

O layout em app/Views/layouts/app.php define a sidebar, topbar e estrutura da área logada. A variável $content contém o HTML da view renderizada:

app/Views/layouts/app.php — estrutura simplificadaphp
<?php $theme = currentTheme(); ?>
<!DOCTYPE html>
<html data-theme="<?= $theme ?>">
<head>
    <title><?= e($title ?? 'TaskFlow Pro') ?></title>
    <link rel="stylesheet" href="<?= asset('css/taskflow.css') ?>">
</head>
<body>
    <aside class="sidebar">...menu...</aside>
    <main class="main-content">
        <?= $content ?>  <!-- VIEW É INJETADA AQUI -->
    </main>
    <script>const API_BASE = '<?= rtrim(url(''), '/') . '/' ?>';</script>
    <script src="<?= asset('js/taskflow.js') ?>"></script>
</body>
</html>
Segurança

Middlewares

Middlewares são executados antes do controller e podem bloquear ou redirecionar a requisição. Ficam em app/Middlewares/ e implementam o método handle(Request $request).

Middlewares Disponíveis

Middleware Quando ativa O que faz
AuthMiddleware Rotas /app/* e /api/* Redireciona para login se não autenticado
GuestMiddleware Rotas /auth/* Redireciona para dashboard se já logado
CsrfMiddleware Rotas POST com dados de formulário Valida _csrf_token ou retorna HTTP 419

Criando um Novo Middleware

app/Middlewares/AdminMiddleware.phpphp
<?php
namespace App\Middlewares;

use App\Core\Request;
use App\Core\Session;

class AdminMiddleware
{
    public function handle(Request $request): void
    {
        // Garante que o usuário é admin
        if (Session::get('user_role') !== 'admin') {
            Session::flash('error', 'Acesso negado. Você não tem permissão.');
            redirect('app/dashboard');
        }
    }
}
Usando nas rotasphp
// Rota com múltiplos middlewares
$router->get('/app/admin', [AdminController::class, 'index'], ['AdminMiddleware']);

// Grupo inteiro protegido
$router->group(['prefix' => '/app/admin', 'middleware' => ['AdminMiddleware']], function ($r) {
    $r->get('/users',    [AdminController::class, 'users']);
    $r->get('/settings', [AdminController::class, 'settings']);
});
Segurança

Autenticação

O sistema usa sessões PHP com proteção contra fixação, CSRF em todos os formulários, e hashing bcrypt para senhas. Sem JWT, sem token stateless.

Fluxo de Login

AuthController::login()php
public function login(): void
{
    $email    = trim($_POST['email']    ?? '');
    $password = $_POST['password']      ?? '';

    // User::authenticate() verifica email, active e bcrypt verify
    $user = $this->userModel->authenticate($email, $password);

    if (!$user) {
        Session::flash('error', 'E-mail ou senha incorretos.');
        flashOldInput(['email' => $email]); // repopula o formulário
        $this->redirect('auth/login');
        return;
    }

    // Regenera ID da sessão (previne Session Fixation Attack)
    session_regenerate_id(true);

    // Armazena dados na sessão
    Session::set('user_id',    $user->id);
    Session::set('user_name',  $user->name);
    Session::set('user_role',  $user->role);
    Session::set('user_theme', $user->theme ?? 'light');

    $this->redirect('app/dashboard');
}

Dados da Sessão Disponíveis

Chave Tipo Acesso
user_id int currentUserId() ou $_SESSION['user_id']
user_name string currentUserName()
user_role string $_SESSION['user_role']
user_theme string currentTheme()

Recuperação de Senha

O fluxo gera um token único de 64 hex chars com validade de 1 hora, salvo na tabela password_resets. Em modo MAIL_DRIVER=dev, o link é exibido na própria tela em vez de enviado por e-mail — ideal para desenvolvimento.

ℹ️

Ativar envio real de e-mail

Mude MAIL_DRIVER=smtp no .env e configure as credenciais SMTP. O sistema usa a classe App\Helpers\Mailer que suporta PHPMailer (inclua via Composer: composer require phpmailer/phpmailer).

Módulos

Projetos & Kanban

Cada projeto tem um dono (owner), pode ter membros convidados e contém tarefas organizadas em um board Kanban com 4 colunas e drag-and-drop.

Colunas do Kanban

Valor no BD Label exibido Cor
todo A Fazer Cinza
in_progress Em Progresso Azul
review Em Revisão Amarelo
done Concluído Verde

Drag and Drop — Como Funciona

O drag-and-drop usa a API nativa HTML5 (draggable="true"). Quando um card é solto em outra coluna, o JS faz uma chamada à API:

Chamada JS ao mover cardjs
// POST /api/update-status
const result = await api(API_BASE + "api/update-status", {
    task_id:   taskId,     // ID da tarefa
    status:    newStatus,  // nova coluna: 'in_progress'
    positions: [           // nova ordem de todas as cards na coluna
        { id: 12, position: 0 },
        { id: 8,  position: 1 },
    ]
});

Papéis de Membro no Projeto

Papel Criar tarefas Editar tarefas Mover no Kanban Gerenciar membros
owner
editor
viewer
Módulos

Tarefas

Cada tarefa pode ter checklist, comentários com reply, anexos, múltiplos responsáveis, notas externas, tags e prazo. A página de detalhe atualiza comentários automaticamente.

Campos da Tabela tasks

Campo Tipo Valores
status ENUM todo | in_progress | review | done
priority ENUM low | medium | high | urgent
due_date DATE Prazo (NULL = sem prazo)
assigned_to INT Responsável primário (FK users)
tags VARCHAR(500) Tags separadas por vírgula
position INT Ordem na coluna do Kanban

Polling de Comentários

A view tasks/show.php inclui um polling automático que busca comentários novos a cada 20 segundos sem recarregar a página:

Lógica do polling (tasks/show.php)js
// GET /api/comments?task_id=42&after=15
// "after" = ID do último comentário exibido — busca apenas os novos
const res = await fetch(API_BASE + `api/comments?task_id=${TASK_ID}&after=${lastId}`);
const data = await res.json();

if (data.comments.length > 0) {
    data.comments.forEach(c => appendComment(c)); // adiciona ao DOM
}
// Indicador visual de sincronização no cabeçalho da seção
Arquivos

Upload de Arquivos

O upload de anexos em tarefas é feito via AJAX com validação de tipo MIME real (não apenas extensão) e nomes aleatórios para evitar conflitos e ataques.

Regras de Upload

  • Tamanho máximo: 10 MB por arquivo
  • Tipos permitidos: JPG, PNG, GIF, WEBP, PDF, TXT, ZIP, DOCX, XLSX
  • O nome salvo em disco é gerado com bin2hex(random_bytes(12)) — ex: a3f9c12e4b56.pdf
  • O nome original é preservado na tabela task_attachments.original_name
  • O tipo MIME é verificado com finfo no servidor (não confia na extensão do cliente)
  • Arquivos ficam em public/uploads/ com .htaccess que bloqueia execução de PHP
TaskController::apiUploadAttachment() — lógica de segurançaphp
// 1. Verificar tamanho
if ($fileSize > 10 * 1024 * 1024) jsonResponse(false, 'Arquivo excede 10 MB.');

// 2. Verificar tipo MIME real com finfo (ignora extensão)
$allowed = ['image/jpeg', 'image/png', 'application/pdf', /* ... */];
$finfo    = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpPath);
if (!in_array($mimeType, $allowed, true)) {
    jsonResponse(false, 'Tipo de arquivo não permitido.');
}

// 3. Nome aleatório — evita conflito e path traversal
$ext      = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
$safeName = bin2hex(random_bytes(12)) . ($ext ? '.' . $ext : '');

// 4. Mover para pasta com .htaccess que bloqueia execução PHP
move_uploaded_file($tmpPath, PUBLIC_PATH . '/uploads/' . $safeName);
Integração

API JSON Interna

O sistema expõe endpoints JSON em /api/* usados pelo frontend JavaScript para operações assíncronas. Todos requerem sessão ativa (AuthMiddleware). Não há CSRF nessas rotas pois usam Content-Type: application/json.

Formato de Resposta Padrão

JSON Responsejson
// Sucesso
{ "success": true, "message": "Tarefa atualizada.", "new_status": "done" }

// Erro
{ "success": false, "message": "Dados inválidos." }

Fazendo uma Chamada da API

JS — usando a função api() do taskflow.jsjavascript
// A função api() já está disponível globalmente no taskflow.js

// POST com corpo JSON
const result = await api(API_BASE + 'api/update-status', {
    task_id: 42,
    status:  'done'
});
if (result.success) Toast.success(result.message);
else Toast.error(result.message);

// GET com query string
const data = await api(API_BASE + 'api/comments?task_id=42&after=0', null, 'GET');
console.log(data.comments);

// Upload de arquivo (multipart/form-data)
const form = new FormData();
form.append('task_id', 42);
form.append('file', fileInput.files[0]);
const r = await apiForm(API_BASE + 'api/attachments/upload', form);

Referência Completa dos Endpoints

Método Endpoint Body / Params Descrição
POST /api/update-status {task_id, status, positions[]} Atualiza status e ordem no kanban
POST /api/checklist/add {task_id, item} Adiciona item ao checklist
POST /api/checklist/toggle {id, is_done} Marca/desmarca item
POST /api/checklist/delete {id} Remove item
POST /api/comments/add {task_id, content, parent_id?} Novo comentário
GET /api/comments ?task_id=N&after=N Polling de novos comentários
POST /api/attachments/upload FormData {task_id, file} Upload de arquivo
POST /api/attachments/delete {id} Remove anexo
POST /api/set-theme {theme: 'dark'|'light'} Salva tema na sessão e BD
POST /api/invite-member {project_id, email, role} Envia convite
GET /api/project-members ?project_id=N Lista membros do projeto
POST /api/project-members-manage {action, project_id, user_id, role?} add/remove/update_role
GET /api/task-assignees ?task_id=N IDs dos responsáveis da tarefa
POST /api/delete-external-note {note_id} Remove observação externa
Frontend

CSS & Temas

Todo o CSS fica em public/assets/css/taskflow.css. O tema dark/light é implementado com variáveis CSS no seletor [data-theme] sem nenhuma dependência externa.

Variáveis CSS Principais

public/assets/css/taskflow.css — variáveiscss
:root {                              /* Tema claro (padrão) */
  --accent:       #6366f1;           /* Cor principal — roxo índigo */
  --accent-light: rgba(99,102,241,0.12);
  --success:      #10b981;           /* Verde */
  --warning:      #f59e0b;           /* Amarelo */
  --danger:       #ef4444;           /* Vermelho */
  --bg:           #f4f5f9;           /* Fundo da página */
  --surface:      #ffffff;           /* Fundo de cards */
  --surface2:     #f0f1f6;           /* Hover, inputs */
  --border:       rgba(0,0,0,0.07);  /* Bordas */
  --text-primary: #111827;           /* Texto principal */
  --text-muted:   #9ca3af;           /* Texto secundário */
  --radius:       10px;              /* Border radius padrão */
}

[data-theme="dark"] {               /* Sobrescreve apenas as cores */
  --bg:           #0b0d14;
  --surface:      #13161f;
  --surface2:     #1a1d2a;
  --border:       rgba(255,255,255,0.07);
  --text-primary: #e8eaf2;
  --text-muted:   #636880;
}

Alterando as Cores do Sistema

Para mudar a cor de destaque do sistema inteiro, basta alterar --accent no :root:

css
/* Mudar para verde */
:root { --accent: #059669; --accent2: #047857; }

/* Mudar para azul */
:root { --accent: #2563eb; --accent2: #1d4ed8; }

/* Mudar para laranja */
:root { --accent: #ea580c; --accent2: #c2410c; }

Classes de Componentes

Classe Uso
.btn .btn-primary Botão com cor de destaque
.btn .btn-secondary Botão neutro
.btn .btn-danger Botão vermelho (ações destrutivas)
.btn .btn-sm Botão pequeno
.card .card-body Container com borda e fundo
.card-header-bar Cabeçalho de card com título e ação
.modal-overlay Overlay de modal (usa classe .open)
.form-control Input, select, textarea
.form-group Agrupamento de label + input
.form-label Label de campo de formulário
.alert .alert-success Mensagem de sucesso
.alert .alert-danger Mensagem de erro
.badge .badge-high Badge de prioridade (low/medium/high/urgent)
.status-pill .status-done Pill de status (todo/in_progress/review/done)
.table-wrap table Tabela responsiva com hover
.page-header .page-title Cabeçalho de página
.empty-state Estado vazio com ícone e mensagem
Frontend

JavaScript

Todo o JS fica em public/assets/js/taskflow.js. Não usa jQuery nem frameworks. As funções são organizadas em módulos IIFE e expostas globalmente no window.

Objetos Globais Disponíveis nas Views

javascript
// ── Toast — notificações não-bloqueantes ─────────────────────────
Toast.success('Projeto criado com sucesso!');
Toast.error('Erro ao salvar. Tente novamente.');
Toast.info('Funcionalidade em beta.');

// ── Modal — modais com overlay ───────────────────────────────────
Modal.open('createProjectModal');   // abre modal pelo ID
Modal.close('createProjectModal');  // fecha modal específico
Modal.closeAll();                   // fecha todos

// ── Confirm — diálogo de confirmação assíncrono ──────────────────
const ok = await Confirm.open('Deseja excluir esta tarefa?');
// ok = true se o usuário confirmou, false se cancelou
if (ok) {
    document.getElementById('deleteForm').submit();
}

// ── API_BASE — base URL injetada pelo layout ─────────────────────
// Disponível em todas as páginas do app:
console.log(API_BASE); // "http://localhost/taskflow-mvc/public/"

Wiring Automático de Modais

Qualquer elemento com data-open-modal ou data-close-modal funciona automaticamente sem escrever JS adicional:

html
<!-- Abre o modal -->
<button data-open-modal="meuModal">Abrir</button>

<!-- Fecha o modal -->
<button data-close-modal="meuModal">Cancelar</button>

<!-- O modal -->
<div class="modal-overlay" id="meuModal">
    <div class="modal">
        <div class="modal-header">
            <span class="modal-title">Título</span>
            <button class="modal-close" data-close-modal="meuModal">✕</button>
        </div>
        <div class="modal-body">Conteúdo</div>
        <div class="modal-footer">...</div>
    </div>
</div>
Utilitários

Helpers & Funções

O arquivo app/Helpers/functions.php é carregado automaticamente pelo Composer em toda requisição. Todas as funções estão no escopo global.

Referência Completa

app/Helpers/functions.phpphp
// ── URLs ──────────────────────────────────────────────────────────
url('app/dashboard')          // http://localhost/taskflow-mvc/public/app/dashboard
asset('css/taskflow.css')     // http://localhost/.../public/assets/css/taskflow.css

// ── HTML Seguro ───────────────────────────────────────────────────
e('<script>alert(1)</script>')  // &lt;script&gt;...  → previne XSS

// ── Sessão ───────────────────────────────────────────────────────
currentUserId()       // (int) ID do usuário logado, ou 0
currentUserName()     // (string) Nome escapado, ou ''
currentTheme()        // 'light' | 'dark'
auth()                // (bool) true se logado
user()                // (object|null) dados completos do usuário

// ── Flash Messages ────────────────────────────────────────────────
flash('success')          // lê e remove da sessão
hasFlash('error')         // verifica sem remover
flashOldInput(['email' => $email]) // salva inputs para repopular form
oldInput('email')         // recupera input salvo (lê e limpa)

// ── CSRF ─────────────────────────────────────────────────────────
csrf_field()    // <input type="hidden" name="_csrf_token" value="abc...">
csrf_token()    // retorna apenas o valor do token

// ── Formatação ───────────────────────────────────────────────────
dateBR('2024-01-15')         // '15/01/2024'
dateTimeBR('2024-01-15 14:30') // '15/01/2024 14:30'
truncate($text, 100, '...')  // Trunca com sufixo

// ── TaskFlow específico ───────────────────────────────────────────
priorityLabel('high')        // 'Alta'
statusLabel('in_progress')   // 'Em Progresso'
tfFormatDate('2024-01-15')   // '15/01/2024'
isOverdue('2024-01-01', 'todo')  // true
avatarInitials('João Silva') // 'JS'

// ── Debug ─────────────────────────────────────────────────────────
dd($variavel, $outra)  // dump e die com output formatado

// ── API ───────────────────────────────────────────────────────────
// Retorna JSON e encerra execução:
jsonResponse(true, 'OK', ['data' => $dados]);
jsonResponse(false, 'Erro de validação.');

// ── Redirect ─────────────────────────────────────────────────────
redirect('auth/login');  // header Location + exit

Adicionando Novos Helpers

Adicione funções diretamente no final de app/Helpers/functions.php. Estarão disponíveis globalmente sem necessidade de use ou require:

php
// app/Helpers/functions.php — adicionar ao final

/**
 * Formata bytes em formato legível (KB, MB, GB)
 */
function formatBytes(int $bytes, int $precision = 1): string
{
    $units = ['B', 'KB', 'MB', 'GB'];
    $bytes = max($bytes, 0);
    $pow   = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow   = min($pow, count($units) - 1);
    return round($bytes / 1024 ** $pow, $precision) . ' ' . $units[$pow];
}

// Uso em qualquer view:
// formatBytes(1536); // "1.5 KB"
Guia do Desenvolvedor

Criar Nova Página

Adicionar uma nova página ao sistema requer apenas 3 passos: criar a view, criar o método no controller, e registrar a rota.

1

Criar a View

Crie o arquivo em app/Views/[modulo]/[pagina].php:

app/Views/kanban/stats.phpphp
<div class="page-header">
    <h1 class="page-title">Estatísticas do Kanban</h1>
</div>

<div class="card card-body">
    <p>Total de tarefas: <strong><?= (int)$totalTasks ?></strong></p>
    <p>Concluídas hoje: <strong><?= (int)$doneTodayCount ?></strong></p>
</div>
2

Adicionar o método no Controller

ProjectController.php (ou novo controller)php
public function stats(int $projectId): void
{
    $userId  = currentUserId();
    $project = $this->projectModel->getProjectForUser($projectId, $userId);
    if (!$project) { $this->redirect('app/projects'); return; }

    $totalTasks     = count($this->taskModel->getByProject($projectId));
    $doneTodayCount = /* query ... */ 3;

    $this->view('kanban.stats', [
        'title'          => 'Estatísticas',
        'project'        => $project,
        'totalTasks'     => $totalTasks,
        'doneTodayCount' => $doneTodayCount,
    ], 'app');
}
3

Registrar a rota

routes/web.phpphp
// Dentro do grupo /app com AuthMiddleware:
$router->get('/projects/{id}/stats', [ProjectController::class, 'stats']);

Pronto. Acesse /app/projects/1/stats.

Guia do Desenvolvedor

Criar Novo Módulo

Um módulo completo consiste em: tabela no BD → Model → Controller → Views → Rotas → Link no menu.

Checklist de Criação de Módulo

# Etapa Arquivo
1 Criar tabela SQL database/migrations/00N_nome.sql
2 Criar Model app/Models/NomeModel.php
3 Criar Controller app/Controllers/NomeController.php
4 Criar Views app/Views/modulo/index.php, etc.
5 Registrar rotas routes/web.php
6 Adicionar classmap vendor/composer/autoload_classmap.php
7 Link no sidebar app/Views/layouts/app.php

Adicionando Link no Menu Lateral

app/Views/layouts/app.php — sidebarphp
<!-- Adicionar dentro do bloco de navegação -->
<a href="<?= url('app/labels') ?>"
   class="nav-item <?= str_contains($uri, '/app/labels') ? 'active' : '' ?>">
    <span class="nav-icon"><i class="fas fa-tag"></i></span>
    <span class="nav-text">Labels</span>
</a>

Atualizando o autoload_classmap.php

ℹ️

O PSR-4 já resolve automaticamente novos arquivos — o classmap é apenas uma otimização. Se preferir, adicione manualmente:

vendor/composer/autoload_classmap.phpphp
'App\\Controllers\\LabelController' => $baseDir . '/app/Controllers/LabelController.php',
'App\\Models\\Label'                 => $baseDir . '/app/Models/Label.php',
Segurança

Segurança do Sistema

O TaskFlow implementa múltiplas camadas de proteção seguindo as principais recomendações do OWASP.

🛡
SQL Injection
100% das queries usam bind() com PDO prepared statements. Nenhum valor é concatenado na query.
🔒
XSS
Toda saída de dados do BD usa e() = htmlspecialchars() com UTF-8. JSON usa json_encode.
🎫
CSRF
Token por sessão com hash_equals() em todo POST de formulário. Regenerado a cada login.
🔑
Senhas
bcrypt com custo 12 via password_hash(). Verificação com password_verify().
📁
Upload Seguro
MIME real via finfo, nomes aleatórios, .htaccess bloqueando PHP na pasta de uploads.
🚫
Path Traversal
basename() no nome original, nome salvo gerado com random_bytes().

Headers de Segurança HTTP

app/Helpers/SecurityHelper.phpphp
// Enviados em toda requisição via Application::__construct()
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: SAMEORIGIN");
header("X-XSS-Protection: 1; mode=block");
header("Referrer-Policy: strict-origin-when-cross-origin");
Produção

Deploy em Produção

VPS com Apache (Ubuntu/Debian)

Configuração do VirtualHost Apacheapache
<VirtualHost *:443>
    ServerName taskflow.seudominio.com
    DocumentRoot /var/www/taskflow-mvc/public

    <Directory /var/www/taskflow-mvc/public>
        AllowOverride All
        Require all granted
        Options -Indexes
    </Directory>

    # SSL (Certbot / Let's Encrypt)
    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/taskflow.seudominio.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/taskflow.seudominio.com/privkey.pem

    # Logs
    ErrorLog  /var/log/apache2/taskflow-error.log
    CustomLog /var/log/apache2/taskflow-access.log combined
</VirtualHost>

Checklist de Produção

.env — configurações de produção
APP_ENV=production
APP_DEBUG=false            # CRÍTICO: nunca true em produção
APP_URL=https://taskflow.seudominio.com
SESSION_SECURE=true        # Exige HTTPS para cookie de sessão
MAIL_DRIVER=smtp           # Envio real de e-mails
🔴

APP_DEBUG=false em produção

Com APP_DEBUG=true em produção, erros PHP expõem paths, SQL queries e variáveis de configuração ao usuário final. Sempre use false.

Permissões em Produção

bash
chown -R www-data:www-data /var/www/taskflow-mvc/
chmod -R 755 /var/www/taskflow-mvc/
chmod -R 775 /var/www/taskflow-mvc/storage/
chmod -R 775 /var/www/taskflow-mvc/public/uploads/

# Proteger o .env
chmod 600 /var/www/taskflow-mvc/.env
Desenvolvimento

Boas Práticas

Regras de Ouro do Projeto

  • Sempre use e() ao exibir dados do banco — sem exceções, mesmo para inteiros
  • Sempre use bind() nas queries — nunca concatene valores na SQL
  • Sempre inclua csrf_field() em formulários POST
  • Mantenha Controllers finos — lógica complexa vai nos Models
  • Valide dados no Controller antes de passar ao Model
  • Use Session::flash() + redirect após POST (padrão PRG)
  • Defina $fillable em todo Model — whitelist obrigatória
  • Nomeie métodos de API com prefixo api — ex: apiAddComment()
  • Use htmlspecialchars(strip_tags(trim(...))) em inputs de texto antes de salvar
  • Nunca commite o .env — use o .env.example como documentação

Padrão PRG (Post/Redirect/Get)

Todo POST que altera dados deve terminar com redirect, nunca renderizar view diretamente:

php
// ✅ CORRETO — PRG pattern
public function store(): void
{
    // processa...
    Session::flash('project_msg', 'Projeto criado!');
    $this->redirect('app/projects'); // ← sempre redireciona
}

// ❌ ERRADO — renderiza após POST (causa resubmit no F5)
public function store(): void
{
    // processa...
    $this->view('projects.index', $data); // ← nunca faça isso em POST
}
Suporte

Solução de Problemas

Problemas Comuns

🔴 "Página em branco" ou 404 em todas as rotas

  • Verifique se mod_rewrite está ativo: sudo a2enmod rewrite
  • Verifique se AllowOverride All está no VirtualHost
  • Confirme que o APP_URL no .env não tem barra final
  • Certifique-se de acessar .../public/, não a raiz do projeto

🔴 Erro 419 — Token CSRF inválido

  • Certifique-se de incluir <?= csrf_field() ?> em todo formulário POST
  • Se a sessão expirou, o token fica inválido — faça logout e login novamente
  • Verifique se a pasta storage/sessions/ tem permissão de escrita

🔴 Erro de conexão ao banco de dados

bash
# Verificar logs de BD
cat storage/logs/db-$(date +%Y-%m-%d).log

# Testar conexão manualmente
mysql -u root -p taskflow_db -e "SELECT 1;"

🔴 JS não funciona (botões, modais, drag-and-drop)

  • Abra o console do browser (F12) e verifique erros JavaScript
  • O erro mais comum é redeclaração de const API_BASE — verifique se há const API_BASE em alguma view além do layout
  • Certifique-se de que public/assets/js/taskflow.js carrega sem erros (aba Network do DevTools)

🔴 Upload de arquivo não funciona

  • Verifique permissões: chmod 775 public/uploads/
  • Verifique o php.ini: upload_max_filesize e post_max_size devem ser ≥ 10M
  • Verifique se a extensão fileinfo está ativa no PHP

🔴 Erro "View não encontrada"

  • Verifique se o path usa pontos: 'kanban.index'Views/kanban/index.php
  • Verifique se o arquivo existe com o nome exato (case-sensitive no Linux)

Ativando Logs Detalhados

ini
# .env
APP_DEBUG=true   # Exibe stack trace completo na tela
bash
# Verificar logs de aplicação
tail -f storage/logs/error-$(date +%Y-%m-%d).log
Suporte

Perguntas Frequentes

Posso usar o sistema sem Composer?

Sim. O ZIP já inclui a pasta vendor/ completa. Compositor é necessário apenas se você quiser adicionar novas dependências ou reinstalar do zero.

Como enviar e-mails de convite em produção?

Configure no .env: MAIL_DRIVER=smtp com suas credenciais SMTP. O sistema usa a classe App\Helpers\Mailer. Para Gmail, ative a autenticação de dois fatores e gere uma "Senha de App". Para usar PHPMailer completo, adicione: composer require phpmailer/phpmailer.

Como alterar o limite de upload de 10MB?

Altere em duas partes: (1) no TaskController::apiUploadAttachment(), mude a constante 10 * 1024 * 1024; (2) no php.ini, ajuste upload_max_filesize e post_max_size. Em XAMPP, o php.ini fica em C:\xampp\php\php.ini.

Como adicionar um novo papel de usuário (ex: "gerente")?

1. Altere o ENUM na tabela users.role no BD: ALTER TABLE users MODIFY role ENUM('admin','member','gerente') DEFAULT 'member';
2. Crie um GerenciadorMiddleware em app/Middlewares/
3. Aplique o middleware nas rotas restritas
4. Atualize a lógica de exibição nas views onde necessário.

O sistema funciona no Windows/XAMPP?

Sim. Coloque a pasta em C:\xampp\htdocs\taskflow-mvc\ e configure APP_URL=http://localhost/taskflow-mvc/public no .env. Certifique-se de habilitar mod_rewrite no XAMPP (Apache > Config > httpd.conf: descomente a linha LoadModule rewrite_module).

Como o tema dark/light é salvo?

O tema é salvo na coluna users.theme no banco de dados e na variável de sessão $_SESSION['user_theme']. O botão de tema chama o endpoint POST /api/set-theme que atualiza ambos. O data-theme no <html> é definido pelo PHP no carregamento da página, então não há flash de tema.

Como criar um endpoint de API que retorna dados para o frontend?

1. Adicione a rota em routes/web.php no grupo /api
2. Crie o método no controller (com prefixo api)
3. Use jsonResponse() para retornar:

php
// routes/web.php
$router->get('/api/my-endpoint', [MyController::class, 'apiGetData']);

// MyController.php
public function apiGetData(): void {
    $data = ['items' => [1, 2, 3]];
    jsonResponse(true, 'OK', $data);
}
Como adicionar campos extras ao perfil do usuário?

1. Adicione a coluna na tabela users: ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT NULL;
2. Adicione o campo ao array $fillable do User model
3. Adicione o input na view profile/index.php
4. Trate o campo no ProfileController::update()

Manutenção

Atualizar o Sistema

Adicionando uma Funcionalidade

1

Banco de dados

Crie um novo arquivo SQL em database/migrations/ com o número sequencial. Execute no banco.

2

Model & Controller

Adicione métodos ao Model existente ou crie um novo. Adicione métodos ao Controller existente ou crie um novo.

3

Views & Rotas

Crie as views necessárias e registre as rotas em routes/web.php.

4

Testar

Teste o fluxo completo: criação, edição, exclusão, validações, permissões e erros.

Backup Antes de Atualizar em Produção

bash
# Backup do banco
mysqldump -u root -p taskflow_db > backup_$(date +%Y%m%d_%H%M%S).sql

# Backup dos uploads
tar -czf uploads_backup_$(date +%Y%m%d).tar.gz public/uploads/

# Backup do .env
cp .env .env.backup.$(date +%Y%m%d)
Configuração

Autoload & Composer

O projeto usa o autoloader PSR-4 do Composer para carregar classes automaticamente. Apenas uma dependência externa é necessária: vlucas/phpdotenv.

Como o Autoload Funciona

O Composer mapeia o namespace App\ para a pasta app/. Isso significa que qualquer classe com namespace App\Controllers\MeuController é carregada automaticamente de app/Controllers/MeuController.php — sem necessidade de require manual.

composer.jsonjson
{
    "name": "taskflow/taskflow-mvc",
    "require": {
        "php": ">=8.1",
        "vlucas/phpdotenv": "^5.5"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "files": [
            "app/Helpers/functions.php"
        ]
    }
}
ℹ️

Dois mecanismos de autoload

PSR-4 carrega classes por namespace. files carrega functions.php automaticamente em cada requisição — por isso as funções globais como url(), e(), flash() estão sempre disponíveis sem use.

Mapeamento PSR-4

Namespace Pasta Exemplo
App\Controllers\ app/Controllers/ App\Controllers\TaskController
App\Models\ app/Models/ App\Models\Task
App\Core\ app/Core/ App\Core\Database
App\Middlewares\ app/Middlewares/ App\Middlewares\AuthMiddleware
App\Helpers\ app/Helpers/ App\Helpers\TaskflowHelper

Adicionando Novas Dependências

Terminalbash
# Instalar PHPMailer para envio real de e-mails
composer require phpmailer/phpmailer

# Instalar uma biblioteca de exportação Excel
composer require phpoffice/phpspreadsheet

# Após qualquer alteração manual no composer.json
composer dump-autoload -o

Regenerar o Classmap (otimização)

O arquivo vendor/composer/autoload_classmap.php mapeia explicitamente cada classe para seu arquivo. Após criar novos controllers ou models, atualize-o:

bash
composer dump-autoload --optimize
# Ou manualmente: adicionar a entrada em autoload_classmap.php
vendor/composer/autoload_classmap.php — adicionar entradaphp
<?php
return array(
    // ... entradas existentes ...
    'App\\Controllers\\LabelController' => $baseDir . '/app/Controllers/LabelController.php',
    'App\\Models\\Label'                 => $baseDir . '/app/Models/Label.php',
);
Módulos

Membros & Convites

O sistema de convites permite convidar colaboradores externos por e-mail. O convidado recebe um link único com validade de 7 dias. O dono do projeto tem controle total sobre papéis e remoção.

Fluxo de Convite

Dono abre modal Membros
POST /api/invite-member
Token gerado (64 hex chars)
E-mail enviado (ou link dev)
Convidado clica no link
GET /app/accept-invite?token=...
Login / Cadastro
project_members inserido
Acesso ao projeto ✅

Tabelas Envolvidas

Tabela Campo Descrição
project_invites token 64 chars hex, único, expira em 7 dias
project_invites status pending | accepted | cancelled
project_invites role editor | viewer
project_members role owner | editor | viewer

Modo Desenvolvimento (sem SMTP)

Com MAIL_DRIVER=dev no .env, o link de aceite aparece na tela do modal de convites em vez de ser enviado por e-mail. Isso permite testar todo o fluxo localmente:

Resposta JSON do endpoint /api/invite-member em modo devjson
{
  "success": true,
  "message": "Convite enviado para colaborador@email.com.",
  "invite_token": "a3f9c12e4b56...",
  "dev_url": "http://localhost/taskflow-mvc/public/app/accept-invite?token=a3f9c..."
}

Gerenciar Membros via API

POST /api/project-members-managejavascript
// Adicionar membro existente diretamente
await api(API_BASE + 'api/project-members-manage', {
    action:     'add',
    project_id: 5,
    user_id:    12,
    role:       'editor'
});

// Remover membro
await api(API_BASE + 'api/project-members-manage', {
    action:     'remove',
    project_id: 5,
    user_id:    12
});

// Alterar papel
await api(API_BASE + 'api/project-members-manage', {
    action:     'update_role',
    project_id: 5,
    user_id:    12,
    role:       'viewer'
});
Frontend

Responsividade

O TaskFlow Pro é totalmente responsivo usando CSS Grid, Flexbox e media queries sem nenhuma dependência de framework CSS. A sidebar usa um sistema off-canvas em mobile.

Breakpoints

Breakpoint Dispositivo Comportamento
> 1200px Desktop grande Layout padrão, sidebar fixa expandida
900px–1200px Desktop médio Sidebar ainda visível, padding reduzido
≤ 900px Tablet / Mobile Sidebar off-canvas, hamburger menu visível
≤ 768px Mobile Kanban empilhado, forms em coluna única

Sidebar Off-Canvas (Mobile)

Em telas ≤ 900px, a sidebar fica escondida com transform: translateX(-100%). O botão hamburger (id="mobileMenuBtn") aciona a classe .open via JS:

css
@media (max-width: 768px) {
  .sidebar {
    transform: translateX(-100%);  /* escondida por padrão */
    transition: transform .18s ease;
    position: fixed; z-index: 90;
  }
  .sidebar.open {
    transform: translateX(0);      /* visível quando JS adiciona .open */
    box-shadow: 0 0 40px rgba(0,0,0,0.4);
  }
  .main-content { margin-left: 0; } /* sem margem em mobile */
}

Sidebar Colapsável (Desktop)

Em desktop, o botão mobileMenuBtn colapsa a sidebar para apenas ícones (64px), preservando espaço na tela. O estado é salvo no localStorage:

css
/* Quando .app-layout tem a classe .sidebar-collapsed: */
.app-layout.sidebar-collapsed .sidebar           { width: 64px; overflow: hidden; }
.app-layout.sidebar-collapsed .sidebar .nav-text,
.app-layout.sidebar-collapsed .sidebar .logo-text { display: none; }
.app-layout.sidebar-collapsed .main-content       { margin-left: 64px; }

Kanban em Mobile

O board Kanban em telas pequenas empilha as colunas verticalmente. A coluna "Concluído" começa colapsada para economizar espaço:

css
@media (max-width: 768px) {
  .kanban-wrapper {
    flex-direction: column;  /* empilha colunas */
    overflow-x: visible;
  }
  .kanban-col {
    min-width: unset;
    width: 100%;
  }
}

Grid de Projetos Responsivo

css
/* auto-fit adapta automaticamente o número de colunas */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 18px;
}
/* 3 colunas em desktop, 2 em tablet, 1 em mobile — automático */

Testando a Responsividade

  • Use o DevTools do browser (F12 → ícone de dispositivo móvel)
  • Teste com os breakpoints: 375px (iPhone), 768px (iPad), 1024px (tablet landscape)
  • Verifique se formulários, modais e tabelas não quebram o layout horizontal
  • Confirme que o kanban é usável com toque (drag-and-drop funciona em touch devices via HTML5)
Guia do Desenvolvedor

Nova Tabela no Banco de Dados

Guia completo para adicionar uma nova tabela ao sistema, desde o SQL até a integração com Model e Controller.

Convenções de Nomenclatura

Item Convenção Exemplo
Nome da tabela snake_case, plural task_labels
Chave primária id INT AUTO_INCREMENT id
Chaves estrangeiras tabela_id task_id, user_id
Timestamps created_at, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
Booleanos TINYINT(1) is_done TINYINT(1) DEFAULT 0
Enums valores em inglês, minúsculo ENUM('low','medium','high')

Exemplo Completo — Tabela de Labels

1. Criar o arquivo de migration SQL

database/migrations/002_add_labels.sqlsql
USE taskflow_db;

-- Tabela de labels (etiquetas coloridas para tarefas)
CREATE TABLE IF NOT EXISTS labels (
    id         INT PRIMARY KEY AUTO_INCREMENT,
    project_id INT NOT NULL,
    name       VARCHAR(80) NOT NULL,
    color      VARCHAR(7) DEFAULT '#6366f1',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB;

-- Tabela pivot tarefa-label (relação N:N)
CREATE TABLE IF NOT EXISTS task_labels (
    task_id  INT NOT NULL,
    label_id INT NOT NULL,
    PRIMARY KEY (task_id, label_id),
    FOREIGN KEY (task_id)  REFERENCES tasks(id)  ON DELETE CASCADE,
    FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE
) ENGINE=InnoDB;

-- Índices para performance
CREATE INDEX idx_labels_project ON labels(project_id);
CREATE INDEX idx_task_labels_task  ON task_labels(task_id);
CREATE INDEX idx_task_labels_label ON task_labels(label_id);
⚠️

ON DELETE CASCADE

Use ON DELETE CASCADE em chaves estrangeiras de tabelas dependentes para que os dados relacionados sejam removidos automaticamente quando o registro pai for excluído. Evita registros órfãos.

2. Criar o Model

app/Models/Label.phpphp
<?php
namespace App\Models;

use App\Core\Model;

class Label extends Model
{
    protected string $table    = 'labels';
    protected array  $fillable = ['project_id', 'name', 'color'];

    public function getByProject(int $projectId): array
    {
        return $this->db
            ->query("SELECT * FROM {$this->table} WHERE project_id = :pid ORDER BY name")
            ->bind(':pid', $projectId)
            ->fetchAll();
    }

    public function attachToTask(int $taskId, int $labelId): void
    {
        $this->db->query("INSERT IGNORE INTO task_labels (task_id, label_id) VALUES (:tid, :lid)")
            ->bind(':tid', $taskId)->bind(':lid', $labelId)->execute();
    }

    public function detachFromTask(int $taskId, int $labelId): void
    {
        $this->db->query("DELETE FROM task_labels WHERE task_id = :tid AND label_id = :lid")
            ->bind(':tid', $taskId)->bind(':lid', $labelId)->execute();
    }

    public function getForTask(int $taskId): array
    {
        return $this->db->query(
            'SELECT l.* FROM labels l
             JOIN task_labels tl ON tl.label_id = l.id
             WHERE tl.task_id = :tid ORDER BY l.name'
        )->bind(':tid', $taskId)->fetchAll();
    }
}

3. Atualizar o autoload_classmap.php

php
// vendor/composer/autoload_classmap.php — adicionar:
'App\\Models\\Label' => $baseDir . '/app/Models/Label.php',

4. Usar no Controller

php
use App\Models\Label;

// No método do controller:
$labelModel = new Label();
$labels     = $labelModel->getByProject($projectId);
$taskLabels = $labelModel->getForTask($taskId);