@lavalogic/scoria 0.40.21 → 0.40.22
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/dist/Components/{AppointmentCalendar/AppointmentCalendar.svelte → ResourceCalendar/ResourceCalendar.svelte} +380 -486
- package/dist/Components/ResourceCalendar/ResourceCalendar.svelte.d.ts +25 -0
- package/dist/Components/ResourceCalendar/ResourceCalendarProps.d.ts +116 -0
- package/dist/Components/{AppointmentCalendar/AppointmentCalendarProps.js → ResourceCalendar/ResourceCalendarProps.js} +1 -2
- package/dist/Helpers/loaderDefaults.svelte.d.ts +1 -1
- package/dist/Helpers/loaderDefaults.svelte.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte.d.ts +0 -4
- package/dist/Components/AppointmentCalendar/AppointmentCalendarProps.d.ts +0 -123
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
generics="E extends CalendarEvent"
|
|
6
|
+
>import { Colour } from "../../scss/colours.js";
|
|
4
7
|
import { Size } from "../../Types/Internal/Size.js";
|
|
5
8
|
import Button from "../Button.svelte";
|
|
6
9
|
import Icon from "../Icon.svelte";
|
|
7
10
|
import LoadingOverlay from "../LoadingOverlay.svelte";
|
|
8
|
-
import { CalendarViewMode } from "./
|
|
9
|
-
let {
|
|
11
|
+
import { CalendarViewMode } from "./ResourceCalendarProps.js";
|
|
12
|
+
let { resources, events, event: eventContent, selectedDate, viewMode, ondatechange, onviewmodechange, onrangeselect, oneventclick, oneventmove, startHour = 0, endHour = 24, slotIntervalMinutes = 15, availability, centreHour = 12, isLoading = false, headerExtra } = $props();
|
|
10
13
|
/**
|
|
11
|
-
* `selectedDate` is always read by the component, never written. The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* for narrowing inside the script.
|
|
14
|
+
* `selectedDate` is always read by the component, never written. The runtime
|
|
15
|
+
* type may be a plain `Date` or a `SvelteDate`; both expose the same `Date`
|
|
16
|
+
* interface. Cast to `Date` for narrowing inside the script.
|
|
15
17
|
*/
|
|
16
18
|
const selected = $derived(selectedDate);
|
|
17
19
|
/**
|
|
18
|
-
* Today's reference Date hoisted out of the per-cell loops so
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* over time on its own; if you need real day-rollover semantics
|
|
22
|
-
* track it with a `SvelteDate` and a `setInterval`.
|
|
20
|
+
* Today's reference Date hoisted out of the per-cell loops so rendering the
|
|
21
|
+
* month grid does not allocate 42 fresh `Date` instances on every state
|
|
22
|
+
* change. `$derived` does not invalidate over time on its own.
|
|
23
23
|
*/
|
|
24
24
|
const today = $derived(new Date());
|
|
25
25
|
// ---- Date helpers ----
|
|
@@ -93,13 +93,6 @@ const timeSlots = $derived.by(() => {
|
|
|
93
93
|
return slots;
|
|
94
94
|
});
|
|
95
95
|
// ---- Availability shading ----
|
|
96
|
-
/**
|
|
97
|
-
* Whether the given slot is outside the door's available windows.
|
|
98
|
-
*
|
|
99
|
-
* A door absent from {@link availability} (or no `availability` map at
|
|
100
|
-
* all) is treated as fully available, so nothing is shaded until the
|
|
101
|
-
* caller supplies windows for that door.
|
|
102
|
-
*/
|
|
103
96
|
function slotUnavailableInWindows(windows, slot) {
|
|
104
97
|
const minutes = slot.hour * 60 + slot.minute;
|
|
105
98
|
for (const window of windows) {
|
|
@@ -109,11 +102,11 @@ function slotUnavailableInWindows(windows, slot) {
|
|
|
109
102
|
}
|
|
110
103
|
return true;
|
|
111
104
|
}
|
|
112
|
-
function isSlotUnavailable(
|
|
105
|
+
function isSlotUnavailable(resourceId, slotIndex) {
|
|
113
106
|
if (!availability) {
|
|
114
107
|
return false;
|
|
115
108
|
}
|
|
116
|
-
const windows = availability.get(
|
|
109
|
+
const windows = availability.get(resourceId);
|
|
117
110
|
if (!windows) {
|
|
118
111
|
return false;
|
|
119
112
|
}
|
|
@@ -124,28 +117,26 @@ function isSlotUnavailable(doorId, slotIndex) {
|
|
|
124
117
|
return slotUnavailableInWindows(windows, slot);
|
|
125
118
|
}
|
|
126
119
|
/**
|
|
127
|
-
* Contiguous unavailable runs per
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* rather than restarting (and visibly seaming) at every slot cell.
|
|
120
|
+
* Contiguous unavailable runs per resource, as top/height percentages of the
|
|
121
|
+
* day column. Rendered as a single overlay block per run so the shading is
|
|
122
|
+
* continuous rather than restarting (and visibly seaming) at every slot.
|
|
131
123
|
*/
|
|
132
|
-
const
|
|
133
|
-
// Local index built inside a $derived; outer reactivity covers reads.
|
|
124
|
+
const unavailableRunsByResource = $derived.by(() => {
|
|
134
125
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
135
|
-
const
|
|
126
|
+
const byResource = new Map();
|
|
136
127
|
const slotCount = timeSlots.length;
|
|
137
128
|
if (!availability || slotCount === 0) {
|
|
138
|
-
return
|
|
129
|
+
return byResource;
|
|
139
130
|
}
|
|
140
|
-
for (const
|
|
141
|
-
const windows = availability.get(
|
|
131
|
+
for (const resource of resources) {
|
|
132
|
+
const windows = availability.get(resource.id);
|
|
142
133
|
if (!windows) {
|
|
143
134
|
continue;
|
|
144
135
|
}
|
|
145
136
|
const runs = [];
|
|
146
137
|
let runStart = null;
|
|
147
|
-
// One extra iteration (i === slotCount) flushes a run that reaches
|
|
148
|
-
//
|
|
138
|
+
// One extra iteration (i === slotCount) flushes a run that reaches the
|
|
139
|
+
// end of the day.
|
|
149
140
|
for (let i = 0; i <= slotCount; i++) {
|
|
150
141
|
const unavailable = i < slotCount && slotUnavailableInWindows(windows, timeSlots[i]);
|
|
151
142
|
if (unavailable) {
|
|
@@ -161,18 +152,15 @@ const unavailableRunsByDoor = $derived.by(() => {
|
|
|
161
152
|
}
|
|
162
153
|
}
|
|
163
154
|
if (runs.length > 0) {
|
|
164
|
-
|
|
155
|
+
byResource.set(resource.id, runs);
|
|
165
156
|
}
|
|
166
157
|
}
|
|
167
|
-
return
|
|
158
|
+
return byResource;
|
|
168
159
|
});
|
|
169
|
-
function unavailableRunsAt(
|
|
170
|
-
return
|
|
160
|
+
function unavailableRunsAt(resourceId) {
|
|
161
|
+
return unavailableRunsByResource.get(resourceId) ?? [];
|
|
171
162
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Build a `YYYY-MM-DD` key for date bucketing. Avoids `toISOString`
|
|
174
|
-
* timezone conversions that would smear appointments across days.
|
|
175
|
-
*/
|
|
163
|
+
/** Build a `YYYY-MM-DD` key for date bucketing without timezone smearing. */
|
|
176
164
|
function dayKey(d) {
|
|
177
165
|
const y = d.getFullYear();
|
|
178
166
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
@@ -180,33 +168,31 @@ function dayKey(d) {
|
|
|
180
168
|
return `${y}-${m}-${day}`;
|
|
181
169
|
}
|
|
182
170
|
/**
|
|
183
|
-
* Precomputed `Map<
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* per render and allocated a fresh `Date` per appointment per pass.
|
|
171
|
+
* Precomputed `Map<resourceId, Map<dayKey, PositionedEvent[]>>` for day and
|
|
172
|
+
* week views — avoids per-cell `events.filter(...)` and per-event `Date`
|
|
173
|
+
* allocations on every render.
|
|
187
174
|
*/
|
|
188
|
-
const
|
|
175
|
+
const positionedByResourceAndDay = $derived.by(() => {
|
|
189
176
|
const totalMinutes = (endHour - startHour) * 60;
|
|
190
|
-
// Local indexes built inside a $derived; outer reactivity already covers reads.
|
|
191
177
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
192
|
-
const
|
|
193
|
-
for (const
|
|
194
|
-
const from = new Date(
|
|
195
|
-
const to = new Date(
|
|
178
|
+
const byResource = new Map();
|
|
179
|
+
for (const ev of events) {
|
|
180
|
+
const from = new Date(ev.start);
|
|
181
|
+
const to = new Date(ev.end);
|
|
196
182
|
const startMin = (from.getHours() - startHour) * 60 + from.getMinutes();
|
|
197
183
|
const endMin = (to.getHours() - startHour) * 60 + to.getMinutes();
|
|
198
184
|
const durationMin = Math.max(endMin - startMin, slotIntervalMinutes);
|
|
199
185
|
const positioned = {
|
|
200
|
-
...
|
|
186
|
+
...ev,
|
|
201
187
|
topPercent: Math.max(0, startMin) / totalMinutes * 100,
|
|
202
188
|
heightPercent: Math.min(durationMin, totalMinutes - startMin) / totalMinutes * 100
|
|
203
189
|
};
|
|
204
190
|
const key = dayKey(from);
|
|
205
|
-
let perDay =
|
|
191
|
+
let perDay = byResource.get(ev.resourceId);
|
|
206
192
|
if (!perDay) {
|
|
207
193
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
208
194
|
perDay = new Map();
|
|
209
|
-
|
|
195
|
+
byResource.set(ev.resourceId, perDay);
|
|
210
196
|
}
|
|
211
197
|
const list = perDay.get(key);
|
|
212
198
|
if (list) {
|
|
@@ -215,43 +201,35 @@ const positionedByDoorAndDay = $derived.by(() => {
|
|
|
215
201
|
perDay.set(key, [positioned]);
|
|
216
202
|
}
|
|
217
203
|
}
|
|
218
|
-
return
|
|
204
|
+
return byResource;
|
|
219
205
|
});
|
|
220
|
-
function
|
|
221
|
-
return
|
|
206
|
+
function eventsAt(resourceId, day) {
|
|
207
|
+
return positionedByResourceAndDay.get(resourceId)?.get(dayKey(day)) ?? [];
|
|
222
208
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Precomputed appointment-count badges per day for the month view.
|
|
225
|
-
* Replaces the previous `O(cells * doors * appointments)` recomputation
|
|
226
|
-
* which scaled to ~42 000 ops on a single render with 10 doors and
|
|
227
|
-
* 100 appointments.
|
|
228
|
-
*/
|
|
209
|
+
/** Precomputed event-count badges per day for the month view. */
|
|
229
210
|
const countsByDay = $derived.by(() => {
|
|
230
|
-
// Local indexes built inside a $derived; outer reactivity already covers reads.
|
|
231
211
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
232
212
|
const map = new Map();
|
|
233
|
-
// One scan over appointments builds a nested Map<dayKey, Map<doorId, count>>.
|
|
234
213
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
235
214
|
const intermediate = new Map();
|
|
236
|
-
for (const
|
|
237
|
-
const key = dayKey(new Date(
|
|
215
|
+
for (const ev of events) {
|
|
216
|
+
const key = dayKey(new Date(ev.start));
|
|
238
217
|
let perDay = intermediate.get(key);
|
|
239
218
|
if (!perDay) {
|
|
240
219
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
241
220
|
perDay = new Map();
|
|
242
221
|
intermediate.set(key, perDay);
|
|
243
222
|
}
|
|
244
|
-
perDay.set(
|
|
223
|
+
perDay.set(ev.resourceId, (perDay.get(ev.resourceId) ?? 0) + 1);
|
|
245
224
|
}
|
|
246
|
-
// Flatten to the door-ordered list the markup expects.
|
|
247
225
|
for (const [key, perDay] of intermediate) {
|
|
248
226
|
const list = [];
|
|
249
|
-
for (const
|
|
250
|
-
const count = perDay.get(
|
|
227
|
+
for (const resource of resources) {
|
|
228
|
+
const count = perDay.get(resource.id);
|
|
251
229
|
if (count) {
|
|
252
230
|
list.push({
|
|
253
|
-
|
|
254
|
-
|
|
231
|
+
resourceId: resource.id,
|
|
232
|
+
resourceLabel: resource.label,
|
|
255
233
|
count
|
|
256
234
|
});
|
|
257
235
|
}
|
|
@@ -267,96 +245,88 @@ function countsAt(day) {
|
|
|
267
245
|
}
|
|
268
246
|
// ---- Click and drag logic ----
|
|
269
247
|
let isDragging = $state(false);
|
|
270
|
-
let
|
|
248
|
+
let dragResourceId = $state(null);
|
|
271
249
|
let dragStartSlotIndex = $state(null);
|
|
272
250
|
let dragEndSlotIndex = $state(null);
|
|
273
|
-
function onSlotMouseDown(
|
|
274
|
-
// Only start a drag-create on the primary (left) button.
|
|
275
|
-
// middle-click (autoscroll) or right-click would open the create modal.
|
|
251
|
+
function onSlotMouseDown(resourceId, slotIndex, e) {
|
|
252
|
+
// Only start a drag-create on the primary (left) button.
|
|
276
253
|
if (e.button !== 0) {
|
|
277
254
|
return;
|
|
278
255
|
}
|
|
279
|
-
if (!
|
|
256
|
+
if (!onrangeselect || eventDragStarted) {
|
|
280
257
|
return;
|
|
281
258
|
}
|
|
282
|
-
|
|
283
|
-
if (isSlotUnavailable(doorId, slotIndex)) {
|
|
259
|
+
if (isSlotUnavailable(resourceId, slotIndex)) {
|
|
284
260
|
return;
|
|
285
261
|
}
|
|
286
262
|
isDragging = true;
|
|
287
|
-
|
|
263
|
+
dragResourceId = resourceId;
|
|
288
264
|
dragStartSlotIndex = slotIndex;
|
|
289
265
|
dragEndSlotIndex = slotIndex;
|
|
290
266
|
}
|
|
291
|
-
function onSlotMouseEnter(
|
|
267
|
+
function onSlotMouseEnter(resourceId, slotIndex, e) {
|
|
292
268
|
// If a drag is in progress but the primary button is no longer held, the
|
|
293
|
-
// pointer-up happened somewhere we did not observe
|
|
294
|
-
|
|
295
|
-
if ((isDragging || appointmentDragStarted) && (e.buttons & 1) === 0) {
|
|
269
|
+
// pointer-up happened somewhere we did not observe. Reset.
|
|
270
|
+
if ((isDragging || eventDragStarted) && (e.buttons & 1) === 0) {
|
|
296
271
|
isDragging = false;
|
|
297
|
-
|
|
272
|
+
dragResourceId = null;
|
|
298
273
|
dragStartSlotIndex = null;
|
|
299
274
|
dragEndSlotIndex = null;
|
|
300
|
-
|
|
275
|
+
resetEventDrag();
|
|
301
276
|
return;
|
|
302
277
|
}
|
|
303
|
-
//
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
if (isSlotUnavailable(doorId, slotIndex)) {
|
|
278
|
+
// Event move drag
|
|
279
|
+
if (eventDragStarted) {
|
|
280
|
+
if (isSlotUnavailable(resourceId, slotIndex)) {
|
|
307
281
|
return;
|
|
308
282
|
}
|
|
309
|
-
|
|
310
|
-
|
|
283
|
+
isDraggingEvent = true;
|
|
284
|
+
moveTargetResourceId = resourceId;
|
|
311
285
|
moveTargetSlotIndex = slotIndex;
|
|
312
286
|
return;
|
|
313
287
|
}
|
|
314
288
|
// Slot drag-create
|
|
315
|
-
if (!isDragging ||
|
|
289
|
+
if (!isDragging || resourceId !== dragResourceId) {
|
|
316
290
|
return;
|
|
317
291
|
}
|
|
318
|
-
|
|
319
|
-
if (isSlotUnavailable(doorId, slotIndex)) {
|
|
292
|
+
if (isSlotUnavailable(resourceId, slotIndex)) {
|
|
320
293
|
return;
|
|
321
294
|
}
|
|
322
295
|
dragEndSlotIndex = slotIndex;
|
|
323
296
|
}
|
|
324
297
|
function onSlotMouseUp() {
|
|
325
|
-
// ---- Handle
|
|
326
|
-
if (
|
|
298
|
+
// ---- Handle event drag-move ----
|
|
299
|
+
if (isDraggingEvent && draggedEvent && moveTargetResourceId != null && moveTargetSlotIndex != null) {
|
|
327
300
|
const slot = timeSlots[moveTargetSlotIndex];
|
|
328
301
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
const samePosition = moveTargetDoorId === draggedAppointment.segmentId && newDateFrom.getTime() === origFrom.getTime();
|
|
302
|
+
const newStart = new Date(selected);
|
|
303
|
+
newStart.setHours(slot.hour, slot.minute, 0, 0);
|
|
304
|
+
const newEnd = new Date(newStart.getTime() + draggedSlotCount * slotIntervalMinutes * 6e4);
|
|
305
|
+
const origFrom = new Date(draggedEvent.start);
|
|
306
|
+
const samePosition = moveTargetResourceId === draggedEvent.resourceId && newStart.getTime() === origFrom.getTime();
|
|
335
307
|
if (samePosition) {
|
|
336
|
-
// Dropped in the same spot - treat as a click
|
|
337
|
-
|
|
338
|
-
} else if (!hasOverlap(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
newDateFrom,
|
|
345
|
-
newDateTo
|
|
308
|
+
// Dropped in the same spot - treat as a click.
|
|
309
|
+
oneventclick?.(draggedEvent);
|
|
310
|
+
} else if (!hasOverlap(moveTargetResourceId, newStart, newEnd, draggedEvent.id)) {
|
|
311
|
+
oneventmove?.({
|
|
312
|
+
event: draggedEvent,
|
|
313
|
+
newResourceId: moveTargetResourceId,
|
|
314
|
+
newStart,
|
|
315
|
+
newEnd
|
|
346
316
|
});
|
|
347
317
|
}
|
|
348
|
-
|
|
318
|
+
resetEventDrag();
|
|
349
319
|
return;
|
|
350
320
|
}
|
|
351
|
-
// ---- Handle
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
321
|
+
// ---- Handle event click (mousedown + mouseup without leaving the block) ----
|
|
322
|
+
if (eventDragStarted && !isDraggingEvent && draggedEvent) {
|
|
323
|
+
oneventclick?.(draggedEvent);
|
|
324
|
+
resetEventDrag();
|
|
355
325
|
return;
|
|
356
326
|
}
|
|
357
|
-
|
|
327
|
+
resetEventDrag();
|
|
358
328
|
// ---- Handle drag-create ----
|
|
359
|
-
if (!isDragging ||
|
|
329
|
+
if (!isDragging || dragResourceId == null || dragStartSlotIndex == null || dragEndSlotIndex == null) {
|
|
360
330
|
return;
|
|
361
331
|
}
|
|
362
332
|
const startIdx = Math.min(dragStartSlotIndex, dragEndSlotIndex);
|
|
@@ -364,27 +334,25 @@ function onSlotMouseUp() {
|
|
|
364
334
|
const startSlot = timeSlots[startIdx];
|
|
365
335
|
const endSlot = timeSlots[endIdx];
|
|
366
336
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
367
|
-
const
|
|
368
|
-
|
|
337
|
+
const start = new Date(selected);
|
|
338
|
+
start.setHours(startSlot.hour, startSlot.minute, 0, 0);
|
|
369
339
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
370
|
-
const
|
|
371
|
-
// End slot is the start of the last selected slot; add one interval
|
|
372
|
-
|
|
373
|
-
const door = doors.find((d) => d.id === dragDoorId);
|
|
340
|
+
const end = new Date(selected);
|
|
341
|
+
// End slot is the start of the last selected slot; add one interval.
|
|
342
|
+
end.setHours(endSlot.hour, endSlot.minute + slotIntervalMinutes, 0, 0);
|
|
374
343
|
const selection = {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
dateTo
|
|
344
|
+
resourceId: dragResourceId,
|
|
345
|
+
start,
|
|
346
|
+
end
|
|
379
347
|
};
|
|
380
348
|
isDragging = false;
|
|
381
|
-
|
|
349
|
+
dragResourceId = null;
|
|
382
350
|
dragStartSlotIndex = null;
|
|
383
351
|
dragEndSlotIndex = null;
|
|
384
|
-
|
|
352
|
+
onrangeselect?.(selection);
|
|
385
353
|
}
|
|
386
|
-
function isSlotInDragRange(
|
|
387
|
-
if (!isDragging ||
|
|
354
|
+
function isSlotInDragRange(resourceId, slotIndex) {
|
|
355
|
+
if (!isDragging || resourceId !== dragResourceId) {
|
|
388
356
|
return false;
|
|
389
357
|
}
|
|
390
358
|
if (dragStartSlotIndex == null || dragEndSlotIndex == null) {
|
|
@@ -396,58 +364,57 @@ function isSlotInDragRange(doorId, slotIndex) {
|
|
|
396
364
|
}
|
|
397
365
|
// ---- Row hover tracking ----
|
|
398
366
|
let hoveredSlotIndex = $state(null);
|
|
399
|
-
// ----
|
|
400
|
-
let
|
|
401
|
-
let
|
|
402
|
-
let
|
|
403
|
-
let
|
|
404
|
-
let
|
|
367
|
+
// ---- Event drag-move logic ----
|
|
368
|
+
let eventDragStarted = $state(false);
|
|
369
|
+
let isDraggingEvent = $state(false);
|
|
370
|
+
let draggedEvent = $state(null);
|
|
371
|
+
let draggedSlotCount = $state(0);
|
|
372
|
+
let moveTargetResourceId = $state(null);
|
|
405
373
|
let moveTargetSlotIndex = $state(null);
|
|
406
|
-
function
|
|
407
|
-
// Only react to the primary (left) button
|
|
408
|
-
// start a move or register as an appointment click.
|
|
374
|
+
function onEventMouseDown(ev, e) {
|
|
375
|
+
// Only react to the primary (left) button.
|
|
409
376
|
if (e.button !== 0) {
|
|
410
377
|
return;
|
|
411
378
|
}
|
|
412
|
-
if (!
|
|
379
|
+
if (!oneventmove) {
|
|
413
380
|
return;
|
|
414
381
|
}
|
|
415
382
|
e.preventDefault();
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const from = new Date(
|
|
419
|
-
const to = new Date(
|
|
383
|
+
eventDragStarted = true;
|
|
384
|
+
draggedEvent = ev;
|
|
385
|
+
const from = new Date(ev.start);
|
|
386
|
+
const to = new Date(ev.end);
|
|
420
387
|
const durationMin = (to.getTime() - from.getTime()) / 6e4;
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
388
|
+
draggedSlotCount = Math.max(1, Math.ceil(durationMin / slotIntervalMinutes));
|
|
389
|
+
}
|
|
390
|
+
function resetEventDrag() {
|
|
391
|
+
eventDragStarted = false;
|
|
392
|
+
isDraggingEvent = false;
|
|
393
|
+
draggedEvent = null;
|
|
394
|
+
draggedSlotCount = 0;
|
|
395
|
+
moveTargetResourceId = null;
|
|
429
396
|
moveTargetSlotIndex = null;
|
|
430
397
|
}
|
|
431
|
-
function isSlotInMoveTarget(
|
|
432
|
-
if (!
|
|
398
|
+
function isSlotInMoveTarget(resourceId, slotIndex) {
|
|
399
|
+
if (!isDraggingEvent || moveTargetResourceId !== resourceId) {
|
|
433
400
|
return false;
|
|
434
401
|
}
|
|
435
402
|
if (moveTargetSlotIndex == null) {
|
|
436
403
|
return false;
|
|
437
404
|
}
|
|
438
|
-
return slotIndex >= moveTargetSlotIndex && slotIndex < moveTargetSlotIndex +
|
|
405
|
+
return slotIndex >= moveTargetSlotIndex && slotIndex < moveTargetSlotIndex + draggedSlotCount;
|
|
439
406
|
}
|
|
440
|
-
function hasOverlap(
|
|
441
|
-
return
|
|
442
|
-
if (
|
|
407
|
+
function hasOverlap(resourceId, start, end, excludeId) {
|
|
408
|
+
return events.some((ev) => {
|
|
409
|
+
if (ev.id === excludeId) {
|
|
443
410
|
return false;
|
|
444
411
|
}
|
|
445
|
-
if (
|
|
412
|
+
if (ev.resourceId !== resourceId) {
|
|
446
413
|
return false;
|
|
447
414
|
}
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
return
|
|
415
|
+
const evFrom = new Date(ev.start);
|
|
416
|
+
const evTo = new Date(ev.end);
|
|
417
|
+
return start < evTo && end > evFrom;
|
|
451
418
|
});
|
|
452
419
|
}
|
|
453
420
|
// ---- Month view helpers ----
|
|
@@ -455,7 +422,7 @@ function getMonthDays(date) {
|
|
|
455
422
|
const year = date.getFullYear();
|
|
456
423
|
const month = date.getMonth();
|
|
457
424
|
const firstDayOfMonth = new Date(year, month, 1);
|
|
458
|
-
// Start from Monday of the week containing the first day
|
|
425
|
+
// Start from Monday of the week containing the first day.
|
|
459
426
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
460
427
|
const startDate = new Date(firstDayOfMonth);
|
|
461
428
|
const dayOfWeek = startDate.getDay();
|
|
@@ -464,7 +431,7 @@ function getMonthDays(date) {
|
|
|
464
431
|
const days = [];
|
|
465
432
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
466
433
|
const current = new Date(startDate);
|
|
467
|
-
// Generate 6 weeks (42 days) to fill the grid
|
|
434
|
+
// Generate 6 weeks (42 days) to fill the grid.
|
|
468
435
|
for (let i = 0; i < 42; i++) {
|
|
469
436
|
days.push({
|
|
470
437
|
date: new Date(current),
|
|
@@ -507,18 +474,10 @@ const viewModes = [
|
|
|
507
474
|
}
|
|
508
475
|
];
|
|
509
476
|
// ---- Centre-on-hour scrolling ----
|
|
510
|
-
/** The whole hour whose row carries the `data-centre-row` marker. */
|
|
511
477
|
const centreRowHour = $derived(centreHour == null ? null : Math.floor(centreHour));
|
|
512
|
-
/** The scrollable calendar body; bound so we can centre it on the marker. */
|
|
513
478
|
let calendarBody = $state();
|
|
514
|
-
/**
|
|
515
|
-
* Whether the grid has been centred for the current spell in a
|
|
516
|
-
* day/week view. Plain (non-reactive) so re-centring only happens when
|
|
517
|
-
* the grid (re)appears, never while the user scrolls or data loads.
|
|
518
|
-
*/
|
|
519
479
|
let hasCentred = false;
|
|
520
480
|
$effect(() => {
|
|
521
|
-
// Re-arm whenever we leave the time-grid views so re-entry recentres.
|
|
522
481
|
if (viewMode !== CalendarViewMode.Day && viewMode !== CalendarViewMode.Week) {
|
|
523
482
|
hasCentred = false;
|
|
524
483
|
return;
|
|
@@ -530,265 +489,252 @@ $effect(() => {
|
|
|
530
489
|
if (!marker) {
|
|
531
490
|
return;
|
|
532
491
|
}
|
|
533
|
-
// Centre the marker row in the viewport via rect maths so nested
|
|
534
|
-
// sticky/relative offset parents don't skew the target.
|
|
535
492
|
const bodyRect = calendarBody.getBoundingClientRect();
|
|
536
493
|
const markerRect = marker.getBoundingClientRect();
|
|
537
494
|
const delta = markerRect.top + markerRect.height / 2 - (bodyRect.top + bodyRect.height / 2);
|
|
538
495
|
calendarBody.scrollTop += delta;
|
|
539
496
|
hasCentred = true;
|
|
540
497
|
});
|
|
541
|
-
</script>
|
|
542
|
-
|
|
543
|
-
<!-- A drag can end anywhere on the page
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
<!-- svelte-ignore
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
class=
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
</
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
class=
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
class=
|
|
621
|
-
class:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
<span class="day-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
<!--
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
<!--
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
class=
|
|
679
|
-
class:
|
|
680
|
-
class:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
class=
|
|
715
|
-
class:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
class=
|
|
734
|
-
class:
|
|
735
|
-
class:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
{
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
class=
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
{#if apt.reference}
|
|
775
|
-
<span class="apt-reference">Ref: {apt.reference}</span>
|
|
776
|
-
{/if}
|
|
777
|
-
</div>
|
|
778
|
-
{/each}
|
|
779
|
-
</div>
|
|
780
|
-
{/each}
|
|
781
|
-
</div>
|
|
782
|
-
{/snippet}
|
|
783
|
-
|
|
784
|
-
<style>.appointment-calendar {
|
|
498
|
+
</script>
|
|
499
|
+
|
|
500
|
+
<!-- A drag can end anywhere on the page; listen on the window so the drag state
|
|
501
|
+
is always finalised and never left "stuck" for the next drag. -->
|
|
502
|
+
<svelte:window onmouseup={onSlotMouseUp} />
|
|
503
|
+
|
|
504
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
505
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
506
|
+
<div
|
|
507
|
+
class="resource-calendar"
|
|
508
|
+
class:is-moving-event={isDraggingEvent}
|
|
509
|
+
role="application"
|
|
510
|
+
>
|
|
511
|
+
<!-- Header: navigation + view toggle -->
|
|
512
|
+
<div class="calendar-header">
|
|
513
|
+
<div class="header-left">
|
|
514
|
+
<Button
|
|
515
|
+
label="Today"
|
|
516
|
+
size={Size.Small}
|
|
517
|
+
onclick={goToToday}
|
|
518
|
+
>
|
|
519
|
+
Today
|
|
520
|
+
</Button>
|
|
521
|
+
<div class="nav-arrows">
|
|
522
|
+
<button
|
|
523
|
+
class="nav-btn"
|
|
524
|
+
onclick={() => {
|
|
525
|
+
navigateDate(-1);
|
|
526
|
+
}}
|
|
527
|
+
aria-label="Previous"
|
|
528
|
+
>
|
|
529
|
+
<Icon
|
|
530
|
+
name="chevron-left"
|
|
531
|
+
size={Size.Small}
|
|
532
|
+
colour={Colour['$ui-blue-14']}
|
|
533
|
+
/>
|
|
534
|
+
</button>
|
|
535
|
+
<button
|
|
536
|
+
class="nav-btn"
|
|
537
|
+
onclick={() => {
|
|
538
|
+
navigateDate(1);
|
|
539
|
+
}}
|
|
540
|
+
aria-label="Next"
|
|
541
|
+
>
|
|
542
|
+
<Icon
|
|
543
|
+
name="chevron-right"
|
|
544
|
+
size={Size.Small}
|
|
545
|
+
colour={Colour['$ui-blue-14']}
|
|
546
|
+
/>
|
|
547
|
+
</button>
|
|
548
|
+
</div>
|
|
549
|
+
<h2 class="current-date-label">{formatDate(selected)}</h2>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="header-right">
|
|
552
|
+
{#if headerExtra}
|
|
553
|
+
{@render headerExtra()}
|
|
554
|
+
{/if}
|
|
555
|
+
<div class="view-toggle">
|
|
556
|
+
{#each viewModes as { mode, label } (mode)}
|
|
557
|
+
<button
|
|
558
|
+
class="view-btn"
|
|
559
|
+
class:active={viewMode === mode}
|
|
560
|
+
onclick={() => {
|
|
561
|
+
onviewmodechange(mode);
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
{label}
|
|
565
|
+
</button>
|
|
566
|
+
{/each}
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<!-- Week day tabs rendered OUTSIDE the scrollable body so they stay frozen -->
|
|
572
|
+
{#if viewMode === CalendarViewMode.Week}
|
|
573
|
+
<div class="week-day-tabs">
|
|
574
|
+
{#each weekDays as day (day.getTime())}
|
|
575
|
+
<button
|
|
576
|
+
class="week-day-tab"
|
|
577
|
+
class:active={sameDay(day, selected)}
|
|
578
|
+
class:today={sameDay(day, today)}
|
|
579
|
+
onclick={() => {
|
|
580
|
+
ondatechange(day);
|
|
581
|
+
}}
|
|
582
|
+
>
|
|
583
|
+
<span class="day-name">{dayNames[day.getDay()].slice(0, 3)}</span>
|
|
584
|
+
<span class="day-num">{day.getDate()}</span>
|
|
585
|
+
</button>
|
|
586
|
+
{/each}
|
|
587
|
+
</div>
|
|
588
|
+
{/if}
|
|
589
|
+
|
|
590
|
+
<!-- Calendar body -->
|
|
591
|
+
<div
|
|
592
|
+
class="calendar-body"
|
|
593
|
+
bind:this={calendarBody}
|
|
594
|
+
>
|
|
595
|
+
{#if isLoading}
|
|
596
|
+
<LoadingOverlay />
|
|
597
|
+
{/if}
|
|
598
|
+
|
|
599
|
+
{#if viewMode === CalendarViewMode.Day}
|
|
600
|
+
<!-- DAY VIEW -->
|
|
601
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
602
|
+
<div
|
|
603
|
+
class="day-view"
|
|
604
|
+
onmouseleave={() => {
|
|
605
|
+
hoveredSlotIndex = null;
|
|
606
|
+
}}
|
|
607
|
+
>
|
|
608
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
|
|
609
|
+
{@render resourceGrid(selected)}
|
|
610
|
+
</div>
|
|
611
|
+
{:else if viewMode === CalendarViewMode.Week}
|
|
612
|
+
<!-- WEEK VIEW: day grid only, tabs are above calendar-body -->
|
|
613
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
614
|
+
<div
|
|
615
|
+
class="day-view"
|
|
616
|
+
onmouseleave={() => {
|
|
617
|
+
hoveredSlotIndex = null;
|
|
618
|
+
}}
|
|
619
|
+
>
|
|
620
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
|
|
621
|
+
{@render resourceGrid(selected)}
|
|
622
|
+
</div>
|
|
623
|
+
{:else}
|
|
624
|
+
<!-- MONTH VIEW -->
|
|
625
|
+
<div class="month-view">
|
|
626
|
+
<div class="month-header-row">
|
|
627
|
+
{#each ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as dayLabel (dayLabel)}
|
|
628
|
+
<div class="month-day-header">{dayLabel}</div>
|
|
629
|
+
{/each}
|
|
630
|
+
</div>
|
|
631
|
+
<div class="month-grid">
|
|
632
|
+
{#each monthDays as { date: dayDate, isCurrentMonth } (dayDate.getTime())}
|
|
633
|
+
<button
|
|
634
|
+
class="month-day-cell"
|
|
635
|
+
class:other-month={!isCurrentMonth}
|
|
636
|
+
class:today={sameDay(dayDate, today)}
|
|
637
|
+
class:selected={sameDay(dayDate, selected)}
|
|
638
|
+
onclick={() => {
|
|
639
|
+
ondatechange(dayDate);
|
|
640
|
+
onviewmodechange(CalendarViewMode.Day);
|
|
641
|
+
}}
|
|
642
|
+
>
|
|
643
|
+
<span class="day-number">{dayDate.getDate()}</span>
|
|
644
|
+
<div class="day-badges">
|
|
645
|
+
{#each countsAt(dayDate) as { resourceId, resourceLabel, count } (resourceId)}
|
|
646
|
+
<span class="resource-badge">
|
|
647
|
+
{resourceLabel}: {count}
|
|
648
|
+
</span>
|
|
649
|
+
{/each}
|
|
650
|
+
</div>
|
|
651
|
+
</button>
|
|
652
|
+
{/each}
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
{/if}
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
{#snippet resourceGrid(day: Date)}
|
|
660
|
+
<div class="grid-header">
|
|
661
|
+
<div class="time-header">Time</div>
|
|
662
|
+
{#each resources as resource (resource.id)}
|
|
663
|
+
<div class="resource-header">{resource.label}</div>
|
|
664
|
+
{/each}
|
|
665
|
+
</div>
|
|
666
|
+
<div class="grid-body">
|
|
667
|
+
<div class="time-column">
|
|
668
|
+
{#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
|
|
669
|
+
<div
|
|
670
|
+
class="time-label"
|
|
671
|
+
class:hour-start={slot.minute === 0}
|
|
672
|
+
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
673
|
+
data-centre-row={slot.hour === centreRowHour && slot.minute === 0 ? '' : undefined}
|
|
674
|
+
>
|
|
675
|
+
{#if slot.minute === 0}
|
|
676
|
+
<span>{slot.label}</span>
|
|
677
|
+
{/if}
|
|
678
|
+
</div>
|
|
679
|
+
{/each}
|
|
680
|
+
</div>
|
|
681
|
+
{#each resources as resource (resource.id)}
|
|
682
|
+
<div class="resource-column">
|
|
683
|
+
<!-- Slot cells for drag interaction (rendered first, below events) -->
|
|
684
|
+
{#each timeSlots as slot, slotIdx (slot.hour * 60 + slot.minute)}
|
|
685
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
686
|
+
<div
|
|
687
|
+
class="slot-cell"
|
|
688
|
+
class:hour-start={slot.minute === 0}
|
|
689
|
+
class:drag-selected={isSlotInDragRange(resource.id, slotIdx)}
|
|
690
|
+
class:move-target={isSlotInMoveTarget(resource.id, slotIdx)}
|
|
691
|
+
class:row-hover={hoveredSlotIndex === slotIdx}
|
|
692
|
+
class:unavailable={isSlotUnavailable(resource.id, slotIdx)}
|
|
693
|
+
onmousedown={(e) => {
|
|
694
|
+
onSlotMouseDown(resource.id, slotIdx, e);
|
|
695
|
+
}}
|
|
696
|
+
onmouseenter={(e) => {
|
|
697
|
+
hoveredSlotIndex = slotIdx;
|
|
698
|
+
onSlotMouseEnter(resource.id, slotIdx, e);
|
|
699
|
+
}}
|
|
700
|
+
></div>
|
|
701
|
+
{/each}
|
|
702
|
+
<!-- Unavailable shading: one continuous block per contiguous run -->
|
|
703
|
+
{#each unavailableRunsAt(resource.id) as run (run.topPercent)}
|
|
704
|
+
<div
|
|
705
|
+
class="unavailable-overlay"
|
|
706
|
+
style="top: {run.topPercent}%; height: {run.heightPercent}%;"
|
|
707
|
+
></div>
|
|
708
|
+
{/each}
|
|
709
|
+
<!-- Event blocks positioned absolutely (rendered after, above slots).
|
|
710
|
+
The consumer renders the card body via the `event` snippet. -->
|
|
711
|
+
{#each eventsAt(resource.id, day) as ev (ev.id)}
|
|
712
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
713
|
+
<div
|
|
714
|
+
class="event-block"
|
|
715
|
+
class:dragging={isDraggingEvent && draggedEvent?.id === ev.id}
|
|
716
|
+
class:movable={!!oneventmove}
|
|
717
|
+
style="top: {ev.topPercent}%; height: {ev.heightPercent}%;"
|
|
718
|
+
onmousedown={(e) => {
|
|
719
|
+
onEventMouseDown(ev, e);
|
|
720
|
+
}}
|
|
721
|
+
>
|
|
722
|
+
{@render eventContent(ev)}
|
|
723
|
+
</div>
|
|
724
|
+
{/each}
|
|
725
|
+
</div>
|
|
726
|
+
{/each}
|
|
727
|
+
</div>
|
|
728
|
+
{/snippet}
|
|
729
|
+
|
|
730
|
+
<style>.resource-calendar {
|
|
785
731
|
display: flex;
|
|
786
732
|
flex-flow: column nowrap;
|
|
787
733
|
height: 100%;
|
|
788
734
|
background-color: #ffffff;
|
|
789
735
|
overflow: hidden;
|
|
790
736
|
}
|
|
791
|
-
.
|
|
737
|
+
.resource-calendar.is-moving-event {
|
|
792
738
|
cursor: grabbing;
|
|
793
739
|
}
|
|
794
740
|
|
|
@@ -899,7 +845,7 @@ $effect(() => {
|
|
|
899
845
|
}
|
|
900
846
|
|
|
901
847
|
.time-header,
|
|
902
|
-
.
|
|
848
|
+
.resource-header {
|
|
903
849
|
padding: 0.5rem;
|
|
904
850
|
font-size: 0.875rem;
|
|
905
851
|
font-weight: 600;
|
|
@@ -919,7 +865,7 @@ $effect(() => {
|
|
|
919
865
|
background-color: #e9ecf1;
|
|
920
866
|
}
|
|
921
867
|
|
|
922
|
-
.
|
|
868
|
+
.resource-header {
|
|
923
869
|
flex: 1;
|
|
924
870
|
min-width: 8rem;
|
|
925
871
|
}
|
|
@@ -964,7 +910,7 @@ $effect(() => {
|
|
|
964
910
|
white-space: nowrap;
|
|
965
911
|
}
|
|
966
912
|
|
|
967
|
-
.
|
|
913
|
+
.resource-column {
|
|
968
914
|
flex: 1;
|
|
969
915
|
min-width: 8rem;
|
|
970
916
|
position: relative;
|
|
@@ -1010,75 +956,23 @@ $effect(() => {
|
|
|
1010
956
|
background-image: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(128, 150, 165, 0.14) 6px, rgba(128, 150, 165, 0.14) 12px);
|
|
1011
957
|
}
|
|
1012
958
|
|
|
1013
|
-
.
|
|
959
|
+
.event-block {
|
|
1014
960
|
position: absolute;
|
|
1015
961
|
left: 2px;
|
|
1016
962
|
right: 2px;
|
|
1017
963
|
z-index: 5;
|
|
1018
964
|
border-radius: 4px;
|
|
1019
|
-
border: solid 1px #9fdaec;
|
|
1020
|
-
background-color: #f4fbfd;
|
|
1021
|
-
padding: 0.25rem 0.4rem;
|
|
1022
|
-
display: flex;
|
|
1023
|
-
flex-flow: column nowrap;
|
|
1024
|
-
gap: 0.15rem;
|
|
1025
965
|
overflow: hidden;
|
|
1026
966
|
cursor: pointer;
|
|
1027
|
-
font-size: 0.75rem;
|
|
1028
|
-
text-align: left;
|
|
1029
|
-
transition: border-color ease-out 0.15s 0s, background-color ease-out 0.15s 0s, color ease-out 0.15s 0s, fill ease-out 0.15s 0s;
|
|
1030
967
|
user-select: none;
|
|
1031
968
|
}
|
|
1032
|
-
.
|
|
969
|
+
.event-block.movable {
|
|
1033
970
|
cursor: grab;
|
|
1034
971
|
}
|
|
1035
|
-
.
|
|
972
|
+
.event-block.dragging {
|
|
1036
973
|
opacity: 0.4;
|
|
1037
974
|
cursor: grabbing;
|
|
1038
975
|
}
|
|
1039
|
-
.appointment-block:hover {
|
|
1040
|
-
background-color: #e6f8fd;
|
|
1041
|
-
border-color: #74c9e4;
|
|
1042
|
-
}
|
|
1043
|
-
.appointment-block.inbound {
|
|
1044
|
-
border-color: #9fdaec;
|
|
1045
|
-
}
|
|
1046
|
-
.appointment-block.inbound .apt-type {
|
|
1047
|
-
color: #259fc7;
|
|
1048
|
-
}
|
|
1049
|
-
.appointment-block.outbound {
|
|
1050
|
-
border-color: #66bb6a;
|
|
1051
|
-
background-color: #eaf7ec;
|
|
1052
|
-
}
|
|
1053
|
-
.appointment-block.outbound .apt-type {
|
|
1054
|
-
color: #2e7d32;
|
|
1055
|
-
}
|
|
1056
|
-
.appointment-block.outbound:hover {
|
|
1057
|
-
background-color: #dcefdd;
|
|
1058
|
-
border-color: #43a047;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
.apt-type {
|
|
1062
|
-
font-weight: 600;
|
|
1063
|
-
font-size: 0.75rem;
|
|
1064
|
-
text-transform: uppercase;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
.apt-label {
|
|
1068
|
-
color: #3a4952;
|
|
1069
|
-
font-weight: 500;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
.apt-sublabel {
|
|
1073
|
-
color: #647d8e;
|
|
1074
|
-
font-size: 0.75rem;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
.apt-reference {
|
|
1078
|
-
color: #647d8e;
|
|
1079
|
-
font-size: 0.75rem;
|
|
1080
|
-
font-weight: 600;
|
|
1081
|
-
}
|
|
1082
976
|
|
|
1083
977
|
.week-day-tabs {
|
|
1084
978
|
display: flex;
|
|
@@ -1222,7 +1116,7 @@ $effect(() => {
|
|
|
1222
1116
|
overflow: hidden;
|
|
1223
1117
|
}
|
|
1224
1118
|
|
|
1225
|
-
.
|
|
1119
|
+
.resource-badge {
|
|
1226
1120
|
font-size: 0.75rem;
|
|
1227
1121
|
color: #259fc7;
|
|
1228
1122
|
background-color: #e6f8fd;
|
|
@@ -1231,4 +1125,4 @@ $effect(() => {
|
|
|
1231
1125
|
white-space: nowrap;
|
|
1232
1126
|
overflow: hidden;
|
|
1233
1127
|
text-overflow: ellipsis;
|
|
1234
|
-
}</style>
|
|
1128
|
+
}</style>
|