Neste guia, você vai criar um calendário para reservar salas de reunião: uma timeline com uma linha para cada sala, reservas arrastadas até o lugar certo, uma regra que impede que uma sala seja reservada duas vezes e um arquivo que coloca uma reserva em qualquer calendário. Tudo é feito com o agendador do @needless-ui/angular, com licença MIT como todo o Needless UI, então a timeline de recursos é gratuita para qualquer aplicação.

Salas e reservas

Salas são recursos: um id, um title e qualquer outra coisa em data. Reservas são eventos com horários ISO simples, como 2026-10-23T09:00, e o id da sala em resource. Uma reserva pode ter várias salas, ou pessoas, em uma lista.

Uma linha para cada sala

Passe salas e reservas para o nui-scheduler com view="timeline". dayStart e dayEnd limitam o dia ao horário de funcionamento, snapMinutes move as reservas de 15 em 15 minutos e businessHours sombreia o tempo em que as salas estão fechadas. nuiSchedulerResource desenha o cabeçalho de cada linha.

booking.html
<nui-scheduler
[events]="bookings()"
[resources]="rooms"
view="timeline"
dayStart="8"
dayEnd="19"
snapMinutes="15"
[businessHours]="hours"
editable
selectable
[allow]="free"
(slotSelect)="book($event)"
(eventChange)="move($event)"
(rangeChange)="range.set($event)"
>
<ng-template nuiSchedulerResource let-room>{{ room.title }}</ng-template>
</nui-scheduler>
<button nuiButton type="button" (click)="download()">Add to calendar</button>

Reserve e mova

O agendador nunca altera seus dados por conta própria: ele informa o que as pessoas fazem. Arrastar pela linha de uma sala escolhe um horário, e slotSelect o informa junto com a sala. Arrastar uma reserva, ou o fim dela, informa os novos horários em eventChange, e a nova sala quando ela muda de sala. Atualize o signal, e a timeline acompanha.

booking.ts
import { Component, signal } from '@angular/core';
import { NuiButton } from '@needless-ui/angular/button';
import {
NuiScheduler,
type NuiSchedulerChange,
type NuiSchedulerEvent,
NuiSchedulerResourceTemplate,
type NuiSchedulerSlot,
} from '@needless-ui/angular/scheduler';
import { downloadIcs } from './booking-ics';

@Component({
selector: 'app-bookings',
imports: [NuiButton, NuiScheduler, NuiSchedulerResourceTemplate],
templateUrl: './booking.html',
})
export class Bookings {
/** Rooms are resources; bookings are events, with their room in `resource`. */
protected readonly rooms = [
{ id: 'atlas', title: 'Atlas' },
{ id: 'boreal', title: 'Boreal' },
];
protected readonly bookings = signal<NuiSchedulerEvent[]>([]);
/** Open on weekdays from 8:00 to 19:00: other time is shaded. */
protected readonly hours = [{ days: [1, 2, 3, 4, 5], start: '08:00', end: '19:00' }];
/** The days on screen: load their bookings when it changes. */
protected readonly range = signal<{ start: string; end: string } | null>(null);

protected book({ start, end, resource }: NuiSchedulerSlot): void {
const booking = { id: crypto.randomUUID(), title: 'New booking', start, end, resource };
this.bookings.update((all) => [...all, booking]);
}

protected move({ event, start, end, resource }: NuiSchedulerChange): void {
this.bookings.update((all) =>
all.map((b) =>
b.id === event.id ? { ...b, start, end, resource: resource ?? b.resource } : b,
),
);
}

/** A room holds one booking at a time. */
protected readonly free = (time: NuiSchedulerChange | NuiSchedulerSlot): boolean => {
const id = 'event' in time ? time.event.id : null;
const room = 'event' in time ? (time.resource ?? time.event.resource) : time.resource;
return !this.bookings().some(
(b) => b.id !== id && b.resource === room && b.start < time.end && b.end > time.start,
);
};

protected download(): void {
downloadIcs(this.bookings(), 'bookings');
}
}

Sem reservas duplas

allow é consultado antes que uma reserva seja criada ou movida. Quando ele retorna false, o arrasto aparece em vermelho e nada acontece ao soltar, e pelo teclado o agendador anuncia que o horário não está disponível. O exemplo confere as outras reservas da sala; confira de novo no seu servidor, que tem a palavra final.

Carregue os dias na tela

rangeChange informa o primeiro e o último dia exibidos, uma vez no início e de novo sempre que alguém muda de página, para você buscar só essas reservas, com httpResource ou qualquer requisição que preferir.

Adicionar ao calendário

nuiSchedulerToIcs() escreve as reservas como um arquivo iCalendar, que o Google Calendar, o Outlook e o Apple Calendar importam. nuiSchedulerFromIcs() lê um desses arquivos e converte os horários de outros fusos horários para o fuso de quem lê.

booking-ics.ts
import { type NuiSchedulerEvent, nuiSchedulerToIcs } from '@needless-ui/angular/scheduler';

/** Saves events as an .ics file, which Google Calendar, Outlook and Apple Calendar import. */
export function downloadIcs(events: readonly NuiSchedulerEvent[], name: string): void {
const file = new Blob([nuiSchedulerToIcs(events, { name })], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = `${name}.ics`;
link.click();
URL.revokeObjectURL(link.href);
}

Teclado e leitores de tela

Tudo funciona sem mouse. A timeline é uma única parada de tabulação: as setas percorrem horários e salas, Shift com as setas escolhe um horário e Enter o reserva. Em uma reserva, Alt com as setas a move para outro horário ou outra sala. Toda reserva é um botão nomeado pelo título, sala, dia e horário.

Para as salas lado a lado em um dia, as reservas que se repetem e todas as outras opções, veja a página do agendador e a API dele.