diff --git a/apps/api/src/modules/mukke/routes.ts b/apps/api/src/modules/music/routes.ts similarity index 100% rename from apps/api/src/modules/mukke/routes.ts rename to apps/api/src/modules/music/routes.ts diff --git a/apps/manacore/apps/web/src/lib/api/services/mukke.ts b/apps/manacore/apps/web/src/lib/api/services/music.ts similarity index 100% rename from apps/manacore/apps/web/src/lib/api/services/mukke.ts rename to apps/manacore/apps/web/src/lib/api/services/music.ts diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/MukkeLibraryWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/MusicLibraryWidget.svelte similarity index 100% rename from apps/manacore/apps/web/src/lib/components/dashboard/widgets/MukkeLibraryWidget.svelte rename to apps/manacore/apps/web/src/lib/components/dashboard/widgets/MusicLibraryWidget.svelte diff --git a/apps/manacore/apps/web/src/lib/data/cross-app-queries.ts b/apps/manacore/apps/web/src/lib/data/cross-app-queries.ts index 8b6582491..a0547059b 100644 --- a/apps/manacore/apps/web/src/lib/data/cross-app-queries.ts +++ b/apps/manacore/apps/web/src/lib/data/cross-app-queries.ts @@ -9,14 +9,14 @@ import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; import { db } from './database'; import type { LocalTask } from '$lib/modules/todo/types'; -import type { LocalEvent } from '$lib/modules/calendar/types'; +import type { LocalTimeBlock } from '$lib/data/time-blocks/types'; import type { LocalContact } from '$lib/modules/contacts/types'; import type { LocalConversation } from '$lib/modules/chat/types'; import type { LocalFavorite } from '$lib/modules/zitare/types'; import type { LocalImage } from '$lib/modules/picture/types'; import type { LocalAlarm, LocalCountdownTimer } from '$lib/modules/times/types'; import type { LocalFile } from '$lib/modules/storage/types'; -import type { LocalSong, LocalPlaylist } from '$lib/modules/mukke/types'; +import type { LocalSong, LocalPlaylist } from '$lib/modules/music/types'; import type { LocalDeck as LocalPresiDeck } from '$lib/modules/presi/types'; import type { LocalDocument, LocalContextSpace } from '$lib/modules/context/types'; import type { LocalDeck as LocalCardDeck, LocalCard } from '$lib/modules/cards/types'; @@ -70,7 +70,7 @@ export function useUpcomingTasks(days = 7) { // ─── Calendar Queries ─────────────────────────────────────── -/** Events in the next N days. */ +/** TimeBlocks (all types) in the next N days. */ export function useUpcomingEvents(days = 7) { return useLiveQueryWithDefault(async () => { const now = new Date(); @@ -80,12 +80,12 @@ export function useUpcomingEvents(days = 7) { const nowStr = now.toISOString(); const futureStr = future.toISOString(); - const all = await db.table('events').orderBy('startDate').toArray(); - return all.filter((e) => { - if (e.deletedAt) return false; - return e.startDate >= nowStr && e.startDate <= futureStr; + const all = await db.table('timeBlocks').orderBy('startDate').toArray(); + return all.filter((b) => { + if (b.deletedAt) return false; + return b.startDate >= nowStr && b.startDate <= futureStr; }); - }, [] as LocalEvent[]); + }, [] as LocalTimeBlock[]); } // ─── Contacts Queries ─────────────────────────────────────── @@ -181,19 +181,19 @@ export function useStorageStats() { ); } -// ─── Mukke Queries ────────────────────────────────────────── +// ─── Music Queries ────────────────────────────────────────── -interface MukkeStats { +interface MusicStats { totalSongs: number; totalPlaylists: number; favoriteCount: number; recentSongs: LocalSong[]; } -/** Mukke library stats + recent songs. */ -export function useMukkeStats() { +/** Music library stats + recent songs. */ +export function useMusicStats() { return useLiveQueryWithDefault( - async (): Promise => { + async (): Promise => { const songs = await db.table('songs').toArray(); const playlists = await db.table('mukkePlaylists').toArray(); const activeSongs = songs.filter((s) => !s.deletedAt); diff --git a/apps/manacore/apps/web/src/lib/data/database.ts b/apps/manacore/apps/web/src/lib/data/database.ts index 8c73a8d1d..2303ec8ef 100644 --- a/apps/manacore/apps/web/src/lib/data/database.ts +++ b/apps/manacore/apps/web/src/lib/data/database.ts @@ -65,7 +65,7 @@ db.version(1).stores({ zitareLists: 'id', zitareListTags: 'id, listId, tagId, [listId+tagId]', - // ─── Mukke (appId: 'mukke') ─── + // ─── Music (appId: 'music') ─── songs: 'id, artist, album, genre, favorite, title', mukkePlaylists: 'id, name', playlistSongs: 'id, playlistId, songId, sortOrder, [playlistId+sortOrder]', @@ -242,6 +242,160 @@ db.version(2) }); }); +// ─── Version 3: Unified Time Model (timeBlocks) ───────────── +// Adds timeBlocks table, updates indexes on events/timeEntries/tasks/habitLogs, +// and migrates existing time data into timeBlocks. + +db.version(3) + .stores({ + // New tables + timeBlocks: + 'id, startDate, kind, type, sourceModule, sourceId, [sourceModule+sourceId], [type+startDate], [kind+startDate]', + timeBlockTags: 'id, blockId, tagId, [blockId+tagId]', + + // Updated indexes (timeBlockId / scheduledBlockId added) + events: 'id, calendarId, timeBlockId', + timeEntries: 'id, projectId, clientId, timeBlockId, guildId, visibility', + tasks: + 'id, dueDate, isCompleted, priority, order, projectId, scheduledBlockId, [isCompleted+order], [projectId+order]', + habitLogs: 'id, habitId, timeBlockId, [habitId+timeBlockId]', + }) + .upgrade(async (tx) => { + const timeBlocksTable = tx.table('timeBlocks'); + + // 1. Migrate calendar events → timeBlocks + const events = await tx.table('events').toArray(); + for (const event of events) { + if (!event.startDate) continue; + const blockId = crypto.randomUUID(); + await timeBlocksTable.add({ + id: blockId, + startDate: event.startDate, + endDate: event.endDate ?? null, + allDay: event.allDay ?? false, + isLive: false, + timezone: null, + recurrenceRule: event.recurrenceRule ?? null, + kind: 'scheduled', + type: 'event', + sourceModule: 'calendar', + sourceId: event.id, + linkedBlockId: null, + title: event.title ?? '', + description: event.description ?? null, + color: event.color ?? null, + icon: null, + projectId: null, + createdAt: event.createdAt ?? new Date().toISOString(), + updatedAt: event.updatedAt ?? new Date().toISOString(), + deletedAt: event.deletedAt ?? null, + }); + await tx.table('events').update(event.id, { timeBlockId: blockId }); + } + + // 2. Migrate time entries → timeBlocks + const entries = await tx.table('timeEntries').toArray(); + for (const entry of entries) { + if (!entry.date && !entry.startTime) continue; // skip entries with no date at all + const blockId = crypto.randomUUID(); + const startDate = entry.startTime ?? `${entry.date}T00:00:00.000Z`; + await timeBlocksTable.add({ + id: blockId, + startDate, + endDate: entry.endTime ?? null, + allDay: false, + isLive: entry.isRunning ?? false, + timezone: null, + recurrenceRule: null, + kind: 'logged', + type: 'timeEntry', + sourceModule: 'times', + sourceId: entry.id, + linkedBlockId: null, + title: entry.description || 'Time Entry', + description: null, + color: null, + icon: null, + projectId: entry.projectId ?? null, + createdAt: entry.createdAt ?? new Date().toISOString(), + updatedAt: entry.updatedAt ?? new Date().toISOString(), + deletedAt: entry.deletedAt ?? null, + }); + await tx.table('timeEntries').update(entry.id, { timeBlockId: blockId }); + } + + // 3. Migrate habit logs → timeBlocks + const logs = await tx.table('habitLogs').toArray(); + const habitsById = new Map>(); + const allHabits = await tx.table('habits').toArray(); + for (const h of allHabits) habitsById.set(h.id as string, h); + + for (const log of logs) { + if (!log.timestamp) continue; + const blockId = crypto.randomUUID(); + const habit = habitsById.get(log.habitId as string); + await timeBlocksTable.add({ + id: blockId, + startDate: log.timestamp, + endDate: null, + allDay: false, + isLive: false, + timezone: null, + recurrenceRule: null, + kind: 'logged', + type: 'habit', + sourceModule: 'habits', + sourceId: log.id, + linkedBlockId: null, + title: (habit?.title as string) ?? 'Habit', + description: null, + color: (habit?.color as string) ?? null, + icon: (habit?.icon as string) ?? null, + projectId: null, + createdAt: log.createdAt ?? new Date().toISOString(), + updatedAt: log.updatedAt ?? log.createdAt ?? new Date().toISOString(), + deletedAt: log.deletedAt ?? null, + }); + await tx.table('habitLogs').update(log.id, { timeBlockId: blockId }); + } + + // 4. Migrate scheduled tasks → timeBlocks + const tasks = await tx.table('tasks').toArray(); + for (const task of tasks) { + if (!task.scheduledDate) continue; + const blockId = crypto.randomUUID(); + const startISO = task.scheduledStartTime + ? `${task.scheduledDate}T${task.scheduledStartTime}:00` + : `${task.scheduledDate}T09:00:00`; + const durationMs = task.estimatedDuration ? task.estimatedDuration * 1000 : 3600000; // default 1h + const endISO = new Date(new Date(startISO).getTime() + durationMs).toISOString(); + + await timeBlocksTable.add({ + id: blockId, + startDate: startISO, + endDate: endISO, + allDay: !task.scheduledStartTime, + isLive: false, + timezone: null, + recurrenceRule: null, + kind: 'scheduled', + type: 'task', + sourceModule: 'todo', + sourceId: task.id, + linkedBlockId: null, + title: task.title ?? '', + description: null, + color: null, + icon: null, + projectId: task.projectId ?? null, + createdAt: task.createdAt ?? new Date().toISOString(), + updatedAt: task.updatedAt ?? new Date().toISOString(), + deletedAt: task.deletedAt ?? null, + }); + await tx.table('tasks').update(task.id, { scheduledBlockId: blockId }); + } + }); + // ─── Sync App Map ────────────────────────────────────────── // Maps each table to its appId for sync routing. // The SyncEngine uses this to group pending changes and push to /sync/{appId}. @@ -255,7 +409,7 @@ export const SYNC_APP_MAP: Record = { picture: ['images', 'boards', 'boardItems', 'imageTags'], cards: ['cardDecks', 'cards', 'deckTags'], zitare: ['zitareFavorites', 'zitareLists', 'zitareListTags'], - mukke: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers', 'songTags'], + music: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers', 'songTags'], storage: ['files', 'storageFolders', 'fileTags'], presi: ['presiDecks', 'slides', 'presiDeckTags'], inventar: ['invCollections', 'invItems', 'invLocations', 'invCategories', 'invItemTags'], @@ -288,6 +442,7 @@ export const SYNC_APP_MAP: Record = { places: ['places', 'locationLogs', 'placeTags'], tags: ['globalTags', 'tagGroups'], links: ['manaLinks'], + timeblocks: ['timeBlocks', 'timeBlockTags'], }; // ─── Reverse Map: Table → AppId ──────────────────────────── @@ -313,7 +468,7 @@ export const TABLE_TO_SYNC_NAME: Record = { // zitare zitareFavorites: 'favorites', zitareLists: 'lists', - // mukke + // music mukkePlaylists: 'playlists', mukkeProjects: 'projects', // storage diff --git a/apps/manacore/apps/web/src/lib/data/time-blocks/collections.ts b/apps/manacore/apps/web/src/lib/data/time-blocks/collections.ts new file mode 100644 index 000000000..c7f251773 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/time-blocks/collections.ts @@ -0,0 +1,87 @@ +/** + * TimeBlock module — collection accessors and guest seed data. + */ + +import { db } from '$lib/data/database'; +import type { LocalTimeBlock, LocalTimeBlockTag } from './types'; + +// ─── Collection Accessors ────────────────────────────────── + +export const timeBlockTable = db.table('timeBlocks'); +export const timeBlockTagTable = db.table('timeBlockTags'); + +// ─── Guest Seed ──────────────────────────────────────────── + +export const TIME_BLOCK_GUEST_SEED = { + timeBlocks: (() => { + const now = new Date(); + const nowISO = now.toISOString(); + const today10 = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0, 0); + const today11 = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 11, 0, 0); + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrow14 = new Date( + tomorrow.getFullYear(), + tomorrow.getMonth(), + tomorrow.getDate(), + 14, + 0, + 0 + ); + const tomorrow15 = new Date( + tomorrow.getFullYear(), + tomorrow.getMonth(), + tomorrow.getDate(), + 15, + 30, + 0 + ); + + return [ + { + id: 'sample-tb-event-1', + startDate: today10.toISOString(), + endDate: today11.toISOString(), + allDay: false, + isLive: false, + timezone: null, + recurrenceRule: null, + kind: 'scheduled' as const, + type: 'event' as const, + sourceModule: 'calendar' as const, + sourceId: 'sample-event-1', + linkedBlockId: null, + title: 'Willkommen bei Kalender!', + description: + 'Dies ist ein Beispieltermin. Tippe darauf, um ihn zu bearbeiten oder zu löschen.', + color: null, + icon: null, + projectId: null, + createdAt: nowISO, + updatedAt: nowISO, + }, + { + id: 'sample-tb-event-2', + startDate: tomorrow14.toISOString(), + endDate: tomorrow15.toISOString(), + allDay: false, + isLive: false, + timezone: null, + recurrenceRule: null, + kind: 'scheduled' as const, + type: 'event' as const, + sourceModule: 'calendar' as const, + sourceId: 'sample-event-2', + linkedBlockId: null, + title: 'Mittagessen mit Freunden', + description: null, + color: null, + icon: null, + projectId: null, + createdAt: nowISO, + updatedAt: nowISO, + }, + ] satisfies LocalTimeBlock[]; + })(), + timeBlockTags: [] as LocalTimeBlockTag[], +}; diff --git a/apps/manacore/apps/web/src/lib/data/time-blocks/index.ts b/apps/manacore/apps/web/src/lib/data/time-blocks/index.ts new file mode 100644 index 000000000..dc57cf0b6 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/time-blocks/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './collections'; +export * from './service'; +export * from './queries'; diff --git a/apps/manacore/apps/web/src/lib/data/time-blocks/queries.ts b/apps/manacore/apps/web/src/lib/data/time-blocks/queries.ts new file mode 100644 index 000000000..07bcb63bf --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/time-blocks/queries.ts @@ -0,0 +1,206 @@ +/** + * Reactive Queries & Pure Helpers for TimeBlocks. + * + * Uses Dexie liveQuery to automatically re-render when IndexedDB changes. + * Components call these hooks at init time; no manual fetch/refresh needed. + * + * Note: useLiveQueryWithDefault takes (querier, default) — no deps array. + * For parameterized queries, use raw liveQuery from Dexie instead. + */ + +import { liveQuery } from 'dexie'; +import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; +import { db } from '$lib/data/database'; +import type { + LocalTimeBlock, + TimeBlock, + TimeBlockKind, + TimeBlockType, + TimeBlockSourceModule, +} from './types'; +import { isSameDay, isWithinInterval } from 'date-fns'; + +// ─── Type Converter ────────────────────────────────────── + +export function toTimeBlock(local: LocalTimeBlock): TimeBlock { + return { + id: local.id, + startDate: local.startDate, + endDate: local.endDate ?? null, + allDay: local.allDay, + isLive: local.isLive, + timezone: local.timezone ?? null, + recurrenceRule: local.recurrenceRule ?? null, + kind: local.kind, + type: local.type, + sourceModule: local.sourceModule, + sourceId: local.sourceId, + linkedBlockId: local.linkedBlockId ?? null, + title: local.title, + description: local.description ?? null, + color: local.color ?? null, + icon: local.icon ?? null, + projectId: local.projectId ?? null, + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +// ─── Svelte 5 Reactive Hooks ───────────────────────────── + +/** All non-deleted timeBlocks. Auto-updates on change. */ +export function useAllTimeBlocks() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('timeBlocks').toArray(); + return locals.filter((b) => !b.deletedAt).map(toTimeBlock); + }, [] as TimeBlock[]); +} + +/** + * All non-deleted timeBlocks within a date range. + * Returns a raw Dexie liveQuery observable (use with $-subscribe in Svelte). + */ +export function timeBlocksInRange$(start: string, end: string) { + return liveQuery(async () => { + const locals = await db + .table('timeBlocks') + .where('startDate') + .between(start, end, true, true) + .toArray(); + return locals.filter((b) => !b.deletedAt).map(toTimeBlock); + }); +} + +/** TimeBlock(s) for a specific source record (raw observable). */ +export function timeBlocksBySource$(sourceModule: TimeBlockSourceModule, sourceId: string) { + return liveQuery(async () => { + const locals = await db + .table('timeBlocks') + .where('[sourceModule+sourceId]') + .equals([sourceModule, sourceId]) + .toArray(); + return locals.filter((b) => !b.deletedAt).map(toTimeBlock); + }); +} + +/** The currently live/running timeBlock (if any). */ +export function useLiveTimeBlock() { + return useLiveQueryWithDefault( + async () => { + // Can't index boolean in Dexie reliably, so scan and filter + const locals = await db.table('timeBlocks').toArray(); + const active = locals.find((b) => b.isLive && !b.deletedAt); + return active ? toTimeBlock(active) : null; + }, + null as TimeBlock | null + ); +} + +// ─── Pure Helpers ───────────────────────────────────────── + +/** Convert a date string or Date to a Date. */ +function toDate(dateStr: string | Date): Date { + return typeof dateStr === 'string' ? new Date(dateStr) : dateStr; +} + +/** Get timeBlocks for a specific day. */ +export function getBlocksForDay(blocks: TimeBlock[], date: Date): TimeBlock[] { + return blocks.filter((block) => { + const blockStart = toDate(block.startDate); + const blockEnd = block.endDate ? toDate(block.endDate) : blockStart; + + if (block.allDay) { + return ( + isWithinInterval(date, { start: blockStart, end: blockEnd }) || isSameDay(date, blockStart) + ); + } + + // Point events: match day of startDate + if (!block.endDate) { + return isSameDay(date, blockStart); + } + + // Range events: any overlap with the day + const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0); + const dayEnd = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); + return blockStart <= dayEnd && blockEnd >= dayStart; + }); +} + +/** Get timeBlocks within a time range. */ +export function getBlocksInRange(blocks: TimeBlock[], start: Date, end: Date): TimeBlock[] { + return blocks.filter((block) => { + const blockStart = toDate(block.startDate); + const blockEnd = block.endDate ? toDate(block.endDate) : blockStart; + return blockStart <= end && blockEnd >= start; + }); +} + +/** Sort timeBlocks by start time. */ +export function sortBlocksByTime(blocks: TimeBlock[]): TimeBlock[] { + return [...blocks].sort( + (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); +} + +/** Filter timeBlocks by kind. */ +export function filterBlocksByKind(blocks: TimeBlock[], kind: TimeBlockKind): TimeBlock[] { + return blocks.filter((b) => b.kind === kind); +} + +/** Filter timeBlocks by type. */ +export function filterBlocksByType(blocks: TimeBlock[], type: TimeBlockType): TimeBlock[] { + return blocks.filter((b) => b.type === type); +} + +/** Filter timeBlocks by visible types (for calendar filter toggles). */ +export function filterBlocksByVisibleTypes( + blocks: TimeBlock[], + visibleTypes: Set +): TimeBlock[] { + return blocks.filter((b) => visibleTypes.has(b.type)); +} + +/** Find overlapping timeBlocks for a given range. */ +export function findOverlaps( + blocks: TimeBlock[], + start: string, + end: string, + excludeId?: string +): TimeBlock[] { + const rangeStart = new Date(start); + const rangeEnd = new Date(end); + + return blocks.filter((block) => { + if (block.id === excludeId) return false; + if (block.allDay) return false; + + const blockStart = new Date(block.startDate); + const blockEnd = block.endDate ? new Date(block.endDate) : blockStart; + return blockStart < rangeEnd && blockEnd > rangeStart; + }); +} + +/** Get the raw wall-clock duration in seconds (derived from start/end). */ +export function getBlockDuration(block: TimeBlock): number { + if (!block.endDate) return 0; + return Math.max( + 0, + (new Date(block.endDate).getTime() - new Date(block.startDate).getTime()) / 1000 + ); +} + +/** Group timeBlocks by date string (YYYY-MM-DD). */ +export function groupBlocksByDate(blocks: TimeBlock[]): Map { + const map = new Map(); + for (const block of blocks) { + const dateKey = block.startDate.split('T')[0]; + const group = map.get(dateKey); + if (group) { + group.push(block); + } else { + map.set(dateKey, [block]); + } + } + return map; +} 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 new file mode 100644 index 000000000..74b4168e4 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/time-blocks/service.ts @@ -0,0 +1,75 @@ +/** + * TimeBlock Service — shared CRUD helper called by all module stores. + * + * Module stores create both their domain record and a timeBlock in the same + * Dexie transaction to keep them consistent. + */ + +import { db } from '$lib/data/database'; +import { timeBlockTable } from './collections'; +import type { LocalTimeBlock, CreateTimeBlockInput, UpdateTimeBlockInput } from './types'; + +/** Create a new timeBlock and return its ID. */ +export async function createBlock(input: CreateTimeBlockInput): Promise { + const now = new Date().toISOString(); + const id = crypto.randomUUID(); + + const block: LocalTimeBlock = { + id, + startDate: input.startDate, + endDate: input.endDate ?? null, + allDay: input.allDay ?? false, + isLive: input.isLive ?? false, + timezone: input.timezone ?? null, + recurrenceRule: input.recurrenceRule ?? null, + kind: input.kind, + type: input.type, + sourceModule: input.sourceModule, + sourceId: input.sourceId, + linkedBlockId: input.linkedBlockId ?? null, + title: input.title, + description: input.description ?? null, + color: input.color ?? null, + icon: input.icon ?? null, + projectId: input.projectId ?? null, + createdAt: now, + updatedAt: now, + }; + + await timeBlockTable.add(block); + return id; +} + +/** Update an existing timeBlock. */ +export async function updateBlock(id: string, input: UpdateTimeBlockInput): Promise { + const now = new Date().toISOString(); + await timeBlockTable.update(id, { + ...input, + updatedAt: now, + }); +} + +/** Soft-delete a timeBlock. */ +export async function deleteBlock(id: string): Promise { + const now = new Date().toISOString(); + await timeBlockTable.update(id, { + deletedAt: now, + updatedAt: now, + }); +} + +/** Link a scheduled block to a logged block (plan vs. reality). */ +export async function linkBlocks(scheduledId: string, loggedId: string): Promise { + const now = new Date().toISOString(); + await db.transaction('rw', timeBlockTable, async () => { + await timeBlockTable.update(scheduledId, { linkedBlockId: loggedId, updatedAt: now }); + await timeBlockTable.update(loggedId, { linkedBlockId: scheduledId, updatedAt: now }); + }); +} + +/** Get a single timeBlock by ID. */ +export async function getBlock(id: string): Promise { + const block = await timeBlockTable.get(id); + if (block?.deletedAt) return undefined; + return block; +} diff --git a/apps/manacore/apps/web/src/lib/data/time-blocks/types.ts b/apps/manacore/apps/web/src/lib/data/time-blocks/types.ts new file mode 100644 index 000000000..0579549a6 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/time-blocks/types.ts @@ -0,0 +1,110 @@ +/** + * Unified Time Model — TimeBlock types. + * + * A TimeBlock represents any time interval across all modules. + * Domain-specific data stays on each module's tables; the TimeBlock + * owns the time dimension (start, end, recurrence, live status). + */ + +import type { BaseRecord } from '@manacore/local-store'; + +// ─── Enums ─────────────────────────────────────────────── + +export type TimeBlockKind = 'scheduled' | 'logged'; + +export type TimeBlockType = 'event' | 'task' | 'habit' | 'timeEntry' | 'focus' | 'break'; + +export type TimeBlockSourceModule = 'calendar' | 'todo' | 'times' | 'habits'; + +// ─── Local Record Types (Dexie) ────────────────────────── + +export interface LocalTimeBlock extends BaseRecord { + // Time + startDate: string; // ISO — always set + endDate: string | null; // ISO — null = point-event or live timer + allDay: boolean; + isLive: boolean; // timer/tracking currently running + timezone?: string | null; + recurrenceRule?: string | null; + + // Classification + kind: TimeBlockKind; + type: TimeBlockType; + + // Link to source module + sourceModule: TimeBlockSourceModule; + sourceId: string; + linkedBlockId?: string | null; // scheduled → logged link + + // Display (denormalized for calendar rendering without joins) + title: string; + description?: string | null; + color?: string | null; + icon?: string | null; + projectId?: string | null; +} + +export interface LocalTimeBlockTag extends BaseRecord { + blockId: string; + tagId: string; +} + +// ─── Domain Types (returned by queries, used by UI) ────── + +export interface TimeBlock { + id: string; + startDate: string; + endDate: string | null; + allDay: boolean; + isLive: boolean; + timezone: string | null; + recurrenceRule: string | null; + kind: TimeBlockKind; + type: TimeBlockType; + sourceModule: TimeBlockSourceModule; + sourceId: string; + linkedBlockId: string | null; + title: string; + description: string | null; + color: string | null; + icon: string | null; + projectId: string | null; + createdAt: string; + updatedAt: string; +} + +// ─── Input Types ───────────────────────────────────────── + +export interface CreateTimeBlockInput { + startDate: string; + endDate?: string | null; + allDay?: boolean; + isLive?: boolean; + timezone?: string | null; + recurrenceRule?: string | null; + kind: TimeBlockKind; + type: TimeBlockType; + sourceModule: TimeBlockSourceModule; + sourceId: string; + linkedBlockId?: string | null; + title: string; + description?: string | null; + color?: string | null; + icon?: string | null; + projectId?: string | null; +} + +export interface UpdateTimeBlockInput { + startDate?: string; + endDate?: string | null; + allDay?: boolean; + isLive?: boolean; + timezone?: string | null; + recurrenceRule?: string | null; + linkedBlockId?: string | null; + title?: string; + description?: string | null; + color?: string | null; + icon?: string | null; + projectId?: string | null; +} diff --git a/apps/manacore/apps/web/src/lib/modules/calendar/ListView.svelte b/apps/manacore/apps/web/src/lib/modules/calendar/ListView.svelte index 4970a553d..e18757c16 100644 --- a/apps/manacore/apps/web/src/lib/modules/calendar/ListView.svelte +++ b/apps/manacore/apps/web/src/lib/modules/calendar/ListView.svelte @@ -4,10 +4,10 @@ Clicking an event opens the detail view. -->