/** * scheduler-model - the pure, framework-free core behind SvGridScheduler (the * calendar / scheduler *view of the grid*). No Svelte, no DOM: just date math * over the native `Date`, so every rule here is unit-tested directly. * * It does three things the view leans on: * 1. resolve rows into concrete event instances for a visible window, expanding * recurring rows via the shared `recurrence` engine (see recurrence.ts); * 2. pack overlapping timed events into side-by-side columns for the time-grid * (week / day) views; * 3. compute the visible date range + navigation for each view. * * Deliberately avoids the arg-less `new Date()` / `Date.now()` (they are * non-deterministic and unavailable in some execution contexts) - the caller * supplies "today"/the anchor date and passes it in. */ import { type DateLike } from './datetime/date-core'; import { type RecurrenceRule } from './recurrence'; import type { SchedulerView, SchedulerResource, SchedulerCollisionMode } from './SvGrid.types'; export type { SchedulerView, SchedulerResource, SchedulerCollisionMode }; /** A concrete event instance placed on the calendar (one row may yield many when * it recurs). `key` is unique per instance; `rowKey` ties it back to its row. */ export type ResolvedEvent = { key: string; rowKey: string; row: TData; title: string; start: Date; end: Date; allDay: boolean; color?: string; /** Optional secondary accent (e.g. a category) shown as a left strip, distinct * from the main `color`. */ color2?: string; /** Free/busy status (busy | free | tentative | oof) for a distinct visual. */ status?: string; resourceId?: string; /** True when this instance came from expanding a recurrence rule. */ recurring: boolean; /** For a recurring instance, its canonical (pre-override) occurrence start - * the identity used to key {@link RecurrenceException} overrides. */ occurrenceStart?: Date; /** True when a recurring instance carries a per-occurrence override. */ isException?: boolean; }; /** A per-occurrence override of a recurring event (the model's shape; the * component maps the consumer's `SchedulerException` onto this). */ export type RecurrenceException = { occurrenceStart: DateLike; deleted?: boolean; start?: DateLike | null; end?: DateLike | null; title?: string; allDay?: boolean; /** Per-occurrence overrides for any other field, merged over the row for just * this occurrence (attendees, calendar, color, resource, ...). */ fields?: Record; }; /** Field accessors the model uses to read an event off a row. The component * builds this from `SchedulerConfig`, applying its move/edit overlay first so * a dragged event resolves at its new time without mutating the source row. */ export type EventSpec = { getKey: (row: TData) => string; getStart: (row: TData) => DateLike | null | undefined; getEnd?: (row: TData) => DateLike | null | undefined; getAllDay?: (row: TData) => boolean; getTitle?: (row: TData) => string; getColor?: (row: TData) => string | undefined; /** Optional secondary accent color (rendered as a left strip). */ getSecondaryColor?: (row: TData) => string | undefined; /** Optional free/busy status. */ getStatus?: (row: TData) => string | undefined; getResource?: (row: TData) => string | undefined; getRecurrence?: (row: TData) => RecurrenceRule | ReadonlyArray | null | undefined; /** Per-occurrence overrides for a recurring row (deleted / moved / edited). */ getExceptions?: (row: TData) => ReadonlyArray | null | undefined; /** Fallback event length (minutes) when a row has a start but no end. Default 60. */ defaultDurationMin?: number; }; /** One working window: `[start, end)` hours, optionally limited to `days` * (weekday 0 = Sun … 6 = Sat). Omitting `days` applies it to every day. */ export type WorkingWindow = { days?: ReadonlyArray; start: number; end: number; }; /** * The working `[startMin, endMin]` intervals for `weekday`, from `windows`, * clamped to `[bandStartMin, bandEndMin]` and merged. Empty = the whole day is * off. Pure; used for per-resource availability shading + enforcement. */ export declare function workingIntervals(weekday: number, windows: ReadonlyArray, bandStartMin: number, bandEndMin: number): Array<[number, number]>; /** True when `[startMin, endMin]` lies wholly inside one working interval. */ export declare function withinWorking(startMin: number, endMin: number, intervals: ReadonlyArray): boolean; /** * True when placing `[start,end]` on `resourceId` would overlap another event on * the SAME resource (ignoring the event identified by `excludeRowKey`). Used to * enforce `disableConflicts` (no double-booking). All-day/timed both count. */ export declare function hasConflict(start: Date, end: Date, resourceId: string | undefined, events: ReadonlyArray>, excludeRowKey?: string): boolean; /** * How many events on the SAME resource overlap `[start,end]` (excluding * `excludeRowKey`). Used to enforce `maxEventsPerSlot` (resource capacity): a * move/create is blocked when this count would reach the cap. */ export declare function overlapCount(start: Date, end: Date, resourceId: string | undefined, events: ReadonlyArray>, excludeRowKey?: string): number; /** True when the minute range `[sMin,eMin)` overlaps any of the given hour bands. */ export declare function overlapsBands(sMin: number, eMin: number, bands: ReadonlyArray<{ start: number; end: number; }>): boolean; /** * Resolve `rows` into the event instances visible in `[rangeStart, rangeEnd]`. * Recurring rows are expanded to one instance per matching day (keeping the base * event's time-of-day and duration); single rows are included when they overlap * the window. Result is sorted by start (all-day first within a start). */ export declare function resolveEvents(rows: ReadonlyArray, spec: EventSpec, rangeStart: Date, rangeEnd: Date): ResolvedEvent[]; /** Events that touch `day` (any calendar day the event spans), timed + all-day. */ export declare function eventsOnDay(events: ReadonlyArray>, day: Date): ResolvedEvent[]; /** * A continuous month-view bar for one event within one week row: the columns it * spans (`startCol`..`endCol`, 0-6) and the `lane` (stack row) it sits in. Events * that cross a week boundary are split into one segment per week (with * `continuesLeft`/`continuesRight` flags so the view can flatten that edge). */ export type MonthSegment = { event: ResolvedEvent; startCol: number; endCol: number; lane: number; continuesLeft: boolean; continuesRight: boolean; }; /** * Lay out one week row of the month grid as continuous spanning bars. `weekStart` * is the local-midnight first day of the 7-day row. Returns each overlapping * event as a {@link MonthSegment} clipped to the week, with greedy lane packing * so bars never overlap, plus the total `laneCount`. */ export declare function monthWeekSegments(events: ReadonlyArray>, weekStart: Date): { segments: MonthSegment[]; laneCount: number; }; /** A timed event positioned within a time-grid column. `topPct`/`heightPct` are * 0-100 of the visible day; `leftPct`/`widthPct`/`zIndex` place it horizontally * among overlapping peers (already resolved for the chosen collision mode). */ export type PositionedEvent = { event: ResolvedEvent; topPct: number; heightPct: number; col: number; colCount: number; leftPct: number; widthPct: number; zIndex: number; }; /** A `+N more` tile emitted by `cap` mode for the events that didn't fit. */ export type OverflowMarker = { topPct: number; heightPct: number; leftPct: number; widthPct: number; /** Number of hidden events this tile stands for. */ count: number; /** The hidden events (chronological), for the "+N more" popover. */ events: ResolvedEvent[]; }; /** The full time-grid layout for one day: positioned events + any overflow tiles. */ export type DayLayout = { events: PositionedEvent[]; overflows: OverflowMarker[]; }; export type LayoutOptions = { dayStartHour?: number; dayEndHour?: number; /** Collision layout mode. Default `split`. */ mode?: SchedulerCollisionMode; /** `cap` mode: max columns before overflow (min 2). Default 3. */ maxColumns?: number; /** `stack` mode: horizontal offset per overlapping event, in % of column. */ stackOffsetPct?: number; /** `stack` mode: the narrowest a stacked event may get, in % of column. */ stackMinWidthPct?: number; }; /** * Lay out the timed events of a single day for the time-grid, resolving * collisions per {@link SchedulerCollisionMode}. `dayStartHour`/`dayEndHour` * bound the visible band (e.g. 8..18); events are clamped to it. All-day events * are ignored here (the view renders them in a separate all-day row). */ export declare function layoutDayEvents(dayEvents: ReadonlyArray>, day: Date, opts?: LayoutOptions): DayLayout; /** One day's bucket of events for the agenda (list) view. */ export type AgendaGroup = { day: Date; events: ResolvedEvent[]; }; /** Group resolved events by calendar day for the agenda view (days with no * events are omitted; groups and their events are in chronological order). */ export declare function agendaGroups(events: ReadonlyArray>): AgendaGroup[]; /** * The visible date window for a view anchored on `anchor`. Used both to size the * grid and to bound event resolution (recurrence expansion clips to it). * `agendaDays` controls the agenda span (default 30). */ export declare function rangeForView(view: SchedulerView, anchor: Date, weekStartsOn?: number, agendaDays?: number): { start: Date; end: Date; }; /** The 7 (or fewer) day columns rendered for the week / day views. */ export declare function daysForView(view: SchedulerView, anchor: Date, weekStartsOn?: number): Date[]; /** Move the anchor one view-unit in `dir` (-1 back, +1 forward). */ export declare function navigateAnchor(view: SchedulerView, anchor: Date, dir: number): Date; /** One column of the timeline's minor (tick) header row. */ export type TimelineTick = { start: Date; end: Date; leftPct: number; widthPct: number; label: string; today: boolean; }; /** One cell of the timeline's major (grouping) header row, spanning several ticks. */ export type TimelineMajor = { label: string; leftPct: number; widthPct: number; }; /** The horizontal axis: its window [start, end] (the day band for `timelineDay`), * the minor `ticks`, and the coarser `majors` above them. */ export type TimelineAxis = { start: Date; end: Date; totalMs: number; ticks: TimelineTick[]; majors: TimelineMajor[]; }; /** * Build the timeline header axis for `view` over `[rangeStart, rangeEnd]`. For * `timelineDay` the axis is clamped to the `dayStartHour..dayEndHour` band (so * events outside it clip, matching the vertical day view). Ticks are hours (day), * days (week / month) or months (year); majors are the date, the month(s), or * quarters. All geometry is percentage of the axis window. */ export declare function timelineAxis(view: SchedulerView, rangeStart: Date, rangeEnd: Date, opts?: { dayStartHour?: number; dayEndHour?: number; today?: Date | null; }): TimelineAxis; /** An event's horizontal geometry within the axis window, or `null` when it * falls entirely outside it. `continuesLeft/Right` flag a clipped edge. */ export declare function timelineGeom(start: Date, end: Date, axisStart: Date, axisMs: number): { leftPct: number; widthPct: number; continuesLeft: boolean; continuesRight: boolean; } | null; /** One resource's timeline row: its events lane-packed so overlaps stack. */ export type TimelineRow = { resource: SchedulerResource | null; laneCount: number; items: { event: ResolvedEvent; lane: number; }[]; }; /** * Bucket `events` by resource into rows (a single `null` row when there are no * resources), then greedily lane-pack each row so overlapping events stack. An * event whose `resourceId` matches no resource is dropped. */ export declare function timelineRows(resources: ReadonlyArray | null | undefined, events: ReadonlyArray>): TimelineRow[];