Módulos visuais
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).
Tarefa 1
Tarefa 2
Tarefa 3
Tarefa 4
Tarefa 5
Tarefa 6
Tarefa 7
Tarefa 8
Tarefa 9
Tarefa 10
Tarefa 11
Tarefa 12
Tarefa 13
Tarefa 14
Tarefa 15
Tarefa 16
Tarefa 17
Tarefa 18
Tarefa 19
{{--
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
<?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);
}
}
<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>