Panduan ini menunjukkan cara membuat kalender untuk memesan ruang rapat: timeline dengan satu baris untuk setiap ruangan, pemesanan yang Anda seret ke tempatnya, aturan yang mencegah ruangan dipesan dua kali, dan file yang memasukkan pemesanan ke kalender mana pun. Kalender ini memakai penjadwal dari @needless-ui/angular, yang berlisensi MIT seperti seluruh Needless UI, sehingga timeline sumber daya gratis untuk aplikasi apa pun.

Ruangan dan pemesanan

Ruangan adalah sumber daya, berisi id, title, dan data lain apa pun di data. Pemesanan adalah acara dengan waktu ISO biasa, seperti 2026-10-23T09:00, dan id ruangannya di resource. Satu pemesanan bisa memuat beberapa ruangan, atau orang, sebagai daftar.

Satu baris untuk setiap ruangan

Teruskan keduanya ke nui-scheduler dengan view="timeline". dayStart dan dayEnd membatasi hari pada jam buka, snapMinutes menggeser pemesanan per seperempat jam, dan businessHours mengarsir waktu saat ruangan tutup. nuiSchedulerResource menggambar header setiap baris.

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>

Pesan dan pindahkan

Penjadwal tidak pernah mengubah data Anda dengan sendirinya: ia hanya melaporkan apa yang dilakukan pengguna. Menyeret melintasi baris sebuah ruangan akan memilih waktu, dan slotSelect melaporkannya beserta ruangannya. Saat pemesanan, atau ujung akhirnya, diseret, eventChange melaporkan waktu barunya, beserta ruangan barunya jika pemesanan itu pindah ke ruangan lain. Perbarui signal-nya, maka timeline akan mengikuti.

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

Tanpa pemesanan ganda

Penjadwal bertanya kepada allow sebelum pemesanan dibuat atau dipindahkan. Jika jawabannya false, seretan berwarna merah dan tidak menghasilkan apa-apa saat dilepas, sedangkan lewat keyboard, penjadwal mengumumkan bahwa waktu itu tidak tersedia. Contoh ini memeriksa pemesanan lain di ruangan yang sama; periksa lagi di server Anda, yang memegang keputusan akhir.

Muat hari yang ditampilkan

rangeChange melaporkan hari pertama dan terakhir yang ditampilkan, sekali di awal dan lagi setiap kali pengguna berpindah halaman, sehingga Anda cukup mengambil pemesanan untuk hari-hari itu saja, dengan httpResource atau request apa pun yang Anda inginkan.

Tambahkan ke kalender

nuiSchedulerToIcs() menulis pemesanan sebagai file iCalendar, yang bisa diimpor oleh Google Calendar, Outlook, dan Apple Calendar. nuiSchedulerFromIcs() membacanya kembali, dan memindahkan waktu dari zona waktu lain ke zona waktu pengguna.

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 dan pembaca layar

Semuanya bisa dilakukan tanpa mouse. Timeline hanya satu perhentian tab: tombol panah bergerak melintasi waktu dan ruangan, Shift bersama tombol panah memilih waktu, dan Enter memesannya. Pada sebuah pemesanan, Alt bersama tombol panah memindahkannya ke waktu atau ruangan lain. Setiap pemesanan adalah tombol yang dinamai dengan judul, ruangan, hari, dan waktunya.

Untuk ruangan yang berdampingan dalam tampilan hari, pemesanan berulang, dan semua opsi lainnya, lihat halaman penjadwal beserta API-nya.