mad-gantt
Gantt chart.
Gráfico Gantt com tarefas hierárquicas, dependências, recursos, baselines, caminho crítico e edição inline.
Spec completa (todos os atributos, sub-tags e schema low-code): ver gantt-builder.md. Esta página é um quick-start; CRUD host+form em detalhe: gantt-crud-form.md.
Três formas de uso:
| API | Quando usar |
|---|---|
<mad-gantt> declarativo (Blade-first) |
Embedar em qualquer view sem subclass. Aceita todas as props + sub-tags |
extends MadGanttComponent + <mad-gantt> no view |
Quando precisa de hooks PHP (callbacks, query custom, mapTask) |
MadGantt::make() builder fluente |
Dados não-banco (memória, API externa, transformação custom) |
API declarativa Blade-first
Use <mad-gantt> em qualquer view (não precisa subclass). Toda config
mora em atributos + sub-tags filhas. Subclass MadGanttComponent é opcional
e entra apenas para hooks comportamentais (callbacks, query custom).
Controller
<?php
use Mad\Calendar\MadGanttComponent;
class ProjetoGantt extends MadGanttComponent
{
protected static string $wrapper = self::INTERNAL;
protected string $model = 'DocProjetoTarefa';
protected string $database = 'minierp';
protected string $nameField = 'titulo';
protected string $startField = 'dt_inicio';
protected string $endField = 'dt_fim';
protected string $parentField = 'parent_id';
protected string $progressField = 'percentual';
protected string $colorField = 'cor';
protected string $ownerField = 'responsavel';
// Opcional — customizar a query das tarefas (Eloquent Builder)
protected function buildQuery(): \Illuminate\Database\Eloquent\Builder
{
return parent::buildQuery()->where('projeto_id', '=', $this->projetoId);
}
public int $projetoId = 0;
public function mount(array $params = []): void
{
$this->projetoId = (int) ($params['projeto_id'] ?? 0);
}
protected function view(): string|array
{
return 'projetos.projeto-gantt';
}
}
Blade
<mad-page-container>
<mad-page-header title="Cronograma" icon="bar-chart-horizontal" />
<mad-page-content>
{!! $gantt !!}
</mad-page-content>
</mad-page-container>
O builder $gantt é injetado automaticamente no Blade quando a subclass
estende MadGanttComponent — ou use <mad-gantt model="..." /> para
declarar inline.
Atributos do <mad-gantt> declarativo
<mad-gantt
model="DocProjetoTarefa" database="minierp"
name-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
parent-field="parent_id"
progress-field="percentual"
color-field="cor"
owner-field="responsavel"
view-mode="months_with_day"
zoom="month"
start-date="2026-01-01"
interval="90 days"
title="Sprint Q1"
striped-rows
full-hours
view-mode-btn
zoom-btn
on-task-click="onTaskClick"
on-task-update="onTaskUpdate"
on-day-click="onDayClick" />
Data source
| Attr | Descrição |
|---|---|
model |
Classe Eloquent (model) das tarefas — habilita auto-query |
database |
Conexão (default MAIN_DATABASE) |
order-by |
Ordenação (default = start-field) |
id-field |
Coluna do task id (default id) |
name-field |
Coluna do nome (suporta dot-notation) |
start-field |
Coluna de timestamp de início |
end-field |
Coluna de fim |
parent-field |
Hierarquia (vazio = flat) |
progress-field |
Coluna de progresso — fração 0..1 ou 0-100; a leitura é decidida por progress-scale |
progress-scale |
auto (default) | fraction (0..1) | percent (0-100) — ver abaixo |
color-field |
Cor hex |
owner-field |
Responsável (string ou relação dotted) |
type-field |
task | summary | milestone |
resource-field |
CSV de resourceIds ou nome de relação hasMany |
progress-scale — normalização do progresso (5.x)
O gantt trabalha internamente com fração 0..1. Como cada banco guarda o
percentual de um jeito, progress-scale declara como ler a coluna de
progress-field:
| Valor | Leitura |
|---|---|
auto (default) |
Heurística: valor > 1 é lido como 0-100 (dividido por 100); <= 1 fica como fração |
fraction |
Sempre 0..1, sem conversão |
percent |
Sempre 0-100, dividido por 100 |
<mad-gantt model="ProjetoTarefa"
progress-field="percentual" progress-scale="percent" ... />
Banco que guarda
0-100deve declararpercent. Noautoo valor1é ambíguo e cai como fração — uma tarefa em 1% aparece como 100% concluída.fractiontem o espelho do problema para bancos0..1.
A normalização acontece no ingest — na carga inicial (normalizeTasks) e
também em cada task que chega depois por MadResponse (normalizeTask com o
mesmo progressScale do config), então tarefa nova/atualizada segue a mesma
escala das já renderizadas.
View modes / zoom
view-mode |
Renderiza |
|---|---|
days (default) |
Coluna por dia |
months |
Coluna por mês |
days_with_hour |
Coluna por dia + sub-coluna por hora |
months_with_day |
Coluna por mês + sub-coluna por dia |
zoom |
hour | day | week | month | year — aliases legados xs→day, sm→week, md→month, lg→year. Default md (≈ month) |
|---|
Display
| Attr | Default | Descrição |
|---|---|---|
interval |
30 days |
Range visível inicial |
start-date |
hoje | Y-m-d |
title |
'' |
Header acima do gantt |
striped-rows |
true | Linhas alternadas (zebra) |
full-hours |
false | Mostra todas as 24h em days_with_hour |
compact-events |
false | Reduz altura de barra de evento |
locale |
pt-br |
Locale para formatação |
Toolbar
| Attr | Descrição |
|---|---|
view-mode-btn |
Botão para alternar view mode |
zoom-btn |
Botão de zoom +/- |
inline-edit |
Edição inline de campos |
multi-select |
Multi-select com Ctrl+click |
Dependências entre tarefas
<mad-gantt
model="Tarefa"
dependency-source="fk:predecessor_id" />
Opções para dependency-source:
| Valor | Comportamento |
|---|---|
'' (vazio) |
Sem dependências |
fk:nome_campo |
Coluna FK na própria tabela de tarefas (FS, sem lag) |
table:NomeModel,from=fk_from,to=fk_to,type=tipo,lag=campo_lag |
Tabela separada de dependências |
Recursos (resources)
<mad-gantt
model="Tarefa"
resource-model="Funcionario"
resource-name-field="nome"
resource-role-field="cargo"
resource-capacity-field="capacidade_horas"
show-workload
workload-mode="hours" />
Working calendar
<mad-gantt
working-days="1,2,3,4,5"
working-hours="08-18">
<mad-gantt-holiday date="2026-01-01" />
<mad-gantt-holiday date="2026-04-21" />
<mad-gantt-holiday date="2026-12-25" />
</mad-gantt>
Sub-tags (listas)
Use sub-tags filhas para listas (columns, filter, header actions, baselines, holidays). Detalhes completos em gantt-builder.md.
<mad-gantt model="ProjetoTarefa" name-field="titulo" start-field="dt_inicio" end-field="dt_fim">
<mad-gantt-column field="name" label="Tarefa" width="240" tree />
<mad-gantt-column field="start" label="Início" width="100" format="date" />
<mad-gantt-column field="duration" label="Dias" width="70" format="days" align="center" />
<mad-gantt-filter field="ativo" op="=" value="1" />
<mad-gantt-filter field="deleted_at" op="is" value="null" />
<mad-gantt-header-action label="Nova" icon="plus" method="onNova" />
<mad-gantt-baseline task-id="42" start="2026-03-01" end="2026-03-15" />
<mad-gantt-resource model="Funcionario" name-field="nome" role-field="cargo" />
<mad-gantt-resource-filter field="ativo" op="=" value="1" />
<mad-gantt-popover>
<strong>{title}</strong><br>{start} → {end}<br>Prog: {percent}%
</mad-gantt-popover>
</mad-gantt>
Ações reativas
| Attr | PHP recebe |
|---|---|
on-task-click |
(string $task_id): MadResponse |
on-task-update |
(string $task_id, string $start, string $end, string $mode = ''): MadResponse |
on-day-click |
(string $date, ?string $row_id = null): MadResponse |
on-dependency-create |
(string $from, string $to, string $type): MadResponse |
on-dependency-delete |
(string $from, string $to): MadResponse |
on-reload |
(string $startDate, string $endDate): void |
Os args ligam por nome a partir do payload que o JS (
mad-gantt.js) envia:{task_id}no clique,{task_id, start, end, mode}no drag/resize (mode= tipo do arraste, não progresso),{date, row_id}no clique de dia,{from, to}na dependência. O nome do parâmetro PHP tem que bater com a chave —$rowId(camelCase) nunca receberiarow_id.
public function onTaskClick(string $task_id): MadResponse
{
return MadResponse::open('TarefaForm', ['id' => $task_id]);
}
public function onTaskUpdate(string $task_id, string $start, string $end, string $mode = ''): MadResponse
{
DB::connection('minierp')->transaction(function () use ($task_id, $start, $end) {
$t = Tarefa::findOrFail($task_id);
$t->dt_inicio = $start;
$t->dt_fim = $end;
$t->save();
});
return (new MadResponse())->toast('Tarefa atualizada!', 'success');
}
API fluente (MadGantt::make())
Para dados não-banco — gera o JSON manualmente.
use Mad\Calendar\MadGantt;
class ProjetoPage extends MadComponent
{
public function mount(): void
{
$this->gantt = MadGantt::make('2026-03-01')
->viewMode(MadGantt::MODE_DAYS)
->zoom('md')
->interval('15 days')
->title('Sprint 12')
->addRow('dev', 'Desenvolvimento')
->addRow('qa', 'QA')
->addEvent('1', 'dev', 'Feature X', '2026-03-01', '2026-03-05', '#3b82f6', 60)
->addEvent('2', 'qa', 'Testes', '2026-03-06', '2026-03-08', '#10b981')
->onEventClick('onEventClick')
->render();
}
public function onEventClick(string $task_id): MadResponse
{
return MadResponse::open('TarefaForm', ['id' => $task_id]);
}
}
{!! $that->gantt !!}
Métodos do builder
| Método | Uso |
|---|---|
make(?string $startDate) |
Construtor |
viewMode($mode) |
days | months | days_with_hour | months_with_day |
zoom($z) |
hour | day | week | month | year — aliases legados xs/sm/md/lg |
interval($i) |
Range inicial (ex: 15 days, 3 months) |
title($t) |
Header |
addRow($id, $label) |
Adiciona linha/recurso |
addEvent($id, $rowId, $title, $start, $end, $color, $percent) |
Adiciona barra/tarefa (modo legado linhas+eventos) |
addTask($id, array $props) |
Modelo hierárquico (Fase 1) — $props: name, parentId, start, end, progress, color, owner, type... |
addDependency($from, $to, $type, $lag) |
FS/SS/FF/SF (Fase 2) |
addResource($id, array $props) |
Recurso para workload (Fase 3) — $props: name, role, capacity, color, avatar |
assignResource($taskId, $resourceId, $hours = null) |
Alocação de recurso |
addBaseline($taskId, $start, $end) |
Snapshot/baseline (Fase 4) |
workingDays(array $days) |
seg-sex = [1,2,3,4,5] |
holidays(array $dates) |
Y-m-d |
workingHours($startHour, $endHour) |
[8, 18] |
enableAutoSchedule(bool $v = true) |
Reagenda automaticamente após drag |
showCriticalPath(bool $v = true) |
Destaca caminho crítico |
showWorkloadBand(bool $v = true) / workloadMode($mode) |
Painel de carga horária — $mode: hours | tasks | toggle |
onEventClick($method) |
Wire click ao método PHP |
onEventUpdate($method) |
Wire drag/resize |
onDayClick($method) |
Wire dia vazio |
render(): string |
Retorna HTML pronto |
Exemplo end-to-end — projeto com dependências
class CronogramaProjeto extends MadGanttComponent
{
protected static string $wrapper = self::INTERNAL;
protected static string $title = 'Cronograma';
protected string $model = 'ProjetoTarefa';
protected string $database = 'minierp';
protected string $nameField = 'titulo';
protected string $startField = 'dt_inicio';
protected string $endField = 'dt_fim';
protected string $parentField = 'parent_id';
protected string $progressField = 'percentual';
protected string $colorField = 'cor';
// Dependências em tabela separada
protected string $dependencySource = 'table:ProjetoTarefaDep,from=tarefa_origem_id,to=tarefa_destino_id,type=tipo,lag=lag_dias';
// Recursos
protected string $resourceModel = 'Funcionario';
protected string $resourceField = 'funcionarios'; // relação hasMany
public int $projetoId = 0;
public function mount(array $params = []): void
{
$this->projetoId = (int) ($params['projeto_id'] ?? 0);
}
protected function buildQuery(): \Illuminate\Database\Eloquent\Builder
{
return parent::buildQuery()
->where('projeto_id', '=', $this->projetoId)
->orderBy('dt_inicio');
}
public function onTaskClick(string $task_id): MadResponse
{
return MadResponse::open('ProjetoTarefaForm', ['id' => $task_id]);
}
// ATENCAO: o parametro TEM que se chamar $task_id — e a chave do payload
// do drag. Um $id (nome antigo) nunca resolve e o handler recebe vazio.
public function onTaskUpdate(string $task_id, string $start, string $end, $progress = null): MadResponse
{
DB::connection('minierp')->transaction(function () use ($task_id, $start, $end, $progress) {
$t = ProjetoTarefa::findOrFail($task_id);
$t->dt_inicio = $start;
$t->dt_fim = $end;
if ($progress !== null) {
$t->percentual = (float) $progress;
}
$t->save();
});
return (new MadResponse())->toast('Salvo!', 'success');
}
protected function view(): string|array
{
return 'projetos.cronograma-projeto';
}
}
<mad-page-container>
<mad-page-header :title="'Cronograma — Projeto #' . $that->projetoId" icon="bar-chart-horizontal">
<actions>
<mad-btn navigate="ProjetoTarefaForm" variant="primary" icon="plus">Nova tarefa</mad-btn>
</actions>
</mad-page-header>
<mad-page-content>
{!! $gantt !!}
</mad-page-content>
</mad-page-container>
Gotchas
$nameField,$startField,$endFieldobrigatórios emMadGanttComponent— sem eles a query falha silenciosamente- Datas no formato
Y-m-d H:i:s(DB-format) — o JS faz parsing nativo parent-fieldexige hierarquia coerente — child com parent_id inexistente é renderizado flat (sem warning)- Dependências em
dependency-source="fk:..."assumem tipo FS (Finish-to-Start) sem lag — para outros tipos, usetable: - Workload mode =
'hours'exigeaddAssignmentcom horas concretas — sem isso, mostra workload zero
Doc completa: ler lib/mad/calendar/MadGantt.php (builder) e
lib/mad/calendar/MadGanttComponent.php (componente declarativo).