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-calendarMadCalendarComponent.mad-calendar-resource<mad-calendar> que ativa o modo resource-timeline (linhas = recursos, colunas = tempo).mad-ganttmad-kanbanMadCalendarComponent — 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 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
| Atributo | Tipo | Descrição |
|---|---|---|
model | string | Classe Eloquent — habilita auto-query. |
database | string | Conexão (default MAIN_DATABASE). |
id-field | string | Coluna do event id (default id). |
title-field | string | Coluna do título (suporta dot-notation: cliente.nome). |
start-field | string | Coluna de timestamp de início. |
end-field | string | Coluna de fim (opcional). |
color-field | string | Coluna com hex color. |
resource-field | string | FK pro recurso — emitida como resourceId no JSON. |
all-day-field | string | Coluna booleana — evento dia inteiro (allDay). |
editable-field | string | Coluna booleana — sobrescreve editable por evento. |
color | string | Cor literal fallback (default #3b82f6). |
color-map | json | {"field":"tipo","map":{"A":"#3b82f6","B":"#f59e0b"}} |
extra-fields | csv | Colunas extras no payload do evento. |
order-by | string | Ordenação da query de eventos. |
events-url | string | Escape hatch — bypassa o auto-query. |
View / período
| Atributo | Tipo | Descrição |
|---|---|---|
default-view | string | dayGridMonth · agendaWeek · agendaDay · listWeek · resourceTimelineDay. |
time-range | string | Formato HH:MM-HH:MM (ex: 07:00-19:00). |
enable-days | csv | Dias visíveis (0=Dom..6=Sáb). |
no-weekend | bool | Atalho para enable-days="1,2,3,4,5". |
slot-duration | string | Duração do slot (ex: 00:30). |
num-days | int | Dias visíveis em multi-day view. |
locale | string | Locale do FullCalendar (default pt-br). |
current-date | string | Data inicial (Y-m-d). |
Interação
| Atributo | Tipo | Descrição |
|---|---|---|
editable | bool | Habilita drag + resize. |
no-dragging | bool | Mantém click mas desliga drag (com editable). |
no-resizing | bool | Mantém drag mas desliga resize. |
auto-update | bool | Default true — grava save() automático no drag/resize. |
confirm-update | string | Mensagem de confirm() antes do save. |
click-target | string | Classe::metodo({id}) — abre form no event-click. |
click-target-mode | string | drawer · modal. |
day-click-target | string | Classe::metodo({date}) — clique em dia vazio. |
slot-click-target | string | Classe::metodo({date},{resourceId}) — clique em slot vazio (resource-timeline). |
event-update-method | string | Override do método PHP de drag/resize (default onEventUpdate). |
event-click-method | string | Override do método PHP do click (default onEventClick). |
day-click-method | string | Override do método PHP do day-click (default onDayClick). |
slot-click-method | string | Override do método PHP do slot-click (default onSlotClick). |
Filtros (MadFiltersTrait)
| Atributo | Tipo | Descrição |
|---|---|---|
period-type | string | none · month-year · date-range · preset. |
date-field | string | Coluna usada pelo filtro de período. |
remember-filters | bool | Persiste filtros em sessão. |
default-current-period | bool | Seed mês/ano com a data atual. |
use-presets | bool | Habilita dropdown de presets (hoje, semana, mês...). |
apply-unit-filter | bool | Filtro automático multi-tenant. |
unit-field | string | Coluna de unit no model alvo (default unit_id). |
Display / outros
| Atributo | Tipo | Descrição |
|---|---|---|
calendar-id | string | DOM id (obrigatório pra refetchCalendar). |
height | int | Px (0 = auto). |
full-height | bool | Altura automática (preenche o container). |
header | string | Override do toolbar nativo do FullCalendar. |
popover-title | string | Template com {title} {start} {end} {extra_*}. |
popover-content | string | Idem, corpo do popover. |
popover-trigger | string | hover (default) · click. |
extra-options | json | Pass-through pro FullCalendar (slotDuration, nowIndicator etc — alias options). |
Sub-tags
| Sub-tag | Tipo | Descrição |
|---|---|---|
| <mad-calendar-toolbar> | block | Body Blade arbitrário renderizado acima do calendário. |
| <mad-calendar-popover> | block | Body do popover (HTML rico). Alternativa aos attrs popover-*. |
| <mad-calendar-resource> | single | Ativa resource-timeline. Attrs: model, title-field, color-field, label etc. |
| <mad-calendar-filter> | repeat | Filtro fixo (field, op, value, value2). Repetível. |
| <mad-calendar-resource-filter> | repeat | Filtros fixos pra query de recursos. |
| <mad-calendar-filters> | block | Filtros user-facing — mesmo motor do <mad-grid-filters>. Style toolbar/chips/drawer/modal/form/sidebar. |
Hooks PHP
| Hook | Categoria | Descrição |
|---|---|---|
onSearch(Builder $q): void | query | Aplica $q->where(...) direto no builder — chamado por buildQuery(). |
buildQuery(): Builder | query | Override total da query de eventos (substitui o auto-query inteiro). |
mapEvent(object $record): array | mapping | Composição custom de título/cor/payload do evento. |
loadEvents(string $start, string $end): array | source | Override total da fonte de eventos (bypassa buildQuery/mapEvent). |
loadResources(): array | source | Recursos hardcoded sem model de recurso. |
beforeEventUpdate($id, $start, $end): bool | interação | Retornar false cancela o auto-update. |
afterEventUpdate($id, $os, $oe, $ns, $ne): void | interação | Pós-save(), ideal pra auditoria/log. |
onEventClick($id, $title, $view): MadResponse | callback | Override total do click — bypassa click-target. |
onDayClick($date, $view): MadResponse | callback | Override total do day-click. |
onSlotClick($date, $resId, $resTitle): MadResponse | callback | Slot click do resource-timeline. |
configureCalendar(MadFullCalendar $cal): void | low-level | Hook 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>
| Atributo | Tipo | Descrição |
|---|---|---|
model / database | string | Fonte de tarefas (Eloquent). |
id-field, name-field, start-field, end-field | string | Mapeamento básico de colunas. |
parent-field | string | FK auto-referência — hierarquia de tarefas/summary tasks. |
progress-field, color-field, owner-field | string | Progresso, cor da barra, responsável. |
progress-scale | string | auto (default) · fraction (0..1) · percent (0-100) — escala do progress-field. Ver callout abaixo. |
type-field | string | Coluna com task · summary · milestone. |
code-field | string | Código curto exibido na sidebar. |
milestone-field | string | Coluna booleana — vira marco (diamante) na timeline. |
dependency-source | string | fk:coluna (FK direta) ou table:Model,from=...,to=...,type=...,lag=... (tabela separada). |
resource-model, assignment-source | string | Recursos + alocações (tabela pivot: Model,task=...,resource=...,hours=...). |
view-mode | string | days · months · days_with_hour · months_with_day. |
zoom | string | xs · sm · md · lg. |
critical-path | bool | Destaca o caminho crítico entre dependências. |
auto-schedule | bool | Reagenda tarefas dependentes automaticamente. |
show-workload / workload-mode | bool / string | Linha de carga de trabalho por recurso (hours ou outro modo). |
working-days | csv | Dias úteis (afeta cálculo de datas/auto-schedule). Ex: working-days="1,2,3,4,5". |
| <mad-gantt-holiday date="..."/> | sub-tag | Feriado (não-útil). Repetível — não existe attr holidays direto em <mad-gantt>. |
on-task-click, on-task-update, on-day-click | string | Nome 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:
| Valor | Conversão |
|---|---|
auto (default) | Heurística: valor > 1 é lido como 0-100 (divide por 100); ≤ 1 é lido como fração. |
fraction | Sempre 0..1 (clamp). |
percent | Sempre 0-100 → divide por 100 (clamp). |
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>
<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>
| Etapa | O que acontece |
|---|---|
| Scroll | onScrollCards() dispara quando falta menos de 50px pro fim da coluna, e só se aquela coluna não estiver carregando nem esgotada. |
| Chamada | MadWire.call(wrapper, 'onLoadMore', [stageId, offset]) — o offset por coluna só avança quando o append é confirmado. |
| Servidor | MadKanban::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). |
| Cliente | A op anexa o HTML e dispara mad-kanban-loaded, que zera loadingMore e grava exhausted a partir do hasMore. |
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>)
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>
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();
});