mad-calendar — Resource Timeline
Visualização com linhas = recursos (salas, equipamentos, pessoas) e colunas = slots de tempo.
Visualização onde linhas = recursos (salas, equipamentos, pessoas, turnos)
e colunas = slots de tempo (horas no resourceTimelineDay, dias no
resourceTimelineWeek). Cada evento aparece numa linha específica via FK pro
recurso.
Doc geral do componente:
full-calendar.md. Este markdown foca só no modo resource e nos atributos novos (resource-field,all-day-field,editable-field).
Importante: quando
resource-field/<mad-calendar-resource>(ou umloadResources()customizado) está presente,MadFullCalendar::view()troca a view renderizada decomponents.full-calendarparacomponents.resource-timeline— uma grade Alpine própria do framework (madResourceTimeline), não o pluginresourceTimelinereal do FullCalendar (que nem está no bundle vendorizado empublic/lib/independent/js/fullcalendar.js). Mecanicamente o resultado é o mesmo descrito abaixo (evento cai na linha certa viaresourceId), só que quem faz o trabalho é esse componente Alpine, não o FullCalendar. Ver tambémresource-timeline.mde a seção Gotchas.
Quando usar
| Cenário | Recurso = linha |
|---|---|
| Reserva de salas | Sala |
| Agenda de equipamentos | Máquina / veículo |
| Escala de funcionários | Pessoa |
| Plantões / turnos | Turno (manhã/tarde/noite) |
| Quadras / mesas | Espaço físico |
Estrutura mínima do banco
Resource timeline precisa de 2 fontes: tabela de recursos + tabela de eventos ligada por FK.
Tabela de recursos (linhas)
CREATE TABLE sala (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
cor VARCHAR(7), -- opcional, hex #3b82f6
ativo CHAR(1) DEFAULT '1' -- opcional, pra filtrar
);
| Coluna | Obrigatória | Mapeada via | Notas |
|---|---|---|---|
id (PK) |
sim | id-field no <mad-calendar-resource> |
int / varchar / uuid |
nome |
sim | title-field |
exibido na coluna esquerda |
cor |
opcional | color-field |
hex #3b82f6 — colore a linha |
Tabela de eventos (barras)
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)
permite_edit CHAR(1) DEFAULT 'S' -- opcional (editable-field)
);
| Coluna | Obrigatória | Mapeada via | Notas |
|---|---|---|---|
id |
sim | id-field |
|
titulo |
sim | title-field |
suporta dot-notation: cliente.nome |
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 |
cor |
opcional | color-field |
hex |
dia_inteiro |
opcional | all-day-field |
bool → allDay no JSON |
permite_edit |
opcional | editable-field |
bool → trava drag/resize per-event |
Setup completo end-to-end
Controller (casca)
<?php
use Mad\Calendar\MadCalendarComponent;
class ReservaCalendar extends MadCalendarComponent
{
// Toda config no Blade
}
View
<mad-page-container>
<mad-page-header title="Reserva de Salas" icon="calendar" />
<mad-page-content>
<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-resource-filter field="ativo" op="=" value="1" />
</mad-calendar>
</mad-page-content>
</mad-page-container>
Atributo resource-field — chave do resource timeline
Problema que resolve
Sem o atributo, mapEvent() emite o JSON de evento sem a key resourceId.
A grade (componente Alpine madResourceTimeline, não o FullCalendar real —
ver disclaimer no topo) não sabe em qual linha plotar o evento e ele
desaparece da timeline.
Como funciona
resource-field="sala_id" faz:
mapEvent()lê$record->sala_id- Emite
{"resourceId": "<valor>", ...}no JSON madResourceTimeline(JS) plota o evento na linha do recurso correspondente
Dot-notation
Suporta navegação por relação (hasOne/belongsTo):
resource-field="reserva.sala_id"
resource-field="agendamento.profissional.id"
Quando NÃO precisa
- Recursos hardcoded via
loadResources()ainda exigeresource-fieldapontando pra coluna que armazena o ID do recurso (não precisa de FK real, basta uma string que bata com oiddo array)
Atributo all-day-field — eventos dia-inteiro
Coluna booleana no model marca o evento como dia inteiro. Renderiza no topo da view sem horário.
Schema
ALTER TABLE reserva ADD COLUMN dia_inteiro CHAR(1) DEFAULT 'N';
Uso
<mad-calendar
model="Reserva"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
all-day-field="dia_inteiro"
default-view="agendaWeek" />
Valores aceitos
_truthy() converte qualquer um destes em true:
| Banco | Vira allDay |
|---|---|
1, '1' |
true |
'S', 's', 'Sim' |
true |
't', 'true', 'TRUE' |
true |
'yes', 'on', 'Y' |
true |
0, '0', '', null |
false |
'N', 'n', 'No', 'false', 'off' |
false |
Accessor Eloquent (lógica derivada)
Quando o flag não existe na tabela mas pode ser inferido:
use Illuminate\Database\Eloquent\Casts\Attribute;
class Reserva extends Model
{
protected function diaInteiro(): Attribute
{
// Dia inteiro = início 00:00 e fim 23:59
return Attribute::make(
get: fn () => substr($this->dt_inicio, 11, 5) === '00:00'
&& substr($this->dt_fim, 11, 5) === '23:59'
? 'S' : 'N',
);
}
}
all-day-field="dia_inteiro"
Atributo editable-field — edição per-event
Trava drag/resize linha-a-linha. Útil pra status "aprovado"/"finalizado" que não devem mais ser movidos.
Schema
ALTER TABLE reserva ADD COLUMN permite_editar CHAR(1) DEFAULT 'S';
Uso
<mad-calendar
model="Reserva"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
editable auto-update
editable-field="permite_editar" />
Lógica
| Atributo | Função |
|---|---|
editable no <mad-calendar> |
Liga drag/resize global (true/false) |
editable-field |
Override per-event vindo do banco |
Evento com permite_editar = 'N' → não arrasta nem redimensiona, mesmo com
editable global ligado.
Accessor Eloquent (derivada do status)
Padrão mais limpo — atributo virtual baseado em regra de negócio:
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"
Não precisa de coluna real no banco — _resolvePath() lê $record->permite_editar
e o Eloquent resolve via accessor (Attribute::make()).
Views suportadas
default-view |
Layout |
|---|---|
resourceTimelineDay |
1 dia, horas como colunas |
resourceTimelineWeek |
7 dias (ou num-days="N"), dias como colunas |
Mudar horário visível
time-range="06:00-22:00" {{-- 6h da manhã até 22h --}}
slot-duration="00:30" {{-- slots de 30min --}}
Multi-day customizado
default-view="resourceTimelineDay"
num-days="10" {{-- 10 dias na timeline --}}
no-weekend {{-- pular sábado/domingo --}}
Filtros nos recursos
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>
<mad-calendar-resource-filter> é repetível (cada um adiciona uma condição
where/whereBetween/whereIn/whereNull ao builder dos recursos).
Operadores aceitos: =, !=, >, <, >=, <=, like, in, not in,
is, between.
Recursos hardcoded (sem tabela)
Quando recursos são fixos — turnos, equipes pequenas, status — override
loadResources() retornando array:
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" />
Tabela plantao precisa ter coluna turno com valores 'manha' / 'tarde'
/ 'noite' (string que bate com os ids do array).
Não declarar <mad-calendar-resource> quando usa loadResources() —
hardcoded ganha precedência.
Slot click → criar evento pré-vinculado ao recurso
Clique num slot vazio passa {date} + {resourceId} automaticamente pro form
destino:
slot-click-target="ReservaForm::onCreate({date},{resourceId})"
Form recebe os dois params:
class ReservaForm extends MadComponent
{
protected static string $wrapper = self::DRAWER;
public function onCreate(string $date, string $resourceId): void
{
$this->form->set('dt_inicio', $date);
$this->form->set('sala_id', $resourceId); // pré-seleciona recurso clicado
}
}
Edição completa — drag/resize + click pra editar
Combinação típica de produção:
<mad-calendar
calendar-id="cal-salas"
model="Reserva"
title-field="titulo"
start-field="dt_inicio" end-field="dt_fim"
resource-field="sala_id"
color-field="cor"
all-day-field="dia_inteiro"
editable-field="permite_editar"
default-view="resourceTimelineDay"
time-range="08:00-18:00" slot-duration="01:00" num-days="5"
editable auto-update
confirm-update="Confirma novo horário?"
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>
Comportamento:
| Ação do usuário | Resultado |
|---|---|
| Clica slot vazio | Abre drawer ReservaForm com dt_inicio + sala_id preenchidos |
| Clica evento existente | Abre drawer ReservaForm com id (modo edição) |
| Arrasta evento | Confirma → store() automático no novo horário/recurso |
| Redimensiona evento | Confirma → store() automático com novo dt_fim |
Evento com permite_editar = 'N' |
Não arrasta nem redimensiona |
Evento com dia_inteiro = 'S' |
Renderiza no topo, sem horário |
Refetch após salvar
Form que salva deve disparar refetch da timeline sem reload da página:
public function onSave(): MadResponse
{
// ... salvar reserva ...
return (new MadResponse())
->toast('Reserva salva!', 'success')
->closeDrawer()
->refetchCalendar('cal-salas'); // mesmo ID do calendar-id no Blade
}
Gotchas específicos do resource timeline
- Sem
resource-field, eventos somem — a grade (madResourceTimeline) não acha a linha end-fieldobrigatório — sem ele evento vira ponto sem largura na timelineresource-fieldaceita dot-notation —reserva.sala_idfunciona- Hardcoded
loadResources()ignora<mad-calendar-resource>— não declarar os dois <mad-calendar-resource-filter>filtra recursos, não eventos — pra filtrar eventos usar<mad-calendar-filter>all-day-field/editable-fieldaceitam qualquer truthy (1/0,S/N,t/f, etc — case-insensitive)- Accessor Eloquent (
Attribute::make()) funciona praall-day-fieldeeditable-field— útil pra derivar de status sem coluna real