يبني هذا الدليل تقويمًا لحجز قاعات الاجتماعات: خطًا زمنيًا فيه صف لكل قاعة، وحجوزات تسحبها إلى أماكنها، وقاعدة تمنع حجز القاعة مرتين، وملفًا يضيف الحجز إلى أي تقويم. ويستخدم المُجدوِل من @needless-ui/angular، وهو برخصة MIT مثل Needless UI كلها، لذا فالخط الزمني للموارد مجاني لأي تطبيق.

القاعات والحجوزات

القاعات موارد: لكلٍّ منها id وtitle، وأي شيء آخر في data. والحجوزات أحداث بأوقات ISO عادية، مثل 2026-10-23T09:00، ومعرّف قاعتها في resource. ويمكن أن يشمل الحجز الواحد عدة قاعات، أو أشخاصًا، في قائمة.

صف لكل قاعة

مرّر القاعات والحجوزات إلى nui-scheduler مع view="timeline". ويحصر dayStart وdayEnd اليوم في ساعات فتح القاعات، ويحرّك snapMinutes الحجوزات بخطوات من ربع ساعة، ويُظلّل businessHours الوقت الذي تُغلق فيه القاعات. ويرسم nuiSchedulerResource عنوان كل صف.

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>

الحجز والنقل

لا يغيّر المُجدوِل بياناتك بنفسه أبدًا: بل يُبلغك بما يفعله الناس. فالسحب عبر صف قاعة يختار وقتًا، ويُبلغ عنه slotSelect مع القاعة. وسحب حجز، أو نهايته، يُبلغ عن أوقاته الجديدة في eventChange، وعن قاعته الجديدة إن نُقل إلى قاعة أخرى. حدّث signal الحجوزات، فيتبعه الخط الزمني.

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

منع الحجز المزدوج

يُسأل allow قبل إنشاء أي حجز أو نقله. وحين يُعيد false، يظهر السحب باللون الأحمر ولا يُضيف شيئًا عند الإفلات، ومن لوحة المفاتيح يُعلن المُجدوِل أن الوقت غير متاح. ويتحقق المثال من حجوزات القاعة الأخرى؛ فتحقّق مرة أخرى على خادمك، فله الكلمة الأخيرة.

تحميل الأيام المعروضة

يُبلغ rangeChange عن أول يوم معروض وآخر يوم، مرةً عند البدء ثم كلما انتقل الناس إلى صفحة أخرى، لتجلب حجوزات تلك الأيام وحدها، عبر httpResource أو أي طلب تريده.

الإضافة إلى التقويم

تكتب nuiSchedulerToIcs() الحجوزات في ملف iCalendar، تستورده Google Calendar وOutlook وApple Calendar. وتقرأ nuiSchedulerFromIcs() ملفًا كهذا، وتنقل الأوقات من المناطق الزمنية الأخرى إلى المنطقة الزمنية للقارئ.

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

لوحة المفاتيح وقارئات الشاشة

يعمل كل ذلك دون فأرة. فالخط الزمني نقطة توقف واحدة لمفتاح Tab: تتنقل مفاتيح الأسهم بين الأوقات والقاعات، ويختار Shift مع الأسهم وقتًا، ويحجزه Enter. وعلى أي حجز، ينقله Alt مع الأسهم إلى وقت آخر أو قاعة أخرى. وكل حجز زر يُسمّى بعنوانه وقاعته ويومه ووقته.

ولعرض القاعات جنبًا إلى جنب في يوم واحد، وللحجوزات المتكررة وسائر الخيارات، راجع صفحة المُجدوِل ومرجع API الخاص به.