Docs›Componentes (Admin)›mad-gantt — especificação completa
Componentes (Admin)

mad-gantt — especificação completa

Referência completa do builder low-code: atributos, sub-tags, dependências, fases/pessoas/recursos, ops parciais.

Componente declarativo de gráfico de Gantt (engine SVG própria, sem libs externas). Toda configuração mora em atributos da tag <mad-gantt> + sub-tags. A classe PHP é uma casca — só adiciona código para os callbacks de interação.

Princípio: controller é casca. Quanto mais atributos no Blade, menos PHP. Este documento é a fonte canônica para gerar a UI visual de criação de Gantt no MAD Builder — a seção Schema low-code (JSON) lista todos os "knobs" de forma machine-readable.

┌─ Pipeline ──────────────────────────────────────────────────────────────┐
│  <mad-gantt model="..." ...>          (Blade)                            │
│        │  MadGanttCompiler            (compile-time: kebab→camel,         │
│        ▼                               extrai sub-tags → array config)    │
│  MadGanttCompiler::renderInline($cfg, $host)                             │
│        │                                                                  │
│        ▼                                                                  │
│  MadGanttComponent::_renderInlineGantt → buildGantt()                    │
│        │  (auto-query Eloquent: tasks, deps, fases, pessoas, recursos)   │
│        ▼                                                                  │
│  MadGantt (builder fluente) → config JSON (~55 chaves)                   │
│        │                                                                  │
│        ▼                                                                  │
│  Alpine madGantt(cfg)  →  render SVG (sidebar + timeline + barras)       │
└──────────────────────────────────────────────────────────────────────────┘

Três formas de uso, mesma engine:

Forma Quando Host PHP
Declarativo <mad-gantt model=...> Padrão. Dados no banco. MadComponent casca + handlers
Model-driven extends MadGanttComponent Quer mapear campos em PHP, zero Blade subclasse de MadGanttComponent
Builder fluente MadGantt::make() Dados que NÃO vêm do banco qualquer MadComponent, monta no PHP

1. Quick start

(a) Declarativo — recomendado

<?php
use Mad\Component\MadComponent;
use Mad\Http\MadResponse;

class CronogramaView extends MadComponent
{
    protected static string $wrapper = self::INTERNAL;

    public function onTaskClick(string $task_id): MadResponse
    {
        return MadResponse::open('TarefaForm', ['id' => $task_id], 'onEdit');
    }

    protected function view(): string|array { return 'projeto.cronograma'; }
}
<mad-page-container>
    <mad-page-header title="Cronograma" icon="bar-chart-horizontal" />
    <mad-page-content>
        <mad-gantt
            model="ProjetoTarefa" database="minierp"
            name-field="titulo" start-field="dt_inicio" end-field="dt_fim"
            parent-field="parent_id" progress-field="percentual"
            zoom="day" on-task-click="onTaskClick">
            <mad-gantt-column field="name" label="Tarefa" width="220" tree />
        </mad-gantt>
    </mad-page-content>
</mad-page-container>

(b) Model-driven (casca)

<?php
use Mad\Calendar\MadGanttComponent;

class CronogramaView extends MadGanttComponent
{
    protected static string $wrapper = self::INTERNAL;

    protected string $model        = 'ProjetoTarefa';
    protected string $nameField    = 'titulo';
    protected string $startField   = 'dt_inicio';
    protected string $endField     = 'dt_fim';
    protected string $parentField  = 'parent_id';
    protected string $progressField = 'percentual';
    // view() default já renderiza {!! $gantt !!}
}

(c) Builder fluente (dados não-banco)

protected function view(): string|array
{
    $gantt = \Mad\Calendar\MadGantt::make('2026-05-01')
        ->zoom('day')->title('Sprint 22')
        ->addTask('t1', ['name' => 'Setup', 'start' => '2026-05-01', 'end' => '2026-05-03', 'progress' => 100])
        ->addTask('t2', ['name' => 'Dev',   'start' => '2026-05-03', 'end' => '2026-05-09'])
        ->addDependency('t1', 't2', 'FS')
        ->render();
    return ['projeto.cronograma', compact('gantt')];
}

2. Tipos de Gantt (presets)

O espectro de configurações — cada linha é um preset que a UI low-code pode oferecer como ponto de partida. Mapeia para um demo real (DocGantt*).

Tipo Quando usar Habilitar Demo
Timeline básico Visualizar tarefas numa escala, sem interação model + datas + <mad-gantt-column> DocGanttBasico
Hierárquico Subtarefas (WBS), summary bars, milestones parent-field + coluna tree DocGanttHierarquia
Por fase Agrupar em swimlanes coloridas (Discovery/Design/…) phase-field + <mad-gantt-phases> DocGanttDependencias
Dependências + Critical Path Predecessor→sucessor, folga, CPM dependency-source + critical-path + auto-schedule DocGanttDependencias
Recursos + Workload Carga por responsável (horas/dia, overload) <mad-gantt-resources> + <mad-gantt-assignments> + show-workload DocGanttRecursos
Agenda horária Granularidade de horas (salas, reuniões) zoom="hour" + tasks com HH:MM DocGanttAgenda
Calendário de trabalho Dias úteis, feriados, baseline planejado×real working-days + <mad-gantt-holiday> + <mad-gantt-baseline> DocGanttCalendario
CRUD interativo Criar/editar/excluir + drag, sem reload par host+form + ops parciais DocGanttDependencias (+Form)

3. Atributos do <mad-gantt>

Data source

Attr Tipo Default Descrição
model model-ref — Classe model Eloquent das tarefas — short name ou FQCN (obrigatório p/ auto-query)
database string business Conexão usada pelas ações default do host (ex.: onTaskUpdate). A query das tarefas em si usa a conexão do próprio model Eloquent
id-field field-ref id PK da tarefa
name-field field-ref name Nome exibido (suporta dot: categoria.nome)
start-field field-ref start Coluna de início (Y-m-d ou Y-m-d H:i)
end-field field-ref end Coluna de fim
parent-field field-ref — FK self → hierarquia (tree). Vazio = lista plana
progress-field field-ref — % conclusão (fração 0..1 ou 0..100) — leitura decidida por progress-scale
progress-scale enum auto auto | fraction (0..1) | percent (0-100). Banco que guarda 0-100 deve declarar percent: no auto o valor 1 é ambíguo e vira 100%
color-field field-ref — Cor #hex por tarefa
owner-field field-ref — Responsável (avatar)
type-field field-ref — task | summary | milestone
code-field field-ref — Código curto exibido na sidebar (ex: DSC-01)
milestone-field field-ref — Booleano → renderiza diamante
phase-field field-ref — Coluna da fase → ativa agrupamento por fase
resource-field field-ref — CSV de resourceIds ou relação hasMany
order-by string start-field Ordenação da query
dependency-source string — fk:campo ou table:Model,from=,to=,type=,lag=
resource-model model-ref — Model dos recursos
resource-id-field field-ref id PK recurso
resource-name-field field-ref name Nome recurso
resource-role-field field-ref role Papel
resource-capacity-field field-ref capacity Capacidade (horas/dia)
assignment-source string — Model,task=,resource=,hours=
phase-source string — Model,id=,name=,code=,hue=,order=
person-source string — Model,id=,name=,initials=,color=
filters array — :filters="[['ativo','=','1']]" (array-DSL aplicado via QuerySource::applyArrayFilters)

Tempo / escala

Attr Tipo Default Descrição
zoom enum md (≈ month) hour | day | week | month | year (aceita também os aliases legacy xs/sm/md/lg)
view-mode enum days days | months | days_with_hour | months_with_day
start-date string hoje − 3 dias Y-m-d
interval string 30 days Ex: 15 days, 2 month
minutes-step int 1440 Snap ao arrastar (1440=dia, 60=hora)
locale string pt-br Locale das datas

Default real de zoom é md, que resolve pelo alias legacy para month (5px/dia) — não day. Se quiser começar em visão de dias (mais comum pra sprints de 2-6 semanas), declare zoom="day" explicitamente.

Visual

Attr Tipo Default Descrição
arrow-style enum elbow elbow | curved | straight (setas de dependência)
density enum comfortable compact | comfortable | spacious (altura das linhas)
task-col-width int 300 Largura da sidebar (px, min 160)
show-minimap bool true Minimapa navegável no rodapé
show-search bool true Busca (⌘K) na topbar
show-avatars bool true Avatares de responsável na sidebar
show-weekends bool true Fundo dos fins de semana
show-grid bool true Linhas de grade verticais
striped-rows bool true Zebra nas linhas
striped-months bool false Zebra nas colunas

Features

Attr Tipo Default Descrição
critical-path bool false Destaca caminho crítico (CPM) em vermelho
auto-schedule bool false Mover predecessor empurra sucessores
show-workload bool false Banda de carga (recursos) abaixo do chart
workload-mode enum hours hours | tasks | toggle
inline-edit bool false Duplo-clique edita célula da sidebar
multi-select bool false Ctrl+clique seleciona múltiplas
view-mode-btn bool true Botão de troca de view mode no header (ligado por padrão — buildGantt() chama enableViewModeButton() sem o attr; desligue com :view-mode-btn="false")
zoom-btn bool true Botão de zoom no header (idem — desligue com :zoom-btn="false")
full-hours bool false 24h em days_with_hour
compact-events bool false Remove espaço vertical entre eventos (legado)

Calendar

Attr Tipo Default Descrição
working-days string/array 1,2,3,4,5 Dias úteis (0=Dom … 6=Sáb)
working-hours string 0-24 Janela útil (8-18)

Callbacks

Attr Args recebidos Descrição
on-task-click (string $task_id) Clique numa barra
on-task-update (string $task_id, string $start, string $end, string $mode) Drag/resize
on-dependency-create (string $from, string $to, string $type, int $lag) Criar dep (arraste)
on-dependency-delete (string $from, string $to) Remover dep
on-day-click (string $date, string $row_id) Clique em célula vazia
on-reload (string $start, string $end) Navegação prev/next/today

Callbacks são nomes de métodos públicos no host PHP. O MadWire resolve os argumentos por nome (ReflectionMethod + array_key_exists($paramName, $payload)) — o nome do parâmetro PHP precisa bater literalmente com a chave que o JS envia (task_id, não id; row_id, não rowId). Se não bater e o parâmetro não tiver default nem aceitar null, a chamada quebra. Use exatamente os nomes da coluna "Args recebidos" acima.


4. Sub-tags

Sub-tag Repetível Atributos Para
<mad-gantt-column> sim field, source, label, width, format, tree, align, hidden Coluna da sidebar (TreeGrid) + fonte do field-mapping
<mad-gantt-filter> sim field, op, value, value2 Filtro server-side fixo das tarefas
<mad-gantt-header-action> sim label, icon, method Botão no header do gantt
<mad-gantt-baseline> sim task-id, start, end Linha base (planejado × realizado)
<mad-gantt-holiday> sim date Feriado (não-útil)
<mad-gantt-resource> não model, id-field, name-field, role-field, capacity-field Recursos (legado, singular)
<mad-gantt-resource-filter> sim field, op, value, value2 Filtro dos recursos
<mad-gantt-toolbar> não body Blade Toolbar custom acima do gantt
<mad-gantt-popover> não body Blade Popover rich (alternativa a popover-*)
v2 inline (set fixo):
<mad-gantt-phase> sim id, name, code, hue Uma fase hardcoded
<mad-gantt-person> sim id, name, initials, color Uma pessoa hardcoded
v2 source (auto-load DB):
<mad-gantt-phases> não model, id-field, name-field, code-field, hue-field, order-by Fases de uma tabela
<mad-gantt-persons> não model, id-field, name-field, initials-field, color-field Pessoas de uma tabela
<mad-gantt-dependencies> não model, from, to, type, lag Dependências de uma tabela
<mad-gantt-resources> não model, id-field, name-field, role-field, capacity-field Recursos de uma tabela
<mad-gantt-assignments> não model, task, resource, hours Alocações de uma tabela

Inline vs source: as duas formas (<mad-gantt-phase> repetível com set fixo vs <mad-gantt-phases model=...> carregando do banco) coexistem. Se ambas presentes para a mesma coleção, a source (DB) tem prioridade.

format da coluna: date | datetime | days | percent | null. align: left | center | right. tree (bool) = coluna com indent + caret.

Colunas como fonte do field-mapping (field + source)

As <mad-gantt-column> declaram o que renderiza E o mapeamento, então você não precisa dos atributos name-field/start-field/progress-field/… "em cima". Cada coluna traz:

  • field — a chave canônica (name, start, end, progress, parentId, owner, code, milestone, phase, id, duration, color, type).
  • source — a coluna do banco (default = field). Use quando o nome no banco difere do canônico (ex: field="start" source="dt_inicio").
  • hidden — coluna só de mapeamento: alimenta o field-mapping/barra mas não renderiza na sidebar (ex: progress, parentId, phase, milestone que você mapeia mas não exibe como coluna).
<mad-gantt model="TarefaProjeto" database="mini_erp">
    <mad-gantt-column field="name"  source="nome"      label="Tarefa" width="240" tree />
    <mad-gantt-column field="start" source="dt_inicio" label="Início" width="100" format="date" />
    <mad-gantt-column field="end"   source="dt_fim"    label="Fim"    width="100" format="date" />
    <mad-gantt-column field="duration" label="Dias" width="60" format="days" align="right" />
    {{-- mapping-only: não renderiza, só mapeia --}}
    <mad-gantt-column field="progress"  source="progresso_pct" hidden />
    <mad-gantt-column field="parentId"  source="parent_id"     hidden />
</mad-gantt>

Regras:

  • A sidebar renderiza exatamente as colunas visíveis declaradas (honra width/label/format/align); largura total da sidebar = soma das larguras visíveis (dispensa task-col-width).
  • field="duration" é calculado de start/end (não tem source).
  • Se nenhuma coluna tiver tree, a primeira visível vira a árvore.
  • Os atributos *-field na tag continuam funcionando e têm prioridade (override) sobre o que a coluna deriva — útil pra mapear algo sem criar coluna.

5. Zoom & view modes

Zoom pxPerDay Header maior Header menor Caso
hour 480 (20px/h, células 2h) dia hora Agenda intra-dia (salas, reuniões)
day 36 mês dia Sprint / tarefas (2-6 semanas)
week 16 mês semana Roadmap trimestral
month 5 trimestre mês Roadmap semestral
year 3 (encaixa ano cheio) ano mês Roadmap multi-ano

Aliases legacy: xs→day, sm→week, md→month, lg→year.

Zoom hour exige horário nas datas — tasks com start/end em Y-m-d H:i:s. Datas sem hora ocupam o dia inteiro. A largura da barra é proporcional às horas (float-precise).


6. Dependências

Tipos (campo type): FS (Finish→Start, padrão) · SS (Start→Start) · FF (Finish→Finish) · SF (Start→Finish). lag em dias.

Três formas de declarar:

{{-- 1. FK na própria tabela de tarefas (sempre FS, sem lag) --}}
<mad-gantt model="Tarefa" dependency-source="fk:predecessor_id">

{{-- 2. Tabela pivot --}}
<mad-gantt model="Tarefa" dependency-source="table:TarefaDep,from=from_id,to=to_id,type=type,lag=lag">

{{-- 3. Sub-tag (equivalente à forma 2, mais legível) --}}
<mad-gantt model="Tarefa">
    <mad-gantt-dependencies model="TarefaDep" from="from_id" to="to_id" type="type" lag="lag" />
</mad-gantt>

UX de criação: ao passar o mouse numa barra aparecem bolinhas nas pontas. Arraste de uma bolinha até outra barra cria a dependência (on-dependency-create). Shift+arraste do corpo da barra também funciona. Ciclos são rejeitados.

critical-path destaca em vermelho as tarefas sem folga. auto-schedule reagenda sucessores quando o predecessor move.


7. Fases & pessoas

phase-field="phase_id" ativa o agrupamento: as tarefas viram swimlanes coloridas por fase. As fases vêm de uma tabela (<mad-gantt-phases>) ou inline (<mad-gantt-phase>). Cada fase tem hue (0-360, OKLCH) que gera o gradiente da barra.

<mad-gantt model="Tarefa" phase-field="phase_id">
    {{-- DB --}}
    <mad-gantt-phases model="Fase" id-field="id" name-field="nome" code-field="sigla" hue-field="hue" order-by="ordem" />
    {{-- ou inline --}}
    <mad-gantt-phase id="discovery" name="Discovery" code="DSC" hue="250" />
    <mad-gantt-phase id="design"    name="Design"    code="DSG" hue="290" />
</mad-gantt>

code-field exibe um código curto na sidebar; milestone-field (booleano) transforma a tarefa num diamante. Avatares vêm de people / <mad-gantt-persons> (owner-field referencia o id da pessoa).


8. Hooks PHP (MadGanttComponent)

Só relevante no modo model-driven. Override quando precisa customizar.

Hook Assinatura Quando
buildQuery() (): \Illuminate\Database\Eloquent\Builder Query custom das tarefas (Eloquent)
buildColumns() (): array Colunas da sidebar
mapTask($record) (object): array Mapear record Eloquent → props da task (delega a MadGantt::recordToTask)
configureGantt($g) (MadGantt): void Hook final (callbacks, header actions, baselines)
loadPhases($g) (MadGantt): void Carregar fases (default lê $phaseSource)
loadPersons($g) (MadGantt): void Carregar pessoas
loadDependencies($g, $ids) (MadGantt, array): void Carregar deps
loadResources($g) (MadGantt): void Carregar recursos
loadAssignments($g) (MadGantt): void Carregar alocações

Callbacks de interação são métodos normais: onTaskClick, onTaskUpdate, onDependencyCreate, onDependencyDelete, onReload, onDayClick.

Helpers reusáveis na base MadComponent (úteis em forms de tarefa com PK slug e árvore):

// Gera id único slug a partir de texto (sufixa _2, _3…). Eloquent puro — sem
// transação obrigatória (só faz um SELECT de unicidade por candidato).
$id = $this->generateUniqueId('Tarefa', $data->nome);   // "Wireframes" → "wireframes"

// Coleta ids de todos os descendentes (cascade delete). Eloquent puro —
// envolva numa transação só se a deleção em si precisar ser atômica.
$ids = $this->collectDescendantIds('Tarefa', $rootId, 'parent_id'); // [root, filho, neto…]

9. Operações parciais (sem reload)

Após persistir, atualize só o necessário no gantt — preserva scroll, zoom, filtro e estado de expand. Métodos do MadResponse:

Método Assinatura Efeito
ganttPatchTask ($id, $start, $end, $selector='.mad-gantt') Reposiciona UMA barra (drag), shape manual
ganttPatchTasks ($tasks, $selector='.mad-gantt') Reposiciona várias (cascade)
ganttUpsertTask ($task, $selector='.mad-gantt') Insere/atualiza UMA tarefa (shape JS completo)
ganttRemoveTask ($id, $alsoRemove=[], $selector='.mad-gantt') Remove tarefa + filhos + deps ligadas
ganttUpsertRecord ($record, $map=[], $selector='.mad-gantt') Idem upsert, mas mapeia o record Eloquent sozinho
ganttPatchRecord ($record, $map=[], $selector='.mad-gantt') Idem patch (só datas) a partir do record Eloquent

Prefira *Record. ganttUpsertRecord($task) / ganttPatchRecord($task) recebem o model Eloquent salvo e geram o shape JS via MadGantt::recordToTask() — o mesmo mapper do render (mapTask). Nada de escrever toJsTask() à mão. O $map (role => coluna) é opcional: vazio → convenção (defaultTaskMap: id/name/start/end/progress/parent_id/owner_id/code/milestone/phase_id). Passe um map inline só quando as colunas fugirem da convenção: ->ganttUpsertRecord($t, ['name'=>'nome','start'=>'dt_inicio']).

// Drag commit — só reposiciona a barra (mapper resolve id/start/end do record)
public function onTaskUpdate(string $task_id, string $start, string $end, string $mode = ''): MadResponse
{
    $t = Tarefa::findOrFail($task_id);
    $t->start = $start;
    $t->end   = $end;
    $t->save();
    return (new MadResponse())->ganttPatchRecord($t)->toast('Reagendada', 'info');
}

MadGantt::recordToTask($record, $map) é a fonte única de verdade do shape: ['id','name','start','end','progress'(0..1),'parentId','owner','code', '<phaseField>','milestone'(bool),'type']. Roles do $map: id, name, start, end, progress, parentId, owner, color, type, code, milestone, phase (a phase é emitida sob o nome da coluna p/ casar com cfg.phaseField).

Quando a mudança é estrutural demais para patch (ex: criou dependência), use $this->forceFullRender() no host e devolva MadResponse vazio — o gantt re-renderiza do zero relendo o banco.


10. Padrão CRUD (host + form)

O par recomendado: um host (gantt) + um form (drawer) separados.

// HOST — abre o form por cima
public function onTaskClick(string $task_id): MadResponse
{
    return MadResponse::open('TarefaForm', ['id' => $task_id], 'onEdit');
}
public function onAddTask(string $name = ''): MadResponse
{
    return MadResponse::open('TarefaForm');   // sem onEdit = nova
}
// FORM (drawer) — TarefaForm
class TarefaForm extends MadComponent
{
    protected static string $wrapper = self::DRAWER;
    public MadForm $form;
    public string $editingId = '';

    public function boot(): void { $this->form = new MadForm('form'); } // toda request (AJAX-safe)
    public function mount(array $p = []): void { $this->parentId = (string)($p['parentId'] ?? ''); $this->fillDefaults(); }
    public function onEdit(string $id): void   // PK slug → string (não int)
    {
        $t = Tarefa::findOrFail($id);          // leitura não precisa de transação
        $this->editingId = (string) $t->id;
        // fill() mapeia coluna→campo por nome; $extra só pro que diverge da UI.
        $this->form->fill($t, [
            'progress'  => (float) $t->progress * 100,   // 0..1 → 0..100
            'milestone' => $t->milestone ? '1' : '0',
        ]);
    }
    public function onSave(): MadResponse
    {
        $data = $this->form->getData();
        $this->form->validate(Tarefa::rules($this->editingId));

        $isNew = $this->editingId === '';
        $t = DB::connection('business')->transaction(function () use ($isNew, $data) {
            $t = $isNew ? new Tarefa() : Tarefa::findOrFail($this->editingId);

            // form->save($record, $extra): fillRecord + save(); $extra cobre só o
            // que diverge da UI (progresso 0..100 → 0..1, null, slug da PK).
            $pct = (float) ($data->progress ?? 0);
            $extra = [
                'progress'  => max(0, min(1, $pct > 1 ? $pct / 100 : $pct)),
                'milestone' => (!empty($data->milestone) && $data->milestone !== '0') ? 1 : 0,
                'owner_id'  => trim((string) ($data->owner_id ?? '')) ?: null,
                'parent_id' => trim((string) ($data->parent_id ?? '')) ?: null,
            ];
            if ($isNew) $extra['id'] = $this->generateUniqueId('Tarefa', (string) $data->name);

            return $this->form->save($t, $extra);
        });

        return (new MadResponse())->toast('Salvo!','success')->closeDrawer()
            ->ganttUpsertRecord($t);   // shape por convenção, sem toJsTask
    }
}

De onde vem o field-mapping

Lado Fonte
Render (a barra) atributos da tag <mad-gantt name-field=... start-field=...> (ou as props do MadGanttComponent model-driven)
Upsert (ganttUpsertRecord) convenção defaultTaskMap quando as colunas seguem o padrão (id/name/start/end/progress/parent_id/owner_id/code/milestone/phase_id) — zero declaração. Map inline só pra colunas fora do padrão.
Conversão UI↔banco (progress %, milestone bool, null) $extra do fill()/save() no próprio form — é concern de UI, não do gantt

Sem função extra no model. As colunas do Tarefa seguindo a convenção, ganttUpsertRecord($t) não precisa de map nem de ganttMap(). O save($record, $extra)/fill($data, $extra) ($extra literal ou Closure fn($atual,$record)=>$novo, aplicado após fromArray, antes do store()) resolve as poucas conversões de unidade que a UI guarda diferente do banco.

Combos do form usam dbcombo auto-load (sem montar options em PHP):

<mad-dbcombo-field name="phase_id" model="Fase" display="nome" order-by="ordem" required />
<mad-dbcombo-field name="owner_id" model="Pessoa" display="nome" placeholder="— Sem responsável —" />
@php $parentFilters = [['milestone','=',0]]; if (!empty($that->editingId)) $parentFilters[] = ['id','!=',$that->editingId]; @endphp
<mad-dbcombo-field name="parent_id" model="Tarefa" display="nome" :filters="$parentFilters" placeholder="— Root —" />

O dbcombo lê o valor selecionado do MadForm automaticamente (via MadRenderContext) — basta form->fill() setar o campo.


11. Exemplo end-to-end (fases + dependências + DB)

class CronogramaView extends MadComponent
{
    protected static string $wrapper = self::INTERNAL;

    public function onTaskClick(string $task_id): MadResponse
    {
        return MadResponse::open('TarefaForm', ['id' => $task_id], 'onEdit');
    }
    public function onTaskUpdate(string $task_id, string $start, string $end, string $mode = ''): MadResponse
    {
        $t = Tarefa::findOrFail($task_id);
        $t->start = $start;
        $t->end   = $end;
        $t->save();
        return (new MadResponse())->ganttPatchRecord($t)->toast('Reagendada','info');
    }
    public function onDependencyCreate(string $from, string $to, string $type='FS', int $lag=0): MadResponse
    {
        $d = new TarefaDep();
        $d->from_id = $from;
        $d->to_id   = $to;
        $d->type    = $type;
        $d->lag     = $lag;
        $d->save();
        $this->forceFullRender();
        return MadToast::success("Dep {$from} → {$to}");
    }
    protected function view(): string|array { return 'projeto.cronograma'; }
}
<mad-page-container>
    <mad-page-header title="Cronograma" icon="bar-chart-horizontal">
        <actions><mad-btn navigate="TarefaForm" variant="primary" icon="plus">Nova</mad-btn></actions>
    </mad-page-header>
    <mad-page-content>
        <mad-gantt
            model="Tarefa" database="minierp"
            name-field="nome" start-field="dt_ini" end-field="dt_fim"
            parent-field="parent_id" progress-field="progresso"
            owner-field="responsavel_id" phase-field="fase_id"
            code-field="codigo" milestone-field="marco" order-by="ordem"
            zoom="day" arrow-style="elbow" density="comfortable"
            critical-path auto-schedule
            on-task-click="onTaskClick" on-task-update="onTaskUpdate"
            on-dependency-create="onDependencyCreate">

            <mad-gantt-phases       model="Fase"     id-field="id" name-field="nome" code-field="sigla" hue-field="hue" order-by="ordem" />
            <mad-gantt-persons      model="Pessoa"   id-field="id" name-field="nome" initials-field="iniciais" color-field="cor" />
            <mad-gantt-dependencies model="TarefaDep" from="from_id" to="to_id" type="type" lag="lag" />

            <mad-gantt-column field="name"     label="Tarefa" width="220" tree />
            <mad-gantt-column field="code"     label="Cód."   width="70" />
            <mad-gantt-column field="duration" label="Dias"   width="50" format="days" align="right" />

            <mad-gantt-header-action label="+ Nova" icon="plus" method="onAddTask" />
        </mad-gantt>
    </mad-page-content>
</mad-page-container>

12. Ponto de partida pra copiar

Não há stub físico no repo — use a seção 1. Quick start (forma declarativa) e o 11. Exemplo end-to-end como template: troque model/database/*-field pelos nomes reais do seu schema. Para o par host+form CRUD completo, copie a seção 10. Padrão CRUD (host + form).


13. Schema low-code (JSON)

Catálogo canônico de knobs para a UI de criação. type define o controle (model-ref→combo de models, field-ref→select de coluna, enum→dropdown, bool→toggle, int→number, string→input, callback-ref→select de método). group organiza em abas/seções. presets ofertam templates de partida.

{
  "tag": "mad-gantt",
  "attributes": [
    { "key": "model", "kebab": "model", "type": "model-ref", "group": "data", "required": true, "default": null, "desc": "Classe model Eloquent das tarefas" },
    { "key": "database", "kebab": "database", "type": "string", "group": "data", "default": "business" },
    { "key": "idField", "kebab": "id-field", "type": "field-ref", "group": "data", "default": "id" },
    { "key": "nameField", "kebab": "name-field", "type": "field-ref", "group": "data", "default": "name", "desc": "Suporta dot-notation" },
    { "key": "startField", "kebab": "start-field", "type": "field-ref", "group": "data", "default": "start" },
    { "key": "endField", "kebab": "end-field", "type": "field-ref", "group": "data", "default": "end" },
    { "key": "parentField", "kebab": "parent-field", "type": "field-ref", "group": "data", "default": null, "desc": "FK self → hierarquia" },
    { "key": "progressField", "kebab": "progress-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "colorField", "kebab": "color-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "ownerField", "kebab": "owner-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "typeField", "kebab": "type-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "codeField", "kebab": "code-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "milestoneField", "kebab": "milestone-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "phaseField", "kebab": "phase-field", "type": "field-ref", "group": "phase", "default": null, "desc": "Ativa agrupamento por fase" },
    { "key": "resourceField", "kebab": "resource-field", "type": "field-ref", "group": "data", "default": null },
    { "key": "orderBy", "kebab": "order-by", "type": "string", "group": "data", "default": "<start-field>" },
    { "key": "dependencySource", "kebab": "dependency-source", "type": "string", "group": "data", "default": null, "desc": "fk:campo ou table:Model,from=,to=,type=,lag=" },
    { "key": "resourceModel", "kebab": "resource-model", "type": "model-ref", "group": "data", "default": null },
    { "key": "resourceIdField", "kebab": "resource-id-field", "type": "field-ref", "group": "data", "default": "id" },
    { "key": "resourceNameField", "kebab": "resource-name-field", "type": "field-ref", "group": "data", "default": "name" },
    { "key": "resourceRoleField", "kebab": "resource-role-field", "type": "field-ref", "group": "data", "default": "role" },
    { "key": "resourceCapacityField", "kebab": "resource-capacity-field", "type": "field-ref", "group": "data", "default": "capacity" },
    { "key": "assignmentSource", "kebab": "assignment-source", "type": "string", "group": "data", "default": null },
    { "key": "phaseSource", "kebab": "phase-source", "type": "string", "group": "phase", "default": null },
    { "key": "personSource", "kebab": "person-source", "type": "string", "group": "phase", "default": null },
    { "key": "filters", "kebab": "filters", "type": "array", "group": "data", "default": null },

    { "key": "zoom", "kebab": "zoom", "type": "enum", "group": "time", "default": "md", "desc": "md resolve via alias legacy para month", "options": ["hour","day","week","month","year"] },
    { "key": "viewMode", "kebab": "view-mode", "type": "enum", "group": "time", "default": "days", "options": ["days","months","days_with_hour","months_with_day"] },
    { "key": "startDate", "kebab": "start-date", "type": "string", "group": "time", "default": "<today>" },
    { "key": "interval", "kebab": "interval", "type": "string", "group": "time", "default": "30 days" },
    { "key": "minutesStep", "kebab": "minutes-step", "type": "int", "group": "time", "default": 1440 },
    { "key": "locale", "kebab": "locale", "type": "string", "group": "time", "default": "pt-br" },

    { "key": "arrowStyle", "kebab": "arrow-style", "type": "enum", "group": "visual", "default": "elbow", "options": ["elbow","curved","straight"] },
    { "key": "density", "kebab": "density", "type": "enum", "group": "visual", "default": "comfortable", "options": ["compact","comfortable","spacious"] },
    { "key": "taskColWidth", "kebab": "task-col-width", "type": "int", "group": "visual", "default": 300, "min": 160 },
    { "key": "showMinimap", "kebab": "show-minimap", "type": "bool", "group": "visual", "default": true },
    { "key": "showSearch", "kebab": "show-search", "type": "bool", "group": "visual", "default": true },
    { "key": "showAvatars", "kebab": "show-avatars", "type": "bool", "group": "visual", "default": true },
    { "key": "showWeekends", "kebab": "show-weekends", "type": "bool", "group": "visual", "default": true },
    { "key": "showGrid", "kebab": "show-grid", "type": "bool", "group": "visual", "default": true },
    { "key": "stripedRows", "kebab": "striped-rows", "type": "bool", "group": "visual", "default": true },
    { "key": "stripedMonths", "kebab": "striped-months", "type": "bool", "group": "visual", "default": false },

    { "key": "criticalPath", "kebab": "critical-path", "type": "bool", "group": "features", "default": false },
    { "key": "autoSchedule", "kebab": "auto-schedule", "type": "bool", "group": "features", "default": false },
    { "key": "showWorkload", "kebab": "show-workload", "type": "bool", "group": "features", "default": false },
    { "key": "workloadMode", "kebab": "workload-mode", "type": "enum", "group": "features", "default": "hours", "options": ["hours","tasks","toggle"] },
    { "key": "inlineEdit", "kebab": "inline-edit", "type": "bool", "group": "features", "default": false },
    { "key": "multiSelect", "kebab": "multi-select", "type": "bool", "group": "features", "default": false },
    { "key": "viewModeBtn", "kebab": "view-mode-btn", "type": "bool", "group": "features", "default": true, "desc": "ligado por padrão mesmo sem declarar o attr" },
    { "key": "zoomBtn", "kebab": "zoom-btn", "type": "bool", "group": "features", "default": true, "desc": "ligado por padrão mesmo sem declarar o attr" },
    { "key": "fullHours", "kebab": "full-hours", "type": "bool", "group": "features", "default": false },
    { "key": "compactEvents", "kebab": "compact-events", "type": "bool", "group": "features", "default": false },

    { "key": "workingDays", "kebab": "working-days", "type": "string", "group": "calendar", "default": "1,2,3,4,5", "desc": "0=Dom..6=Sab" },
    { "key": "workingHours", "kebab": "working-hours", "type": "string", "group": "calendar", "default": "0-24" }
  ],
  "callbacks": [
    { "key": "onTaskClick", "kebab": "on-task-click", "args": ["task_id"] },
    { "key": "onTaskUpdate", "kebab": "on-task-update", "args": ["task_id","start","end","mode"] },
    { "key": "onDependencyCreate", "kebab": "on-dependency-create", "args": ["from","to","type","lag"] },
    { "key": "onDependencyDelete", "kebab": "on-dependency-delete", "args": ["from","to"] },
    { "key": "onDayClick", "kebab": "on-day-click", "args": ["date","row_id"] },
    { "key": "onReload", "kebab": "on-reload", "args": ["start","end"] }
  ],
  "subTags": [
    { "tag": "mad-gantt-column", "repeatable": true, "attrs": ["field","source","label","width","format","tree","align","hidden"], "note": "field=chave canônica; source=coluna do banco (default=field); hidden=só mapeia, não renderiza. As colunas SÃO a fonte do field-mapping (os *-field viram override opcional)" },
    { "tag": "mad-gantt-filter", "repeatable": true, "attrs": ["field","op","value","value2"] },
    { "tag": "mad-gantt-header-action", "repeatable": true, "attrs": ["label","icon","method"] },
    { "tag": "mad-gantt-baseline", "repeatable": true, "attrs": ["task-id","start","end"] },
    { "tag": "mad-gantt-holiday", "repeatable": true, "attrs": ["date"] },
    { "tag": "mad-gantt-phase", "repeatable": true, "attrs": ["id","name","code","hue"] },
    { "tag": "mad-gantt-person", "repeatable": true, "attrs": ["id","name","initials","color"] },
    { "tag": "mad-gantt-phases", "repeatable": false, "attrs": ["model","id-field","name-field","code-field","hue-field","order-by"] },
    { "tag": "mad-gantt-persons", "repeatable": false, "attrs": ["model","id-field","name-field","initials-field","color-field"] },
    { "tag": "mad-gantt-dependencies", "repeatable": false, "attrs": ["model","from","to","type","lag"] },
    { "tag": "mad-gantt-resources", "repeatable": false, "attrs": ["model","id-field","name-field","role-field","capacity-field"] },
    { "tag": "mad-gantt-assignments", "repeatable": false, "attrs": ["model","task","resource","hours"] },
    { "tag": "mad-gantt-resource", "repeatable": false, "attrs": ["model","id-field","name-field","role-field","capacity-field"] },
    { "tag": "mad-gantt-resource-filter", "repeatable": true, "attrs": ["field","op","value","value2"] },
    { "tag": "mad-gantt-toolbar", "repeatable": false, "body": "blade" },
    { "tag": "mad-gantt-popover", "repeatable": false, "body": "blade" }
  ],
  "partialOps": [
    { "method": "ganttPatchTask", "args": ["id","start","end","selector?"] },
    { "method": "ganttPatchTasks", "args": ["tasks","selector?"] },
    { "method": "ganttUpsertTask", "args": ["task","selector?"] },
    { "method": "ganttUpsertRecord", "args": ["record","map?","selector?"], "preferred": true, "note": "record Eloquent -> shape via MadGantt::recordToTask; map vazio = convenção defaultTaskMap" },
    { "method": "ganttPatchRecord", "args": ["record","map?","selector?"], "preferred": true },
    { "method": "ganttRemoveTask", "args": ["id","alsoRemove?","selector?"] }
  ],
  "recordMapper": {
    "method": "MadGantt::recordToTask",
    "args": ["record","map?"],
    "defaultMap": { "id":"id","name":"name","start":"start","end":"end","progress":"progress","parentId":"parent_id","owner":"owner_id","code":"code","milestone":"milestone","phase":"phase_id" },
    "roles": ["id","name","start","end","progress","parentId","owner","color","type","code","milestone","phase"],
    "note": "record->shape JS; map vazio = convenção (defaultMap); SEM função ganttMap no model; phase emitida sob o nome da coluna (casa com cfg.phaseField); scale de UI NÃO se aplica (barra usa unidade do banco)"
  },
  "formSave": {
    "save": { "args": ["record","extra?"], "note": "fillRecord(record) + extra (array ou Closure) + record->save(); extra aplicado pós-fillRecord, pré-save" },
    "fill": { "args": ["data","extra?"], "note": "preenche campos; extra sobrescreve" },
    "note": "conversões UI<->banco (progress %, milestone bool, null) vivem no extra do form — concern de UI, não há contrato no model"
  },
  "presets": [
    { "id": "basic", "label": "Timeline básico", "enables": [], "subTags": ["mad-gantt-column"] },
    { "id": "hierarchical", "label": "Hierárquico (WBS)", "enables": ["parent-field"], "subTags": ["mad-gantt-column"] },
    { "id": "phased", "label": "Por fase", "enables": ["phase-field"], "subTags": ["mad-gantt-phases","mad-gantt-column"] },
    { "id": "dependencies", "label": "Dependências + Critical Path", "enables": ["dependency-source","critical-path","auto-schedule"], "subTags": ["mad-gantt-dependencies","mad-gantt-column"] },
    { "id": "resources", "label": "Recursos + Workload", "enables": ["show-workload"], "subTags": ["mad-gantt-resources","mad-gantt-assignments"] },
    { "id": "hourly", "label": "Agenda horária", "enables": ["zoom=hour"], "subTags": ["mad-gantt-column"], "note": "tasks precisam de HH:MM" },
    { "id": "calendar", "label": "Calendário de trabalho", "enables": ["working-days","working-hours"], "subTags": ["mad-gantt-holiday","mad-gantt-baseline"] },
    { "id": "crud", "label": "CRUD interativo", "enables": ["inline-edit"], "subTags": ["mad-gantt-header-action"], "note": "par host+form com ops parciais" }
  ]
}

14. NUNCA fazer

Errado Certo
<mad-gantt> literal dentro de {{-- comentário --}} Blade O compilador casa a tag antes do Blade processar o comentário → ParseError. Escreva mad-gantt sem <> ou use &lt;/&gt;
phase-field="fase" mas a task não tem coluna fase phase-field deve bater com uma coluna real da task — senão tudo cai em "— Sem fase —"
zoom="hour" com tasks só Y-m-d Zoom hora precisa de Y-m-d H:i:s em start/end; sem hora a barra ocupa o dia inteiro
Inicializar MadForm no mount() do form mount() só roda na 1ª carga; o onSave (AJAX) acharia $this->form indefinido. Use boot()
onEdit(int $id) com PK slug (string) 'wireframe' vira 0. Use onEdit(string $id)
Montar options de combo em PHP (setItems) no form Use <mad-dbcombo-field model=...> — auto-load + selected do form
MadWire.refresh(host) para atualizar o gantt Sem action o host responde partial vazio. Use uma action com forceFullRender() ou ops parciais (ganttUpsertTask)
Recriar todo o gantt após drag Use ganttPatchTask($id,$start,$end) — atualiza só a barra, preserva scroll/zoom
Esperar botão Export/Undo na topbar Removidos. Não existem mais enable-export/enable-undo