MAD Lab
/
Tutor > Módulos visuais > Kanban

Kanban

Módulos visuais

mad-kanban mad-kanban-card mad-kanban-toolbar mad-kanban-filters
Board completo

mad-kanban com filtros, toolbar, cards ricos (badges/meta/rodapé) e ações de card/coluna — mad-kanban-filters, mad-kanban-toolbar e mad-kanban-card juntos no mesmo board (ver nota de instância única no código-fonte do controller).

Filter by prioridade

Arraste os cards entre colunas — a ordem e o estado são salvos no banco.
Backlog 7
R$ 2.527,60
#1 Backlog

Tarefa 1

Bruno Lima
media
R$ 428,30 17/09/2026
#2 Backlog

Tarefa 2

Carla Dias
alta
R$ 327,00 23/09/2026
#3 Backlog

Tarefa 3

Diego Alves
baixa
R$ 703,90 16/09/2026
#4 Backlog

Tarefa 4

Elaine Melo
media
R$ 485,20 09/09/2026
#5 Backlog

Tarefa 5

Fábio Rocha
alta
R$ 84,50 06/09/2026
#6 Backlog

Tarefa 6

Ana Souza
baixa
R$ 363,40 12/09/2026
#7 Backlog

Tarefa 7

Bruno Lima
media
R$ 135,30 12/09/2026
Loading…
Em andamento 4
R$ 1.498,10
#8 Em andamento

Tarefa 8

Carla Dias
alta
R$ 298,80 09/09/2026
#9 Em andamento

Tarefa 9

Diego Alves
baixa
R$ 354,50 11/09/2026
#10 Em andamento

Tarefa 10

Elaine Melo
media
R$ 232,50 21/09/2026
#11 Em andamento

Tarefa 11

Fábio Rocha
alta
R$ 612,30 09/09/2026
Loading…
Revisão 3
R$ 1.263,70
#12 Revisão

Tarefa 12

Ana Souza
baixa
R$ 378,00 18/09/2026
#13 Revisão

Tarefa 13

Bruno Lima
media
R$ 205,80 07/09/2026
#14 Revisão

Tarefa 14

Carla Dias
alta
R$ 679,90 16/09/2026
Loading…
Concluído 5
R$ 1.931,90
#15 Concluído

Tarefa 15

Diego Alves
baixa
R$ 782,80 20/09/2026
#16 Concluído

Tarefa 16

Elaine Melo
media
R$ 133,90 18/09/2026
#17 Concluído

Tarefa 17

Fábio Rocha
alta
R$ 192,30 18/09/2026
#18 Concluído

Tarefa 18

Ana Souza
baixa
R$ 637,40 18/09/2026
#19 Concluído

Tarefa 19

Bruno Lima
media
R$ 185,50 15/09/2026
Loading…
Arquivado 0
Loading…
resources/views/tutor/examples/modulos-kanban/board.blade.php
{{--
    Partial roda no escopo Blade isolado do x-tutor-demo — $that não atravessa
    esse aninhamento sozinho (mesmo problema do $__component em
    resources/views/tutor/examples/grids-formatacao/agrupamento.blade.php).
    O compiler do kanban emite "$that ?? null": sem reabrir, cai num
    MadKanbanStandalone (sem os métodos onAprovar/onArquivar/onNovoCard do
    controller e sem o auto-filtro de "prioridade" via MadFiltersTrait).

    Duas armadilhas de compilador regex, ambas já pegas neste arquivo:
    1) NUNCA escreva o texto literal de uma tag kanban entre menor-que e
       maior-que dentro de QUALQUER comentário deste arquivo — nem comentário
       de linha (barra-barra) dentro de bloco de código PHP inline, NEM
       comentário Blade (chaves + duas travessões) como este aqui. O
       compilador de tags do MAD roda sobre o texto CRU do template ANTES
       até da limpeza de comentário do Blade, então casaria essa abertura
       falsa com o fechamento real lá embaixo — o board inteiro virava filho
       de um "mad-kanban" sem atributos, stage-field/model saindo vazios,
       SEM lançar exceção nenhuma nos módulos mais permissivos (achado real:
       aconteceu sem erro no calendário, só apareceu comparando o JSON
       renderizado com o esperado).
    2) NUNCA escreva o nome da diretiva de bloco PHP inline (arroba + "php")
       por extenso dentro de QUALQUER comentário deste arquivo — o parser de
       diretivas do Blade casa por regex simples e emparelha essa ocorrência
       com o fechamento real mais próximo, virando tudo no meio em código
       PHP (erro de sintaxe na primeira palavra em português que aparecer).
--}}
@php
    $that = \Mad\Component\MadRenderContext::getComponent();
@endphp

<mad-kanban-filters style="toolbar">
    <mad-select-field name="prioridade" label="Prioridade" placeholder="Todas">
        <option value="">Todas</option>
        <option value="baixa">Baixa</option>
        <option value="media">Média</option>
        <option value="alta">Alta</option>
    </mad-select-field>
</mad-kanban-filters>

<mad-kanban model="TutorKanbanCard" stage-model="TutorKanbanEstado"
    stage-field="estado_id" stage-title-field="nome" card-order-field="ordem" value-field="valor"
    database="business" cards-per-load="20" top-scroll>

    <mad-kanban-toolbar>
        <span style="font-size:12px; color:var(--mad-text-muted);">
            Arraste os cards entre colunas — a ordem e o estado são salvos no banco.
        </span>
    </mad-kanban-toolbar>

    <mad-kanban-card title="{titulo}">
        <mad-kanban-badge type="id" />
        <mad-kanban-badge type="text" path="estado.nome" color-path="estado.cor" />
        <mad-kanban-meta icon="circle-user-round" path="cliente" />
        <mad-kanban-meta icon="flag" path="prioridade" inline muted />
        <mad-kanban-meta-group direction="row" justify="between" gap="8">
            <mad-kanban-meta icon="banknote" path="valor" type="money" prefix="R$ " />
            <mad-kanban-meta icon="calendar" path="dt_prevista" type="date" muted />
        </mad-kanban-meta-group>
        <mad-kanban-footer type="money" path="valor" prefix="R$" />
        <mad-kanban-action label="Aprovar" icon="check" variant="success" method="onAprovar"
            when-field="estado_id" when-nin="4,5" />
        <mad-kanban-action label="Arquivar" icon="archive" variant="danger" method="onArquivar"
            confirm="Arquivar este card?" when-field="estado_id" when-nin="5" />
    </mad-kanban-card>

    <mad-kanban-stage-action icon="plus" label="Adicionar card" method="onNovoCard" empty-state />
    <mad-kanban-stage-action icon="plus" label="Novo" method="onNovoCard" />
</mad-kanban>
Código-fonte completo da página
app/control/Tutor/TutorModulosKanban.php
<?php

namespace App\Control\Tutor;

use App\Models\TutorDemo\TutorKanbanCard;
use App\Models\TutorDemo\TutorKanbanEstado;
use App\Support\Tutor\TutorCatalog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Mad\Calendar\MadKanban;
use Mad\Http\MadResponse;

/**
 * TutorModulosKanban — página "Kanban" do MAD Lab (LOTE L9 / Onda 1).
 *
 * Estende Mad\Calendar\MadKanban diretamente (molde: app/control/Sys/
 * KanbanDevView.php) em vez de TutorPage — MadKanban já É a base do módulo,
 * então o par catalogKey/view() de TutorPage é reimplementado aqui à mão
 * (PHP não tem herança múltipla).
 *
 * Board ÚNICO na página, de propósito: MadKanban guarda model/stageModel/
 * stageField/etc como props MUTÁVEIS no host a cada `<mad-kanban>` compilado
 * (_applyInlineKanbanConfig) e persiste a config do board mais recente em
 * sessão sob a chave `mad_kb_cfg_latest.<classe>` (MadKanban.php:1560) — o
 * fallback que a ação assíncrona de drag/load-more usa quando o wire-state
 * ainda não trouxe o `kbCfgKey` do board certo. Um SEGUNDO `<mad-kanban>` no
 * mesmo host sobrescreveria esse "latest" e quebraria as ações do primeiro
 * board. Por isso as 4 tags do catálogo (mad-kanban, mad-kanban-card,
 * mad-kanban-toolbar, mad-kanban-filters) vivem juntas em UM board só —
 * ver relatório do lote para o detalhe completo.
 */
class TutorModulosKanban extends MadKanban
{
    protected static string $wrapper = self::INTERNAL;

    protected string $catalogKey = 'modulos_kanban';

    protected string $model          = 'TutorKanbanCard';
    protected string $database       = 'business';
    protected string $stageModel     = 'TutorKanbanEstado';
    protected string $stageField     = 'estado_id';
    protected string $cardOrderField = 'ordem';
    protected string $valueField     = 'valor';
    protected string $valueFormat    = 'currency:R$ :2';

    /** Filtro auto-aplicado via MadFiltersTrait (<mad-kanban-filters>) — casa com a coluna `prioridade`. */
    public string $prioridade = '';

    public function mount(array $params = []): void
    {
        $this->ensureFixture();
        parent::mount($params);
    }

    protected function view(): string|array
    {
        return TutorCatalog::page($this->catalogKey)['view'];
    }

    // ── Ações de card / coluna (mad-kanban-action / mad-kanban-stage-action) ──

    public function onAprovar(int $id): MadResponse
    {
        TutorKanbanCard::query()->whereKey($id)->update(['estado_id' => 4]);

        return (new MadResponse())
            ->toast("Card #{$id} aprovado", 'success')
            ->manageCard($id, static::class, $this);
    }

    public function onArquivar(int $id): MadResponse
    {
        TutorKanbanCard::query()->whereKey($id)->update(['estado_id' => 5]);

        return (new MadResponse())
            ->toast("Card #{$id} arquivado", 'warning')
            ->manageCard($id, static::class, $this);
    }

    public function onNovoCard(string $id): MadResponse
    {
        $card = TutorKanbanCard::query()->create([
            'titulo'      => 'Novo card',
            'estado_id'   => (int) $id,
            'ordem'       => 999,
            'valor'       => 0,
            'cliente'     => '—',
            'prioridade'  => 'media',
            'dt_prevista' => date('Y-m-d', strtotime('+7 days')),
        ]);

        return (new MadResponse())
            ->toast("Card #{$card->id} criado", 'success')
            ->manageCard((int) $card->id, static::class, $this);
    }

    // ── Fixture (molde: KanbanDevView::ensureFixture) ───────────────────────

    private function ensureFixture(): void
    {
        $schema = Schema::connection('business');

        if (!$schema->hasTable('tutor_kanban_estados')) {
            $schema->create('tutor_kanban_estados', function ($t) {
                $t->increments('id');
                $t->string('nome');
                $t->string('cor', 16);
                $t->integer('ordem');
            });
        }
        if (!$schema->hasTable('tutor_kanban_cards')) {
            $schema->create('tutor_kanban_cards', function ($t) {
                $t->increments('id');
                $t->string('titulo');
                $t->integer('estado_id');
                $t->integer('ordem');
                $t->decimal('valor', 12, 2)->default(0);
                $t->string('cliente')->nullable();
                $t->string('prioridade', 16)->default('media');
                $t->string('dt_prevista', 10)->nullable();
            });
        }

        if (TutorKanbanEstado::query()->count() > 0) {
            return;
        }

        mt_srand(9001);

        DB::connection('business')->table('tutor_kanban_estados')->insert([
            ['id' => 1, 'nome' => 'Backlog',      'cor' => '#64748b', 'ordem' => 1],
            ['id' => 2, 'nome' => 'Em andamento', 'cor' => '#f59e0b', 'ordem' => 2],
            ['id' => 3, 'nome' => 'Revisão',      'cor' => '#8b5cf6', 'ordem' => 3],
            ['id' => 4, 'nome' => 'Concluído',    'cor' => '#16a34a', 'ordem' => 4],
            ['id' => 5, 'nome' => 'Arquivado',    'cor' => '#ef4444', 'ordem' => 5],
        ]);

        $clientes    = ['Ana Souza', 'Bruno Lima', 'Carla Dias', 'Diego Alves', 'Elaine Melo', 'Fábio Rocha'];
        $prioridades = ['baixa', 'media', 'alta'];
        $porEstado   = [1 => 7, 2 => 4, 3 => 3, 4 => 5, 5 => 0];

        $rows  = [];
        $count = 0;
        foreach ($porEstado as $estadoId => $qtd) {
            for ($i = 1; $i <= $qtd; $i++) {
                $count++;
                $rows[] = [
                    'titulo'      => "Tarefa {$count}",
                    'estado_id'   => $estadoId,
                    'ordem'       => $i,
                    'valor'       => round(mt_rand(500, 8000) / 10, 2),
                    'cliente'     => $clientes[$count % count($clientes)],
                    'prioridade'  => $prioridades[$count % count($prioridades)],
                    'dt_prevista' => date('Y-m-d', strtotime('+' . mt_rand(1, 20) . ' days')),
                ];
            }
        }
        DB::connection('business')->table('tutor_kanban_cards')->insert($rows);
    }
}
resources/views/tutor/modulos/kanban.blade.php
<x-tutor-page page="modulos_kanban">
    <x-tutor-demo demo="tutor.examples.modulos-kanban.board"
        title="Board completo" desc="mad-kanban com filtros, toolbar, cards ricos (badges/meta/rodapé) e ações de card/coluna — mad-kanban-filters, mad-kanban-toolbar e mad-kanban-card juntos no mesmo board (ver nota de instância única no código-fonte do controller)." />
</x-tutor-page>