Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Kanban Laravel Package

alessandro-nuunes/filament-kanban

View on GitHub
Deep Wiki
Context7

Filament Kanban – Blueprint de Implementação

Plugin de Kanban para Filament v5 (PHP 8.2+ / Laravel 11-12).
Inspirado em mokhosh/filament-kanban, reescrito do zero para a API do Filament 5.


Índice

  1. Visão geral
  2. Identidade do pacote
  3. Estrutura de arquivos
  4. API pública
  5. Arquitetura interna
  6. UI / UX
  7. Autorização e segurança
  8. Sorting
  9. Eventos e extension points
  10. Testes
  11. Artisan Command
  12. Plano de releases
  13. Documentação prevista
  14. Exemplo de uso – Tickets (MEI)

1. Visão geral

O plugin permite adicionar páginas Kanban a qualquer Resource do Filament v5.
O board é totalmente Livewire-driven e suporta:

  • Colunas configuradas por Enum ou array estático.
  • Drag-and-drop com atualização de status via Livewire action.
  • Regras de transição de status (permitir/bloquear movimentos).
  • Modal de edição inline com schema customizável.
  • Ordenação persistida por coluna (opcional).
  • Autorização por record e/ou policy.
  • Views publicáveis e customizáveis por board.
  • Suporte a dark mode nativo do Filament.

Não faz parte do v1:

  • Sync em tempo real (broadcasting/Echo).
  • Limites WIP por coluna.
  • Motor automático de SLA.

2. Identidade do pacote

{
    "name": "alessandro-nuunes/filament-kanban",
    "description": "Kanban board pages for Filament v5 panels.",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2|^8.3|^8.4",
        "filament/filament": "^5.0",
        "spatie/laravel-package-tools": "^1.16",
        "illuminate/contracts": "^11.0|^12.0"
    }
}

Namespace PHP: AlessandroNuunes\FilamentKanban


3. Estrutura de arquivos

packages/filament-kanban/
├── composer.json
├── README.md
├── CHANGELOG.md
├── LICENSE.md
├── pint.json
├── docs/
│   └── BLUEPRINT.md             ← este arquivo
│   └── PLUGIN-DOCUMENTATION.md  ← docs para usuário final
├── config/
│   └── filament-kanban.php
├── resources/
│   ├── lang/
│   │   ├── en/default.php
│   │   └── pt_BR/default.php
│   └── views/
│       ├── kanban-board.blade.php    ← wrapper + colunas
│       ├── kanban-column.blade.php   ← header + cards da coluna
│       ├── kanban-card.blade.php     ← card individual
│       ├── kanban-empty.blade.php    ← empty state por coluna
│       └── kanban-scripts.blade.php  ← JS Alpine para drag
└── src/
    ├── FilamentKanbanPlugin.php
    ├── FilamentKanbanServiceProvider.php
    ├── Concerns/
    │   ├── InteractsWithKanbanStatuses.php  ← trait para usar no Enum
    │   └── HasKanbanBoard.php               ← trait com lógica para a Page
    ├── Contracts/
    │   └── HasKanbanStatuses.php            ← interface para o Enum
    ├── Exceptions/
    │   └── KanbanTransitionNotAllowedException.php
    ├── Pages/
    │   └── KanbanBoard.php                  ← classe base que a app estende
    ├── Support/
    │   ├── ConfigHelper.php
    │   ├── KanbanRecordRepository.php
    │   └── KanbanSortManager.php
    └── Console/
        └── Commands/
            ├── InstallCommand.php
            └── MakeKanbanCommand.php        ← gera page na app

4. API pública

4.1 Classe base — KanbanBoard

use AlessandroNuunes\FilamentKanban\Pages\KanbanBoard;

class TicketsKanban extends KanbanBoard
{
    // obrigatório
    protected static string $model = Task::class;

    // atributo do model que armazena o status
    protected static string $recordStatusAttribute = 'status';

    // atributo usado como título no card
    protected static string $recordTitleAttribute = 'title';

    // enum ou array manual de statuses (ver seção 4.2)
    protected static ?string $statusEnum = TaskStatus::class;

    // navegação
    protected static ?string $navigationLabel = 'Kanban';
    protected static ?string $navigationIcon  = 'heroicon-o-view-columns';
    protected static ?string $navigationGroup = null;
    protected static ?int $navigationSort = null;

    // modal de edição
    public bool $disableEditModal = false;
    protected string $editModalWidth = '2xl';
    protected bool $editModalSlideOver = false;
    protected string $editModalSaveButtonLabel = 'Save';
    protected string $editModalCancelButtonLabel = 'Cancel';
}

4.2 Statuses — via Enum ou array

Opção A – Enum com interface (recomendado):

use AlessandroNuunes\FilamentKanban\Contracts\HasKanbanStatuses;
use AlessandroNuunes\FilamentKanban\Concerns\InteractsWithKanbanStatuses;

enum TaskStatus: string implements HasKanbanStatuses
{
    use InteractsWithKanbanStatuses;

    case Pending    = 'pending';
    case InProgress = 'in_progress';
    case Completed  = 'completed';
    case Cancelled  = 'cancelled';

    // opcional – filtrar quais aparecem no board
    public static function kanbanCases(): array
    {
        return [self::Pending, self::InProgress, self::Completed];
    }

    // opcional – título customizado por case
    public function getKanbanTitle(): string
    {
        return $this->getLabel(); // usa HasLabel se implementado
    }

    // opcional – cor da coluna
    public function getKanbanColor(): string
    {
        return $this->getColor(); // usa HasColor se implementado
    }
}

Opção B – array manual (quando não existe Enum):

protected function statuses(): Collection
{
    return collect([
        ['id' => 'todo',        'title' => 'To Do',       'color' => 'gray'],
        ['id' => 'in_progress', 'title' => 'In Progress',  'color' => 'blue'],
        ['id' => 'done',        'title' => 'Done',         'color' => 'green'],
    ]);
}

4.3 Métodos sobrescrevíveis

// filtrar/customizar os records carregados
protected function records(): Collection

// permite ou bloqueia a movimentação de um card
protected function canMoveRecord(Model $record, string $from, string $to): bool

// callback pós-status-change (update, log, evento, etc.)
protected function onStatusChanged(
    int|string $recordId,
    string $newStatus,
    array $fromOrderedIds,
    array $toOrderedIds,
): void

// callback pós-sort dentro da mesma coluna
protected function onSortChanged(
    int|string $recordId,
    string $status,
    array $orderedIds,
): void

// customizar dados antes de serem passados para a view do card
protected function mutateRecordDataForCard(array $data, Model $record): array

// schema do modal de edição inline
protected function getEditModalFormSchema(int|string|null $recordId): array

// lógica de salvar via modal de edição
protected function editRecord(int|string $recordId, array $data, array $state): void

// quais transições são permitidas (null = todas)
protected function allowedTransitions(): ?array
// ex.: ['pending' => ['in_progress', 'cancelled'], 'in_progress' => ['completed', 'awaiting']]

4.4 Registrar como página de Resource

// TaskResource.php
public static function getPages(): array
{
    return [
        'index'  => Pages\ListTasks::route('/'),
        'kanban' => Pages\TicketsKanban::route('/kanban'),
        'view'   => Pages\ViewTask::route('/{record}'),
    ];
}

4.5 Plugin (opcional – registro global no panel)

use AlessandroNuunes\FilamentKanban\FilamentKanbanPlugin;

public function panel(Panel $panel): Panel
{
    return $panel->plugins([
        FilamentKanbanPlugin::make(),
    ]);
}

5. Arquitetura interna

5.1 KanbanBoard (Page Livewire)

  • Estende Filament\Resources\Pages\Page.
  • Usa o trait HasKanbanBoard que centraliza:
    • loadStatuses() — resolve via Enum ou statuses().
    • loadRecords() — chama records() e agrupa por status.
    • moveRecord($id, $status, $fromOrderedIds, $toOrderedIds) — Livewire action.
    • sortRecord($id, $status, $orderedIds) — Livewire action.
    • openEditModal($id) / saveEditModal($data) — Livewire actions.
  • Estado Livewire: $groupedRecords, $statuses, $editingRecordId, $editModalData.

5.2 KanbanRecordRepository

Responsável por queries:

  • getRecordsGroupedByStatus(Builder $query, Collection $statuses): array
  • updateStatus(Model $record, string $status, array $payload): void
  • updateOrder(string $model, array $orderedIds, string $orderColumn): void

5.3 KanbanSortManager

  • Detecta se o model tem coluna de ordenação configurada.
  • Persiste a ordem quando shouldPersistSorting() retorna true.
  • Estratégia: incremento 10, 20, 30... para espaçamento entre records.

5.4 KanbanTransitionNotAllowedException

  • Lançada quando canMoveRecord() retorna false ou transição não está em allowedTransitions().
  • Capturada no Livewire action e exibida como notificação de erro.

6. UI / UX

6.1 Layout do board

┌──────────────────────────────────────────────────────┐
│  [Pendente (3)]  [Em Andamento (1)]  [Concluído (5)] │
│  ─────────────   ─────────────────   ─────────────── │
│  [ card ]        [ card ]             [ card ]        │
│  [ card ]                             [ card ]        │
│  [ card ]        ↕ drop zone          ...             │
└──────────────────────────────────────────────────────┘
  • Colunas em scroll horizontal no overflow.
  • Header sticky com cor de badge do status.
  • Cards com drag handle explícito (acessibilidade).
  • Drop zone visual realçado durante drag.
  • Loading spinner no card sendo movido.
  • Toast notification (Filament Notification) no sucesso/erro.

6.2 Card padrão

┌──────────────────────────────┐
│ [drag handle]  Título do     │
│                record        │
│                              │
│ [badge tipo]  [badge prazo]  │
└──────────────────────────────┘

Campos exibidos no card configuráveis via mutateRecordDataForCard().

6.3 Modal de edição

  • Modal padrão do Filament (Filament\Actions\Action).
  • Schema definido por getEditModalFormSchema().
  • Ao salvar chama editRecord() com os dados.
  • Dismiss limpa $editingRecordId.

6.4 Customização de views por board

// na classe do board
protected static string $boardView   = 'filament-kanban::kanban-board';
protected static string $columnView  = 'filament-kanban::kanban-column';
protected static string $cardView    = 'filament-kanban::kanban-card';
protected static string $emptyView   = 'filament-kanban::kanban-empty';
protected static string $scriptsView = 'filament-kanban::kanban-scripts';

Publicar todas as views:

php artisan vendor:publish --tag="filament-kanban-views"

7. Autorização e segurança

7.1 Camadas

Camada Onde Descrição
Visibilidade da página canViewAny() no Resource Quem pode ver o Kanban na navegação
Filtragem de records records() Scoped query por usuário/role
Movimentação de card canMoveRecord() Por record e target status
Transições permitidas allowedTransitions() Mapa de status-origem → destinos válidos
Policy Laravel opcional Verificada antes de persistir o update

7.2 Modo de autorização

Configurável em config/filament-kanban.php:

'authorization_mode' => 'record', // 'record' | 'policy'
  • record: chama canMoveRecord() na Page.
  • policy: chama $policy->update($user, $record) antes de persistir.

7.3 Regras de transição

protected function allowedTransitions(): ?array
{
    return [
        'pending'     => ['in_progress', 'cancelled'],
        'in_progress' => ['awaiting_mei', 'completed', 'pending'],
        'awaiting_mei'=> ['in_progress', 'completed'],
        // null ou ausente = sem restrição para aquele status
    ];
}

8. Sorting

8.1 Sem coluna de ordenação (padrão)

  • Ordem visual é por created_at DESC ou scope customizado em records().
  • Reordenar dentro da mesma coluna é visual-only (não persiste).

8.2 Com coluna de ordenação

// na classe do board
protected function getSortColumn(): ?string
{
    return 'sort_order';
}

protected function shouldPersistSorting(): bool
{
    return true;
}

Quando configurado:

  1. onSortChanged() chama KanbanSortManager::updateOrder().
  2. O manager atualiza o sort_order dos records em $orderedIds com incremento 10, 20, 30....
  3. Registros em outros statuses não são afetados.

9. Eventos e extension points

9.1 Eventos despachados

// antes de persistir
KanbanRecordStatusChanging::class
// payload: $userId, $recordId, $fromStatus, $toStatus

// após persistir
KanbanRecordStatusChanged::class
// payload: $userId, $recordId, $fromStatus, $toStatus, $fromOrderedIds, $toOrderedIds

// após reordenação
KanbanRecordSortingChanged::class
// payload: $userId, $recordId, $status, $orderedIds

9.2 Ouvir nos listeners da app

Event::listen(KanbanRecordStatusChanged::class, function ($event) {
    // ex.: criar timeline, enviar notificação, etc.
});

10. Testes

10.1 Matriz CI

PHP Laravel Filament
8.2 11 5.x latest
8.3 11 5.x latest
8.3 12 5.x latest
8.4 12 5.x latest

10.2 Feature tests (Pest 4)

// board renderiza com statuses
it('renders the kanban board with statuses', function () {
    $user = User::factory()->superAdmin()->create();

    livewire(TicketsKanban::class)
        ->actingAs($user)
        ->assertSeeHtml('pending')
        ->assertSeeHtml('in_progress');
});

// card aparece na coluna correta
it('shows records in the correct status column', function () {
    $task = Task::factory()->create(['status' => 'pending']);

    livewire(TicketsKanban::class)
        ->assertSee($task->title);
});

// mover card atualiza status
it('updates status when a card is moved', function () {
    $task = Task::factory()->create(['status' => 'pending']);

    livewire(TicketsKanban::class)
        ->call('moveRecord', $task->id, 'in_progress', [], [])
        ->assertNotified();

    expect($task->fresh()->status->value)->toBe('in_progress');
});

// transição não permitida bloqueia
it('blocks a forbidden status transition', function () {
    $task = Task::factory()->create(['status' => 'completed']);

    livewire(TicketsKanban::class)
        ->call('moveRecord', $task->id, 'pending', [], [])
        ->assertNotified(); // erro

    expect($task->fresh()->status->value)->toBe('completed');
});

// usuário sem acesso não vê o record
it('scopes records to the authenticated user', function () {
    $otherTask = Task::factory()->create();
    $myTask    = Task::factory()->myTasks()->create();

    livewire(TicketsKanban::class)
        ->assertSee($myTask->title)
        ->assertDontSee($otherTask->title);
});

11. Artisan Command

11.1 filament-kanban:install

php artisan filament-kanban:install

Publica:

  • config → config/filament-kanban.php
  • translations → lang/vendor/filament-kanban/

11.2 make:filament-kanban

php artisan make:filament-kanban TicketsKanban --resource=TaskResource

Gera:

  • app/Filament/{Panel}/Resources/Tasks/Pages/TicketsKanban.php

Conteúdo gerado:

<?php

namespace App\Filament\Admin\Resources\Tasks\Pages;

use App\Filament\Admin\Resources\Tasks\TaskResource;
use AlessandroNuunes\FilamentKanban\Pages\KanbanBoard;
use App\Models\Task;
use App\Enums\TaskStatus;

class TicketsKanban extends KanbanBoard
{
    protected static string $resource = TaskResource::class;

    protected static string $model = Task::class;

    protected static string $recordStatusAttribute = 'status';

    protected static string $recordTitleAttribute = 'protocol';

    protected static ?string $statusEnum = TaskStatus::class;

    protected function onStatusChanged(
        int|string $recordId,
        string $newStatus,
        array $fromOrderedIds,
        array $toOrderedIds,
    ): void {
        Task::find($recordId)?->update(['status' => $newStatus]);
    }
}

12. Plano de releases

v0.1.0 – MVP

  • KanbanBoard base page
  • Statuses via Enum ou array
  • Records agrupados por status
  • Drag-and-drop via Alpine.js → Livewire action
  • onStatusChanged hook
  • Artisan make:filament-kanban
  • Testes básicos
  • Dark mode
  • pt_BR + en

v0.2.0

  • Modal de edição com schema customizável
  • allowedTransitions() com validação
  • canMoveRecord() por record
  • Card slots (subtitle, badges, footer)
  • Sorting persistido

v0.3.0

  • Integração com filtros do Resource
  • Header actions (ex.: "Create ticket")
  • Acessibilidade por teclado
  • Empty-state customizável por coluna

v1.0.0

  • API estável e contrato de backwards-compatibility
  • Guia de migração de v0.x → v1.0
  • Documentação completa
  • Exemplos no README

13. Documentação prevista

Arquivo Conteúdo
README.md Quick start, install, uso básico
docs/BLUEPRINT.md Este arquivo
docs/PLUGIN-DOCUMENTATION.md Docs completo para usuário final
docs/AUTHORIZATION.md Layers de autorização e exemplos
docs/CUSTOMIZATION.md Views, cards, modal, statuses
docs/SORTING.md Sorting com e sem coluna persistida
docs/EVENTS.md Eventos e listeners
docs/TESTING.md Como testar boards na app
docs/UPGRADING.md Guia de upgrade entre versões
CHANGELOG.md Histórico de versões

14. Exemplo de uso – Tickets (MEI)

Contexto: painel Admin, resource TaskResource, model Task, enum TaskStatus.

Colunas do board:

  • Pendente (pending) — warning
  • Em andamento (in_progress) — info
  • Aguardando MEI (awaiting_mei) — gray
  • Concluído (completed) — success

Cancelado (cancelled) e Reaberto (reopened) ficam fora do board (ver kanbanCases()).

Fluxo de transições permitidas:

pending → in_progress, cancelled
in_progress → awaiting_mei, completed, pending
awaiting_mei → in_progress, completed

Integração com TaskResource:

public static function getPages(): array
{
    return [
        'index'  => Pages\ListTasks::route('/'),
        'kanban' => Pages\TicketsKanban::route('/kanban'),
        'view'   => Pages\ViewTask::route('/{record}'),
    ];
}

Link de navegação (sub-nav entre List e Kanban):

// na ListTasks e TicketsKanban
public function getSubNavigation(): array
{
    return [
        TaskResource::getUrl('index')  => 'Lista',
        TaskResource::getUrl('kanban') => 'Kanban',
    ];
}

Lógica de movimentação com side-effects (timeline, notificação):

protected function onStatusChanged(
    int|string $recordId,
    string $newStatus,
    array $fromOrderedIds,
    array $toOrderedIds,
): void {
    $task = Task::find($recordId);
    if (!$task) return;

    $payload = ['status' => $newStatus];

    if ($newStatus === TaskStatus::Completed->value) {
        $payload['completed_at'] = now();
    }

    if ($newStatus === TaskStatus::InProgress->value && $task->attendant_id === null) {
        $payload['attendant_id'] = auth()->id();
        $payload['assigned_at']  = now();
    }

    $task->update($payload);
}
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope