diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte index 6e35a9f00..49f6471f4 100644 --- a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/CalendarEventsWidget.svelte @@ -1,56 +1,25 @@
@@ -86,18 +55,20 @@ 🗓️ {$_('dashboard.widgets.calendar.title')} - {#if (data || []).length > 0} + {#if (events.value ?? []).length > 0} - {(data || []).length} + {(events.value ?? []).length} {/if}
- {#if state === 'loading'} - - {:else if state === 'error'} - - {:else if (data || []).length === 0} + {#if events.loading} +
+ {#each Array(4) as _} +
+ {/each} +
+ {:else if (events.value ?? []).length === 0}
📅

@@ -106,7 +77,7 @@

{:else}
- {#each displayedEvents as event} + {#each displayedEvents as event (event.id)}
/** - * ContactsFavoritesWidget - Favorite contacts + * ContactsFavoritesWidget - Favorite contacts (local-first) + * + * Reads directly from Contacts' IndexedDB via cross-app reader. + * Reactive: auto-updates when contacts change (sync, other tabs). */ - import { onMount } from 'svelte'; import { _ } from 'svelte-i18n'; - import { contactsService, type Contact } from '$lib/api/services'; + import { useFavoriteContacts } from '$lib/data/cross-app-queries'; + import type { CrossAppContact } from '$lib/data/cross-app-stores'; import { APP_URLS } from '@manacore/shared-branding'; - import WidgetSkeleton from '../WidgetSkeleton.svelte'; - import WidgetError from '../WidgetError.svelte'; - - let state = $state<'loading' | 'success' | 'error'>('loading'); - let data = $state([]); - let error = $state(null); - let retrying = $state(false); - let retryCount = $state(0); const MAX_DISPLAY = 5; + const contacts = useFavoriteContacts(MAX_DISPLAY); - // Determine app URL based on environment const isDev = typeof window !== 'undefined' && window.location.hostname === 'localhost'; const contactsUrl = isDev ? APP_URLS.contacts.dev : APP_URLS.contacts.prod; - async function load() { - state = 'loading'; - retrying = true; - - const result = await contactsService.getFavoriteContacts(MAX_DISPLAY); - - if (result.data) { - data = result.data; - state = 'success'; - retryCount = 0; - } else { - error = result.error; - state = 'error'; - - // Don't retry if service is unavailable (network error) - const isServiceUnavailable = error?.includes('nicht erreichbar'); - if (!isServiceUnavailable && retryCount < 3) { - retryCount++; - setTimeout(load, 5000 * retryCount); - } - } - - retrying = false; + function getDisplayName(contact: CrossAppContact): string { + const parts = [contact.firstName, contact.lastName].filter(Boolean); + return parts.length > 0 ? parts.join(' ') : contact.email || 'Unbekannt'; } - onMount(load); - - function getDisplayName(contact: Contact): string { - return contactsService.getDisplayName(contact); - } - - function getInitials(contact: Contact): string { + function getInitials(contact: CrossAppContact): string { const name = getDisplayName(contact); const parts = name.split(' '); if (parts.length >= 2) { @@ -66,18 +35,20 @@

- =e + 👥 {$_('dashboard.widgets.contacts.title')}

- {#if state === 'loading'} - - {:else if state === 'error'} - - {:else if data.length === 0} + {#if contacts.loading} +
+ {#each Array(4) as _} +
+ {/each} +
+ {:else if (contacts.value ?? []).length === 0}
-
=�
+
👤

{$_('dashboard.widgets.contacts.empty')}

@@ -92,7 +63,7 @@
{:else}
- {#each data as contact} + {#each contacts.value ?? [] as contact (contact.id)} {/if}
- P + {/each}
@@ -129,7 +100,7 @@ rel="noopener" class="mt-3 block text-center text-sm text-primary hover:underline" > - {$_('dashboard.widgets.contacts.view_all')} � + {$_('dashboard.widgets.contacts.view_all')} → {/if}
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte index 7aaebf7f3..613c6ba5d 100644 --- a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte +++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/TasksTodayWidget.svelte @@ -1,18 +1,19 @@
@@ -86,18 +56,20 @@ 📅 {$_('dashboard.widgets.tasks_upcoming.title')} - {#if data.length > 0} + {#if (tasks.value ?? []).length > 0} - {data.length} + {(tasks.value ?? []).length} {/if}
- {#if state === 'loading'} - - {:else if state === 'error'} - - {:else if data.length === 0} + {#if tasks.loading} +
+ {#each Array(4) as _} +
+ {/each} +
+ {:else if (tasks.value ?? []).length === 0}
📭

@@ -106,7 +78,7 @@

{:else}
- {#each displayedTasks as task} + {#each displayedTasks as task (task.id)}

{task.title}

- - {#if task.labels && task.labels.length > 0} -
- {#each task.labels.slice(0, 2) as label} - - - {label.name} - - {/each} -
- {/if}
@@ -145,7 +103,7 @@ task.dueDate ) ? 'bg-red-500/10 text-red-500' - : isToday(task.dueDate) + : isTodayDate(task.dueDate) ? 'bg-orange-500/10 text-orange-500' : 'bg-muted text-muted-foreground'}" > 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 new file mode 100644 index 000000000..3205c919f --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/cross-app-queries.ts @@ -0,0 +1,114 @@ +/** + * Cross-App Reactive Queries + * + * Live queries that read directly from other apps' IndexedDB databases. + * Auto-update when data changes (local writes, sync, other tabs). + * Replaces REST API polling with instant reactive reads. + */ + +import { useLiveQueryWithDefault } from '@manacore/local-store/svelte'; +import { + crossTaskCollection, + crossEventCollection, + crossContactCollection, + type CrossAppTask, + type CrossAppEvent, + type CrossAppContact, +} from './cross-app-stores'; + +// ─── Todo Queries ─────────────────────────────────────────── + +/** All open (incomplete) tasks, sorted by order. */ +export function useOpenTasks() { + return useLiveQueryWithDefault(async () => { + const all = await crossTaskCollection.getAll(undefined, { + sortBy: 'order', + sortDirection: 'asc', + }); + return all.filter((t) => !t.isCompleted && !t.deletedAt); + }, [] as CrossAppTask[]); +} + +/** Tasks due today or overdue. */ +export function useTodayTasks() { + return useLiveQueryWithDefault(async () => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const todayStr = today.toISOString().slice(0, 10); + + const all = await crossTaskCollection.getAll(undefined, { + sortBy: 'order', + sortDirection: 'asc', + }); + + return all.filter((t) => { + if (t.isCompleted || t.deletedAt) return false; + if (!t.dueDate) return false; + const due = t.dueDate.slice(0, 10); + return due <= todayStr; + }); + }, [] as CrossAppTask[]); +} + +/** Tasks upcoming in the next N days. */ +export function useUpcomingTasks(days = 7) { + return useLiveQueryWithDefault(async () => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const todayStr = today.toISOString().slice(0, 10); + + const future = new Date(today); + future.setDate(future.getDate() + days); + const futureStr = future.toISOString().slice(0, 10); + + const all = await crossTaskCollection.getAll(undefined, { + sortBy: 'dueDate', + sortDirection: 'asc', + }); + + return all.filter((t) => { + if (t.isCompleted || t.deletedAt) return false; + if (!t.dueDate) return false; + const due = t.dueDate.slice(0, 10); + return due > todayStr && due <= futureStr; + }); + }, [] as CrossAppTask[]); +} + +// ─── Calendar Queries ─────────────────────────────────────── + +/** Events in the next N days. */ +export function useUpcomingEvents(days = 7) { + return useLiveQueryWithDefault(async () => { + const now = new Date(); + const future = new Date(now); + future.setDate(future.getDate() + days); + + const nowStr = now.toISOString(); + const futureStr = future.toISOString(); + + const all = await crossEventCollection.getAll(undefined, { + sortBy: 'startDate', + sortDirection: 'asc', + }); + + return all.filter((e) => { + if (e.deletedAt) return false; + return e.startDate >= nowStr && e.startDate <= futureStr; + }); + }, [] as CrossAppEvent[]); +} + +// ─── Contacts Queries ─────────────────────────────────────── + +/** Favorite contacts. */ +export function useFavoriteContacts(limit = 5) { + return useLiveQueryWithDefault(async () => { + const all = await crossContactCollection.getAll(undefined, { + sortBy: 'firstName', + sortDirection: 'asc', + }); + + return all.filter((c) => c.isFavorite && !c.isArchived && !c.deletedAt).slice(0, limit); + }, [] as CrossAppContact[]); +} diff --git a/apps/manacore/apps/web/src/lib/data/cross-app-stores.ts b/apps/manacore/apps/web/src/lib/data/cross-app-stores.ts new file mode 100644 index 000000000..e9ad9fc74 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/cross-app-stores.ts @@ -0,0 +1,134 @@ +/** + * Cross-App IndexedDB Readers + * + * Opens other apps' IndexedDB databases for direct read access. + * All apps on the same origin share IndexedDB, so ManaCore can + * read from manacore-todo, manacore-calendar, etc. directly. + * + * Data is reactive via Dexie's liveQuery — updates when any app + * writes to the same database (including via sync). + * + * NOTE: These stores are read-only from ManaCore's perspective. + * Writes that need sync should go through the owning app's collections. + */ + +import { createLocalStore, type BaseRecord } from '@manacore/local-store'; + +// ─── Todo Types ───────────────────────────────────────────── + +export interface CrossAppTask extends BaseRecord { + title: string; + description?: string; + projectId?: string | null; + priority: 'low' | 'medium' | 'high' | 'urgent'; + isCompleted: boolean; + completedAt?: string | null; + dueDate?: string | null; + dueTime?: string | null; + scheduledDate?: string | null; + estimatedDuration?: number | null; + order: number; + subtasks?: { id: string; title: string; isCompleted: boolean; order: number }[] | null; + labels?: { id: string; name: string; color: string }[]; +} + +export interface CrossAppProject extends BaseRecord { + name: string; + color: string; + icon?: string | null; + order: number; + isArchived: boolean; + isDefault: boolean; +} + +// ─── Calendar Types ───────────────────────────────────────── + +export interface CrossAppEvent extends BaseRecord { + calendarId: string; + title: string; + description?: string | null; + startDate: string; + endDate: string; + allDay: boolean; + location?: string | null; + recurrenceRule?: string | null; + color?: string | null; +} + +export interface CrossAppCalendar extends BaseRecord { + name: string; + color: string; + isDefault: boolean; + isVisible: boolean; +} + +// ─── Contacts Types ───────────────────────────────────────── + +export interface CrossAppContact extends BaseRecord { + firstName?: string; + lastName?: string; + email?: string; + phone?: string; + company?: string; + jobTitle?: string; + photoUrl?: string; + isFavorite?: boolean; + isArchived?: boolean; +} + +// ─── Store Instances ──────────────────────────────────────── +// These open existing IndexedDB databases created by other apps. +// No sync config — ManaCore only reads, the owning app handles sync. + +export const todoReader = createLocalStore({ + appId: 'todo', + collections: [ + { + name: 'tasks', + indexes: [ + 'projectId', + 'dueDate', + 'isCompleted', + 'priority', + 'order', + '[isCompleted+order]', + '[projectId+order]', + ], + }, + { + name: 'projects', + indexes: ['order', 'isArchived'], + }, + ], +}); + +export const calendarReader = createLocalStore({ + appId: 'calendar', + collections: [ + { + name: 'events', + indexes: ['calendarId', 'startDate', 'endDate', 'allDay', '[calendarId+startDate]'], + }, + { + name: 'calendars', + indexes: ['isDefault', 'isVisible'], + }, + ], +}); + +export const contactsReader = createLocalStore({ + appId: 'contacts', + collections: [ + { + name: 'contacts', + indexes: ['firstName', 'lastName', 'email', 'company', 'isFavorite', 'isArchived'], + }, + ], +}); + +// Typed collection accessors +export const crossTaskCollection = todoReader.collection('tasks'); +export const crossProjectCollection = todoReader.collection('projects'); +export const crossEventCollection = calendarReader.collection('events'); +export const crossCalendarCollection = calendarReader.collection('calendars'); +export const crossContactCollection = contactsReader.collection('contacts'); diff --git a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte index 30163a97f..21a205ae5 100644 --- a/apps/manacore/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/+layout.svelte @@ -10,6 +10,7 @@ import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui'; import { tagLocalStore, tagMutations, useAllTags } from '$lib/stores/tags.svelte'; import { manacoreStore } from '$lib/data/local-store'; + import { todoReader, calendarReader, contactsReader } from '$lib/data/cross-app-stores'; import { dashboardStore } from '$lib/stores/dashboard.svelte'; import { THEME_DEFINITIONS, @@ -203,7 +204,14 @@ } // Initialize local-first databases (opens IndexedDB, seeds guest data) - await Promise.all([manacoreStore.initialize(), tagLocalStore.initialize()]); + await Promise.all([ + manacoreStore.initialize(), + tagLocalStore.initialize(), + // Cross-app readers (read-only, no sync — owning apps handle sync) + todoReader.initialize(), + calendarReader.initialize(), + contactsReader.initialize(), + ]); // Start syncing to server const getToken = () => authStore.getValidToken();