From a1c3e99c7c3c549b92325d0d544ec783ef69a2fa Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 5 Apr 2026 17:10:04 +0200 Subject: [PATCH] feat(timeblocks): drag external items, conflict detection, plan vs reality, timeline view - Drag & drop: external timeBlocks (tasks, habits, timeEntries) can now be dragged and resized directly in calendar views via updateBlock() - Conflict detection: ConflictWarning component shows overlapping timeBlocks in EventForm and QuickEventPopover in real-time - Plan vs Reality: startFromScheduled() creates linked logged blocks from scheduled blocks, EventCard shows checkmark badge for linked blocks, linkBlocks() now validates kind compatibility - Timeline view: full-page /timeline route with chronological day view, day navigation, type filters, duration stats, live indicators, and connected dot+line visualization Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/lib/data/time-blocks/service.ts | 39 ++ .../components/ConflictWarning.svelte | 99 ++++ .../calendar/components/EventCard.svelte | 14 +- .../calendar/components/EventForm.svelte | 7 + .../components/QuickEventPopover.svelte | 8 + .../composables/useEventDragDrop.svelte.ts | 13 + .../web/src/lib/modules/calendar/types.ts | 2 + .../src/routes/(app)/timeline/+page.svelte | 508 ++++++++++++++++++ 8 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 apps/manacore/apps/web/src/lib/modules/calendar/components/ConflictWarning.svelte create mode 100644 apps/manacore/apps/web/src/routes/(app)/timeline/+page.svelte diff --git a/apps/manacore/apps/web/src/lib/data/time-blocks/service.ts b/apps/manacore/apps/web/src/lib/data/time-blocks/service.ts index 74b4168e4..60a305957 100644 --- a/apps/manacore/apps/web/src/lib/data/time-blocks/service.ts +++ b/apps/manacore/apps/web/src/lib/data/time-blocks/service.ts @@ -62,11 +62,50 @@ export async function deleteBlock(id: string): Promise { export async function linkBlocks(scheduledId: string, loggedId: string): Promise { const now = new Date().toISOString(); await db.transaction('rw', timeBlockTable, async () => { + const scheduled = await timeBlockTable.get(scheduledId); + const logged = await timeBlockTable.get(loggedId); + if (!scheduled || !logged) throw new Error('Block not found'); + if (scheduled.kind !== 'scheduled') throw new Error('First block must be scheduled'); + if (logged.kind !== 'logged') throw new Error('Second block must be logged'); + await timeBlockTable.update(scheduledId, { linkedBlockId: loggedId, updatedAt: now }); await timeBlockTable.update(loggedId, { linkedBlockId: scheduledId, updatedAt: now }); }); } +/** + * Start a logged block from a scheduled block (plan → reality). + * Creates a new "logged" block linked to the scheduled one and returns its ID. + */ +export async function startFromScheduled( + scheduledId: string, + overrides?: { title?: string; color?: string; icon?: string; projectId?: string | null } +): Promise { + const scheduled = await timeBlockTable.get(scheduledId); + if (!scheduled || scheduled.deletedAt) throw new Error('Scheduled block not found'); + + const now = new Date().toISOString(); + const loggedId = await createBlock({ + startDate: now, + endDate: null, + isLive: true, + kind: 'logged', + type: scheduled.type, + sourceModule: scheduled.sourceModule, + sourceId: scheduled.sourceId, + linkedBlockId: scheduledId, + title: overrides?.title ?? scheduled.title, + color: overrides?.color ?? scheduled.color ?? null, + icon: overrides?.icon ?? scheduled.icon ?? null, + projectId: overrides?.projectId ?? scheduled.projectId ?? null, + }); + + // Link back from scheduled → logged + await timeBlockTable.update(scheduledId, { linkedBlockId: loggedId, updatedAt: now }); + + return loggedId; +} + /** Get a single timeBlock by ID. */ export async function getBlock(id: string): Promise { const block = await timeBlockTable.get(id); diff --git a/apps/manacore/apps/web/src/lib/modules/calendar/components/ConflictWarning.svelte b/apps/manacore/apps/web/src/lib/modules/calendar/components/ConflictWarning.svelte new file mode 100644 index 000000000..d6ee82087 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calendar/components/ConflictWarning.svelte @@ -0,0 +1,99 @@ + + + +{#if conflicts.length > 0} +
+ +
+ + {conflicts.length === 1 ? 'Zeitkonflikt' : `${conflicts.length} Konflikte`} + + {#each conflicts.slice(0, 2) as conflict} + + {conflict.title} + ({format(new Date(conflict.startDate), 'HH:mm')}–{conflict.endDate + ? format(new Date(conflict.endDate), 'HH:mm') + : '...'}) + + {/each} + {#if conflicts.length > 2} + +{conflicts.length - 2} weitere + {/if} +
+
+{/if} + + diff --git a/apps/manacore/apps/web/src/lib/modules/calendar/components/EventCard.svelte b/apps/manacore/apps/web/src/lib/modules/calendar/components/EventCard.svelte index 439a4a244..e18d88f1a 100644 --- a/apps/manacore/apps/web/src/lib/modules/calendar/components/EventCard.svelte +++ b/apps/manacore/apps/web/src/lib/modules/calendar/components/EventCard.svelte @@ -1,7 +1,7 @@ + +
+ +
+
+

{formatHeaderDate(currentDate)}

+ +
+ +
+ {#if totalSeconds > 0} + {formatDuration(totalSeconds)} erfasst + {/if} + +
+
+ + {#if showFilters} +
+ {#each typeConfig as cfg} + {@const active = visibleTypes.has(cfg.type)} + + {/each} +
+ {/if} + + +
+ {#if blocks.length === 0} +
+ +

{isToday(currentDate) ? 'Noch nichts heute' : 'Keine Einträge an diesem Tag'}

+
+ {:else} +
+ {#each blocks as block, i (block.id)} + {@const duration = getBlockDuration(block)} + {@const habitIcon = + block.type === 'habit' && block.icon ? getIconComponent(block.icon) : null} + {@const typeCfg = typeConfig.find((c) => c.type === block.type)} + +
+ +
+ {format(new Date(block.startDate), 'HH:mm')} +
+ + +
+
+ {#if i < blocks.length - 1} +
+ {/if} +
+ + +
+
+ {#if habitIcon} + + {:else if typeCfg} + + {/if} + {block.title} + {#if block.linkedBlockId} + erledigt + {/if} + {#if block.isLive} + live + {/if} +
+ +
+ {formatBlockTime(block)} + {#if duration > 0} + {formatDuration(duration)} + {/if} +
+ + {#if block.description} +

{block.description}

+ {/if} +
+
+ {/each} +
+ {/if} +
+
+ +