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
- 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
starteend. - 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. - Inclua a versão de cada reserva. Um número que o servidor incrementa a cada alteração, enviado em
extendedProps. - Trate o arrasto e o redimensionamento. Os dois chamam a mesma função, que envia início, fim e versão ao servidor.
- Desfaça quando o servidor recusar. O objeto do evento de arrasto traz
revert(), que devolve o bloco ao lugar original. - 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?
Referência: https://fullcalendar.io/docs/google-calendar