@innosolutions/inno-calendar 1.0.2 → 1.0.4
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/AGENT.md +1253 -0
- package/README.md +99 -1185
- package/dist/{agenda-dropdown-DRS-BzmH.js → agenda-dropdown-Bt7X_Zfs.js} +130 -130
- package/dist/agenda-dropdown-Bt7X_Zfs.js.map +1 -0
- package/dist/agenda-dropdown-BxS1w8zQ.cjs +2 -0
- package/dist/agenda-dropdown-BxS1w8zQ.cjs.map +1 -0
- package/dist/{agenda-view-Bq2AFesz.js → agenda-view-BDVwfLye.js} +131 -129
- package/dist/{agenda-view-Bq2AFesz.js.map → agenda-view-BDVwfLye.js.map} +1 -1
- package/dist/agenda-view-CZPZMrdq.cjs +11 -0
- package/dist/{agenda-view-BWBRB_iZ.cjs.map → agenda-view-CZPZMrdq.cjs.map} +1 -1
- package/dist/components/index.cjs +1 -1
- package/dist/components/index.mjs +2 -2
- package/dist/components/views/day-view.d.ts.map +1 -1
- package/dist/components/views/week-view.d.ts.map +1 -1
- package/dist/core/context/inno-calendar-provider.d.ts +2 -8
- package/dist/core/context/inno-calendar-provider.d.ts.map +1 -1
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.mjs +3 -3
- package/dist/core/types.d.ts +12 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils/date-utils.d.ts +3 -16
- package/dist/core/utils/date-utils.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +5 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/{inno-calendar-provider-BwP6uUAq.cjs → inno-calendar-provider-BOdwC6tk.cjs} +2 -2
- package/dist/{inno-calendar-provider-BwP6uUAq.cjs.map → inno-calendar-provider-BOdwC6tk.cjs.map} +1 -1
- package/dist/{inno-calendar-provider-B0e1XLeo.js → inno-calendar-provider-Buls2aP9.js} +2 -2
- package/dist/{inno-calendar-provider-B0e1XLeo.js.map → inno-calendar-provider-Buls2aP9.js.map} +1 -1
- package/dist/presets/index.cjs +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/slot-selection-context-3WVXrd0q.cjs +2 -0
- package/dist/slot-selection-context-3WVXrd0q.cjs.map +1 -0
- package/dist/{slot-selection-context-BCNnYGHC.js → slot-selection-context-msJxhYfm.js} +123 -123
- package/dist/slot-selection-context-msJxhYfm.js.map +1 -0
- package/dist/styles/index.css +1 -0
- package/dist/styles/index.d.ts +1 -0
- package/dist/{tailwind-calendar-BlRF-alx.js → tailwind-calendar-CyIGh6Xu.js} +93 -93
- package/dist/{tailwind-calendar-BlRF-alx.js.map → tailwind-calendar-CyIGh6Xu.js.map} +1 -1
- package/dist/tailwind-calendar-eBJzso44.cjs +2 -0
- package/dist/{tailwind-calendar-D6AeGjVG.cjs.map → tailwind-calendar-eBJzso44.cjs.map} +1 -1
- package/dist/{use-calendar-time-config-F-8WV_Sd.cjs → use-calendar-time-config-DQTW52ca.cjs} +2 -2
- package/dist/{use-calendar-time-config-F-8WV_Sd.cjs.map → use-calendar-time-config-DQTW52ca.cjs.map} +1 -1
- package/dist/{use-calendar-time-config-CtKkXw_L.js → use-calendar-time-config-DWTnhQx9.js} +3 -3
- package/dist/{use-calendar-time-config-CtKkXw_L.js.map → use-calendar-time-config-DWTnhQx9.js.map} +1 -1
- package/package.json +5 -2
- package/dist/agenda-dropdown-C3HEFmnk.cjs +0 -2
- package/dist/agenda-dropdown-C3HEFmnk.cjs.map +0 -1
- package/dist/agenda-dropdown-DRS-BzmH.js.map +0 -1
- package/dist/agenda-view-BWBRB_iZ.cjs +0 -11
- package/dist/slot-selection-context-BCNnYGHC.js.map +0 -1
- package/dist/slot-selection-context-C52lU9W4.cjs +0 -2
- package/dist/slot-selection-context-C52lU9W4.cjs.map +0 -1
- package/dist/tailwind-calendar-D6AeGjVG.cjs +0 -2
package/AGENT.md
ADDED
|
@@ -0,0 +1,1253 @@
|
|
|
1
|
+
# @inno/calendar - Agent Implementation Guide
|
|
2
|
+
|
|
3
|
+
> This document contains comprehensive implementation details for AI coding agents. Copy this entire file into your context when working with @inno/calendar.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Package Overview
|
|
8
|
+
|
|
9
|
+
**@innosolutions/inno-calendar** is a headless-first, fully customizable React calendar package for enterprise applications.
|
|
10
|
+
|
|
11
|
+
- **Version**: 1.0.4+
|
|
12
|
+
- **React**: >=18.0.0
|
|
13
|
+
- **TypeScript**: Full type coverage
|
|
14
|
+
- **Styling**: TailwindCSS compatible, CSS custom properties
|
|
15
|
+
- **Dependencies**: Minimal (clsx, tailwind-merge, class-variance-authority)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation & Setup
|
|
20
|
+
|
|
21
|
+
### Install Package
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @innosolutions/inno-calendar
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Install Optional Peer Dependencies
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# For enhanced UI (popovers, tooltips, dropdowns)
|
|
31
|
+
npm install @radix-ui/react-popover @radix-ui/react-tooltip @radix-ui/react-dropdown-menu
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Import Styles
|
|
35
|
+
|
|
36
|
+
**Required** - Import in your app's entry point:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// In main.tsx or App.tsx
|
|
40
|
+
import '@innosolutions/inno-calendar/styles';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or in CSS:
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
@import '@innosolutions/inno-calendar/styles';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Core Types
|
|
52
|
+
|
|
53
|
+
### CalendarEvent
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
interface CalendarEvent<TData = Record<string, unknown>> {
|
|
57
|
+
id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
startDate: Date;
|
|
60
|
+
endDate: Date;
|
|
61
|
+
color?: TEventColor;
|
|
62
|
+
description?: string;
|
|
63
|
+
isCanceled?: boolean;
|
|
64
|
+
isAllDay?: boolean;
|
|
65
|
+
scheduleTypeId?: number;
|
|
66
|
+
scheduleTypeName?: string;
|
|
67
|
+
participants?: ICalendarUser[];
|
|
68
|
+
data?: TData;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### TEventColor
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
type TEventColor =
|
|
76
|
+
| 'blue' | 'green' | 'red' | 'yellow'
|
|
77
|
+
| 'purple' | 'orange' | 'pink' | 'teal'
|
|
78
|
+
| 'gray' | 'indigo';
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### TCalendarView
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
type TCalendarView =
|
|
85
|
+
| 'day' | 'week' | 'month' | 'year' | 'agenda'
|
|
86
|
+
| 'timeline-day' | 'timeline-3day' | 'timeline-week';
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### ICalendarUser
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
interface ICalendarUser {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
avatar?: string;
|
|
96
|
+
email?: string;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### IScheduleType
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
interface IScheduleType {
|
|
104
|
+
id: number;
|
|
105
|
+
name: string;
|
|
106
|
+
color?: string;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### TWorkingHours
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
interface IWorkingHoursDay {
|
|
114
|
+
enabled: boolean;
|
|
115
|
+
from: number; // 0-23
|
|
116
|
+
to: number; // 0-23
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type TWorkingHours = Record<number, IWorkingHoursDay>;
|
|
120
|
+
// Key: 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### ISelectionResult
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
interface ISelectionResult {
|
|
127
|
+
startDate: Date;
|
|
128
|
+
endDate: Date;
|
|
129
|
+
mode?: 'time' | 'day';
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### IDropResult
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
interface IDropResult<TData = Record<string, unknown>> {
|
|
137
|
+
event: CalendarEvent<TData>;
|
|
138
|
+
newStartDate: Date;
|
|
139
|
+
newEndDate: Date;
|
|
140
|
+
oldStartDate: Date;
|
|
141
|
+
oldEndDate: Date;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Main Components
|
|
148
|
+
|
|
149
|
+
### InnoCalendar
|
|
150
|
+
|
|
151
|
+
The primary component providing a complete calendar experience.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { InnoCalendar, type CalendarEvent } from '@innosolutions/inno-calendar';
|
|
155
|
+
|
|
156
|
+
<InnoCalendar
|
|
157
|
+
// Required
|
|
158
|
+
events={events}
|
|
159
|
+
|
|
160
|
+
// Data (optional)
|
|
161
|
+
users={users}
|
|
162
|
+
scheduleTypes={scheduleTypes}
|
|
163
|
+
|
|
164
|
+
// Initial state
|
|
165
|
+
initialView="week"
|
|
166
|
+
initialDate={new Date()}
|
|
167
|
+
initialSelectedUserId="all"
|
|
168
|
+
initialScheduleTypeIds={[]}
|
|
169
|
+
initialParticipantIds={[]}
|
|
170
|
+
initialWorkingHoursView="default"
|
|
171
|
+
initialSearchQuery=""
|
|
172
|
+
|
|
173
|
+
// Preferences
|
|
174
|
+
preferencesConfig={{
|
|
175
|
+
defaults: {
|
|
176
|
+
badgeVariant: 'colored',
|
|
177
|
+
slotDuration: 30,
|
|
178
|
+
workingHours: defaultWorkingHours,
|
|
179
|
+
},
|
|
180
|
+
locked: {
|
|
181
|
+
badgeVariant: false,
|
|
182
|
+
slotDuration: false,
|
|
183
|
+
},
|
|
184
|
+
}}
|
|
185
|
+
|
|
186
|
+
// Callbacks
|
|
187
|
+
onEventClick={(event) => {}}
|
|
188
|
+
onSlotClick={(date, hour) => {}}
|
|
189
|
+
onSlotSelect={(selection) => {}}
|
|
190
|
+
onAddEvent={() => {}}
|
|
191
|
+
onEventDrop={(result) => {}}
|
|
192
|
+
onDateChange={(date, view) => {}}
|
|
193
|
+
onViewChange={(view) => {}}
|
|
194
|
+
|
|
195
|
+
// UI options
|
|
196
|
+
showHeader={true}
|
|
197
|
+
minSelectionMinutes={30}
|
|
198
|
+
className="h-full"
|
|
199
|
+
|
|
200
|
+
// Custom rendering
|
|
201
|
+
renderPopover={({ event, onClose }) => <CustomPopover event={event} onClose={onClose} />}
|
|
202
|
+
settingsContent={<SettingsPanel />}
|
|
203
|
+
filterContent={<FiltersRow />}
|
|
204
|
+
/>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### InnoCalendarProvider
|
|
208
|
+
|
|
209
|
+
Context provider for shared state across components.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import {
|
|
213
|
+
InnoCalendarProvider,
|
|
214
|
+
useInnoCalendar,
|
|
215
|
+
CalendarHeader,
|
|
216
|
+
WeekView
|
|
217
|
+
} from '@innosolutions/inno-calendar';
|
|
218
|
+
|
|
219
|
+
function App() {
|
|
220
|
+
return (
|
|
221
|
+
<InnoCalendarProvider
|
|
222
|
+
initialEvents={events}
|
|
223
|
+
initialUsers={users}
|
|
224
|
+
initialScheduleTypes={scheduleTypes}
|
|
225
|
+
initialView="week"
|
|
226
|
+
initialDate={new Date()}
|
|
227
|
+
preferencesConfig={preferencesConfig}
|
|
228
|
+
onDateChange={(date, view) => updateURL(date, view)}
|
|
229
|
+
onViewChange={(view) => updateURL(view)}
|
|
230
|
+
>
|
|
231
|
+
<CalendarContent />
|
|
232
|
+
</InnoCalendarProvider>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function CalendarContent() {
|
|
237
|
+
const {
|
|
238
|
+
// View & Navigation
|
|
239
|
+
view,
|
|
240
|
+
setView,
|
|
241
|
+
selectedDate,
|
|
242
|
+
setSelectedDate,
|
|
243
|
+
|
|
244
|
+
// Data
|
|
245
|
+
events,
|
|
246
|
+
setEvents,
|
|
247
|
+
filteredEvents,
|
|
248
|
+
users,
|
|
249
|
+
scheduleTypes,
|
|
250
|
+
|
|
251
|
+
// Filters
|
|
252
|
+
selectedUserId,
|
|
253
|
+
setSelectedUserId,
|
|
254
|
+
selectedScheduleTypeIds,
|
|
255
|
+
setSelectedScheduleTypeIds,
|
|
256
|
+
searchQuery,
|
|
257
|
+
setSearchQuery,
|
|
258
|
+
|
|
259
|
+
// Visual Settings
|
|
260
|
+
badgeVariant,
|
|
261
|
+
setBadgeVariant,
|
|
262
|
+
|
|
263
|
+
// Time Configuration
|
|
264
|
+
slotDuration,
|
|
265
|
+
setSlotDuration,
|
|
266
|
+
visibleHours,
|
|
267
|
+
setVisibleHours,
|
|
268
|
+
workingHours,
|
|
269
|
+
setWorkingHours,
|
|
270
|
+
showWorkingHoursOnly,
|
|
271
|
+
setShowWorkingHoursOnly,
|
|
272
|
+
|
|
273
|
+
// Preferences
|
|
274
|
+
isPreferenceLocked,
|
|
275
|
+
} = useInnoCalendar();
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div>
|
|
279
|
+
<CalendarHeader />
|
|
280
|
+
<WeekView events={filteredEvents} date={selectedDate} />
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Individual View Components
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
import {
|
|
290
|
+
DayView,
|
|
291
|
+
WeekView,
|
|
292
|
+
MonthView,
|
|
293
|
+
YearView,
|
|
294
|
+
AgendaView,
|
|
295
|
+
TimelineView
|
|
296
|
+
} from '@innosolutions/inno-calendar';
|
|
297
|
+
|
|
298
|
+
// Day View
|
|
299
|
+
<DayView
|
|
300
|
+
events={events}
|
|
301
|
+
date={selectedDate}
|
|
302
|
+
visibleHours={{ startHour: 8, endHour: 18 }}
|
|
303
|
+
workingHours={workingHours}
|
|
304
|
+
slotDuration={30}
|
|
305
|
+
badgeVariant="colored"
|
|
306
|
+
onEventClick={handleEventClick}
|
|
307
|
+
onSlotSelect={handleSlotSelect}
|
|
308
|
+
renderPopover={renderCustomPopover}
|
|
309
|
+
/>
|
|
310
|
+
|
|
311
|
+
// Week View
|
|
312
|
+
<WeekView
|
|
313
|
+
events={events}
|
|
314
|
+
date={selectedDate}
|
|
315
|
+
weekStartsOn={1} // 0 = Sunday, 1 = Monday
|
|
316
|
+
visibleHours={{ startHour: 0, endHour: 24 }}
|
|
317
|
+
workingHours={workingHours}
|
|
318
|
+
slotDuration={30}
|
|
319
|
+
badgeVariant="colored"
|
|
320
|
+
onEventClick={handleEventClick}
|
|
321
|
+
onSlotSelect={handleSlotSelect}
|
|
322
|
+
onDayClick={handleDayClick}
|
|
323
|
+
renderPopover={renderCustomPopover}
|
|
324
|
+
/>
|
|
325
|
+
|
|
326
|
+
// Month View
|
|
327
|
+
<MonthView
|
|
328
|
+
events={events}
|
|
329
|
+
date={selectedDate}
|
|
330
|
+
badgeVariant="colored"
|
|
331
|
+
onEventClick={handleEventClick}
|
|
332
|
+
onDayClick={handleDayClick}
|
|
333
|
+
renderPopover={renderCustomPopover}
|
|
334
|
+
/>
|
|
335
|
+
|
|
336
|
+
// Year View
|
|
337
|
+
<YearView
|
|
338
|
+
events={events}
|
|
339
|
+
year={2026}
|
|
340
|
+
onMonthClick={handleMonthClick}
|
|
341
|
+
onDayClick={handleDayClick}
|
|
342
|
+
/>
|
|
343
|
+
|
|
344
|
+
// Agenda View
|
|
345
|
+
<AgendaView
|
|
346
|
+
events={events}
|
|
347
|
+
date={selectedDate}
|
|
348
|
+
onEventClick={handleEventClick}
|
|
349
|
+
renderPopover={renderCustomPopover}
|
|
350
|
+
/>
|
|
351
|
+
|
|
352
|
+
// Timeline View (Resource-based)
|
|
353
|
+
<TimelineView
|
|
354
|
+
events={events}
|
|
355
|
+
users={users}
|
|
356
|
+
selectedDate={selectedDate}
|
|
357
|
+
daysToShow={3} // 1, 3, or 7
|
|
358
|
+
visibleHours={{ from: 8, to: 18 }}
|
|
359
|
+
onEventClick={handleEventClick}
|
|
360
|
+
renderPopover={renderCustomPopover}
|
|
361
|
+
/>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### CalendarHeader
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
import { CalendarHeader } from '@innosolutions/inno-calendar';
|
|
368
|
+
|
|
369
|
+
<CalendarHeader
|
|
370
|
+
settingsContent={<SettingsPanel />}
|
|
371
|
+
filterContent={<FiltersRow />}
|
|
372
|
+
showAddButton={true}
|
|
373
|
+
onAddEvent={() => openCreateDialog()}
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Settings Components
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
import {
|
|
381
|
+
SlotDurationSetting,
|
|
382
|
+
VisibleHoursSetting,
|
|
383
|
+
BadgeVariantSetting,
|
|
384
|
+
WorkingHoursSetting,
|
|
385
|
+
} from '@innosolutions/inno-calendar';
|
|
386
|
+
|
|
387
|
+
function SettingsPanel() {
|
|
388
|
+
return (
|
|
389
|
+
<div className="space-y-4 p-4">
|
|
390
|
+
<SlotDurationSetting />
|
|
391
|
+
<BadgeVariantSetting />
|
|
392
|
+
<VisibleHoursSetting />
|
|
393
|
+
<WorkingHoursSetting />
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Filter Components
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { UserFilter, ScheduleTypeFilter } from '@innosolutions/inno-calendar';
|
|
403
|
+
|
|
404
|
+
function FiltersRow() {
|
|
405
|
+
const {
|
|
406
|
+
users,
|
|
407
|
+
selectedUserId,
|
|
408
|
+
setSelectedUserId,
|
|
409
|
+
scheduleTypes,
|
|
410
|
+
selectedScheduleTypeIds,
|
|
411
|
+
setSelectedScheduleTypeIds
|
|
412
|
+
} = useInnoCalendar();
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div className="flex gap-2">
|
|
416
|
+
<UserFilter
|
|
417
|
+
users={users}
|
|
418
|
+
selectedUserId={selectedUserId}
|
|
419
|
+
onSelect={setSelectedUserId}
|
|
420
|
+
/>
|
|
421
|
+
<ScheduleTypeFilter
|
|
422
|
+
scheduleTypes={scheduleTypes}
|
|
423
|
+
selectedIds={selectedScheduleTypeIds}
|
|
424
|
+
onChange={setSelectedScheduleTypeIds}
|
|
425
|
+
/>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Data Transformation
|
|
434
|
+
|
|
435
|
+
Transform your API data to match the calendar's expected format:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
// transformers.ts
|
|
439
|
+
import type { CalendarEvent, ICalendarUser, IScheduleType } from '@innosolutions/inno-calendar';
|
|
440
|
+
|
|
441
|
+
export function transformApiEvents(
|
|
442
|
+
apiEvents: ApiEvent[],
|
|
443
|
+
scheduleTypes: ApiScheduleType[]
|
|
444
|
+
): CalendarEvent[] {
|
|
445
|
+
return apiEvents.map((event) => {
|
|
446
|
+
const scheduleType = scheduleTypes.find((st) => st.id === event.scheduleTypeId);
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
id: String(event.id),
|
|
450
|
+
title: event.subject || event.title,
|
|
451
|
+
startDate: new Date(event.startDate),
|
|
452
|
+
endDate: new Date(event.endDate),
|
|
453
|
+
description: event.description ?? undefined,
|
|
454
|
+
color: mapHexToColor(scheduleType?.colorHex) ?? 'blue',
|
|
455
|
+
scheduleTypeId: event.scheduleTypeId,
|
|
456
|
+
scheduleTypeName: scheduleType?.code || scheduleType?.name,
|
|
457
|
+
isCanceled: event.isCanceled ?? false,
|
|
458
|
+
isAllDay: event.isAllDay ?? false,
|
|
459
|
+
participants: event.participants?.map(transformParticipant),
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function transformParticipants(apiParticipants: ApiParticipant[]): ICalendarUser[] {
|
|
465
|
+
return apiParticipants.map((p) => ({
|
|
466
|
+
id: String(p.id),
|
|
467
|
+
name: `${p.firstName} ${p.lastName}`.trim() || p.email,
|
|
468
|
+
avatar: p.pictureUrl || p.avatar,
|
|
469
|
+
email: p.email,
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function transformScheduleTypes(apiTypes: ApiScheduleType[]): IScheduleType[] {
|
|
474
|
+
return apiTypes.map((st) => ({
|
|
475
|
+
id: st.id,
|
|
476
|
+
name: st.resourceKey ?? st.code ?? st.name ?? `Type ${st.id}`,
|
|
477
|
+
color: st.colorHex,
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Helper to map hex colors to TEventColor
|
|
482
|
+
function mapHexToColor(hex?: string): TEventColor | undefined {
|
|
483
|
+
if (!hex) return undefined;
|
|
484
|
+
|
|
485
|
+
const colorMap: Record<string, TEventColor> = {
|
|
486
|
+
'#3b82f6': 'blue',
|
|
487
|
+
'#22c55e': 'green',
|
|
488
|
+
'#ef4444': 'red',
|
|
489
|
+
'#eab308': 'yellow',
|
|
490
|
+
'#a855f7': 'purple',
|
|
491
|
+
'#f97316': 'orange',
|
|
492
|
+
'#ec4899': 'pink',
|
|
493
|
+
'#14b8a6': 'teal',
|
|
494
|
+
'#6b7280': 'gray',
|
|
495
|
+
'#6366f1': 'indigo',
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// Find closest match or default
|
|
499
|
+
const normalized = hex.toLowerCase();
|
|
500
|
+
return colorMap[normalized] ?? 'blue';
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## URL Synchronization
|
|
507
|
+
|
|
508
|
+
### With TanStack Router
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
import { getRouteApi } from '@tanstack/react-router';
|
|
512
|
+
import { z } from 'zod';
|
|
513
|
+
|
|
514
|
+
// Schema for search params
|
|
515
|
+
export const calendarSearchSchema = z.object({
|
|
516
|
+
view: z.string().optional(),
|
|
517
|
+
selectedDate: z.string().optional(),
|
|
518
|
+
startDate: z.string().optional(),
|
|
519
|
+
endDate: z.string().optional(),
|
|
520
|
+
scheduleTypeIds: z.array(z.number()).optional(),
|
|
521
|
+
participants: z.array(z.string()).optional(),
|
|
522
|
+
search: z.string().optional(),
|
|
523
|
+
workingHoursView: z.enum(['default', 'working']).optional(),
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Route definition
|
|
527
|
+
export const Route = createFileRoute('/_layout/agenda/')({
|
|
528
|
+
validateSearch: calendarSearchSchema,
|
|
529
|
+
loaderDeps: ({ search }) => ({
|
|
530
|
+
startDate: search.startDate,
|
|
531
|
+
endDate: search.endDate,
|
|
532
|
+
scheduleTypeIds: search.scheduleTypeIds,
|
|
533
|
+
participants: search.participants,
|
|
534
|
+
}),
|
|
535
|
+
loader: async ({ context, deps }) => {
|
|
536
|
+
const [events, participants, scheduleTypes] = await Promise.all([
|
|
537
|
+
context.queryClient.ensureQueryData({
|
|
538
|
+
queryKey: ['agenda', deps.startDate, deps.endDate],
|
|
539
|
+
queryFn: () => fetchAgendaEvents(deps),
|
|
540
|
+
}),
|
|
541
|
+
context.queryClient.ensureQueryData({
|
|
542
|
+
queryKey: ['participants'],
|
|
543
|
+
queryFn: fetchParticipants,
|
|
544
|
+
}),
|
|
545
|
+
context.queryClient.ensureQueryData({
|
|
546
|
+
queryKey: ['schedule-types'],
|
|
547
|
+
queryFn: fetchScheduleTypes,
|
|
548
|
+
}),
|
|
549
|
+
]);
|
|
550
|
+
return { events, participants, scheduleTypes };
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Component
|
|
555
|
+
const routeApi = getRouteApi('/_layout/agenda/');
|
|
556
|
+
|
|
557
|
+
function AgendaPage() {
|
|
558
|
+
const navigate = routeApi.useNavigate();
|
|
559
|
+
const searchParams = routeApi.useSearch();
|
|
560
|
+
const loaderData = routeApi.useLoaderData();
|
|
561
|
+
|
|
562
|
+
const handleDateChange = useCallback((date: Date, view: TCalendarView) => {
|
|
563
|
+
const { startDate, endDate } = getViewDateRange(date, view);
|
|
564
|
+
navigate({
|
|
565
|
+
search: (prev) => ({
|
|
566
|
+
...prev,
|
|
567
|
+
selectedDate: formatDate(date),
|
|
568
|
+
startDate: formatDate(startDate),
|
|
569
|
+
endDate: formatDate(endDate),
|
|
570
|
+
}),
|
|
571
|
+
replace: true,
|
|
572
|
+
});
|
|
573
|
+
}, [navigate]);
|
|
574
|
+
|
|
575
|
+
const handleViewChange = useCallback((view: TCalendarView) => {
|
|
576
|
+
navigate({
|
|
577
|
+
search: (prev) => ({ ...prev, view }),
|
|
578
|
+
replace: true,
|
|
579
|
+
});
|
|
580
|
+
}, [navigate]);
|
|
581
|
+
|
|
582
|
+
return (
|
|
583
|
+
<InnoCalendar
|
|
584
|
+
events={loaderData.events}
|
|
585
|
+
initialView={searchParams.view ?? 'week'}
|
|
586
|
+
initialDate={parseDate(searchParams.selectedDate)}
|
|
587
|
+
onDateChange={handleDateChange}
|
|
588
|
+
onViewChange={handleViewChange}
|
|
589
|
+
/>
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Date Range Helpers
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
export function getViewDateRange(date: Date, view: TCalendarView): { startDate: Date; endDate: Date } {
|
|
598
|
+
const d = new Date(date);
|
|
599
|
+
|
|
600
|
+
switch (view) {
|
|
601
|
+
case 'day':
|
|
602
|
+
return {
|
|
603
|
+
startDate: startOfDay(d),
|
|
604
|
+
endDate: endOfDay(d),
|
|
605
|
+
};
|
|
606
|
+
case 'week':
|
|
607
|
+
case 'timeline-week':
|
|
608
|
+
return {
|
|
609
|
+
startDate: startOfWeek(d),
|
|
610
|
+
endDate: endOfWeek(d),
|
|
611
|
+
};
|
|
612
|
+
case 'month':
|
|
613
|
+
// Include surrounding weeks for month grid
|
|
614
|
+
const monthStart = startOfMonth(d);
|
|
615
|
+
const monthEnd = endOfMonth(d);
|
|
616
|
+
return {
|
|
617
|
+
startDate: startOfWeek(monthStart),
|
|
618
|
+
endDate: endOfWeek(monthEnd),
|
|
619
|
+
};
|
|
620
|
+
case 'year':
|
|
621
|
+
return {
|
|
622
|
+
startDate: new Date(d.getFullYear(), 0, 1),
|
|
623
|
+
endDate: new Date(d.getFullYear(), 11, 31),
|
|
624
|
+
};
|
|
625
|
+
default:
|
|
626
|
+
return {
|
|
627
|
+
startDate: startOfMonth(d),
|
|
628
|
+
endDate: endOfMonth(d),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function formatDateForApi(date: Date): string {
|
|
634
|
+
return date.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Dialog Integration
|
|
641
|
+
|
|
642
|
+
### With Global Dialog Store
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
import { useDialog } from '@/hooks/use-dialog';
|
|
646
|
+
import dayjs from 'dayjs';
|
|
647
|
+
|
|
648
|
+
function CalendarWithDialogs() {
|
|
649
|
+
const { initialize } = useDialog();
|
|
650
|
+
|
|
651
|
+
const handleSlotSelect = useCallback((selection: ISelectionResult) => {
|
|
652
|
+
initialize({
|
|
653
|
+
title: 'Create Event',
|
|
654
|
+
children: (
|
|
655
|
+
<EventForm
|
|
656
|
+
startDate={dayjs(selection.startDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
657
|
+
endDate={dayjs(selection.endDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
658
|
+
/>
|
|
659
|
+
),
|
|
660
|
+
classes: { content: 'max-w-3xl' },
|
|
661
|
+
});
|
|
662
|
+
}, [initialize]);
|
|
663
|
+
|
|
664
|
+
const handleEventClick = useCallback((event: CalendarEvent) => {
|
|
665
|
+
initialize({
|
|
666
|
+
title: 'Event Details',
|
|
667
|
+
children: <EventDetailsDialog eventId={event.id} />,
|
|
668
|
+
});
|
|
669
|
+
}, [initialize]);
|
|
670
|
+
|
|
671
|
+
const handleEventDrop = useCallback((result: IDropResult) => {
|
|
672
|
+
initialize({
|
|
673
|
+
title: 'Reschedule Event',
|
|
674
|
+
children: (
|
|
675
|
+
<EventForm
|
|
676
|
+
id={result.event.id}
|
|
677
|
+
startDate={dayjs(result.newStartDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
678
|
+
endDate={dayjs(result.newEndDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
679
|
+
/>
|
|
680
|
+
),
|
|
681
|
+
});
|
|
682
|
+
}, [initialize]);
|
|
683
|
+
|
|
684
|
+
return (
|
|
685
|
+
<InnoCalendar
|
|
686
|
+
events={events}
|
|
687
|
+
onSlotSelect={handleSlotSelect}
|
|
688
|
+
onEventClick={handleEventClick}
|
|
689
|
+
onEventDrop={handleEventDrop}
|
|
690
|
+
/>
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Custom Event Popover
|
|
698
|
+
|
|
699
|
+
```tsx
|
|
700
|
+
function CustomPopover({
|
|
701
|
+
event,
|
|
702
|
+
onClose
|
|
703
|
+
}: {
|
|
704
|
+
event: CalendarEvent;
|
|
705
|
+
onClose: () => void;
|
|
706
|
+
}) {
|
|
707
|
+
const { data: details, isLoading } = useQuery({
|
|
708
|
+
queryKey: ['event-details', event.id],
|
|
709
|
+
queryFn: () => fetchEventDetails(event.id),
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
if (isLoading) {
|
|
713
|
+
return <div className="p-4 animate-pulse">Loading...</div>;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return (
|
|
717
|
+
<div className="p-4 space-y-3 min-w-[300px]">
|
|
718
|
+
{/* Header */}
|
|
719
|
+
<div className="flex items-start justify-between">
|
|
720
|
+
<div>
|
|
721
|
+
<h3 className="font-semibold text-lg">{event.title}</h3>
|
|
722
|
+
{event.scheduleTypeName && (
|
|
723
|
+
<span className="text-xs px-2 py-0.5 rounded bg-muted">
|
|
724
|
+
{event.scheduleTypeName}
|
|
725
|
+
</span>
|
|
726
|
+
)}
|
|
727
|
+
</div>
|
|
728
|
+
<button onClick={onClose} className="text-muted-foreground">
|
|
729
|
+
✕
|
|
730
|
+
</button>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
{/* Time */}
|
|
734
|
+
<div className="text-sm text-muted-foreground">
|
|
735
|
+
{formatDateRange(event.startDate, event.endDate)}
|
|
736
|
+
</div>
|
|
737
|
+
|
|
738
|
+
{/* Description */}
|
|
739
|
+
{event.description && (
|
|
740
|
+
<p className="text-sm">{event.description}</p>
|
|
741
|
+
)}
|
|
742
|
+
|
|
743
|
+
{/* Participants */}
|
|
744
|
+
{details?.participants?.length > 0 && (
|
|
745
|
+
<div className="flex -space-x-2">
|
|
746
|
+
{details.participants.map((p) => (
|
|
747
|
+
<img
|
|
748
|
+
key={p.id}
|
|
749
|
+
src={p.avatar}
|
|
750
|
+
alt={p.name}
|
|
751
|
+
className="w-8 h-8 rounded-full border-2 border-white"
|
|
752
|
+
/>
|
|
753
|
+
))}
|
|
754
|
+
</div>
|
|
755
|
+
)}
|
|
756
|
+
|
|
757
|
+
{/* Actions */}
|
|
758
|
+
<div className="flex gap-2 pt-3 border-t">
|
|
759
|
+
<button
|
|
760
|
+
onClick={() => handleEdit(event)}
|
|
761
|
+
className="flex-1 px-3 py-1.5 text-sm border rounded hover:bg-muted"
|
|
762
|
+
>
|
|
763
|
+
Edit
|
|
764
|
+
</button>
|
|
765
|
+
<button
|
|
766
|
+
onClick={() => handleDelete(event)}
|
|
767
|
+
className="px-3 py-1.5 text-sm text-destructive border border-destructive rounded hover:bg-destructive/10"
|
|
768
|
+
>
|
|
769
|
+
Delete
|
|
770
|
+
</button>
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Usage
|
|
777
|
+
<InnoCalendar
|
|
778
|
+
events={events}
|
|
779
|
+
renderPopover={({ event, onClose }) => (
|
|
780
|
+
<CustomPopover event={event} onClose={onClose} />
|
|
781
|
+
)}
|
|
782
|
+
/>
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Working Hours Configuration
|
|
788
|
+
|
|
789
|
+
```tsx
|
|
790
|
+
// Default working hours
|
|
791
|
+
const DEFAULT_WORKING_HOURS: TWorkingHours = {
|
|
792
|
+
0: { enabled: false, from: 8, to: 17 }, // Sunday - disabled
|
|
793
|
+
1: { enabled: true, from: 8, to: 17 }, // Monday
|
|
794
|
+
2: { enabled: true, from: 8, to: 17 }, // Tuesday
|
|
795
|
+
3: { enabled: true, from: 8, to: 17 }, // Wednesday
|
|
796
|
+
4: { enabled: true, from: 8, to: 17 }, // Thursday
|
|
797
|
+
5: { enabled: true, from: 8, to: 17 }, // Friday
|
|
798
|
+
6: { enabled: true, from: 8, to: 12 }, // Saturday - half day
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Pass to calendar
|
|
802
|
+
<InnoCalendar
|
|
803
|
+
events={events}
|
|
804
|
+
preferencesConfig={{
|
|
805
|
+
defaults: {
|
|
806
|
+
workingHours: DEFAULT_WORKING_HOURS,
|
|
807
|
+
showWorkingHoursOnly: false,
|
|
808
|
+
},
|
|
809
|
+
}}
|
|
810
|
+
/>
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Preferences Configuration
|
|
816
|
+
|
|
817
|
+
```tsx
|
|
818
|
+
interface IPreferencesConfig {
|
|
819
|
+
defaults?: {
|
|
820
|
+
badgeVariant?: 'dot' | 'colored' | 'mixed';
|
|
821
|
+
slotDuration?: 15 | 30 | 60;
|
|
822
|
+
visibleHours?: { start: number; end: number };
|
|
823
|
+
workingHours?: TWorkingHours;
|
|
824
|
+
showWorkingHoursOnly?: boolean;
|
|
825
|
+
};
|
|
826
|
+
locked?: {
|
|
827
|
+
badgeVariant?: boolean;
|
|
828
|
+
slotDuration?: boolean;
|
|
829
|
+
visibleHours?: boolean;
|
|
830
|
+
workingHours?: boolean;
|
|
831
|
+
showWorkingHoursOnly?: boolean;
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Example: Lock certain settings
|
|
836
|
+
<InnoCalendar
|
|
837
|
+
events={events}
|
|
838
|
+
preferencesConfig={{
|
|
839
|
+
defaults: {
|
|
840
|
+
badgeVariant: 'colored',
|
|
841
|
+
slotDuration: 30,
|
|
842
|
+
},
|
|
843
|
+
locked: {
|
|
844
|
+
badgeVariant: true, // Users cannot change
|
|
845
|
+
slotDuration: false, // Users can change
|
|
846
|
+
},
|
|
847
|
+
}}
|
|
848
|
+
/>
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
## CSS Custom Properties
|
|
854
|
+
|
|
855
|
+
The package uses CSS custom properties for theming. Override in your styles:
|
|
856
|
+
|
|
857
|
+
```css
|
|
858
|
+
:root {
|
|
859
|
+
/* Core colors */
|
|
860
|
+
--inno-border-color: #e5e7eb;
|
|
861
|
+
--inno-background: #ffffff;
|
|
862
|
+
--inno-foreground: #111827;
|
|
863
|
+
--inno-muted: #f3f4f6;
|
|
864
|
+
--inno-muted-foreground: #6b7280;
|
|
865
|
+
--inno-primary: #3b82f6;
|
|
866
|
+
--inno-primary-foreground: #ffffff;
|
|
867
|
+
|
|
868
|
+
/* Event colors */
|
|
869
|
+
--inno-event-blue: #3b82f6;
|
|
870
|
+
--inno-event-green: #22c55e;
|
|
871
|
+
--inno-event-red: #ef4444;
|
|
872
|
+
--inno-event-yellow: #eab308;
|
|
873
|
+
--inno-event-purple: #a855f7;
|
|
874
|
+
--inno-event-orange: #f97316;
|
|
875
|
+
--inno-event-pink: #ec4899;
|
|
876
|
+
--inno-event-teal: #14b8a6;
|
|
877
|
+
--inno-event-gray: #6b7280;
|
|
878
|
+
--inno-event-indigo: #6366f1;
|
|
879
|
+
|
|
880
|
+
/* Layout */
|
|
881
|
+
--inno-hour-height: 96px;
|
|
882
|
+
--inno-header-height: 56px;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/* Dark mode */
|
|
886
|
+
.dark {
|
|
887
|
+
--inno-border-color: #374151;
|
|
888
|
+
--inno-background: #111827;
|
|
889
|
+
--inno-foreground: #f9fafb;
|
|
890
|
+
--inno-muted: #1f2937;
|
|
891
|
+
--inno-muted-foreground: #9ca3af;
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
## Complete Implementation Example
|
|
898
|
+
|
|
899
|
+
```tsx
|
|
900
|
+
// features/agenda/components/agenda-page.tsx
|
|
901
|
+
import { useDialog } from '@/hooks/use-dialog';
|
|
902
|
+
import { useCurrentUser } from '@/hooks/use-current-user';
|
|
903
|
+
import { getRouteApi } from '@tanstack/react-router';
|
|
904
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
905
|
+
import { useTranslation } from 'react-i18next';
|
|
906
|
+
import dayjs from 'dayjs';
|
|
907
|
+
|
|
908
|
+
import {
|
|
909
|
+
InnoCalendar,
|
|
910
|
+
type CalendarEvent,
|
|
911
|
+
type ISelectionResult,
|
|
912
|
+
type TCalendarView,
|
|
913
|
+
type IDropResult,
|
|
914
|
+
type IScheduleType,
|
|
915
|
+
} from '@innosolutions/inno-calendar';
|
|
916
|
+
import '@innosolutions/inno-calendar/styles';
|
|
917
|
+
|
|
918
|
+
import { transformApiEvents, transformParticipants } from '../transformers';
|
|
919
|
+
import { getViewDateRange, formatDateForApi } from '../helpers';
|
|
920
|
+
import { EventPopover } from './event-popover';
|
|
921
|
+
import { EventForm } from '../forms/event-form';
|
|
922
|
+
import { CalendarSettings } from './calendar-settings';
|
|
923
|
+
import { UserSelect, ScheduleTypeSelect } from './filters';
|
|
924
|
+
|
|
925
|
+
const routeApi = getRouteApi('/_protectedLayout/agenda/');
|
|
926
|
+
|
|
927
|
+
export function AgendaPage() {
|
|
928
|
+
const { t } = useTranslation();
|
|
929
|
+
const { initialize } = useDialog();
|
|
930
|
+
const navigate = routeApi.useNavigate();
|
|
931
|
+
const searchParams = routeApi.useSearch();
|
|
932
|
+
|
|
933
|
+
// Role-based access
|
|
934
|
+
const { user } = useCurrentUser();
|
|
935
|
+
const isAdmin = user?.roles?.some(
|
|
936
|
+
(r) => r.name === 'Admin' || r.name === 'Super Admin'
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
// Route data
|
|
940
|
+
const loaderData = routeApi.useLoaderData();
|
|
941
|
+
const rawEvents = loaderData?.data?.data ?? [];
|
|
942
|
+
const rawParticipants = loaderData?.participants?.data ?? [];
|
|
943
|
+
const rawScheduleTypes = loaderData?.scheduleTypes?.data ?? [];
|
|
944
|
+
|
|
945
|
+
// Transform data
|
|
946
|
+
const calendarEvents = useMemo(
|
|
947
|
+
() => transformApiEvents(rawEvents, rawScheduleTypes),
|
|
948
|
+
[rawEvents, rawScheduleTypes]
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
const calendarUsers = useMemo(
|
|
952
|
+
() => transformParticipants(rawParticipants),
|
|
953
|
+
[rawParticipants]
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
const scheduleTypes = useMemo(
|
|
957
|
+
() => rawScheduleTypes.map((st) => ({
|
|
958
|
+
id: st.id,
|
|
959
|
+
name: st.resourceKey ?? st.code ?? `Type ${st.id}`,
|
|
960
|
+
color: st.colorHex,
|
|
961
|
+
})) as IScheduleType[],
|
|
962
|
+
[rawScheduleTypes]
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Parse URL params
|
|
966
|
+
const initialDate = useMemo(() => {
|
|
967
|
+
if (searchParams.selectedDate) {
|
|
968
|
+
const parsed = new Date(searchParams.selectedDate);
|
|
969
|
+
if (!Number.isNaN(parsed.getTime())) return parsed;
|
|
970
|
+
}
|
|
971
|
+
return new Date();
|
|
972
|
+
}, [searchParams.selectedDate]);
|
|
973
|
+
|
|
974
|
+
const initialView = searchParams.view as TCalendarView | undefined;
|
|
975
|
+
|
|
976
|
+
// Handlers
|
|
977
|
+
const handleSlotSelect = useCallback(
|
|
978
|
+
(selection: ISelectionResult) => {
|
|
979
|
+
initialize({
|
|
980
|
+
title: t('actionText.addAgenda'),
|
|
981
|
+
children: (
|
|
982
|
+
<EventForm
|
|
983
|
+
startDate={dayjs(selection.startDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
984
|
+
endDate={dayjs(selection.endDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
985
|
+
/>
|
|
986
|
+
),
|
|
987
|
+
classes: { content: 'max-w-3xl !h-fit' },
|
|
988
|
+
});
|
|
989
|
+
},
|
|
990
|
+
[initialize, t]
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
const handleSlotClick = useCallback(
|
|
994
|
+
(date: Date, hour?: number) => {
|
|
995
|
+
const startDate = new Date(date);
|
|
996
|
+
if (typeof hour === 'number') {
|
|
997
|
+
startDate.setHours(hour, 0, 0, 0);
|
|
998
|
+
}
|
|
999
|
+
const endDate = new Date(startDate);
|
|
1000
|
+
endDate.setHours(endDate.getHours() + 1);
|
|
1001
|
+
handleSlotSelect({ startDate, endDate });
|
|
1002
|
+
},
|
|
1003
|
+
[handleSlotSelect]
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
const handleAddEvent = useCallback(() => {
|
|
1007
|
+
handleSlotClick(new Date());
|
|
1008
|
+
}, [handleSlotClick]);
|
|
1009
|
+
|
|
1010
|
+
const handleEventDrop = useCallback(
|
|
1011
|
+
(result: IDropResult) => {
|
|
1012
|
+
initialize({
|
|
1013
|
+
title: t('forms.agenda.editMeeting'),
|
|
1014
|
+
children: (
|
|
1015
|
+
<EventForm
|
|
1016
|
+
id={result.event.id}
|
|
1017
|
+
startDate={dayjs(result.newStartDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
1018
|
+
endDate={dayjs(result.newEndDate).format('YYYY-MM-DDTHH:mm:ss')}
|
|
1019
|
+
/>
|
|
1020
|
+
),
|
|
1021
|
+
classes: { content: 'max-w-3xl !h-fit' },
|
|
1022
|
+
});
|
|
1023
|
+
},
|
|
1024
|
+
[initialize, t]
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const handleDateChange = useCallback(
|
|
1028
|
+
(newDate: Date, view: TCalendarView) => {
|
|
1029
|
+
const { startDate, endDate } = getViewDateRange(newDate, view);
|
|
1030
|
+
navigate({
|
|
1031
|
+
search: (prev) => ({
|
|
1032
|
+
...prev,
|
|
1033
|
+
selectedDate: dayjs(newDate).format('YYYY-MM-DD'),
|
|
1034
|
+
startDate: formatDateForApi(startDate),
|
|
1035
|
+
endDate: formatDateForApi(endDate),
|
|
1036
|
+
}),
|
|
1037
|
+
replace: true,
|
|
1038
|
+
});
|
|
1039
|
+
},
|
|
1040
|
+
[navigate]
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
const handleViewChange = useCallback(
|
|
1044
|
+
(newView: TCalendarView) => {
|
|
1045
|
+
const { startDate, endDate } = getViewDateRange(initialDate, newView);
|
|
1046
|
+
navigate({
|
|
1047
|
+
search: (prev) => ({
|
|
1048
|
+
...prev,
|
|
1049
|
+
view: newView,
|
|
1050
|
+
startDate: formatDateForApi(startDate),
|
|
1051
|
+
endDate: formatDateForApi(endDate),
|
|
1052
|
+
}),
|
|
1053
|
+
replace: true,
|
|
1054
|
+
});
|
|
1055
|
+
},
|
|
1056
|
+
[navigate, initialDate]
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
const renderPopover = useCallback(
|
|
1060
|
+
({ event, onClose }: { event: CalendarEvent; onClose: () => void }) => (
|
|
1061
|
+
<EventPopover eventId={event.id} onClose={onClose} />
|
|
1062
|
+
),
|
|
1063
|
+
[]
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
return (
|
|
1067
|
+
<div className="h-full">
|
|
1068
|
+
<InnoCalendar
|
|
1069
|
+
events={calendarEvents}
|
|
1070
|
+
users={calendarUsers}
|
|
1071
|
+
scheduleTypes={scheduleTypes}
|
|
1072
|
+
initialView={initialView}
|
|
1073
|
+
initialDate={initialDate}
|
|
1074
|
+
initialWorkingHoursView={searchParams.workingHoursView ?? 'default'}
|
|
1075
|
+
renderPopover={renderPopover}
|
|
1076
|
+
onSlotSelect={handleSlotSelect}
|
|
1077
|
+
onSlotClick={handleSlotClick}
|
|
1078
|
+
onAddEvent={handleAddEvent}
|
|
1079
|
+
onEventDrop={handleEventDrop}
|
|
1080
|
+
onDateChange={handleDateChange}
|
|
1081
|
+
onViewChange={handleViewChange}
|
|
1082
|
+
settingsContent={isAdmin ? <CalendarSettings /> : undefined}
|
|
1083
|
+
filterContent={
|
|
1084
|
+
<div className="flex items-center gap-2">
|
|
1085
|
+
{calendarUsers.length > 0 && <UserSelect />}
|
|
1086
|
+
{scheduleTypes.length > 0 && <ScheduleTypeSelect />}
|
|
1087
|
+
</div>
|
|
1088
|
+
}
|
|
1089
|
+
/>
|
|
1090
|
+
</div>
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
---
|
|
1096
|
+
|
|
1097
|
+
## Migration from FullCalendar
|
|
1098
|
+
|
|
1099
|
+
```tsx
|
|
1100
|
+
// Before (FullCalendar)
|
|
1101
|
+
import FullCalendar from '@fullcalendar/react';
|
|
1102
|
+
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
1103
|
+
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
1104
|
+
|
|
1105
|
+
<FullCalendar
|
|
1106
|
+
plugins={[dayGridPlugin, timeGridPlugin]}
|
|
1107
|
+
initialView="timeGridWeek"
|
|
1108
|
+
events={events}
|
|
1109
|
+
eventClick={(info) => handleClick(info.event)}
|
|
1110
|
+
slotMinTime="08:00:00"
|
|
1111
|
+
slotMaxTime="18:00:00"
|
|
1112
|
+
businessHours={{ daysOfWeek: [1,2,3,4,5], startTime: '09:00', endTime: '17:00' }}
|
|
1113
|
+
/>
|
|
1114
|
+
|
|
1115
|
+
// After (@inno/calendar)
|
|
1116
|
+
import { InnoCalendar } from '@innosolutions/inno-calendar';
|
|
1117
|
+
import '@innosolutions/inno-calendar/styles';
|
|
1118
|
+
|
|
1119
|
+
<InnoCalendar
|
|
1120
|
+
events={transformedEvents}
|
|
1121
|
+
initialView="week"
|
|
1122
|
+
onEventClick={(event) => handleClick(event)}
|
|
1123
|
+
preferencesConfig={{
|
|
1124
|
+
defaults: {
|
|
1125
|
+
visibleHours: { start: 8, end: 18 },
|
|
1126
|
+
workingHours: {
|
|
1127
|
+
0: { enabled: false, from: 9, to: 17 },
|
|
1128
|
+
1: { enabled: true, from: 9, to: 17 },
|
|
1129
|
+
2: { enabled: true, from: 9, to: 17 },
|
|
1130
|
+
3: { enabled: true, from: 9, to: 17 },
|
|
1131
|
+
4: { enabled: true, from: 9, to: 17 },
|
|
1132
|
+
5: { enabled: true, from: 9, to: 17 },
|
|
1133
|
+
6: { enabled: false, from: 9, to: 17 },
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
}}
|
|
1137
|
+
/>
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
---
|
|
1141
|
+
|
|
1142
|
+
## Exports Reference
|
|
1143
|
+
|
|
1144
|
+
```tsx
|
|
1145
|
+
// Main component
|
|
1146
|
+
export { InnoCalendar } from '@innosolutions/inno-calendar';
|
|
1147
|
+
|
|
1148
|
+
// Provider & hooks
|
|
1149
|
+
export {
|
|
1150
|
+
InnoCalendarProvider,
|
|
1151
|
+
useInnoCalendar,
|
|
1152
|
+
useOptionalInnoCalendar,
|
|
1153
|
+
} from '@innosolutions/inno-calendar';
|
|
1154
|
+
|
|
1155
|
+
// View components
|
|
1156
|
+
export {
|
|
1157
|
+
DayView,
|
|
1158
|
+
WeekView,
|
|
1159
|
+
MonthView,
|
|
1160
|
+
YearView,
|
|
1161
|
+
AgendaView,
|
|
1162
|
+
TimelineView,
|
|
1163
|
+
} from '@innosolutions/inno-calendar';
|
|
1164
|
+
|
|
1165
|
+
// Header & settings
|
|
1166
|
+
export {
|
|
1167
|
+
CalendarHeader,
|
|
1168
|
+
SlotDurationSetting,
|
|
1169
|
+
VisibleHoursSetting,
|
|
1170
|
+
BadgeVariantSetting,
|
|
1171
|
+
WorkingHoursSetting,
|
|
1172
|
+
} from '@innosolutions/inno-calendar';
|
|
1173
|
+
|
|
1174
|
+
// Filters
|
|
1175
|
+
export {
|
|
1176
|
+
UserFilter,
|
|
1177
|
+
ScheduleTypeFilter,
|
|
1178
|
+
} from '@innosolutions/inno-calendar';
|
|
1179
|
+
|
|
1180
|
+
// Types
|
|
1181
|
+
export type {
|
|
1182
|
+
CalendarEvent,
|
|
1183
|
+
TCalendarView,
|
|
1184
|
+
TEventColor,
|
|
1185
|
+
TBadgeVariant,
|
|
1186
|
+
TWorkingHours,
|
|
1187
|
+
IWorkingHoursDay,
|
|
1188
|
+
TSlotDuration,
|
|
1189
|
+
ICalendarUser,
|
|
1190
|
+
IScheduleType,
|
|
1191
|
+
ISelectionResult,
|
|
1192
|
+
IDropResult,
|
|
1193
|
+
IVisibleHours,
|
|
1194
|
+
IPreferencesConfig,
|
|
1195
|
+
} from '@innosolutions/inno-calendar';
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
---
|
|
1199
|
+
|
|
1200
|
+
## Troubleshooting
|
|
1201
|
+
|
|
1202
|
+
### Styles not applying
|
|
1203
|
+
|
|
1204
|
+
Ensure you import the styles:
|
|
1205
|
+
|
|
1206
|
+
```tsx
|
|
1207
|
+
import '@innosolutions/inno-calendar/styles';
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
### Working hours striped pattern not showing
|
|
1211
|
+
|
|
1212
|
+
The `.bg-calendar-disabled-hour` class requires CSS custom properties. Import styles or add:
|
|
1213
|
+
|
|
1214
|
+
```css
|
|
1215
|
+
.bg-calendar-disabled-hour {
|
|
1216
|
+
background-image: repeating-linear-gradient(
|
|
1217
|
+
-60deg,
|
|
1218
|
+
var(--inno-border-color, #e5e7eb) 0 0.5px,
|
|
1219
|
+
transparent 0.5px 8px
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
### TypeScript errors with custom event data
|
|
1225
|
+
|
|
1226
|
+
Use the generic parameter:
|
|
1227
|
+
|
|
1228
|
+
```tsx
|
|
1229
|
+
import type { CalendarEvent } from '@innosolutions/inno-calendar';
|
|
1230
|
+
|
|
1231
|
+
interface MyData {
|
|
1232
|
+
customField: string;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const events: CalendarEvent<MyData>[] = [...];
|
|
1236
|
+
|
|
1237
|
+
<InnoCalendar<MyData>
|
|
1238
|
+
events={events}
|
|
1239
|
+
onEventClick={(event) => {
|
|
1240
|
+
// event.data is typed as MyData | undefined
|
|
1241
|
+
console.log(event.data?.customField);
|
|
1242
|
+
}}
|
|
1243
|
+
/>
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
### Events not filtering
|
|
1247
|
+
|
|
1248
|
+
Ensure you're using `filteredEvents` from context, not the raw `events`:
|
|
1249
|
+
|
|
1250
|
+
```tsx
|
|
1251
|
+
const { filteredEvents } = useInnoCalendar();
|
|
1252
|
+
// Use filteredEvents for rendering, not the original events prop
|
|
1253
|
+
```
|