mad-full-calendar
Calendário FullCalendar.io.
Componente declarativo de calendário baseado em FullCalendar v5.5.1, com API
Blade-first. Toda configuração mora em atributos da tag <mad-calendar>. A
classe PHP é uma casca (extends MadCalendarComponent) — adiciona código
apenas para hooks de comportamento custom.
Princípio: controller é casca. Quanto mais atributos no Blade, menos PHP. Pensado para geradores de código visuais (low-code).
Quick start
Controller mínimo (sem hooks)
<?php
use Mad\Calendar\MadCalendarComponent;
class AgendaPage extends MadCalendarComponent
{
// Vazio. Toda config no Blade.
}
View
<mad-page-container>
<mad-page-header title="Agenda" icon="calendar" />
<mad-page-content>
<mad-calendar
model="Agendamento" database="minierp"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
color-field="cor"
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>
<mad-calendar-filter field="ativo" op="=" value="1" />
<mad-calendar-filter field="deleted_at" op="is" value="null" />
</mad-calendar>
</mad-page-content>
</mad-page-container>
Atributos do <mad-calendar>
Data source
| Attr | Descrição |
|---|---|
model |
Classe model Eloquent — habilita auto-query |
database |
Conexão (default MAIN_DATABASE) |
id-field |
Coluna do event id (default id) |
title-field |
Coluna do título (suporta dot-notation: cliente.nome) |
start-field |
Coluna de timestamp de início |
end-field |
Coluna de fim (opcional) |
color-field |
Coluna com hex color |
color |
Cor literal fallback (#3b82f6) |
color-map |
{"field":"tipo","map":{"A":"#3b82f6","B":"#f59e0b"}} |
resource-field |
Coluna FK pro recurso. Auto-mapeada como resourceId no JSON (resource timeline) |
all-day-field |
Coluna booleana → allDay no JSON (evento dia inteiro) |
editable-field |
Coluna booleana → editable per-event (override do drag/resize por linha) |
extra-fields |
CSV de colunas extras no payload do evento |
events-url |
Escape hatch — URL custom (bypassa auto-query) |
order-by |
Ordenação default |
View / período
| Attr | Default | Descrição |
|---|---|---|
default-view |
dayGridMonth |
month | agendaWeek | agendaDay | listWeek |
time-range |
00:00-24:00 |
HH:MM-HH:MM |
enable-days |
0,1,2,3,4,5,6 |
Dias visíveis (0=Dom..6=Sab) |
no-weekend |
false | Atalho: enable-days=1,2,3,4,5 |
slot-duration |
01:00 |
Slot do agenda view |
num-days |
4 | Dias visíveis em multi-day view |
locale |
pt-br |
Locale FullCalendar |
current-date |
hoje | Y-m-d inicial |
Interação
| Attr | Descrição |
|---|---|
editable |
Habilita drag + resize |
no-dragging |
Desabilita arrastar (mantém resize, se editable) |
no-resizing |
Desabilita redimensionar (mantém drag, se editable) |
auto-update |
Default true — grava store() automático no drag |
confirm-update |
Mensagem de confirm() antes do save |
click-target |
Classe::metodo({id}) — abre form no event-click |
click-target-mode |
drawer | modal |
day-click-target |
Classe::metodo({date}) — clique em dia vazio |
event-update-method |
Nome do método PHP custom (default onEventUpdate) |
event-click-method |
Override método PHP do click |
day-click-method |
Override método PHP do day click |
Filtros (MadFiltersTrait integrado)
| Attr | Descrição |
|---|---|
period-type |
none | month-year | date-range | preset |
date-field |
Coluna usada pelo filtro de período |
remember-filters |
Persiste filtros em sessão |
default-current-period |
Seed mes/ano com data atual |
apply-unit-filter |
Multi-tenant |
Display
| Attr | Descrição |
|---|---|
calendar-id |
DOM id (obrigatório para refetchCalendar) |
height |
Px (0 = auto) |
full-height |
Altura automática (preenche container) |
popover-title |
Template com {title} {start} {end} {extra_*} |
popover-content |
Idem |
popover-trigger |
hover (default) | click — como o popover abre |
extra-options |
JSON pass-through para FullCalendar (slotDuration, nowIndicator, etc) |
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 nos eventos. Attrs: field, op, value, value2 |
<mad-calendar-resource-filter> |
repeat | Filtros fixos para a query de recursos |
<mad-calendar-filters> |
block | Filtros user-facing (MadFiltersTrait). Ver grid-filters |
Exemplo: toolbar + popover + filtro fixo
<mad-calendar
model="Agendamento"
title-field="titulo"
start-field="dt_inicio"
extra-fields="cliente.nome,tipo"
popover-title="{titulo}"
popover-content="{cliente_nome} — {start} → {end}">
<mad-calendar-toolbar>
<mad-btn navigate="AgendamentoForm" variant="primary" icon="plus">Novo</mad-btn>
<mad-btn mad:click="onReload" variant="ghost" icon="refresh-cw">Recarregar</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>
Exemplo: resource timeline (linhas=recursos, colunas=tempo)
<mad-calendar
calendar-id="cal-salas"
model="Reserva"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
resource-field="sala_id"
default-view="resourceTimelineDay"
time-range="08:00-18:00"
slot-duration="01:00"
editable auto-update
slot-click-target="ReservaForm::onCreate({date},{resourceId})"
click-target="ReservaForm::onEdit({id})">
<mad-calendar-resource model="Sala" title-field="nome" color-field="cor" label="Sala" />
</mad-calendar>
Guia completo: ver seção Resource Timeline abaixo.
Hooks PHP
| Hook | Quando |
|---|---|
onSearch(Builder $q): void |
Filtros adicionais — builder-native, aplica $q->where(...) direto (mesmo contrato do Kanban/Grid/Dashboard) |
buildQuery(): Builder |
Override total do builder de eventos (bypassa model + filtros automáticos) |
mapEvent(object $record): array |
Composição custom de título/cor/payload |
loadEvents(string $start, string $end): array |
Override total da fonte de eventos |
loadResources(): array |
Recursos hardcoded (sem precisar de tabela de recursos no banco) |
beforeEventUpdate($id, $start, $end): bool |
Retornar false cancela o auto-update |
afterEventUpdate($id, $os, $oe, $ns, $ne): void |
Pós-store, ideal para auditoria/log |
onEventClick($id, $title, $view): MadResponse |
Override total do click (bypassa click-target) |
onDayClick($date, $view): MadResponse |
Override total do day click |
onSlotClick($date, $resId, $resTitle): MadResponse |
Resource-timeline slot click |
configureCalendar(MadFullCalendar $cal): void |
Hook raw — chamar ->option() não expostos via attr |
Exemplo: hooks de query e mapeamento
use Illuminate\Database\Eloquent\Builder;
class AgendaCliente extends MadCalendarComponent
{
public int $clienteId = 0;
public function mount(array $params = []): void
{
$this->clienteId = (int) ($params['cliente_id'] ?? 0);
parent::mount($params);
}
// Filtro fixo derivado de prop pública — builder-native
public function onSearch(Builder $q): void
{
$q->where('cliente_id', '=', $this->clienteId);
}
// Mapeamento custom (título concatenado, cor de relação)
protected function mapEvent(object $r): array
{
return [
'id' => (string) $r->id,
'title' => ($r->tipo->nome ?? '') . ' — ' . $r->descricao,
'start' => $r->dt_inicio,
'end' => $r->dt_fim,
'color' => $r->tipo->cor ?? '#3b82f6',
];
}
// Auditoria pós-drag/resize (auto-update grava + este hook só loga)
protected function afterEventUpdate(int $id, string $os, string $oe, string $ns, string $ne): void
{
AgendamentoHistorico::create([
'agendamento_id' => $id,
'de' => $os,
'para' => $ns,
]);
}
}
Filtros declarativos — <mad-calendar-filters>
Mesma API de <mad-grid-filters>. Ver filtros declarativos.
<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'=>'Média']" />
<mad-period-monthyear />
</mad-calendar-filters>
<mad-calendar model="Evento" ... />
Controller:
class AgendaPage extends MadCalendarComponent
{
protected string $periodType = 'month-year';
public string $cliente_id = ''; // auto-discovery
public string $prioridade = '';
}
Atualizar dados sem reload
Após salvar/criar/deletar em form:
public function onSave(): MadResponse
{
// ... salvar evento ...
return (new MadResponse())
->toast('Salvo!', 'success')
->closeDrawer()
->refetchCalendar('cal-agenda'); // ID do <mad-calendar calendar-id="cal-agenda">
}
Atributos do <mad-calendar-resource>
| Attr | Descrição |
|---|---|
model |
Classe model Eloquent do recurso |
database |
Conexão do recurso (default: a mesma do <mad-calendar>) |
id-field |
Default id |
title-field |
Coluna do nome |
color-field |
Coluna com hex |
label |
Label visível no header (ex: "Sala", "Funcionário") |
order-by |
Ordenação |
slot-duration |
Override do slot-duration herdado do <mad-calendar> |
num-days |
Override do num-days herdado do <mad-calendar> |
slot-click-target |
Pode ser declarado aqui em vez de no <mad-calendar> pai |
Resource Timeline — guia completo
Resource Timeline = linhas são recursos (salas, equipamentos, pessoas), colunas
são slots de tempo. É renderizado por uma grade CSS/Alpine própria do MAD
(madResourceTimeline), não pelo plugin resourceTimeline do FullCalendar
(que não está no bundle). Diferente das views de mês/semana padrão, cada evento
precisa apontar pra um recurso via FK.
Estrutura mínima do banco
Tabela de recursos (linhas da timeline):
CREATE TABLE sala (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
cor VARCHAR(7) -- opcional, hex #3b82f6
);
| Coluna | Obrigatória | Mapeada via | Notas |
|---|---|---|---|
id (PK) |
sim | id-field no <mad-calendar-resource> |
int / varchar / uuid |
nome |
sim | title-field |
texto exibido na linha |
cor |
opcional | color-field |
hex #3b82f6 |
Tabela de eventos (barras na timeline):
CREATE TABLE reserva (
id SERIAL PRIMARY KEY,
sala_id INT NOT NULL REFERENCES sala(id), -- FK obrigatória
titulo VARCHAR(200) NOT NULL,
dt_inicio TIMESTAMP NOT NULL,
dt_fim TIMESTAMP NOT NULL,
cor VARCHAR(7),
dia_inteiro CHAR(1) DEFAULT 'N' -- opcional (all-day-field)
);
| Coluna | Obrigatória | Mapeada via | Notas |
|---|---|---|---|
id |
sim | id-field |
|
titulo |
sim | title-field |
|
dt_inicio |
sim | start-field |
datetime |
dt_fim |
sim p/ timeline | end-field |
sem isso, evento vira ponto |
sala_id |
sim | resource-field |
chave do resource timeline — sem ela, evento não aparece na linha |
cor |
opcional | color-field |
|
dia_inteiro |
opcional | all-day-field |
bool — vira allDay no JSON |
Setup completo
class ReservaCalendar extends MadCalendarComponent
{
// Casca — toda config no Blade
}
<mad-calendar
calendar-id="cal-salas"
model="Reserva" database="minierp"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
resource-field="sala_id"
color-field="cor"
default-view="resourceTimelineDay"
time-range="08:00-18:00" slot-duration="01:00" num-days="5"
editable auto-update
slot-click-target="ReservaForm::onCreate({date},{resourceId})"
click-target="ReservaForm::onEdit({id})">
<mad-calendar-resource
model="Sala"
title-field="nome"
color-field="cor"
label="Sala"
order-by="nome" />
<mad-calendar-filter field="deleted_at" op="is" value="null" />
</mad-calendar>
Como resource-field funciona
Sem o atributo, mapEvent() emite o JSON sem resourceId — a grade de resource
timeline (grid Alpine próprio do MAD, madResourceTimeline, não um plugin do
FullCalendar) não sabe em qual linha colocar o evento e ele some.
Com resource-field="sala_id":
mapEvent()lê$record->sala_id- Emite
{"resourceId": "<valor>", ...}no JSON - FullCalendar plota o evento na linha do recurso correspondente
Suporta dot-notation pra navegação por relação:
resource-field="reserva.sala_id"
Views suportadas
default-view |
Layout |
|---|---|
resourceTimelineDay |
1 dia, horas como colunas |
resourceTimelineWeek |
7 dias, dias como colunas |
resourceTimeline (multi-day) |
N dias via num-days="N" |
Recursos hardcoded (sem tabela)
Override loadResources() quando recursos são fixos (turnos, equipes pequenas):
class TurnoCalendar extends MadCalendarComponent
{
protected function loadResources(): array
{
return [
['id' => 'manha', 'title' => 'Manhã', 'color' => '#3b82f6'],
['id' => 'tarde', 'title' => 'Tarde', 'color' => '#f59e0b'],
['id' => 'noite', 'title' => 'Noite', 'color' => '#6366f1'],
];
}
}
<mad-calendar
model="Plantao"
title-field="funcionario.nome"
start-field="dt_inicio" end-field="dt_fim"
resource-field="turno"
default-view="resourceTimelineDay" />
Sem <mad-calendar-resource> — loadResources() ganha precedência.
Filtros nos recursos
Pra restringir quais recursos aparecem na timeline (ex: só salas ativas):
<mad-calendar ...>
<mad-calendar-resource model="Sala" title-field="nome" />
<mad-calendar-resource-filter field="ativo" op="=" value="1" />
<mad-calendar-resource-filter field="deleted_at" op="is" value="null" />
</mad-calendar>
Slot click → criar evento já vinculado ao recurso
slot-click-target recebe {date} e {resourceId} automaticamente:
slot-click-target="ReservaForm::onCreate({date},{resourceId})"
Controller:
public function onCreate(string $date, string $resourceId): void
{
$this->form->set('dt_inicio', $date);
$this->form->set('sala_id', $resourceId); // pré-seleciona o recurso clicado
}
Eventos dia-inteiro — all-day-field
Coluna booleana no model marca o evento como dia inteiro (renderiza no topo, sem horário):
ALTER TABLE reserva ADD COLUMN dia_inteiro CHAR(1) DEFAULT 'N';
<mad-calendar
model="Reserva"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
all-day-field="dia_inteiro"
default-view="agendaWeek" />
_truthy() aceita 1/0, t/f, S/N, true/false, yes/no. Qualquer valor
não-zero/não-vazio = true.
Edição per-event — editable-field
Trava drag/resize linha-a-linha. Útil pra status do tipo "aprovado/finalizado" que não devem mais ser movidos:
ALTER TABLE reserva ADD COLUMN permite_editar CHAR(1) DEFAULT 'S';
<mad-calendar
model="Reserva"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
editable auto-update
editable-field="permite_editar" />
Lógica:
editableno<mad-calendar>= default global (true/false)editable-field= override per-event vindo do banco- Evento com
permite_editar = 'N'→ não arrasta nem redimensiona
Combina bem com regras de negócio via accessor Eloquent (Attribute::make()) —
o _resolvePath() lê $record->permite_editar normalmente, e o Eloquent
resolve via accessor mesmo sem coluna real na tabela:
use Illuminate\Database\Eloquent\Casts\Attribute;
class Reserva extends Model
{
protected function permiteEditar(): Attribute
{
return Attribute::make(
get: fn () => in_array($this->status, ['rascunho', 'pendente'], true) ? 'S' : 'N',
);
}
}
editable-field="permite_editar"
Gotchas
title-fieldestart-fieldobrigatórios — sem eles a query auto não monta- Dot-notation em
title-fieldfunciona (cliente.nome) — navega relações Eloquent via_resolvePath()(suportabelongsTo/hasOne) auto-updatechama$record->save()direto — se precisa de validação extra, overridebeforeEventUpdateouonEventUpdaterefetchCalendar('id')requercalendar-id="id"— sem isso, o JS não acha o DOMextra-fieldslista colunas necessárias para os popover/click targets — campos não listados não chegam ao client- Resource timeline sem
resource-field— eventos somem (a grade própriamadResourceTimelinenão sabe em qual linha plotar). Sempre declarar a coluna FK all-day-field/editable-fieldaceitam qualquer truthy —1/0,S/N,t/f,true/false(case-insensitive) — usa_truthy()
Fonte: packages/mad-framework/src/mad/calendar/MadCalendarComponent.php
(componente) e MadFullCalendar.php (builder FullCalendar). Resource timeline
em detalhe: full-calendar-resource.md.