Docs›Componentes (Admin)›mad-calendar — Resource Timeline
Componentes (Admin)

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 um loadResources() customizado) está presente, MadFullCalendar::view() troca a view renderizada de components.full-calendar para components.resource-timeline — uma grade Alpine própria do framework (madResourceTimeline), não o plugin resourceTimeline real do FullCalendar (que nem está no bundle vendorizado em public/lib/independent/js/fullcalendar.js). Mecanicamente o resultado é o mesmo descrito abaixo (evento cai na linha certa via resourceId), só que quem faz o trabalho é esse componente Alpine, não o FullCalendar. Ver também resource-timeline.md e 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:

  1. mapEvent() lê $record->sala_id
  2. Emite {"resourceId": "<valor>", ...} no JSON
  3. 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 exige resource-field apontando pra coluna que armazena o ID do recurso (não precisa de FK real, basta uma string que bata com o id do 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-field obrigatório — sem ele evento vira ponto sem largura na timeline
  • resource-field aceita dot-notation — reserva.sala_id funciona
  • 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-field aceitam qualquer truthy (1/0, S/N, t/f, etc — case-insensitive)
  • Accessor Eloquent (Attribute::make()) funciona pra all-day-field e editable-field — útil pra derivar de status sem coluna real