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 paramonth(5px/dia) — nãoday. Se quiser começar em visão de dias (mais comum pra sprints de 2-6 semanas), declarezoom="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ãoid;row_id, nãorowId). Se não bater e o parâmetro não tiver default nem aceitarnull, 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,milestoneque 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 (dispensatask-col-width). field="duration"é calculado destart/end(não temsource).- Se nenhuma coluna tiver
tree, a primeira visível vira a árvore. - Os atributos
*-fieldna 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
hourexige horário nas datas — tasks comstart/endemY-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 viaMadGantt::recordToTask()— o mesmo mapper do render (mapTask). Nada de escrevertoJsTask()à 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
Tarefaseguindo a convenção,ganttUpsertRecord($t)não precisa de map nem deganttMap(). Osave($record, $extra)/fill($data, $extra)($extraliteral ou Closurefn($atual,$record)=>$novo, aplicado apósfromArray, antes dostore()) 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
MadFormautomaticamente (viaMadRenderContext) — bastaform->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 </> |
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 |