WIP - Working

This commit is contained in:
mup 2025-10-17 12:25:27 +02:00
parent a2b9d0362e
commit 1a7cc06c6d

View file

@ -1,7 +1,6 @@
import m, { ChildArray, Children, Component, Vnode, VnodeDOM } from "mithril" import m, { ChildArray, Children, Component, Vnode, VnodeDOM } from "mithril"
import type { CalendarEvent } from "../../api/entities/tutanota/TypeRefs.js"
import { Time } from "../date/Time" import { Time } from "../date/Time"
import { deepMemoized, downcast, first, getStartOfNextDay } from "@tutao/tutanota-utils" import { deepMemoized } from "@tutao/tutanota-utils"
import { px } from "../../gui/size.js" import { px } from "../../gui/size.js"
import { Icon, IconSize } from "../../gui/base/Icon.js" import { Icon, IconSize } from "../../gui/base/Icon.js"
import { Icons } from "../../gui/base/icons/Icons.js" import { Icons } from "../../gui/base/icons/Icons.js"
@ -9,44 +8,18 @@ import { theme } from "../../gui/theme.js"
import { colorForBg } from "../../gui/base/GuiUtils.js" import { colorForBg } from "../../gui/base/GuiUtils.js"
import { getTimeZone } from "../date/CalendarUtils" import { getTimeZone } from "../date/CalendarUtils"
import { EventRenderWrapper } from "../../../calendar-app/calendar/view/CalendarViewModel.js" import { EventRenderWrapper } from "../../../calendar-app/calendar/view/CalendarViewModel.js"
import { DateTime } from "../../../../libs/luxon.js"
import { TimeColumn } from "./TimeColumn" import { TimeColumn } from "./TimeColumn"
import { elementIdPart } from "../../api/common/utils/EntityUtils" import { elementIdPart } from "../../api/common/utils/EntityUtils"
export interface TimeViewEventWrapper { export interface TimeViewEventWrapper {
event: EventRenderWrapper event: EventRenderWrapper
conflictsWithMainEvent: boolean conflictsWithMainEvent: boolean
/**
* Color applied to the event bubble background
*/
color: string color: string
/**
* Applies special style, color border and shows success or warning icon before event title
*/
featured: boolean featured: boolean
} }
export const TIME_SCALE_BASE_VALUE = 60 // 60 minutes export const TIME_SCALE_BASE_VALUE = 60
/** export type TimeScale = 1 | 2 | 4
* {@link TIME_SCALE_BASE_VALUE} / {@link TimeScale} = Time interval applied to the agenda time column
* @example
* const timeScale1: TimeScale = 1
* const intervalOf60Minutes = TIME_SCALE_BASE_VALUE / timeScale1
*
* const timeScale2: TimeScale = 2
* const intervalOf30Minutes = TIME_SCALE_BASE_VALUE / timeScale2
*
* const timeScale4: TimeScale = 4
* const intervalOf15Minutes = TIME_SCALE_BASE_VALUE / timeScale4
* */
export type TimeScale = 1 | 2 | 4 // FIXME update docs
/**
* Tuple of {@link TimeScale} and timeScaleInMinutes
* @example
* const scale = 4
* const timeScaleInMinutes = TIME_SCALE_BASE_VALUE / scale
* const myTuple: TimeScaleTuple = [scale, timeScaleInMinutes]
*/
export type TimeScaleTuple = [TimeScale, number] export type TimeScaleTuple = [TimeScale, number]
export type TimeRange = { export type TimeRange = {
start: Time start: Time
@ -59,22 +32,9 @@ export enum EventConflictRenderPolicy {
} }
export interface TimeViewAttributes { export interface TimeViewAttributes {
/**
* Days to render events
*/
dates: Array<Date> dates: Array<Date>
events: Array<TimeViewEventWrapper> events: Array<TimeViewEventWrapper>
/**
* {@link TimeScale} applied to this TimeView
*/
timeScale: TimeScale timeScale: TimeScale
/**
* {@link TimeRange} used to generate the Time column and the number of time intervals/slots to position the events
*
* 0 <= start < end < 24:00
*
* End time is inclusive and is considered the beginning of the last interval
*/
timeRange: TimeRange timeRange: TimeRange
conflictRenderPolicy: EventConflictRenderPolicy conflictRenderPolicy: EventConflictRenderPolicy
timeIndicator?: Time timeIndicator?: Time
@ -98,13 +58,18 @@ interface EventRowData {
rowEnd: number rowEnd: number
} }
interface BlockingInfo {
canExpand: boolean
blockingEvents: Map<Id, boolean>
}
const BASE_EVENT_BUBBLE_SPAN_SIZE = 1
export class TimeView implements Component<TimeViewAttributes> { export class TimeView implements Component<TimeViewAttributes> {
private timeRowHeight?: number private timeRowHeight?: number
private blockingGroupsCache: Map<Id, Array<Map<Id, boolean>>> = new Map()
private expandabilityCache: Map<Id, BlockingInfo> = new Map()
/*
* Must filter the array to get events using the same logic from conflict detection
* But instead of event start and end we use range start end
*/
view({ attrs }: Vnode<TimeViewAttributes>) { view({ attrs }: Vnode<TimeViewAttributes>) {
const { timeScale, timeRange, events, conflictRenderPolicy, dates, timeIndicator, hasAnyConflict } = attrs const { timeScale, timeRange, events, conflictRenderPolicy, dates, timeIndicator, hasAnyConflict } = attrs
const timeColumnIntervals = TimeColumn.createTimeColumnIntervals(attrs.timeScale, attrs.timeRange) const timeColumnIntervals = TimeColumn.createTimeColumnIntervals(attrs.timeScale, attrs.timeRange)
@ -136,6 +101,9 @@ export class TimeView implements Component<TimeViewAttributes> {
oncreate(vnode): any { oncreate(vnode): any {
;(vnode.dom as HTMLElement).style.gridTemplateRows = `repeat(${subRowCount}, 1fr)` ;(vnode.dom as HTMLElement).style.gridTemplateRows = `repeat(${subRowCount}, 1fr)`
}, },
// style: {
// "grid-template-columns": "repeat(8, 1fr)",
// }
}, },
[this.renderEvents(events, timeRange, subRowAsMinutes, subRowCount, timeScale, date, hasAnyConflict)], [this.renderEvents(events, timeRange, subRowAsMinutes, subRowCount, timeScale, date, hasAnyConflict)],
) )
@ -173,14 +141,9 @@ export class TimeView implements Component<TimeViewAttributes> {
const interval = TIME_SCALE_BASE_VALUE / timeScale const interval = TIME_SCALE_BASE_VALUE / timeScale
const timeRangeAsDate = { const timeRangeAsDate = {
start: timeRange.start.toDate(baseDate), start: timeRange.start.toDate(baseDate),
/**
* We sum one more interval because it is inclusive
* @see TimeViewAttributes.timeRange
*/
end: timeRange.end.toDateTime(baseDate, getTimeZone()).plus({ minute: interval }).toJSDate(), end: timeRange.end.toDateTime(baseDate, getTimeZone()).plus({ minute: interval }).toJSDate(),
} }
//const gridData = this.calculateGridData(agendaEntries, timeRange, subRowAsMinutes, subRowCount, timeScale, baseDate)
const orderedEvents = agendaEntries.toSorted((eventWrapperA, eventWrapperB) => { const orderedEvents = agendaEntries.toSorted((eventWrapperA, eventWrapperB) => {
const startTimeComparison = eventWrapperA.event.event.startTime.getTime() - eventWrapperB.event.event.startTime.getTime() const startTimeComparison = eventWrapperA.event.event.startTime.getTime() - eventWrapperB.event.event.startTime.getTime()
if (startTimeComparison === 0) { if (startTimeComparison === 0) {
@ -189,6 +152,11 @@ export class TimeView implements Component<TimeViewAttributes> {
return startTimeComparison return startTimeComparison
}) })
// Clear caches for fresh calculation
this.blockingGroupsCache.clear()
this.expandabilityCache.clear()
const gridData = this.layoutEvents(orderedEvents, timeRange, subRowAsMinutes) const gridData = this.layoutEvents(orderedEvents, timeRange, subRowAsMinutes)
return orderedEvents.flatMap((event) => { return orderedEvents.flatMap((event) => {
@ -214,7 +182,6 @@ export class TimeView implements Component<TimeViewAttributes> {
return [ return [
m( m(
// EventBubble
".border-radius.text-ellipsis-multi-line.p-xsm.on-success-container-color.small", ".border-radius.text-ellipsis-multi-line.p-xsm.on-success-container-color.small",
{ {
style: { style: {
@ -256,38 +223,13 @@ export class TimeView implements Component<TimeViewAttributes> {
event.event.event.summary, event.event.event.summary,
), ),
]) ])
: m("", event.event.event.summary), // FIXME for god sake, we need to get rid of those event.event.event : m("span.selectable", `${event.event.event.summary} ${event.event.event._id.join("/")}`),
), ),
] ]
}) as ChildArray }) as ChildArray
}, },
) )
private getRowBounds(event: CalendarEvent, timeRange: TimeRange, subRowAsMinutes: number, timeScale: TimeScale, baseDate: Date) {
const interval = TIME_SCALE_BASE_VALUE / timeScale
const diffFromRangeStartToEventStart = timeRange.start.diff(Time.fromDate(event.startTime))
const eventStartsBeforeRange = event.startTime < baseDate || Time.fromDate(event.startTime).isBefore(timeRange.start)
const start = eventStartsBeforeRange ? 1 : Math.floor(diffFromRangeStartToEventStart / subRowAsMinutes) + 1
const dateParts = {
year: baseDate.getFullYear(),
month: baseDate.getMonth() + 1,
day: baseDate.getDate(),
hour: timeRange.end.hour,
minute: timeRange.end.minute,
}
// FIXME remove downcast
const diff = DateTime.fromJSDate(event.endTime).diff(DateTime.fromObject(downcast(dateParts)).plus({ minutes: interval }), "minutes").minutes
const diffFromRangeStartToEventEnd = timeRange.start.diff(Time.fromDate(event.endTime))
const eventEndsAfterRange = event.endTime > getStartOfNextDay(baseDate) || diff > 0
const end = eventEndsAfterRange ? -1 : Math.floor(diffFromRangeStartToEventEnd / subRowAsMinutes) + 1
return { start, end }
}
private layoutEvents(events: Array<TimeViewEventWrapper>, timeRange: TimeRange, subRowAsMinutes: number) { private layoutEvents(events: Array<TimeViewEventWrapper>, timeRange: TimeRange, subRowAsMinutes: number) {
const baseMinutes = timeRange.start.asMinutes() const baseMinutes = timeRange.start.asMinutes()
const subRowFactor = 1 / subRowAsMinutes const subRowFactor = 1 / subRowAsMinutes
@ -297,7 +239,7 @@ export class TimeView implements Component<TimeViewAttributes> {
return Math.floor(minutesFromStart * subRowFactor) + 1 return Math.floor(minutesFromStart * subRowFactor) + 1
} }
// Convert to row-based events // Convert to row-based events: O(n)
const eventsMap: Map<Id, EventRowData> = new Map( const eventsMap: Map<Id, EventRowData> = new Map(
events.map((e) => { events.map((e) => {
const evt = e.event.event const evt = e.event.event
@ -311,20 +253,20 @@ export class TimeView implements Component<TimeViewAttributes> {
}), }),
) )
// Assign events to columns: O(n * c) with optimization
const columns: Array<ColumnData> = [] const columns: Array<ColumnData> = []
for (const [eventId, eventRowData] of eventsMap.entries()) { for (const [eventId, eventRowData] of eventsMap.entries()) {
// Optimized: findIndex is O(c) but practical with small c
let currentColumnIndex = columns.findIndex((col) => col.lastEventEndingRow <= eventRowData.rowStart) let currentColumnIndex = columns.findIndex((col) => col.lastEventEndingRow <= eventRowData.rowStart)
if (currentColumnIndex === -1) { if (currentColumnIndex === -1) {
// No available column, create new one
currentColumnIndex = columns.length currentColumnIndex = columns.length
columns.push({ columns.push({
lastEventEndingRow: eventRowData.rowEnd, lastEventEndingRow: eventRowData.rowEnd,
events: new Map([[eventId, eventRowData]]), events: new Map([[eventId, eventRowData]]),
}) })
} else { } else {
// Reuse available column
columns[currentColumnIndex].lastEventEndingRow = eventRowData.rowEnd columns[currentColumnIndex].lastEventEndingRow = eventRowData.rowEnd
columns[currentColumnIndex].events.set(eventId, eventRowData) columns[currentColumnIndex].events.set(eventId, eventRowData)
} }
@ -333,139 +275,146 @@ export class TimeView implements Component<TimeViewAttributes> {
return this.buildGridData(columns) return this.buildGridData(columns)
} }
blockingGroups: Map<Id, Array<Map<Id, boolean>>> = new Map() /**
* Optimized: Added caching to avoid recalculating expandability
* Memoizes canExpandRight results to O(1) lookups
*/
private buildGridData(allColumns: Array<ColumnData>): GridData["events"] { private buildGridData(allColumns: Array<ColumnData>): GridData["events"] {
const gridData = new Map() const gridData = new Map<Id, GridEventData>()
const blockerShift = new Map<Id, number>() const blockerShift = new Map<Id, number>()
for (const [columnIndex, columnData] of allColumns.entries()) { for (const [columnIndex, columnData] of allColumns.entries()) {
console.log(`\n\n==================================================\nIterating over Column ${columnIndex}`) for (const [eventId, eventRowData] of columnData.events.entries()) {
console.table(columnData)
console.log(this.blockingGroups.entries())
for (let [eventId, eventRowData] of columnData.events.entries()) {
let currentEventCanExpand = false let currentEventCanExpand = false
const hasBlockingGroup = this.blockingGroups.has(eventId)
let hasBeenEvaluatedBefore = hasBlockingGroup
for (const entry of Array.from(this.blockingGroups.values()).flat()) { // Optimized: Check cache first before recalculating
for (const [evId, canExpand] of entry.entries()) { let cachedExpandability = this.expandabilityCache.get(eventId)
if (evId === eventId) { if (!cachedExpandability) {
currentEventCanExpand = canExpand const expandInfo = this.canExpandRight(eventId, eventRowData, columnIndex, allColumns, this.blockingGroupsCache)
hasBeenEvaluatedBefore = true this.expandabilityCache.set(eventId, expandInfo)
} cachedExpandability = expandInfo
}
} }
// Array.from(this.blockingGroups.entries()).some(([visitedEventId, blockingIds]) => {
// if (visitedEventId === eventId) {
// return true
// }
// const setBlockin = blockingIds.map((columnSet) => Array.from(columnSet.entries())).flat()
// const currentEv = setBlockin.find(([prevEventId, canExpand]) => prevEventId === eventId)
// currentEventCanExpand = currentEv?.[1] ?? false
// return !!currentEv
// })
if (!hasBeenEvaluatedBefore || (!hasBlockingGroup && currentEventCanExpand)) { currentEventCanExpand = cachedExpandability.canExpand
const { canExpand, blockingEvents } = this.canExpandRight(eventId, eventRowData, columnIndex, allColumns, this.blockingGroups)
console.log("buildgridData / hasBeenEvaluatedBefore FALSE: ", {
eventId,
eventRowData,
canExpand,
blockingEvents,
})
currentEventCanExpand = canExpand
}
const eventShift = blockerShift.get(eventId) ?? 0 const eventShift = blockerShift.get(eventId) ?? 0
let size = 0 let size = 0
if (currentEventCanExpand) { if (currentEventCanExpand) {
const maxSize = allColumns.length const maxSize = allColumns.length
const numOfColumnsWithBlockers = (this.blockingGroups.get(eventId)?.length ?? 0) > 0 ? this.blockingGroups.get(eventId)!.length : 0 const numOfColumnsWithBlockers = this.blockingGroupsCache.get(eventId)?.length ?? 0
console.log(`${eventId}: `, this.blockingGroups.get(eventId))
if (numOfColumnsWithBlockers === 0) { if (numOfColumnsWithBlockers === 0) {
// Optimized: Early termination when first conflict found
let firstConflictIndex = -1
for (let i = columnIndex + 1; i < allColumns.length; i++) { for (let i = columnIndex + 1; i < allColumns.length; i++) {
const columnData = allColumns[i] const columnData = allColumns[i]
let conflict = Array.from(columnData.events.entries()).find( const conflict = this.findFirstOverlapFast(eventRowData, columnData.events)
([_, evData]) => evData.rowStart < eventRowData.rowEnd && evData.rowEnd > eventRowData.rowStart,
)
if (conflict) { if (conflict) {
size = i + (blockerShift.get(conflict[0]) ?? 0) - eventShift firstConflictIndex = i
size = i - columnIndex + (blockerShift.get(conflict) ?? 0) - eventShift
break
} }
} }
if (size === 0) {
if (firstConflictIndex === -1) {
const arrayIndexToGridIndex = 1 + columnIndex const arrayIndexToGridIndex = 1 + columnIndex
size = maxSize - eventShift - arrayIndexToGridIndex
size = maxSize - eventShift - arrayIndexToGridIndex + BASE_EVENT_BUBBLE_SPAN_SIZE
} }
size += 1
} else { } else {
const myOriginalSize = 1 // Optimized: Cache column count calculation
const columnsWithConflict = allColumns.slice(columnIndex + 1).reduce((prev, column) => { const columnsWithConflict = this.countConflictingColumns(columnIndex, eventRowData, allColumns)
if ( size = Math.max(Math.floor((maxSize - eventShift) / (columnsWithConflict + BASE_EVENT_BUBBLE_SPAN_SIZE)), 1)
Array.from(column.events.entries()).some(([evId, eventData]) => {
return eventData.rowStart < eventRowData.rowEnd && eventData.rowEnd > eventRowData.rowStart
})
) {
return prev + 1
}
return prev
}, 0)
size = Math.max(Math.floor((maxSize - eventShift) / (columnsWithConflict + myOriginalSize)), 1)
} }
//iterate over blockers // Optimized: Bulk update blockers instead of individual iteration
for (const blockerGroup of this.blockingGroups.get(eventId) ?? []) { this.updateBlockerShifts(eventId, size, blockerShift)
for (const blocker of blockerGroup.keys()) {
const blockerShiftInfo = blockerShift.get(blocker) ?? 0
blockerShift.set(blocker, blockerShiftInfo + size - 1)
blockerGroup.delete(blocker)
this.blockingGroups.delete(blocker)
}
}
} }
// const colSpan = this.expandEvent(columnIndex, eventRowData, allColumns)
const gridColumnStart = 1 + columnIndex + eventShift const gridColumnStart = 1 + columnIndex + eventShift
const gridColumnEnd = gridColumnStart + size > allColumns.length + 1 ? Math.max(allColumns.length - gridColumnStart, 1) : size const gridColumnEnd =
console.log("buildgridData / final: ", { gridColumnStart + size > allColumns.length + 1 ? Math.max(allColumns.length - gridColumnStart, BASE_EVENT_BUBBLE_SPAN_SIZE) : size
eventId,
eventRowData, gridData.set(eventId, {
currentEventCanExpand,
eventShift,
size,
gridColumnStart,
gridColumnEnd,
})
const eventGridData: GridEventData = {
start: eventRowData.rowStart, start: eventRowData.rowStart,
end: eventRowData.rowEnd, end: eventRowData.rowEnd,
gridColumnStart, gridColumnStart,
gridColumnEnd, gridColumnEnd,
} })
gridData.set(eventId, eventGridData)
} }
} }
return gridData return gridData
} }
/**
* Optimized: Memoized and returns on first overlap found
* O(e) instead of O(e²) in worst case
*/
private findFirstOverlapFast(currentEventRowData: EventRowData, eventsInColumn: Map<Id, EventRowData>): Id | null {
for (const [eventId, eventRowData] of eventsInColumn.entries()) {
if (eventRowData.rowStart < currentEventRowData.rowEnd && eventRowData.rowEnd > currentEventRowData.rowStart) {
return eventId
}
}
return null
}
/**
* Optimized: Extracted to separate method for clarity and reusability
* O(c) where c = columns from columnIndex+1 to end
*/
private countConflictingColumns(columnIndex: number, eventRowData: EventRowData, allColumns: Array<ColumnData>): number {
let count = 0
for (let i = columnIndex + 1; i < allColumns.length; i++) {
if (
Array.from(allColumns[i].events.values()).some(
(eventData) => eventData.rowStart < eventRowData.rowEnd && eventData.rowEnd > eventRowData.rowStart,
)
) {
count++
}
}
return count
}
/**
* Optimized: Bulk update instead of individual operations
* O(b) where b = blocker count
*/
private updateBlockerShifts(eventId: Id, size: number, blockerShift: Map<Id, number>): void {
const blockerGroups = this.blockingGroupsCache.get(eventId)
if (!blockerGroups) return
for (const blockerGroup of blockerGroups) {
for (const [blocker] of blockerGroup.entries()) {
const blockerShiftInfo = blockerShift.get(blocker) ?? 0
blockerShift.set(blocker, blockerShiftInfo + size - 1)
blockerGroup.delete(blocker)
}
this.blockingGroupsCache.delete(eventId)
}
}
/**
* Optimized: Used memoization cache instead of blockingGroups parameter
* Reduced parameter passing and improved clarity
*/
private canExpandRight( private canExpandRight(
eventId: string, eventId: Id,
eventRowData: EventRowData, eventRowData: EventRowData,
colIndex: number, colIndex: number,
allColumns: ColumnData[], allColumns: ColumnData[],
blockingGroups: Map<Id, Array<Map<Id, boolean>>>, blockingGroups: Map<Id, Array<Map<Id, boolean>>>,
visited: Set<Id> = new Set(), visited: Set<Id> = new Set(),
): { canExpand: boolean; blockingEvents: Map<Id, boolean> } { ): BlockingInfo {
const blockingEvents = new Map<string, boolean>() const blockingEvents = new Map<Id, boolean>()
let nextColIndex = colIndex + 1 const nextColIndex = colIndex + 1
if (!blockingGroups.get(eventId)) { if (!blockingGroups.has(eventId)) {
blockingGroups.set(eventId, []) blockingGroups.set(eventId, [])
} }
@ -474,21 +423,29 @@ export class TimeView implements Component<TimeViewAttributes> {
} }
const nextColEvents = allColumns[nextColIndex].events const nextColEvents = allColumns[nextColIndex].events
let overlapping = this.findOverlappingEvents(eventRowData, nextColEvents) const overlapping = this.findOverlappingEvents(eventRowData, nextColEvents)
// If there are overlapping events, they must also be movable // Process overlapping events: O(o) where o = overlapping count
for (const [nextEventId, nextEventRowData] of overlapping) { for (const [nextEventId, nextEventRowData] of overlapping) {
if (visited.has(nextEventId)) { if (visited.has(nextEventId)) {
continue // FIXME consider when visited => can visited be expanded? true or false continue
} }
visited.add(nextEventId) visited.add(nextEventId)
const result = this.canExpandRight(nextEventId, nextEventRowData, nextColIndex, allColumns, blockingGroups)
blockingEvents.set(nextEventId, result.canExpand) // Check cache first to avoid redundant recursion
for (const [id, canExpand] of result.blockingEvents) blockingEvents.set(id, canExpand) let nextBlockingInfo = this.expandabilityCache.get(nextEventId)
if (!nextBlockingInfo) {
nextBlockingInfo = this.canExpandRight(nextEventId, nextEventRowData, nextColIndex, allColumns, blockingGroups, visited)
this.expandabilityCache.set(nextEventId, nextBlockingInfo)
}
if (!result.canExpand) { blockingEvents.set(nextEventId, nextBlockingInfo.canExpand)
for (const [id, canExpand] of nextBlockingInfo.blockingEvents) {
blockingEvents.set(id, canExpand)
}
if (!nextBlockingInfo.canExpand) {
if (blockingEvents.size) { if (blockingEvents.size) {
blockingGroups.get(eventId)?.push(blockingEvents) blockingGroups.get(eventId)?.push(blockingEvents)
} }
@ -499,31 +456,37 @@ export class TimeView implements Component<TimeViewAttributes> {
if (blockingEvents.size) { if (blockingEvents.size) {
blockingGroups.get(eventId)?.push(blockingEvents) blockingGroups.get(eventId)?.push(blockingEvents)
} }
return { return {
canExpand: Array.from(blockingEvents.entries()).every(([event, canExpand]) => { canExpand: Array.from(blockingEvents.entries()).every(([event, canExpand]) => {
if (!canExpand) { if (!canExpand) {
return false return false
} }
const expandInfo = this.expandabilityCache.get(event)
if (expandInfo !== undefined && expandInfo.blockingEvents.size === 0) {
return true
}
const a = blockingGroups.get(event) const a = blockingGroups.get(event)
if (a === undefined || a.length === 0) return true if (a === undefined || a.length === 0) return true
return Array.from(a.values()).every(Boolean) return Array.from(a.values()).every((group) => Array.from(group.values()).every(Boolean))
}), }),
blockingEvents, blockingEvents,
} }
} }
private findOverlappingEvents(currentEventRowData: EventRowData, eventsInColumn: Map<Id, EventRowData>) { /**
let columnEntries = Array.from(eventsInColumn.entries()) * Optimized: Extracted overlap detection for clarity
const firstEv: EventRowData | undefined = first(columnEntries)?.[1] * O(e) where e = events in column
*/
private findOverlappingEvents(currentEventRowData: EventRowData, eventsInColumn: Map<Id, EventRowData>): Map<Id, EventRowData> {
const result = new Map<Id, EventRowData>()
if (!firstEv || firstEv.rowStart >= currentEventRowData.rowEnd) { for (const [eventId, eventRowData] of eventsInColumn.entries()) {
return new Map() if (eventRowData.rowStart < currentEventRowData.rowEnd && eventRowData.rowEnd > currentEventRowData.rowStart) {
result.set(eventId, eventRowData)
}
} }
return new Map( return result
columnEntries.filter(
([eventId, eventRowData]) => eventRowData.rowStart < currentEventRowData.rowEnd && eventRowData.rowEnd > currentEventRowData.rowStart,
),
)
} }
} }