Docs›Avançado›Calendários, Kanban, Gantt
Avançado

Calendários, Kanban, Gantt

Deep dive em FullCalendar, Kanban drag-drop e Gantt scheduling.

O MAD oferece quatro componentes para visualização temporal de dados: <mad-calendar> (FullCalendar com auto-query Eloquent), <mad-calendar-resource> (sub-tag que ativa timeline horizontal por recurso), <mad-gantt> e <mad-kanban>. Os quatro seguem o mesmo princípio Blade-first e se integram ao ciclo reativo do MadComponent (drag/resize/click disparam actions PHP via AJAX, sem reload de página).

mad-calendar
FullCalendar.io com auto-query Eloquent + filtros + click-target. Estende MadCalendarComponent.
mad-calendar-resource
Sub-tag de <mad-calendar> que ativa o modo resource-timeline (linhas = recursos, colunas = tempo).
mad-gantt
Gantt chart com dependências, fases, marcos (milestones), caminho crítico e progresso por tarefa.
mad-kanban
Quadro Kanban com colunas drag-and-drop, scroll infinito por coluna e cards 100% customizáveis no Blade.

MadCalendarComponent — princípio Blade-first

Toda a configuração mora no Blade via atributos do <mad-calendar>. A classe PHP (Mad\Calendar\MadCalendarComponent) é uma casca que você estende — só adiciona código quando precisa de um hook custom (onSearch, mapEvent, beforeEventUpdate etc). Sem hooks, a subclasse pode ficar vazia.

use Mad\Calendar\MadCalendarComponent;

class AgendaPage extends MadCalendarComponent
{
    // Vazio. Toda a config vive no Blade.
}
<mad-calendar
    model="Agendamento" database="business"
    title-field="titulo" start-field="dt_inicio" end-field="dt_fim"
    color-field="cor"
    extra-fields="cliente.nome,tipo"
    default-view="agendaWeek" time-range="07:00-19:00" no-weekend
    editable auto-update
    click-target="AgendamentoForm::onEdit({id})" click-target-mode="drawer"
    day-click-target="AgendamentoForm::onCreate({date})"
    period-type="date-range" date-field="dt_inicio" remember-filters
    popover-title="{titulo}" popover-content="{cliente_nome} — {start} → {end}">

    <mad-calendar-toolbar>
        <mad-btn navigate="AgendamentoForm" variant="primary" icon="plus">Novo</mad-btn>
    </mad-calendar-toolbar>

    <mad-calendar-filter field="ativo" op="=" value="1" />
    <mad-calendar-filter field="deleted_at" op="is" value="null" />
</mad-calendar>

Quando precisa de filtro derivado de prop pública, mapeamento custom de evento ou auditoria pós-drag, a subclasse implementa os hooks correspondentes — todos operando direto sobre Illuminate\Database\Eloquent\Builder, sem nenhuma camada de critério intermediária:

use Illuminate\Database\Eloquent\Builder;
use Mad\Calendar\MadCalendarComponent;

class AgendaPage extends MadCalendarComponent
{
    public int $negociacaoId = 0;

    public function mount(array $params = []): void
    {
        $this->negociacaoId = (int) ($params['negociacao_id'] ?? 0);
        parent::mount($params);
    }

    // Filtro derivado de prop pública — aplicado direto no builder.
    public function onSearch(Builder $q): void
    {
        $q->where('negociacao_id', '=', $this->negociacaoId);
    }

    // Mapeamento custom do evento (título concatenado, cor de relação)
    protected function mapEvent(object $r): array
    {
        return [
            'id'    => (string) $r->id,
            'title' => ($r->tipoAtividade->nome ?? '') . ' - ' . $r->descricao,
            'start' => $r->horario_inicial,
            'end'   => $r->horario_final,
            'color' => $r->tipoAtividade->cor ?? '#3b82f6',
        ];
    }

    // Auditoria pós-drag/resize (auto-update já gravou — este hook só audita)
    protected function afterEventUpdate(int $id, string $os, string $oe, string $ns, string $ne): void
    {
        AtividadeHistorico::create([
            'atividade_id' => $id,
            'de'           => $os,
            'para'         => $ns,
        ]);
    }
}
onSearch(Builder $q) — assinatura builder-native

onSearch recebe o Eloquent\Builder já com os filtros fixos (<mad-calendar-filter>) e o viewport (intervalo visível do calendário) aplicados — só falta o seu where. É a mesma assinatura usada por MadDataGrid, MadKanban e MadDashboard (via MadFiltersTrait): zero classes de critério, builder puro.

Atributos do <mad-calendar>

Data source

AtributoTipoDescrição
modelstringClasse Eloquent — habilita auto-query.
databasestringConexão (default MAIN_DATABASE).
id-fieldstringColuna do event id (default id).
title-fieldstringColuna do título (suporta dot-notation: cliente.nome).
start-fieldstringColuna de timestamp de início.
end-fieldstringColuna de fim (opcional).
color-fieldstringColuna com hex color.
resource-fieldstringFK pro recurso — emitida como resourceId no JSON.
all-day-fieldstringColuna booleana — evento dia inteiro (allDay).
editable-fieldstringColuna booleana — sobrescreve editable por evento.
colorstringCor literal fallback (default #3b82f6).
color-mapjson{"field":"tipo","map":{"A":"#3b82f6","B":"#f59e0b"}}
extra-fieldscsvColunas extras no payload do evento.
order-bystringOrdenação da query de eventos.
events-urlstringEscape hatch — bypassa o auto-query.

View / período

AtributoTipoDescrição
default-viewstringdayGridMonth · agendaWeek · agendaDay · listWeek · resourceTimelineDay.
time-rangestringFormato HH:MM-HH:MM (ex: 07:00-19:00).
enable-dayscsvDias visíveis (0=Dom..6=Sáb).
no-weekendboolAtalho para enable-days="1,2,3,4,5".
slot-durationstringDuração do slot (ex: 00:30).
num-daysintDias visíveis em multi-day view.
localestringLocale do FullCalendar (default pt-br).
current-datestringData inicial (Y-m-d).

Interação

AtributoTipoDescrição
editableboolHabilita drag + resize.
no-draggingboolMantém click mas desliga drag (com editable).
no-resizingboolMantém drag mas desliga resize.
auto-updateboolDefault true — grava save() automático no drag/resize.
confirm-updatestringMensagem de confirm() antes do save.
click-targetstringClasse::metodo({id}) — abre form no event-click.
click-target-modestringdrawer · modal.
day-click-targetstringClasse::metodo({date}) — clique em dia vazio.
slot-click-targetstringClasse::metodo({date},{resourceId}) — clique em slot vazio (resource-timeline).
event-update-methodstringOverride do método PHP de drag/resize (default onEventUpdate).
event-click-methodstringOverride do método PHP do click (default onEventClick).
day-click-methodstringOverride do método PHP do day-click (default onDayClick).
slot-click-methodstringOverride do método PHP do slot-click (default onSlotClick).

Filtros (MadFiltersTrait)

AtributoTipoDescrição
period-typestringnone · month-year · date-range · preset.
date-fieldstringColuna usada pelo filtro de período.
remember-filtersboolPersiste filtros em sessão.
default-current-periodboolSeed mês/ano com a data atual.
use-presetsboolHabilita dropdown de presets (hoje, semana, mês...).
apply-unit-filterboolFiltro automático multi-tenant.
unit-fieldstringColuna de unit no model alvo (default unit_id).

Display / outros

AtributoTipoDescrição
calendar-idstringDOM id (obrigatório pra refetchCalendar).
heightintPx (0 = auto).
full-heightboolAltura automática (preenche o container).
headerstringOverride do toolbar nativo do FullCalendar.
popover-titlestringTemplate com {title} {start} {end} {extra_*}.
popover-contentstringIdem, corpo do popover.
popover-triggerstringhover (default) · click.
extra-optionsjsonPass-through pro FullCalendar (slotDuration, nowIndicator etc — alias options).

Sub-tags

Sub-tagTipoDescrição
<mad-calendar-toolbar>blockBody Blade arbitrário renderizado acima do calendário.
<mad-calendar-popover>blockBody do popover (HTML rico). Alternativa aos attrs popover-*.
<mad-calendar-resource>singleAtiva resource-timeline. Attrs: model, title-field, color-field, label etc.
<mad-calendar-filter>repeatFiltro fixo (field, op, value, value2). Repetível.
<mad-calendar-resource-filter>repeatFiltros fixos pra query de recursos.
<mad-calendar-filters>blockFiltros user-facing — mesmo motor do <mad-grid-filters>. Style toolbar/chips/drawer/modal/form/sidebar.

Hooks PHP

HookCategoriaDescrição
onSearch(Builder $q): voidqueryAplica $q->where(...) direto no builder — chamado por buildQuery().
buildQuery(): BuilderqueryOverride total da query de eventos (substitui o auto-query inteiro).
mapEvent(object $record): arraymappingComposição custom de título/cor/payload do evento.
loadEvents(string $start, string $end): arraysourceOverride total da fonte de eventos (bypassa buildQuery/mapEvent).
loadResources(): arraysourceRecursos hardcoded sem model de recurso.
beforeEventUpdate($id, $start, $end): boolinteraçãoRetornar false cancela o auto-update.
afterEventUpdate($id, $os, $oe, $ns, $ne): voidinteraçãoPós-save(), ideal pra auditoria/log.
onEventClick($id, $title, $view): MadResponsecallbackOverride total do click — bypassa click-target.
onDayClick($date, $view): MadResponsecallbackOverride total do day-click.
onSlotClick($date, $resId, $resTitle): MadResponsecallbackSlot click do resource-timeline.
configureCalendar(MadFullCalendar $cal): voidlow-levelHook raw — chamar ->option(...) pra atributos não expostos via Blade.

Filtros user-facing — <mad-calendar-filters>

Mesma infraestrutura do <mad-grid-filters>/<mad-kanban-filters> — reaproveita o compilador de filtros declarativos (MadDashFiltersCompiler). MadCalendarComponent implementa MadFilterable, então funciona sem nenhum código adicional na subclasse.

<mad-calendar-filters style="toolbar">
    <mad-dbcombo-field name="cliente_id" label="Cliente" model="Pessoa" display="nome" />
    <mad-select-field name="prioridade" label="Prioridade" :items="['1'=>'Alta','2'=>'Media']" />
    <mad-date-field name="dtIni" label="De" />
    <mad-date-field name="dtFim" label="Ate" />
</mad-calendar-filters>

<mad-calendar model="Evento" ... />

MadGantt — princípio Blade-first

Toda configuração mora no Blade via atributos do <mad-gantt>. A classe PHP estende Mad\Calendar\MadGanttComponent e declara $model + mapeamento de campos. A query de tarefas é Eloquent puro — sobrescreva buildQuery(): Builder quando precisar de filtros derivados de props públicas.

use Illuminate\Database\Eloquent\Builder;
use Mad\Calendar\MadGanttComponent;
use Mad\Http\MadResponse;

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

    protected string $model         = ProjetoTarefa::class;
    protected string $nameField     = 'titulo';
    protected string $startField    = 'dt_inicio';
    protected string $endField      = 'dt_fim';
    protected string $parentField   = 'parent_id';     // hierarquia
    protected string $progressField = 'percentual';
    protected string $colorField    = 'cor';
    protected string $ownerField    = 'responsavel';

    // Dependências FK (finish-to-start, sem lag)
    protected string $dependencySource = 'fk:predecessor_id';

    public int $projetoId = 0;

    public function mount(array $params = []): void
    {
        $this->projetoId = (int) ($params['projeto_id'] ?? 0);
    }

    protected function buildQuery(): Builder
    {
        return ProjetoTarefa::query()
            ->where('projeto_id', '=', $this->projetoId)
            ->orderBy('dt_inicio');
    }

    // O JS (madGantt) manda só { task_id } no click — o nome do parametro
    // tem que casar com essa chave pra resolucao automatica (_resolveAndCall).
    public function onTaskClick(string $task_id): MadResponse
    {
        return MadResponse::open('TarefaForm', ['id' => $task_id], 'onEdit');
    }

    protected function view(): string|array
    {
        return 'projetos.cronograma-projeto';
    }
}
<mad-page-container>
    <mad-page-header title="Cronograma" icon="bar-chart-horizontal" />
    <mad-page-content>
        <mad-gantt model="ProjetoTarefa" name-field="titulo"
            start-field="dt_inicio" end-field="dt_fim" parent-field="parent_id"
            progress-field="percentual" color-field="cor"
            dependency-source="fk:predecessor_id"
            view-mode="days" zoom="md" critical-path
            on-task-click="onTaskClick" />
    </mad-page-content>
</mad-page-container>

onTaskUpdate(string $id, string $start, string $end, $progress = null) já grava no banco automaticamente quando o gantt é declarativo — não precisa sobrescrever pra simplesmente salvar. Só sobrescreva pra lógica extra (auditoria, recalcular dependências):

public function onTaskUpdate(string $id, string $start, string $end, $progress = null): MadResponse
{
    $resp = parent::onTaskUpdate($id, $start, $end, $progress);
    // ... auditoria extra ...
    return $resp;
}

Para dados fora de banco (memória/API), use o builder fluente Mad\Calendar\MadGantt direto no view() do controller:

use Mad\Calendar\MadGantt;

$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('onTaskClick')
    ->render();

// Dentro de MadComponent::view():
return ['doc.gantt', ['gantt' => $gantt]];

Principais atributos do <mad-gantt>

AtributoTipoDescrição
model / databasestringFonte de tarefas (Eloquent).
id-field, name-field, start-field, end-fieldstringMapeamento básico de colunas.
parent-fieldstringFK auto-referência — hierarquia de tarefas/summary tasks.
progress-field, color-field, owner-fieldstringProgresso, cor da barra, responsável.
progress-scalestringauto (default) · fraction (0..1) · percent (0-100) — escala do progress-field. Ver callout abaixo.
type-fieldstringColuna com task · summary · milestone.
code-fieldstringCódigo curto exibido na sidebar.
milestone-fieldstringColuna booleana — vira marco (diamante) na timeline.
dependency-sourcestringfk:coluna (FK direta) ou table:Model,from=...,to=...,type=...,lag=... (tabela separada).
resource-model, assignment-sourcestringRecursos + alocações (tabela pivot: Model,task=...,resource=...,hours=...).
view-modestringdays · months · days_with_hour · months_with_day.
zoomstringxs · sm · md · lg.
critical-pathboolDestaca o caminho crítico entre dependências.
auto-scheduleboolReagenda tarefas dependentes automaticamente.
show-workload / workload-modebool / stringLinha de carga de trabalho por recurso (hours ou outro modo).
working-dayscsvDias úteis (afeta cálculo de datas/auto-schedule). Ex: working-days="1,2,3,4,5".
<mad-gantt-holiday date="..."/>sub-tagFeriado (não-útil). Repetível — não existe attr holidays direto em <mad-gantt>.
on-task-click, on-task-update, on-day-clickstringNome do método PHP do callback correspondente.

Sub-tags <mad-gantt-phases>, <mad-gantt-persons>, <mad-gantt-dependencies>, <mad-gantt-resources> e <mad-gantt-assignments> (plural) carregam essas coleções de uma tabela via Eloquent — preferíveis em uso real a <mad-gantt-phase>/ <mad-gantt-person> (singular, hardcoded inline), que só fazem sentido pra conjuntos pequenos e estáticos. Feriados usam só a forma repetível singular — <mad-gantt-holiday date="2026-12-25"/> — não há uma coleção plural pra eles.

progress-scale — a escala do progresso

O runtime normaliza todo progress para fração 0..1 num único ponto do bundle (normProgress), tanto no ingest inicial quanto no upsert parcial de uma tarefa. progress-scale diz de que escala o dado vem:

ValorConversão
auto (default)Heurística: valor > 1 é lido como 0-100 (divide por 100); ≤ 1 é lido como fração.
fractionSempre 0..1 (clamp).
percentSempre 0-100 → divide por 100 (clamp).
Ambiguidade do valor 1

No auto, o valor 1 é inerentemente ambíguo: é tratado como 100% em fração. Se o banco guarda 0-100, uma tarefa com 1% apareceria concluída — declare progress-scale="percent" explícito nesse caso (ou ->progressScale('percent') no builder Mad\Calendar\MadGantt). Valores não numéricos viram 0.

<mad-gantt model="ProjetoTarefa" name-field="titulo"
    start-field="dt_inicio" end-field="dt_fim"
    progress-field="percentual" progress-scale="percent" />

MadKanban — Blade-first

Mesmo princípio: a subclasse PHP só declara $model, $stageModel, $stageField. O layout do card (badges, meta, footer, ações) vive no Blade via <mad-kanban-card> e filhos.

use Illuminate\Database\Eloquent\Builder;
use Mad\Calendar\MadKanban;
use Mad\Http\MadResponse;

class NegociacaoKanbanView extends MadKanban
{
    protected static string $wrapper = self::INTERNAL;

    protected string $model      = Negociacao::class;
    protected string $stageModel = EtapaNegociacao::class;
    protected string $stageField = 'etapa_negociacao_id';
    protected string $valueField = 'valor';
    protected bool   $rememberFilters = true;

    // Filtros declarativos auto-discovery (props públicas = filtro)
    protected array $skipAutoFilter = ['busca'];
    public string $busca       = '';
    public string $vendedor_id = '';

    public function onSearch(Builder $q): void
    {
        $q->whereNotIn('etapa_negociacao_id', [9, 10]); // exclui canceladas/finalizadas

        if ($this->busca !== '') {
            $v = trim($this->busca);
            ctype_digit($v)
                ? $q->where('id', '=', (int) $v)
                : $q->where('obs', 'like', "%{$v}%");
        }
    }

    public function onAprovar(int $id): MadResponse
    {
        // ... lógica de aprovação ...
        return (new MadResponse())
            ->toast("Aprovado #{$id}", 'success')
            ->manageCard($id, static::class);
    }

    protected function view(): string|array
    {
        return 'crm.negociacao-kanban-view';
    }
}
<mad-page-container>
    <mad-page-header title="Negociações" icon="layout-grid">
        <actions>
            <mad-btn navigate="NegociacaoForm" variant="primary" icon="plus">Nova</mad-btn>
        </actions>
    </mad-page-header>

    <mad-page-content>
        <mad-kanban-filters style="toolbar">
            <mad-input-field name="busca" label="Busca" />
            <mad-dbcombo-field name="vendedor_id" label="Vendedor" model="Pessoa" display="nome" />
        </mad-kanban-filters>

        <mad-kanban top-scroll click-target="NegociacaoForm::onEdit({id})">

            <mad-kanban-toolbar>
                <mad-btn navigate="NegociacaoForm" variant="primary" size="sm" icon="plus">Nova</mad-btn>
                <mad-btn mad:click="onReload" variant="ghost" size="sm" icon="refresh-cw">Recarregar</mad-btn>
            </mad-kanban-toolbar>

            <mad-kanban-stage-action
                target="NegociacaoForm::onCreateForStage({stageId})"
                icon="plus" label="Nova nesta etapa" />

            <mad-kanban-card title="obs">
                <mad-kanban-meta icon="circle-user-round" path="cliente.nome" />
                <mad-kanban-meta icon="user" path="vendedor.nome" muted />
                <mad-kanban-footer type="money" path="valor" prefix="R$" />
                <mad-kanban-footer type="date"  path="dt_previsao" icon="calendar" format="d/m/Y" />

                <mad-kanban-action method="onAprovar" icon="check-circle" label="Aprovar"
                    variant="success" confirm="Aprovar?"
                    display-condition="NegociacaoKanbanView::podeAprovar" />
            </mad-kanban-card>

        </mad-kanban>
    </mad-page-content>
</mad-page-container>
Filtros declarativos compartilhados

<mad-grid-filters>, <mad-dash-filters>, <mad-kanban-filters>, <mad-calendar-filters> e <mad-gantt-filters> são aliases do mesmo compilador (MadDashFiltersCompiler) — mesma sintaxe de filhos (mad-input-field, mad-dbcombo-field, mad-daterange-field etc) e os mesmos 6 style (toolbar/chips/drawer/modal/form/sidebar).

Kanban — scroll infinito por coluna

Cada coluna carrega no máximo cards-per-load cards (default 20) e vai buscando o resto conforme o usuário rola. Não há nada pra implementar: o ciclo é todo do framework.

<mad-kanban model="Negociacao" stage-field="etapa_id"
    cards-per-load="30" card-order-field="dt_previsao">
    ...
</mad-kanban>
EtapaO que acontece
ScrollonScrollCards() dispara quando falta menos de 50px pro fim da coluna, e só se aquela coluna não estiver carregando nem esgotada.
ChamadaMadWire.call(wrapper, 'onLoadMore', [stageId, offset]) — o offset por coluna só avança quando o append é confirmado.
ServidorMadKanban::onLoadMore() repete a query base filtrada pela etapa, com limit(cardsPerLoad) + offset, renderiza os cards e devolve só a op append_cards (sem full render).
ClienteA op anexa o HTML e dispara mad-kanban-loaded, que zera loadingMore e grava exhausted a partir do hasMore.
hasMore é autoritativo — e a op vai sempre

hasMore é calculado como "página cheia (count($items) === cardsPerLoad) ⇒ provavelmente há mais". A op append_cards é emitida mesmo quando a página volta vazia: sem ela o cliente nunca receberia mad-kanban-loaded e o loadingMore da coluna travaria em true pra sempre. O exhausted vem desse evento, nunca do count inicial da config (que fica stale depois de mover cards).

Se sobrescrever onLoadMore(), mantenha o contrato: filtro da etapa + limit/offset, $this->_skipFullRender = true e a op append_cards com stageId, html e hasMore. Filtros próprios NÃO vão nesse método: coloque-os em onSearch() (ou nos filtros declarativos), porque a query base do board — período, unidade, auto-filters e onSearch — é montada num único ponto interno reaproveitado pelo load inicial, pelo load-more e pela checagem de escopo do onCardMove. Filtrar só no load inicial faria as páginas seguintes trazerem cards fora do escopo.

Resource Timeline (sub-tag de <mad-calendar>)

Não é o plugin resourceTimeline do FullCalendar.io

Quando resource-model é definido (via <mad-calendar-resource>) ou default-view começa com resourceTimeline, MadFullCalendar::view() troca de view e renderiza components.resource-timeline — um grid CSS próprio, montado no Alpine component madResourceTimeline (assets/builder-ui/mad-ui.js). Não é o FullCalendar.io: o bundle vendorizado em public/lib/independent/js/fullcalendar.js nem inclui o plugin resourceTimeline. Na prática isso significa que extra-options e header (pass-through pro FullCalendar) não se aplicam nesse modo — madResourceTimeline só lê numDays de extraOptions e ignora o resto.

<mad-calendar
    calendar-id="cal-salas"
    default-view="resourceTimelineDay"
    time-range="08:00-18:00"
    slot-duration="01:00"
    slot-click-target="ReservaForm::onCreate({date},{resourceId})">

    {{-- Recursos via auto-query (preferido) — label vira o título da coluna --}}
    <mad-calendar-resource model="Sala" title-field="nome" color-field="cor" label="Sala" />
</mad-calendar>

Recursos vêm de <mad-calendar-resource model="..."> (auto-query) OU de um override de loadResources(): array retornando um array hardcoded. Bom pra agenda de salas, equipamentos ou escala de funcionários.

Atualizar dados sem reload

public function onSave(): MadResponse
{
    // ... salvar evento ...
    return (new MadResponse())
        ->toast('Salvo!', 'success')
        ->closeDrawer()
        ->refetchCalendar('cal-agenda'); // ID do <mad-calendar calendar-id="cal-agenda">
}

Kanban tem o equivalente manageCard($id, KanbanClass::class) (atualiza/insere card com highlight) e removeCard($id) — vistos no exemplo acima (onAprovar).

Personalização visual

Gantt: use color-field (cor por linha do model) ou, via builder fluente, MadGantt::make()->criticalPath(true) pra destacar o caminho crítico.

Kanban: override total do card via view Blade:

<mad-kanban card-view="crm.negociacao-card-custom">
    {{-- tags <mad-kanban-card> são ignoradas quando card-view é declarado --}}
</mad-kanban>
renderCard() — override no PHP

Como alternativa ao card-view, sobrescreva renderCard(object $item): string no controller — retorna HTML pronto via MadBlade::render().

NUNCA fazer

// ERRADO: TCriteria/TFilter não existem mais — buildCriteria() foi removido.
protected function buildCriteria(): \TCriteria
{
    $c = new \TCriteria();
    $c->add(new \TFilter('negociacao_id', '=', $this->negociacaoId));
    return $c;
}

// CERTO: builder-native, direto no onSearch ou no buildQuery().
public function onSearch(\Illuminate\Database\Eloquent\Builder $q): void
{
    $q->where('negociacao_id', '=', $this->negociacaoId);
}
// ERRADO: TTransaction explícita — classe removida do framework.
\TTransaction::open('business');
$t = new DocProjetoTarefa($id);
$t->dt_inicio = $start;
$t->store();
\TTransaction::close();

// CERTO: DB::transaction() nativo do Laravel (ou deixe o auto-update do
// gantt/calendar gravar sozinho — onTaskUpdate/onEventUpdate já fazem isso).
DB::connection('business')->transaction(function () use ($id, $start) {
    $t = ProjetoTarefa::findOrFail($id);
    $t->dt_inicio = $start;
    $t->save();
});

Próximos passos