@tuturuuu/ui 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/package.json +6 -6
- package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
- package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
- package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
- package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
- package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
- package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
- package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
- package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
- package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
- package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
- package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
- package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
- package/src/components/ui/finance/invoices/hooks.ts +3 -100
- package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
- package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
- package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
- package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
- package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
- package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
- package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
- package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
- package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
- package/src/components/ui/finance/invoices/types.ts +3 -3
- package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
- package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
- package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
- package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
- package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
- package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
- package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1554
- package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
- package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
- package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
- package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
- package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
- package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +1 -1
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +1 -1
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
- package/src/components/ui/text-editor/editor.tsx +3 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
- package/src/components/ui/text-editor/tool-bar.tsx +18 -74
- package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
- package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +101 -7
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +27 -0
- package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
- package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +39 -0
- package/src/hooks/calendar-connection-types.ts +49 -0
- package/src/hooks/calendar-event-update-queue-types.ts +12 -0
- package/src/hooks/calendar-event-write-payload.ts +33 -0
- package/src/hooks/collaboration-document-hydration.ts +35 -0
- package/src/hooks/supabase-provider.ts +39 -41
- package/src/hooks/use-calendar-sync.tsx +41 -62
- package/src/hooks/use-calendar.tsx +24 -38
- package/src/hooks/use-local-storage.ts +9 -11
- package/src/hooks/use-native-calendar-occurrences.ts +85 -0
- package/src/hooks/use-yjs-collaboration.ts +23 -0
|
@@ -1,60 +1,32 @@
|
|
|
1
1
|
import { Calendar, ChevronDown, ChevronUp } from '@tuturuuu/icons';
|
|
2
2
|
import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
|
|
3
3
|
import { useCalendar } from '@tuturuuu/ui/hooks/use-calendar';
|
|
4
|
+
import { useCalendarClock } from '@tuturuuu/ui/hooks/use-calendar-clock';
|
|
4
5
|
import { useCalendarSync } from '@tuturuuu/ui/hooks/use-calendar-sync';
|
|
5
6
|
import { calendarEventStyle } from '@tuturuuu/utils/calendar-event-colors';
|
|
6
7
|
import { getEventStyles } from '@tuturuuu/utils/color-helper';
|
|
7
8
|
import { cn } from '@tuturuuu/utils/format';
|
|
8
9
|
import dayjs from 'dayjs';
|
|
9
|
-
import isBetween from 'dayjs/plugin/isBetween';
|
|
10
|
-
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
|
11
|
-
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
|
12
10
|
import timezone from 'dayjs/plugin/timezone';
|
|
13
11
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
14
12
|
import {
|
|
15
13
|
calendarDayBoundary,
|
|
16
14
|
calendarDayKey,
|
|
17
15
|
} from '../../../../lib/calendar-day';
|
|
16
|
+
import {
|
|
17
|
+
calculateAllDayEventLayout,
|
|
18
|
+
type EventSpan,
|
|
19
|
+
MAX_EVENTS_DISPLAY,
|
|
20
|
+
} from './all-day-event-layout';
|
|
18
21
|
import { MIN_COLUMN_WIDTH } from './config';
|
|
19
22
|
import { CalendarEventProviderIcon } from './event-provider-display';
|
|
20
23
|
import { LocationTimeline } from './location-timeline';
|
|
24
|
+
import { pastEventTreatment } from './past-event-treatment';
|
|
21
25
|
import { useCalendarSettings } from './settings/settings-context';
|
|
22
26
|
import { getEventLocationType } from './working-location';
|
|
23
27
|
|
|
24
|
-
dayjs.extend(isBetween);
|
|
25
|
-
dayjs.extend(isSameOrAfter);
|
|
26
|
-
dayjs.extend(isSameOrBefore);
|
|
27
28
|
dayjs.extend(timezone);
|
|
28
29
|
|
|
29
|
-
function getZonedDay(date: Date, zone?: string) {
|
|
30
|
-
const boundary = dayjs(calendarDayBoundary(date, zone));
|
|
31
|
-
return zone && zone !== 'auto' ? boundary.tz(zone) : boundary;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const MAX_EVENTS_DISPLAY = 2;
|
|
35
|
-
|
|
36
|
-
interface EventSpan {
|
|
37
|
-
event: CalendarEvent;
|
|
38
|
-
startIndex: number;
|
|
39
|
-
endIndex: number;
|
|
40
|
-
span: number;
|
|
41
|
-
isCutOffStart: boolean; // Event starts before visible range
|
|
42
|
-
isCutOffEnd: boolean; // Event ends after visible range
|
|
43
|
-
actualStartDate: dayjs.Dayjs; // Actual start date of the event
|
|
44
|
-
actualEndDate: dayjs.Dayjs; // Actual end date of the event
|
|
45
|
-
row: number; // Add row property for proper stacking
|
|
46
|
-
isMerged?: boolean;
|
|
47
|
-
mergedEventIds?: string[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface EventLayout {
|
|
51
|
-
spans: EventSpan[];
|
|
52
|
-
maxVisibleEventsPerDay: number;
|
|
53
|
-
eventsByDay: EventSpan[][];
|
|
54
|
-
locationSpansFromMerge: EventSpan[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Drag state interface
|
|
58
30
|
interface DragState {
|
|
59
31
|
isDragging: boolean;
|
|
60
32
|
draggedEvent: CalendarEvent | null;
|
|
@@ -65,7 +37,6 @@ interface DragState {
|
|
|
65
37
|
currentY: number;
|
|
66
38
|
targetDateIndex: number | null;
|
|
67
39
|
originalDateIndex: number;
|
|
68
|
-
// Add preview span for better visual feedback
|
|
69
40
|
previewSpan: {
|
|
70
41
|
startIndex: number;
|
|
71
42
|
span: number;
|
|
@@ -73,7 +44,6 @@ interface DragState {
|
|
|
73
44
|
} | null;
|
|
74
45
|
}
|
|
75
46
|
|
|
76
|
-
// 1. Extract EventContent component for shared rendering
|
|
77
47
|
const EventContent = ({ event }: { event: CalendarEvent }) => (
|
|
78
48
|
<>
|
|
79
49
|
<CalendarEventProviderIcon
|
|
@@ -85,14 +55,20 @@ const EventContent = ({ event }: { event: CalendarEvent }) => (
|
|
|
85
55
|
);
|
|
86
56
|
|
|
87
57
|
export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
88
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
openModal,
|
|
60
|
+
updateEvent,
|
|
61
|
+
addEvent,
|
|
62
|
+
deleteEvent,
|
|
63
|
+
preservePastEventOpacity,
|
|
64
|
+
} = useCalendar();
|
|
89
65
|
const { allDayEvents } = useCalendarSync();
|
|
66
|
+
const now = useCalendarClock();
|
|
90
67
|
const { settings } = useCalendarSettings();
|
|
91
68
|
const showWeekends = settings.appearance.showWeekends;
|
|
92
69
|
const tz = settings?.timezone?.timezone;
|
|
93
70
|
const [expandedDates, setExpandedDates] = useState<string[]>([]);
|
|
94
71
|
|
|
95
|
-
// Drag and drop state
|
|
96
72
|
const [dragState, setDragState] = useState<DragState>({
|
|
97
73
|
isDragging: false,
|
|
98
74
|
draggedEvent: null,
|
|
@@ -106,27 +82,22 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
106
82
|
previewSpan: null,
|
|
107
83
|
});
|
|
108
84
|
|
|
109
|
-
// Use ref to store the current drag state for stable handlers
|
|
110
85
|
const dragStateRef = useRef<DragState>(dragState);
|
|
111
86
|
dragStateRef.current = dragState;
|
|
112
87
|
|
|
113
|
-
// Refs for drag handling
|
|
114
88
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
115
89
|
const dragPreviewRef = useRef<HTMLDivElement>(null);
|
|
116
90
|
|
|
117
|
-
// Add refs for drag threshold and timer
|
|
118
91
|
const dragStartPos = useRef<{ x: number; y: number } | null>(null);
|
|
119
92
|
const dragInitiated = useRef(false);
|
|
120
93
|
const longPressTimer = useRef<NodeJS.Timeout | null>(null);
|
|
121
94
|
const DRAG_THRESHOLD = 5; // px
|
|
122
95
|
const LONG_PRESS_DURATION = 250; // ms
|
|
123
96
|
|
|
124
|
-
// Constants for layout
|
|
125
97
|
const EVENT_LEFT_OFFSET = 4; // 4px offset from left edge
|
|
126
98
|
/** Height of the LocationTimeline strip in rem. Used to offset regular events when location events are present. */
|
|
127
99
|
const LOCATION_TIMELINE_HEIGHT_REM = 1.5;
|
|
128
100
|
|
|
129
|
-
// Filter out weekend days if showWeekends is false
|
|
130
101
|
const visibleDates = showWeekends
|
|
131
102
|
? dates
|
|
132
103
|
: dates.filter((date) => {
|
|
@@ -134,11 +105,9 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
134
105
|
return day !== 0 && day !== 6; // 0 = Sunday, 6 = Saturday
|
|
135
106
|
});
|
|
136
107
|
|
|
137
|
-
// Stable drag event handlers using refs
|
|
138
108
|
const handleDragStart = useCallback(
|
|
139
109
|
(e: React.MouseEvent, eventSpan: EventSpan) => {
|
|
140
|
-
|
|
141
|
-
if (visibleDates.length <= 1) return;
|
|
110
|
+
if (visibleDates.length <= 1 || eventSpan.event.seriesId) return;
|
|
142
111
|
|
|
143
112
|
e.preventDefault();
|
|
144
113
|
e.stopPropagation();
|
|
@@ -146,7 +115,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
146
115
|
const rect = containerRef.current?.getBoundingClientRect();
|
|
147
116
|
if (!rect) return;
|
|
148
117
|
|
|
149
|
-
// Calculate initial preview span
|
|
150
118
|
const previewSpan = {
|
|
151
119
|
startIndex: eventSpan.startIndex,
|
|
152
120
|
span: eventSpan.span,
|
|
@@ -166,14 +134,12 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
166
134
|
previewSpan,
|
|
167
135
|
});
|
|
168
136
|
|
|
169
|
-
// Set cursor and prevent text selection
|
|
170
137
|
document.body.style.cursor = 'grabbing';
|
|
171
138
|
document.body.classList.add('select-none');
|
|
172
139
|
},
|
|
173
140
|
[visibleDates.length]
|
|
174
141
|
);
|
|
175
142
|
|
|
176
|
-
// Stable drag move handler
|
|
177
143
|
const handleDragMove = useCallback(
|
|
178
144
|
(e: MouseEvent) => {
|
|
179
145
|
const currentDragState = dragStateRef.current;
|
|
@@ -187,7 +153,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
187
153
|
const rect = containerRef.current.getBoundingClientRect();
|
|
188
154
|
const relativeX = e.clientX - rect.left;
|
|
189
155
|
|
|
190
|
-
// Calculate which date column we're over
|
|
191
156
|
const columnWidth = rect.width / visibleDates.length;
|
|
192
157
|
const targetDateIndex = Math.floor(relativeX / columnWidth);
|
|
193
158
|
const clampedTargetIndex = Math.max(
|
|
@@ -195,7 +160,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
195
160
|
Math.min(targetDateIndex, visibleDates.length - 1)
|
|
196
161
|
);
|
|
197
162
|
|
|
198
|
-
// Calculate new preview span based on target position
|
|
199
163
|
const originalSpan = currentDragState.draggedEventSpan.span;
|
|
200
164
|
const newStartIndex = clampedTargetIndex;
|
|
201
165
|
const newEndIndex = Math.min(
|
|
@@ -221,7 +185,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
221
185
|
[visibleDates.length]
|
|
222
186
|
);
|
|
223
187
|
|
|
224
|
-
// Stable drag end handler
|
|
225
188
|
const handleDragEnd = useCallback(async () => {
|
|
226
189
|
const currentDragState = dragStateRef.current;
|
|
227
190
|
if (
|
|
@@ -234,17 +197,14 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
234
197
|
const targetDateIndex = currentDragState.targetDateIndex;
|
|
235
198
|
const originalDateIndex = currentDragState.originalDateIndex;
|
|
236
199
|
|
|
237
|
-
// Only validate originalDateIndex (targetDateIndex is always clamped)
|
|
238
200
|
if (originalDateIndex < 0 || originalDateIndex >= visibleDates.length) {
|
|
239
201
|
console.warn('Invalid original date index for drag operation');
|
|
240
202
|
return;
|
|
241
203
|
}
|
|
242
204
|
|
|
243
|
-
// Helper for dayjs + timezone
|
|
244
205
|
const getDayjsDate = (d: string | Date) =>
|
|
245
206
|
tz === 'auto' ? dayjs(d) : dayjs(d).tz(tz);
|
|
246
207
|
|
|
247
|
-
// Reset drag state and cursor
|
|
248
208
|
setDragState({
|
|
249
209
|
isDragging: false,
|
|
250
210
|
draggedEvent: null,
|
|
@@ -261,13 +221,11 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
261
221
|
document.body.style.cursor = '';
|
|
262
222
|
document.body.classList.remove('select-none');
|
|
263
223
|
|
|
264
|
-
// If dropped on the same date or invalid target, do nothing
|
|
265
224
|
if (targetDateIndex === null || targetDateIndex === originalDateIndex) {
|
|
266
225
|
return;
|
|
267
226
|
}
|
|
268
227
|
|
|
269
228
|
try {
|
|
270
|
-
// Use helper for all dayjs conversions
|
|
271
229
|
const originalStartDate = getDayjsDate(
|
|
272
230
|
visibleDates[originalDateIndex] ?? new Date()
|
|
273
231
|
);
|
|
@@ -293,7 +251,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
293
251
|
}
|
|
294
252
|
}, [visibleDates, tz, updateEvent]);
|
|
295
253
|
|
|
296
|
-
// Set up global mouse event listeners with stable handlers
|
|
297
254
|
React.useEffect(() => {
|
|
298
255
|
if (dragState.isDragging) {
|
|
299
256
|
document.addEventListener('mousemove', handleDragMove);
|
|
@@ -306,302 +263,23 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
306
263
|
}
|
|
307
264
|
}, [dragState.isDragging, handleDragMove, handleDragEnd]);
|
|
308
265
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
.map(() => []);
|
|
315
|
-
|
|
316
|
-
// First pass: create event spans without row assignment
|
|
317
|
-
const tempSpans: Omit<EventSpan, 'row'>[] = [];
|
|
318
|
-
|
|
319
|
-
// Process each all-day event
|
|
320
|
-
(visibleDates.length ? allDayEvents : []).forEach((event) => {
|
|
321
|
-
const eventStart =
|
|
322
|
-
tz === 'auto' ? dayjs(event.start_at) : dayjs(event.start_at).tz(tz);
|
|
323
|
-
const eventEnd =
|
|
324
|
-
tz === 'auto' ? dayjs(event.end_at) : dayjs(event.end_at).tz(tz);
|
|
325
|
-
|
|
326
|
-
// Find the start and end indices within our visible dates
|
|
327
|
-
let startIndex = -1;
|
|
328
|
-
let endIndex = -1;
|
|
329
|
-
|
|
330
|
-
// First pass: find any overlap with visible dates
|
|
331
|
-
const firstVisibleDate = getZonedDay(visibleDates[0]!, tz);
|
|
332
|
-
const lastVisibleDate = getZonedDay(
|
|
333
|
-
visibleDates[visibleDates.length - 1]!,
|
|
334
|
-
tz
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
// Check if event overlaps with our visible date range at all
|
|
338
|
-
// Event overlaps if: event_start < visible_end AND event_end > visible_start
|
|
339
|
-
const eventOverlaps =
|
|
340
|
-
eventStart.isBefore(lastVisibleDate.add(1, 'day'), 'day') &&
|
|
341
|
-
eventEnd.isAfter(firstVisibleDate, 'day');
|
|
342
|
-
|
|
343
|
-
if (!eventOverlaps) {
|
|
344
|
-
return; // Skip this event if it doesn't overlap with visible dates
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Improved logic for multi-day events
|
|
348
|
-
// Start index: first visible date that overlaps with the event
|
|
349
|
-
if (eventStart.isSameOrBefore(firstVisibleDate, 'day')) {
|
|
350
|
-
// Event starts before or on the first visible date
|
|
351
|
-
startIndex = 0;
|
|
352
|
-
} else {
|
|
353
|
-
// Find the first visible date that matches the event start
|
|
354
|
-
for (let i = 0; i < visibleDates.length; i++) {
|
|
355
|
-
const currentDate = getZonedDay(visibleDates[i]!, tz);
|
|
356
|
-
if (
|
|
357
|
-
currentDate.isSameOrAfter(eventStart, 'day') &&
|
|
358
|
-
currentDate.isBefore(eventEnd, 'day')
|
|
359
|
-
) {
|
|
360
|
-
startIndex = i;
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// End index: last visible date that overlaps with the event
|
|
367
|
-
if (eventEnd.isAfter(lastVisibleDate.add(1, 'day'), 'day')) {
|
|
368
|
-
// Event ends after the last visible date
|
|
369
|
-
endIndex = visibleDates.length - 1;
|
|
370
|
-
} else {
|
|
371
|
-
// Find the last visible date that the event covers
|
|
372
|
-
for (let i = visibleDates.length - 1; i >= 0; i--) {
|
|
373
|
-
const currentDate = getZonedDay(visibleDates[i]!, tz);
|
|
374
|
-
if (
|
|
375
|
-
currentDate.isBefore(eventEnd, 'day') &&
|
|
376
|
-
currentDate.isSameOrAfter(eventStart, 'day')
|
|
377
|
-
) {
|
|
378
|
-
endIndex = i;
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Include events that have at least one day visible in our date range
|
|
385
|
-
if (startIndex !== -1 && endIndex !== -1) {
|
|
386
|
-
const span = endIndex - startIndex + 1;
|
|
387
|
-
|
|
388
|
-
// Fix: Proper cut-off logic for all-day events
|
|
389
|
-
// For all-day events, end_at is typically start of next day (exclusive)
|
|
390
|
-
// So we need to check actual duration, not just date comparison
|
|
391
|
-
const actualDurationDays = eventEnd.diff(eventStart, 'day');
|
|
392
|
-
const isActuallyMultiDay = actualDurationDays > 1;
|
|
393
|
-
|
|
394
|
-
// Only show cut-off indicators for events that actually span multiple days
|
|
395
|
-
// AND are cut off by the visible range
|
|
396
|
-
const isCutOffStart =
|
|
397
|
-
isActuallyMultiDay && eventStart.isBefore(firstVisibleDate, 'day');
|
|
398
|
-
const isCutOffEnd =
|
|
399
|
-
isActuallyMultiDay &&
|
|
400
|
-
eventEnd.isAfter(lastVisibleDate.add(1, 'day'), 'day');
|
|
401
|
-
|
|
402
|
-
const eventSpan: Omit<EventSpan, 'row'> = {
|
|
403
|
-
event,
|
|
404
|
-
startIndex,
|
|
405
|
-
endIndex,
|
|
406
|
-
span,
|
|
407
|
-
// Add indicators for cut-off events
|
|
408
|
-
isCutOffStart,
|
|
409
|
-
isCutOffEnd,
|
|
410
|
-
actualStartDate: eventStart,
|
|
411
|
-
actualEndDate: eventEnd,
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
tempSpans.push(eventSpan);
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Second pass: Merge consecutive SINGLE-DAY events with same title and color
|
|
419
|
-
// Optimized O(n log n) approach: group by mergeKey, sort groups, linear scan per group
|
|
420
|
-
const mergedTempSpans: Omit<EventSpan, 'row'>[] = [];
|
|
421
|
-
|
|
422
|
-
// Group single-day spans by mergeKey; multi-day events go directly to output
|
|
423
|
-
const groups = new Map<string, Omit<EventSpan, 'row'>[]>();
|
|
424
|
-
|
|
425
|
-
for (const span of tempSpans) {
|
|
426
|
-
if (span.span !== 1) {
|
|
427
|
-
// Multi-day events - don't merge, just add directly
|
|
428
|
-
mergedTempSpans.push(span);
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const mergeKey = `${(span.event.title ?? '').toLowerCase().trim()}|${span.event.color ?? 'BLUE'}`;
|
|
433
|
-
const group = groups.get(mergeKey) ?? [];
|
|
434
|
-
group.push(span);
|
|
435
|
-
groups.set(mergeKey, group);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Process each group with linear merge
|
|
439
|
-
for (const [, group] of groups) {
|
|
440
|
-
// Sort by startIndex (O(k log k) per group, where k is group size)
|
|
441
|
-
group.sort((a, b) => a.startIndex - b.startIndex);
|
|
442
|
-
|
|
443
|
-
let currentMerged: Omit<EventSpan, 'row'> | null = null;
|
|
444
|
-
let mergedIds: string[] = [];
|
|
445
|
-
|
|
446
|
-
for (const span of group) {
|
|
447
|
-
if (!currentMerged) {
|
|
448
|
-
// Start a new potential merge chain
|
|
449
|
-
currentMerged = { ...span };
|
|
450
|
-
mergedIds = [span.event.id];
|
|
451
|
-
} else if (span.startIndex === currentMerged.endIndex + 1) {
|
|
452
|
-
// Adjacent - extend the merge
|
|
453
|
-
currentMerged.endIndex = span.endIndex;
|
|
454
|
-
currentMerged.span =
|
|
455
|
-
currentMerged.endIndex - currentMerged.startIndex + 1;
|
|
456
|
-
currentMerged.isCutOffEnd =
|
|
457
|
-
currentMerged.isCutOffEnd || span.isCutOffEnd;
|
|
458
|
-
currentMerged.actualEndDate = currentMerged.actualEndDate.isAfter(
|
|
459
|
-
span.actualEndDate
|
|
460
|
-
)
|
|
461
|
-
? currentMerged.actualEndDate
|
|
462
|
-
: span.actualEndDate;
|
|
463
|
-
mergedIds.push(span.event.id);
|
|
464
|
-
} else {
|
|
465
|
-
// Gap - push current merged span and start a new one
|
|
466
|
-
if (mergedIds.length > 1) {
|
|
467
|
-
currentMerged.isMerged = true;
|
|
468
|
-
currentMerged.mergedEventIds = mergedIds;
|
|
469
|
-
}
|
|
470
|
-
mergedTempSpans.push(currentMerged);
|
|
471
|
-
currentMerged = { ...span };
|
|
472
|
-
mergedIds = [span.event.id];
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Push the final merged span from this group
|
|
477
|
-
if (currentMerged) {
|
|
478
|
-
if (mergedIds.length > 1) {
|
|
479
|
-
currentMerged.isMerged = true;
|
|
480
|
-
currentMerged.mergedEventIds = mergedIds;
|
|
481
|
-
}
|
|
482
|
-
mergedTempSpans.push(currentMerged);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Sort final output by startIndex for consistent ordering
|
|
487
|
-
mergedTempSpans.sort((a, b) => a.startIndex - b.startIndex);
|
|
488
|
-
|
|
489
|
-
// Third pass: assign rows (ONLY for non-location events)
|
|
490
|
-
// Location events will be rendered separately as a compact timeline strip
|
|
491
|
-
// Extract location spans before filtering them out for row assignment
|
|
492
|
-
const locationSpansFromMerge = mergedTempSpans
|
|
493
|
-
.filter((span) => getEventLocationType(span.event) !== null)
|
|
494
|
-
.map((span) => ({ ...span, row: 0 })); // Give them row 0 as placeholder
|
|
495
|
-
|
|
496
|
-
const nonLocationSpans = mergedTempSpans.filter(
|
|
497
|
-
(span) => getEventLocationType(span.event) === null
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
// Sort: 1) by span length (longer events first for better packing), 2) by start date
|
|
501
|
-
const sortedTempSpans = nonLocationSpans.sort((a, b) => {
|
|
502
|
-
// Longer events first (they need to claim their full row first)
|
|
503
|
-
const spanDiff = b.span - a.span;
|
|
504
|
-
if (spanDiff !== 0) return spanDiff;
|
|
505
|
-
|
|
506
|
-
// Then by start date
|
|
507
|
-
return a.actualStartDate.diff(b.actualStartDate);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Track occupied rows for each day
|
|
511
|
-
const occupiedRows: boolean[][] = Array(visibleDates.length)
|
|
512
|
-
.fill(null)
|
|
513
|
-
.map(() => []);
|
|
514
|
-
|
|
515
|
-
// Assign rows to events
|
|
516
|
-
sortedTempSpans.forEach((tempSpan) => {
|
|
517
|
-
// Find the first available row that works for all days this event spans
|
|
518
|
-
let row = 0;
|
|
519
|
-
let rowFound = false;
|
|
520
|
-
|
|
521
|
-
while (!rowFound) {
|
|
522
|
-
// Check if this row is available for all days the event spans
|
|
523
|
-
let canUseRow = true;
|
|
524
|
-
for (
|
|
525
|
-
let dayIndex = tempSpan.startIndex;
|
|
526
|
-
dayIndex <= tempSpan.endIndex;
|
|
527
|
-
dayIndex++
|
|
528
|
-
) {
|
|
529
|
-
if (occupiedRows[dayIndex]?.[row]) {
|
|
530
|
-
canUseRow = false;
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (canUseRow) {
|
|
536
|
-
// Mark this row as occupied for all days the event spans
|
|
537
|
-
for (
|
|
538
|
-
let dayIndex = tempSpan.startIndex;
|
|
539
|
-
dayIndex <= tempSpan.endIndex;
|
|
540
|
-
dayIndex++
|
|
541
|
-
) {
|
|
542
|
-
if (!occupiedRows[dayIndex]) {
|
|
543
|
-
occupiedRows[dayIndex] = [];
|
|
544
|
-
}
|
|
545
|
-
occupiedRows[dayIndex]![row] = true;
|
|
546
|
-
}
|
|
547
|
-
rowFound = true;
|
|
548
|
-
} else {
|
|
549
|
-
row++;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Create the final event span with row assignment
|
|
554
|
-
const eventSpan: EventSpan = {
|
|
555
|
-
...tempSpan,
|
|
556
|
-
row,
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
spans.push(eventSpan);
|
|
560
|
-
|
|
561
|
-
// Add this event to each day it spans
|
|
562
|
-
for (let i = tempSpan.startIndex; i <= tempSpan.endIndex; i++) {
|
|
563
|
-
eventsByDay[i]?.push(eventSpan);
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
// Calculate max visible events per day for layout purposes
|
|
568
|
-
let maxVisibleEventsPerDay = 0;
|
|
569
|
-
eventsByDay.forEach((dayEvents, dayIndex) => {
|
|
570
|
-
const dateKey = calendarDayKey(visibleDates[dayIndex]!);
|
|
571
|
-
|
|
572
|
-
const shouldShowAll = dayEvents.length === MAX_EVENTS_DISPLAY + 1;
|
|
573
|
-
const isExpanded = expandedDates.includes(dateKey) || shouldShowAll;
|
|
574
|
-
const visibleCount = isExpanded
|
|
575
|
-
? dayEvents.length
|
|
576
|
-
: Math.min(dayEvents.length, MAX_EVENTS_DISPLAY);
|
|
577
|
-
|
|
578
|
-
maxVisibleEventsPerDay = Math.max(maxVisibleEventsPerDay, visibleCount);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
return {
|
|
582
|
-
spans,
|
|
583
|
-
maxVisibleEventsPerDay,
|
|
584
|
-
eventsByDay,
|
|
585
|
-
locationSpansFromMerge,
|
|
586
|
-
};
|
|
587
|
-
}, [allDayEvents, visibleDates, tz, expandedDates]);
|
|
266
|
+
const eventLayout = useMemo(
|
|
267
|
+
() =>
|
|
268
|
+
calculateAllDayEventLayout(allDayEvents, visibleDates, tz, expandedDates),
|
|
269
|
+
[allDayEvents, visibleDates, tz, expandedDates]
|
|
270
|
+
);
|
|
588
271
|
|
|
589
|
-
// Extract location events (Home/Office) for compact timeline display
|
|
590
|
-
// Deduplicate: only show one location event per day cell (first one wins)
|
|
591
272
|
const locationSpans = useMemo(() => {
|
|
592
273
|
const allLocationSpans = eventLayout.locationSpansFromMerge || [];
|
|
593
274
|
|
|
594
|
-
// Track which day indices have been claimed by a location event
|
|
595
275
|
const claimedDays = new Set<number>();
|
|
596
276
|
const deduplicatedSpans: EventSpan[] = [];
|
|
597
277
|
|
|
598
|
-
// Sort by start index to process in order
|
|
599
278
|
const sortedSpans = [...allLocationSpans].sort(
|
|
600
279
|
(a, b) => a.startIndex - b.startIndex
|
|
601
280
|
);
|
|
602
281
|
|
|
603
282
|
for (const span of sortedSpans) {
|
|
604
|
-
// Check if any day in this span is already claimed
|
|
605
283
|
let hasConflict = false;
|
|
606
284
|
for (let day = span.startIndex; day <= span.endIndex; day++) {
|
|
607
285
|
if (claimedDays.has(day)) {
|
|
@@ -611,7 +289,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
611
289
|
}
|
|
612
290
|
|
|
613
291
|
if (!hasConflict) {
|
|
614
|
-
// Claim all days for this span
|
|
615
292
|
for (let day = span.startIndex; day <= span.endIndex; day++) {
|
|
616
293
|
claimedDays.add(day);
|
|
617
294
|
}
|
|
@@ -622,19 +299,16 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
622
299
|
return deduplicatedSpans;
|
|
623
300
|
}, [eventLayout.locationSpansFromMerge]);
|
|
624
301
|
|
|
625
|
-
// Get non-location spans for regular event display
|
|
626
302
|
const regularSpans = useMemo(() => {
|
|
627
303
|
return eventLayout.spans.filter(
|
|
628
304
|
(span) => getEventLocationType(span.event) === null
|
|
629
305
|
);
|
|
630
306
|
}, [eventLayout.spans]);
|
|
631
307
|
|
|
632
|
-
// Get unique events for a specific date (for expansion logic)
|
|
633
308
|
const getUniqueEventsForDate = (dateIndex: number): EventSpan[] => {
|
|
634
309
|
return eventLayout.eventsByDay[dateIndex] ?? [];
|
|
635
310
|
};
|
|
636
311
|
|
|
637
|
-
// Check if we have any all-day events to display
|
|
638
312
|
if (eventLayout.spans.length === 0) {
|
|
639
313
|
return null;
|
|
640
314
|
}
|
|
@@ -647,7 +321,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
647
321
|
);
|
|
648
322
|
};
|
|
649
323
|
|
|
650
|
-
// Calculate dynamic height based on visible events
|
|
651
324
|
const locationTopOffset =
|
|
652
325
|
locationSpans.length > 0 ? LOCATION_TIMELINE_HEIGHT_REM : 0;
|
|
653
326
|
const regularRowsHeight =
|
|
@@ -656,9 +329,8 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
656
329
|
: 0;
|
|
657
330
|
const barHeight = Math.max(1.9, locationTopOffset + regularRowsHeight);
|
|
658
331
|
|
|
659
|
-
// Enhanced mouse and touch handlers
|
|
660
332
|
const handleEventMouseDown = (e: React.MouseEvent, eventSpan: EventSpan) => {
|
|
661
|
-
if (visibleDates.length <= 1) return;
|
|
333
|
+
if (visibleDates.length <= 1 || eventSpan.event.seriesId) return;
|
|
662
334
|
e.preventDefault();
|
|
663
335
|
e.stopPropagation();
|
|
664
336
|
dragStartPos.current = { x: e.clientX, y: e.clientY };
|
|
@@ -675,13 +347,11 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
675
347
|
dragInitiated.current = true;
|
|
676
348
|
handleDragStart(e, eventSpan);
|
|
677
349
|
}
|
|
678
|
-
// If drag already started, let handleDragMove do its job
|
|
679
350
|
};
|
|
680
351
|
const onMouseUp = () => {
|
|
681
352
|
document.removeEventListener('mousemove', onMouseMove);
|
|
682
353
|
document.removeEventListener('mouseup', onMouseUp);
|
|
683
354
|
if (!dragInitiated.current) {
|
|
684
|
-
// Treat as click (open modal)
|
|
685
355
|
openModal(eventSpan.event.id, 'all-day');
|
|
686
356
|
}
|
|
687
357
|
dragStartPos.current = null;
|
|
@@ -692,13 +362,12 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
692
362
|
};
|
|
693
363
|
|
|
694
364
|
const handleEventTouchStart = (e: React.TouchEvent, eventSpan: EventSpan) => {
|
|
695
|
-
if (visibleDates.length <= 1) return;
|
|
365
|
+
if (visibleDates.length <= 1 || eventSpan.event.seriesId) return;
|
|
696
366
|
if (e.touches.length !== 1) return;
|
|
697
367
|
const touch = e.touches[0];
|
|
698
368
|
if (!touch) return;
|
|
699
369
|
dragStartPos.current = { x: touch.clientX, y: touch.clientY };
|
|
700
370
|
dragInitiated.current = false;
|
|
701
|
-
// Start long-press timer
|
|
702
371
|
longPressTimer.current = setTimeout(() => {
|
|
703
372
|
dragInitiated.current = true;
|
|
704
373
|
handleDragStart(
|
|
@@ -723,7 +392,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
723
392
|
!dragInitiated.current &&
|
|
724
393
|
Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD
|
|
725
394
|
) {
|
|
726
|
-
// Cancel long-press, treat as scroll
|
|
727
395
|
if (longPressTimer.current) clearTimeout(longPressTimer.current);
|
|
728
396
|
longPressTimer.current = null;
|
|
729
397
|
dragStartPos.current = null;
|
|
@@ -738,7 +406,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
738
406
|
document.removeEventListener('touchmove', onTouchMove);
|
|
739
407
|
document.removeEventListener('touchend', onTouchEnd);
|
|
740
408
|
if (!dragInitiated.current) {
|
|
741
|
-
// Treat as tap (open modal)
|
|
742
409
|
openModal(eventSpan.event.id, 'all-day');
|
|
743
410
|
}
|
|
744
411
|
dragStartPos.current = null;
|
|
@@ -782,7 +449,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
782
449
|
? Math.max(0, dateEvents.length - MAX_EVENTS_DISPLAY)
|
|
783
450
|
: 0;
|
|
784
451
|
|
|
785
|
-
// Check if this column is a drop target
|
|
786
452
|
const isDropTarget =
|
|
787
453
|
dragState.isDragging && dragState.targetDateIndex === dateIndex;
|
|
788
454
|
const isOriginalColumn =
|
|
@@ -793,13 +459,11 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
793
459
|
key={`all-day-column-${dateKey}`}
|
|
794
460
|
className={cn(
|
|
795
461
|
'group flex h-full flex-col justify-start border-l transition-colors duration-200 last:border-r',
|
|
796
|
-
// Drop zone visual feedback
|
|
797
462
|
isDropTarget &&
|
|
798
463
|
!isOriginalColumn &&
|
|
799
464
|
'border-blue-300 bg-blue-100 dark:border-blue-700 dark:bg-blue-900/30',
|
|
800
465
|
isOriginalColumn &&
|
|
801
466
|
'border-red-300 bg-red-100 dark:border-red-700 dark:bg-red-900/30',
|
|
802
|
-
// Normal hover state (only when not dragging)
|
|
803
467
|
!dragState.isDragging && 'hover:bg-muted/20'
|
|
804
468
|
)}
|
|
805
469
|
>
|
|
@@ -864,8 +528,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
864
528
|
/>
|
|
865
529
|
)}
|
|
866
530
|
|
|
867
|
-
{/* Absolute positioned spanning events */}
|
|
868
|
-
{/* Location timeline strip at the top */}
|
|
869
531
|
<LocationTimeline
|
|
870
532
|
visibleDates={visibleDates}
|
|
871
533
|
locationSpans={locationSpans}
|
|
@@ -876,7 +538,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
876
538
|
deleteEvent={deleteEvent}
|
|
877
539
|
/>
|
|
878
540
|
|
|
879
|
-
{/* Absolute positioned spanning events */}
|
|
880
541
|
{regularSpans.map((eventSpan) => {
|
|
881
542
|
const { event, startIndex, span, row, isCutOffStart, isCutOffEnd } =
|
|
882
543
|
eventSpan;
|
|
@@ -884,7 +545,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
884
545
|
const eventStyle = calendarEventStyle(event);
|
|
885
546
|
const eventRow = row;
|
|
886
547
|
|
|
887
|
-
// Check if this event should be visible based on expansion state
|
|
888
548
|
const shouldHideEvent = visibleDates.some((date, dateIndex) => {
|
|
889
549
|
if (dateIndex < startIndex || dateIndex > startIndex + span - 1)
|
|
890
550
|
return false;
|
|
@@ -903,11 +563,9 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
903
563
|
|
|
904
564
|
if (shouldHideEvent) return null;
|
|
905
565
|
|
|
906
|
-
// Check if this event is currently being dragged
|
|
907
566
|
const isDraggedEvent =
|
|
908
567
|
dragState.isDragging && dragState.draggedEvent?.id === event.id;
|
|
909
568
|
|
|
910
|
-
// Calculate top offset based on location strip presence
|
|
911
569
|
const topOffset = locationTopOffset;
|
|
912
570
|
const optimisticStatus = (
|
|
913
571
|
event as CalendarEvent & {
|
|
@@ -926,15 +584,22 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
926
584
|
key={`spanning-event-${event.id}`}
|
|
927
585
|
className={cn(
|
|
928
586
|
'absolute flex items-center rounded-sm border-l-2 px-2 py-1 font-semibold text-xs transition-all duration-200',
|
|
929
|
-
|
|
587
|
+
pastEventTreatment(
|
|
588
|
+
event,
|
|
589
|
+
preservePastEventOpacity,
|
|
590
|
+
isDraggedEvent,
|
|
591
|
+
now.getTime(),
|
|
592
|
+
calendarDayBoundary(
|
|
593
|
+
eventSpan.actualEndDate.toDate(),
|
|
594
|
+
tz
|
|
595
|
+
).getTime()
|
|
596
|
+
),
|
|
930
597
|
dragState.isDragging
|
|
931
598
|
? 'cursor-grabbing'
|
|
932
599
|
: 'cursor-grab hover:cursor-grab',
|
|
933
|
-
// Visual feedback for dragging
|
|
934
600
|
isDraggedEvent && 'scale-95 outline outline-dashed',
|
|
935
601
|
isPendingMutation &&
|
|
936
602
|
'outline outline-dashed outline-1 outline-primary',
|
|
937
|
-
// Special styling for cut-off events
|
|
938
603
|
(isCutOffStart || isCutOffEnd) && 'border-dashed'
|
|
939
604
|
)}
|
|
940
605
|
style={{
|
|
@@ -947,7 +612,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
|
|
|
947
612
|
zIndex: isDraggedEvent ? 10 : 5,
|
|
948
613
|
}}
|
|
949
614
|
onClick={() => {
|
|
950
|
-
// Only open modal if not dragging (locked events can still be clicked to edit)
|
|
951
615
|
if (!dragState.isDragging) {
|
|
952
616
|
openModal(event.id, 'all-day');
|
|
953
617
|
}
|