En esta guía vas a crear un calendario para reservar salas de reuniones: una línea de tiempo con una fila para cada sala, reservas que se arrastran a su sitio, una regla que impide reservar dos veces la misma sala y un archivo que añade una reserva a cualquier calendario. Usarás el planificador de @needless-ui/angular, con licencia MIT como todo Needless UI, así que la línea de tiempo de recursos es gratuita para cualquier aplicación.

Salas y reservas

Las salas son recursos: un id, un title y cualquier otra cosa en data. Las reservas son eventos con horas ISO simples, como 2026-10-23T09:00, y el id de su sala en resource. Una reserva puede incluir varias salas, o personas, en forma de lista.

Una fila para cada sala

Pasa las salas y las reservas a nui-scheduler con view="timeline". dayStart y dayEnd limitan el día al horario de apertura, snapMinutes mueve las reservas en pasos de un cuarto de hora y businessHours sombrea el tiempo en que las salas están cerradas. nuiSchedulerResource dibuja el encabezado de cada fila.

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>

Reserva y mueve

El planificador nunca cambia tus datos por su cuenta: te indica lo que hace la gente. Al arrastrar sobre la fila de una sala se elige un intervalo, y slotSelect lo comunica junto con la sala. Al arrastrar una reserva, o su final, eventChange comunica sus horas nuevas, y su sala nueva si ha pasado a otra. Actualiza el signal y la línea de tiempo se pone al día.

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');
}
}

Sin dobles reservas

allow se consulta antes de crear o mover una reserva. Cuando devuelve false, el arrastre se ve en rojo y al soltarlo no pasa nada, y con el teclado el planificador anuncia que esa hora no está disponible. El ejemplo comprueba las demás reservas de la sala; vuelve a comprobarlo en tu servidor, que tiene la última palabra.

Carga los días en pantalla

rangeChange comunica el primer y el último día que se muestran, al principio y de nuevo con cada cambio de página, para que puedas pedir solo esas reservas, con httpResource o con la petición que prefieras.

Añadir al calendario

nuiSchedulerToIcs() escribe las reservas como un archivo iCalendar, que Google Calendar, Outlook y Apple Calendar importan. nuiSchedulerFromIcs() lee uno de esos archivos y pasa las horas de otras zonas horarias a la de quien lo lee.

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 y lectores de pantalla

Todo funciona sin ratón. La línea de tiempo es una sola parada de tabulación: las flechas recorren horas y salas, Mayús con las flechas elige un intervalo e Intro lo reserva. En una reserva, Alt con las flechas la mueve a otra hora o a otra sala. Cada reserva es un botón que toma su nombre del título, la sala, el día y la hora.

Para ver las salas lado a lado en un día, las reservas que se repiten y todas las demás opciones, consulta la página del planificador y su API.