Sa gabay na ito, gagawa tayo ng calendar para sa pag-book ng mga meeting room: isang timeline na may row para sa bawat room, mga booking na dina-drag mo sa puwesto, isang patakarang pumipigil na ma-book nang dalawang beses ang isang room, at isang file na naglalagay ng booking sa kahit anong calendar. Ginagamit nito ang scheduler mula sa @needless-ui/angular, na MIT-licensed gaya ng buong Needless UI, kaya libre ang resource timeline para sa kahit anong app.

Mga room at booking

Mga resource ang mga room: isang id, isang title, at kahit ano pa sa data. Mga event naman ang mga booking, na may plain na ISO time gaya ng 2026-10-23T09:00, at ang id ng room nila sa resource. Puwedeng maglaman ang isang booking ng ilang room, o tao, bilang list.

Isang row para sa bawat room

Ipasa ang dalawa sa nui-scheduler na may view="timeline". Pinapanatili ng dayStart at dayEnd ang araw sa loob ng opening hours, inililipat ng snapMinutes ang mga booking nang tig-15 minuto, at sini-shade ng businessHours ang oras na sarado ang mga room. Iginuguhit ng nuiSchedulerResource ang header ng bawat row.

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>

Mag-book at maglipat

Hindi kailanman binabago nang kusa ng scheduler ang data mo: iniuulat lang nito ang ginagawa ng mga tao. Pumipili ng oras ang pag-drag sa row ng isang room, at iniuulat ito ng slotSelect kasama ang room. Kapag ni-drag ang isang booking, o ang dulo nito, iniuulat ng eventChange ang mga bagong oras nito, at ang bagong room nito kapag inilipat ito sa iba. I-update ang signal, at susunod ang timeline.

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

Walang double booking

Tinatanong ang allow bago gawin o ilipat ang isang booking. Kapag false ang ibinalik nito, nagiging pula ang drag at walang nangyayari kapag binitawan, at kapag keyboard ang gamit, sinasabi ng scheduler na hindi available ang oras. Sinusuri ng example ang ibang booking ng room; suriin ulit sa server mo, dahil ito ang may huling salita.

I-load ang mga araw na nasa screen

Iniuulat ng rangeChange ang una at huling araw na ipinapakita, minsan sa simula at muli tuwing lilipat ng page ang mga tao, para ma-fetch mo lang ang mga booking ng mga araw na iyon, gamit ang httpResource o kahit anong request na gusto mo.

Idagdag sa calendar

Isinusulat ng nuiSchedulerToIcs() ang mga booking bilang iCalendar file, na ini-import ng Google Calendar, Outlook at Apple Calendar. Binabasa naman ng nuiSchedulerFromIcs() pabalik ang ganoong file, at inililipat nito ang mga oras mula sa ibang time zone papunta sa time zone ng user.

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

Keyboard at mga screen reader

Gumagana ang lahat ng ito kahit walang mouse. Iisang tab stop ang timeline: gumagalaw sa mga oras at room ang mga arrow key, pumipili ng oras ang Shift kasama ang mga arrow, at bina-book ito ng Enter. Sa isang booking, inililipat ito ng Alt kasama ang mga arrow sa ibang oras o room. Isang button ang bawat booking na pinangalanan ng title, room, araw at oras nito.

Para sa mga room na magkakatabi sa view ng araw, mga recurring booking at bawat iba pang opsyon, tingnan ang page ng scheduler at ang API nito.