Docs›Componentes (Admin)›mad-gantt — CRUD host + form
Componentes (Admin)

mad-gantt — CRUD host + form

Par host (gantt) + form (drawer): mapeamento por convenção e ops parciais ganttUpsertRecord/ganttPatchRecord.

Documenta o par host (gantt) + form (drawer): mapeamento de tarefa por convenção (sem ganttMap() no model), form->save($record, $extra) e as ops parciais que atualizam só a barra alterada (sem reload do gráfico). Spec completa do componente: gantt-builder.md.


O round-trip gantt ↔ form

┌─ BLADE (render) ──────────────────────────────────────────────┐
│ <mad-gantt model="GanttTask">                                 │ ← field-mapping nas
│   <mad-gantt-column field="name" tree />        ┐ field+source │   sub-tags <column>
│   <mad-gantt-column field="start" hidden /> ... ┘ = mapping    │   (fonte ÚNICA, só do render)
│   on-task-click="onTaskClick" on-task-update="onTaskUpdate">  │ ← wiring evento JS → PHP do host
└────────────────────────────────────────────────────────────────┘
        │ clique numa barra                       ▲ salvou no form
        ▼                                         │
  HOST.onTaskClick(id)                       FORM.onSave()
   → MadResponse::open(Form, ['id'=>id], 'onEdit')   → ganttUpsertRecord($task)
        │ SÓ o id cruza                        │ o model volta (shape por convenção)
        ▼                                         │
  FORM.onEdit(string $id)  ── recarrega GanttTask::find($id), fill() ──┘
Cruza a fronteira Direção Carga
onTaskClick → MadResponse::open(...,'onEdit') gantt → form só o id
ganttUpsertRecord($task) form → gantt o model Eloquent (shape por convenção)
field-mapping (colunas field/source) — nunca cruza — fica na tag

O form não recebe o field-mapping da tag (declarado nas colunas field/source). Ele recarrega a GanttTask por id e, no upsert, usa a convenção de MadGantt::recordToTask(). Por isso um ganttMap() no model seria redundante.


onEdit(string $id) — carregar pra edição

PK slug (string) → recebe o id via MadResponse::open(..., 'onEdit'). fill() mapeia coluna→campo por nome; o $extra ajusta só o que a UI guarda diferente do banco.

public function onEdit(string $id): void   // PK slug → string (não int)
{
    $task = GanttTask::findOrFail($id);   // leitura não precisa de transação
    $this->editingId = (string) $task->id;

    // fill() mapeia colunas → campos por nome; $extra ajusta só o que
    // diverge da UI (progress fração 0..1 → %, milestone bool → switch).
    $this->form->fill($task, [
        'progress'  => (float) $task->progress * 100,
        'milestone' => $task->milestone ? '1' : '0',
    ]);
}

fill($data, $extra): preenche pelos dados; o $extra sobrescreve (aqui: conversões banco→UI). Nada de gantt-específico — é MadForm puro.


onSave() — persistir + atualizar só a barra

use Illuminate\Support\Facades\DB;

public function onSave(): MadResponse
{
    try {
        $data = $this->form->getData();
        $this->form->validate(GanttTask::rules($this->editingId));

        $task = DB::transaction(function () use ($data) {
            $isNew = $this->editingId === '';
            $task  = $isNew ? new GanttTask() : GanttTask::findOrFail($this->editingId);

            // form->save() faz fillRecord + save(); $extra cobre só o que diverge
            // da UI (progress 0..100 → 0..1, null, slug da PK numa criação).
            $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(GanttTask::class, (string) $data->name);
            }

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

        $msg = ($this->editingId !== '' ? 'Tarefa atualizada' : 'Tarefa criada') . " ({$task->id})";

        // Partial: insere/atualiza só essa barra (shape via convenção do
        // recordToTask); deps re-roteiam sozinhas no recompute.
        return (new MadResponse())
            ->toast($msg, 'success')
            ->closeDrawer()
            ->ganttUpsertRecord($task);

    } catch (MadValidationException $e) {
        return $e->asModal();
    } catch (Throwable $e) {
        return MadMessage::error('Erro ao salvar', $e->getMessage());
    }
}

Dois detalhes centrais:

  1. form->save($task, $extra) — $extra (literal ou Closure) é aplicado após o preenchimento dos campos do form e antes do save() do Eloquent. As colunas que batem com os campos do form (name, code, phase_id, start, end) entram sozinhas; o $extra só normaliza o resto. Sem atribuição linha a linha.

  2. ->ganttUpsertRecord($task) — sem $map. Mapeia o model pelo recordToTask na convenção e devolve a barra pro gantt (op gantt_upsert_task) sem reload.


onDelete() — apaga tarefa + filhos + deps

public function onDelete(): MadResponse
{
    if ($this->editingId === '') return MadToast::warning('Nada para excluir');
    $id = $this->editingId;

    try {
        $children = DB::transaction(function () use ($id) {
            $ids = $this->collectDescendantIds(GanttTask::class, $id, 'parent_id'); // root + descendentes

            // Remove dependências que tocam qualquer um dos ids
            GanttDependency::query()
                ->where(function ($q) use ($ids) {
                    $q->whereIn('from_id', $ids)->orWhereIn('to_id', $ids);
                })
                ->delete();

            GanttTask::whereIn('id', $ids)->delete();

            return array_values(array_filter($ids, fn ($x) => $x !== $id));
        });

        return (new MadResponse())
            ->toast("Removida ({$id})", 'success')
            ->closeDrawer()
            ->ganttRemoveTask($id, $children);   // tira a barra + filhos + deps no client
    } catch (Throwable $e) {
        return MadMessage::error('Erro', $e->getMessage());
    }
}

collectDescendantIds() e generateUniqueId() moram no MadComponent base (rodam puro Eloquent, sem transação obrigatória) — reutilizáveis por qualquer CRUD hierárquico, não só gantt. Ver helpers no <mad-detail-form>/field-list para outros padrões de manipulação de árvore.


Lado do host — drag e dependência (não passam pelo form)

// Drag move/resize → persiste e move só a barra
public function onTaskUpdate(string $task_id, string $start, string $end, string $mode = ''): MadResponse
{
    $task = GanttTask::find($task_id);
    if (!$task) {
        return MadToast::error("Tarefa {$task_id} não encontrada");
    }
    $task->start = $start;
    $task->end   = $end;
    $task->save();

    return (new MadResponse())
        ->ganttPatchRecord($task)            // ou ->ganttPatchTask($task_id, $start, $end)
        ->toast("Reagendada {$task_id}", 'info');
}

// Click numa barra → abre o form (só o id cruza)
public function onTaskClick(string $task_id): MadResponse
{
    return MadResponse::open('GanttTaskForm', ['id' => $task_id], 'onEdit');
}

Helpers do framework

Método Onde Assinatura Faz
recordToTask MadGantt (static) recordToTask($record, $map = []) Model Eloquent → shape JS. $map vazio = convenção. Fonte única do mapeamento (render usa via mapTask).
ganttUpsertRecord MadResponse ganttUpsertRecord($record, $map = [], $selector = '.mad-gantt') Upsert de barra a partir do model (usa recordToTask).
ganttPatchRecord MadResponse ganttPatchRecord($record, $map = [], $selector = '.mad-gantt') Só datas (id/start/end) a partir do model.
save MadForm save(object $record, array|Closure $extra = []) fillRecord + aplica $extra (literal/Closure) + $record->save() + hooks pós-store (uploads, etc).

$extra do save/fill aceita literal ou Closure fn($atual, $record) => $novo:

$this->form->save($task, [
    'progress'  => fn ($v) => $v / 100,                 // Closure: lê o valor atual
    'milestone' => (!empty($data->milestone)) ? 1 : 0,  // literal
]);

save() também aceita uma Closure única fn($record) que recebe o model já preenchido e o muta direto — útil pra lógica imperativa cross-campo:

$this->form->save($task, function ($task) use ($data) {
    $task->progress = $task->progress > 1 ? $task->progress / 100 : $task->progress;
    $task->owner_id = trim((string) $task->owner_id) ?: null;
    if (empty($task->id)) {
        $task->id = $this->generateUniqueId(GanttTask::class, (string) $data->name);
    }
});

Convenção defaultTaskMap — quando NÃO precisa de map

recordToTask/ganttUpsertRecord sem $map usam:

role (chave JS) coluna
id id
name name
start start
end end
progress progress
parentId parent_id
owner owner_id
code code
milestone milestone
phase phase_id (emitida sob o nome da coluna — casa com cfg.phaseField)

Colunas no padrão → ->ganttUpsertRecord($task) e pronto. Colunas fora do padrão → map inline no call (espelha o que a tag declara):

->ganttUpsertRecord($t, ['name' => 'titulo', 'start' => 'dt_inicio', 'end' => 'dt_fim'])

A progress na barra usa a unidade do banco (fração 0..1). O recordToTask não escala — a conversão pra % é só do form ($extra).


O que foi removido — e por quê

  • toJsTask() (no form) — versões antigas montavam o shape JS à mão, espelhando o mapTask do render. Duplicação pura → substituído por recordToTask (uma fonte só, usada pelos dois lados).
  • ganttMap() no model — declarava role→coluna no model. Mas isso é o que os atributos da tag já fazem (name-field, start-field, …). Como o form recarrega o model por id e usa convenção no upsert, o map não é necessário na maioria dos casos.

NUNCA fazer

// ERRADO: montar o shape JS à mão (volta o toJsTask)
return (new MadResponse())->ganttUpsertTask([
    'id' => $t->id, 'name' => $t->name, 'start' => $t->start, /* ... */
]);
// CERTO: deixa o recordToTask cuidar
return (new MadResponse())->ganttUpsertRecord($t);

// ERRADO: recriar um ganttMap() no model só pra isso
public static function ganttMap(): array { return [...]; }
// CERTO: convenção (defaultTaskMap) ou map inline no call quando fugir do padrão

// ERRADO: atribuir campo a campo o que o fillRecord já resolve
$task->name = $data->name; $task->code = $data->code; $task->start = $data->start; // ...
// CERTO: save() + $extra só pro que diverge da UI
$this->form->save($task, ['progress' => $pct / 100, 'milestone' => $ms]);

// ERRADO: full reload depois de salvar (perde scroll/zoom/filtro/expand)
return (new MadResponse())->script('location.reload()');
// CERTO: op parcial
return (new MadResponse())->closeDrawer()->ganttUpsertRecord($task);

// ERRADO: esperar o form "herdar" os atributos da tag (gantt e form são
// componentes/requests separados — não há estado compartilhado)
// CERTO: o form recarrega o model por id (onEdit) e usa convenção no upsert