Programação Por Equipe DevBras

Capa do artigo: Arrastar evento no FullCalendar: o servidor confirma, a grade só mostra

Arrastar evento no FullCalendar: o servidor confirma, a grade só mostra

Faça a grade do calendário ler a sua API, mande cada arrasto para o servidor validar conflito e versão, desfaça o movimento recusado e atualize sem perder a semana que a pessoa olhava.

A recepcionista do coworking arrastou a reserva da Sala Azul de terça para quarta, às 10h. A grade mostrou o bloco no lugar novo e ela foi tomar café. O servidor tinha recusado: quarta às 10h já era de outra empresa. Como o código não desfazia o movimento, a grade dela continuou mostrando a reserva na quarta, e na quarta duas equipes chegaram para a mesma sala. A grade do calendário é uma vista. O que vale é o que o servidor aceitou.

A resposta curta: a biblioteca de calendário lê os eventos da sua API; cada arrasto ou redimensionamento vira uma requisição que o servidor aceita ou recusa, verificando conflito e versão do registro; se recusar, a grade desfaz o movimento e diz o motivo. Atualizar a grade é pedir os eventos de novo, sem destruir o calendário nem perder a semana em que a pessoa estava.

Por que a grade não pode ser a fonte da verdade

Bibliotecas como o FullCalendar são excelentes para desenhar semanas, arrastar blocos e redimensionar horários. Quando você liga a edição, a biblioteca move o bloco antes de qualquer confirmação. Isso é bom para a sensação de rapidez, mas significa que, por um instante, a tela mostra algo que ainda não existe. Se o código não tratar a resposta do servidor, esse instante vira permanente.

Há também o problema de várias pessoas olhando a mesma agenda. Enquanto a recepcionista tinha a grade aberta desde as 8h, outra pessoa reservou a quarta pelo aplicativo. A grade dela estava desatualizada, e o arrasto partiu de uma foto antiga. O servidor precisa saber disso.

Sobre a integração com o Google: o plugin de Google Calendar do FullCalendar serve para exibir eventos de uma agenda do Google. Ele não transforma a agenda externa no banco do seu sistema. Se você também sincroniza com o Google, a reserva continua nascendo no seu banco, como mostra o guia de agendamento com Google Agenda.

Como ligar a grade à sua API

  1. Sirva os eventos por uma rota sua. Ao usar uma URL como fonte, o FullCalendar pede os eventos do intervalo visível, enviando os parâmetros start e end.
  2. Devolva datas com fuso explícito. Instantes em ISO 8601 com deslocamento, como 2026-10-21T10:00:00-03:00, para a grade não adivinhar o fuso.
  3. Inclua a versão de cada reserva. Um número que o servidor incrementa a cada alteração, enviado em extendedProps.
  4. Trate o arrasto e o redimensionamento. Os dois chamam a mesma função, que envia início, fim e versão ao servidor.
  5. Desfaça quando o servidor recusar. O objeto do evento de arrasto traz revert(), que devolve o bloco ao lugar original.
  6. Atualize sem recriar. Use refetchEvents() para buscar de novo; a semana visível continua a mesma.
[
  {
    "id": "r-8812",
    "title": "Sala Azul - Acme",
    "start": "2026-10-20T10:00:00-03:00",
    "end": "2026-10-20T11:00:00-03:00",
    "extendedProps": { "versao": 3, "sala": "azul" }
  }
]

O código do lado do navegador

document.addEventListener('DOMContentLoaded', function () {
  const aviso = document.getElementById('aviso-agenda');

  function avisar(texto) {
    aviso.textContent = texto;
  }

  async function remarcar(info) {
    const ev = info.event;
    try {
      const resp = await fetch('/api/reservas/' + encodeURIComponent(ev.id), {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          inicio: ev.start.toISOString(),
          fim: ev.end.toISOString(),
          versao: ev.extendedProps.versao
        })
      });
      if (!resp.ok) {
        const corpo = await resp.json().catch(function () { return {}; });
        info.revert();
        avisar(corpo.mensagem || 'Não foi possível remarcar. A reserva continua no horário anterior.');
        if (resp.status === 409) calendario.refetchEvents();
        return;
      }
      const atualizada = await resp.json();
      ev.setExtendedProp('versao', atualizada.versao);
      avisar('Reserva remarcada.');
    } catch (erro) {
      info.revert();
      avisar('Sem conexão. A reserva continua no horário anterior.');
    }
  }

  const calendario = new FullCalendar.Calendar(document.getElementById('agenda'), {
    initialView: 'timeGridWeek',
    editable: true,
    events: '/api/reservas',
    eventDrop: remarcar,
    eventResize: remarcar
  });
  calendario.render();
});

O elemento aviso-agenda pode ser um parágrafo com aria-live="polite", para que quem usa leitor de tela também saiba que a remarcação foi recusada. Repare que a mensagem diz o que aconteceu com a reserva, não só que houve erro: a pessoa precisa saber onde a reserva está agora.

O exemplo usa o fuso do navegador, o padrão da biblioteca, e o servidor manda os horários com deslocamento. Se a agenda precisar ser exibida no fuso da sala e não no de quem olha, a documentação de timeZone do FullCalendar explica as opções e os plugins necessários. A regra geral de gravar o instante e exibir no fuso certo está em grave o instante em UTC.

O que o servidor confere em cada arrasto

A rota PATCH faz três verificações. Primeiro, permissão: essa pessoa pode mexer nessa reserva. Segundo, versão: o UPDATE só altera a linha se a versão no banco for a mesma que veio do navegador, e incrementa a versão. Se nenhuma linha foi alterada, alguém mudou a reserva antes, e a resposta é 409. Terceiro, conflito: a sala não pode ter duas reservas sobrepostas, verificado com uma restrição no banco ou dentro de uma transação.

UPDATE reservas
   SET inicio = $1, fim = $2, versao = versao + 1
 WHERE id = $3 AND versao = $4
RETURNING versao;

Com isso, o caso da Sala Azul termina diferente: a recepcionista arrasta, o servidor responde 409 com "quarta às 10h já está reservada", o bloco volta para terça e a grade recarrega mostrando a reserva da outra empresa. O guia vizinho, a grade do calendário lê a sua API, detalha a rota de leitura.

Erros comuns

  • Não chamar revert() na recusa. A grade mostra a reserva num horário que o servidor nunca aceitou, e a pessoa age com base nisso.
  • Mandar só o novo horário, sem versão. Uma grade aberta há horas sobrescreve a alteração que outra pessoa fez nesse meio-tempo.
  • Destruir e recriar o calendário para atualizar. A grade pisca e volta para a semana atual, e a pessoa perde o lugar onde estava.
  • Datas sem fuso na API. A mesma reserva aparece uma ou três horas deslocada, dependendo do computador de quem olha.
  • Usar a agenda externa como único armazenamento. Se a conta ou a integração cair, você não tem o histórico de reservas.
  • Erro genérico sem dizer onde a reserva ficou. A pessoa não sabe se precisa avisar o cliente.

Checklist

  • Os eventos vêm de uma rota sua, filtrados pelo intervalo visível.
  • Todos os horários da API têm fuso explícito.
  • Cada evento carrega a versão do registro.
  • Arrasto e redimensionamento passam pelo servidor antes de valer.
  • Recusa desfaz o movimento e mostra o motivo num aviso acessível.
  • O servidor responde 409 para versão desatualizada e para conflito de horário.
  • A atualização usa refetchEvents() e mantém a semana visível.

Faça o teste com duas abas abertas na mesma semana: remarque uma reserva na primeira, depois arraste a mesma reserva na segunda. A sua grade aceita o segundo movimento como se nada tivesse acontecido?

Avalie este artigo

Referência: https://fullcalendar.io/docs/google-calendar