diff --git a/apps/todo/apps/web/src/lib/api/network.ts b/apps/todo/apps/web/src/lib/api/network.ts deleted file mode 100644 index 63fcfd96b..000000000 --- a/apps/todo/apps/web/src/lib/api/network.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Network Graph API Client - */ - -import { apiClient } from './client'; - -export interface NetworkTag { - id: string; - name: string; - color: string | null; -} - -export interface NetworkNode { - id: string; - name: string; - photoUrl: string | null; - company: string | null; // Project name - isFavorite: boolean; - tags: NetworkTag[]; - connectionCount: number; -} - -export interface NetworkLink { - source: string; - target: string; - type: 'tag'; - strength: number; - sharedTags: string[]; -} - -export interface NetworkGraphResponse { - nodes: NetworkNode[]; - links: NetworkLink[]; -} - -export const networkApi = { - /** - * Get the network graph of tasks connected by shared labels - */ - async getGraph(): Promise { - return apiClient.get('/api/v1/network/graph'); - }, -}; diff --git a/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte b/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte index 81739d864..85e770b05 100644 --- a/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte +++ b/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte @@ -1,6 +1,5 @@ diff --git a/apps/todo/apps/web/src/lib/components/kanban/KanbanBoard.svelte b/apps/todo/apps/web/src/lib/components/kanban/KanbanBoard.svelte index a2c8d394f..f861fb60c 100644 --- a/apps/todo/apps/web/src/lib/components/kanban/KanbanBoard.svelte +++ b/apps/todo/apps/web/src/lib/components/kanban/KanbanBoard.svelte @@ -79,7 +79,12 @@ // Get projectId from current board if available const currentBoard = kanbanStore.currentBoard; const taskProjectId = currentBoard?.projectId ?? projectId; - await kanbanStore.createTaskInColumn(columnId, title, taskProjectId ?? undefined); + const result = await kanbanStore.createTaskInColumn(columnId, title, taskProjectId ?? undefined); + + // Show auth gate if authentication required (demo mode) + if (result && 'error' in result && result.error === 'auth_required') { + window.dispatchEvent(new CustomEvent('show-auth-gate')); + } } async function handleTaskMove(taskId: string, toColumnId: string, order: number) { diff --git a/apps/todo/apps/web/src/lib/components/kanban/KanbanColumn.svelte b/apps/todo/apps/web/src/lib/components/kanban/KanbanColumn.svelte index 8e366a5e2..e356a58c2 100644 --- a/apps/todo/apps/web/src/lib/components/kanban/KanbanColumn.svelte +++ b/apps/todo/apps/web/src/lib/components/kanban/KanbanColumn.svelte @@ -62,10 +62,16 @@ } async function handleToggleComplete(task: Task) { + let result; if (task.isCompleted) { - await tasksStore.uncompleteTask(task.id); + result = await tasksStore.uncompleteTask(task.id); } else { - await tasksStore.completeTask(task.id); + result = await tasksStore.completeTask(task.id); + } + + // Show auth gate if authentication required (demo mode) + if (result && 'error' in result && result.error === 'auth_required') { + window.dispatchEvent(new CustomEvent('show-auth-gate')); } } @@ -85,7 +91,12 @@ if (data.metadata !== undefined) updateData.metadata = data.metadata; if (data.labels !== undefined) updateData.labelIds = data.labels?.map((l) => l.id); - await tasksStore.updateTask(task.id, updateData); + const result = await tasksStore.updateTask(task.id, updateData); + + // Show auth gate if authentication required (demo mode) + if (result && 'error' in result && result.error === 'auth_required') { + window.dispatchEvent(new CustomEvent('show-auth-gate')); + } } async function handleDeleteTask(task: Task) { diff --git a/apps/todo/apps/web/src/lib/components/statistics/ActivityHeatmap.svelte b/apps/todo/apps/web/src/lib/components/statistics/ActivityHeatmap.svelte deleted file mode 100644 index 958fac1c1..000000000 --- a/apps/todo/apps/web/src/lib/components/statistics/ActivityHeatmap.svelte +++ /dev/null @@ -1,299 +0,0 @@ - - -
-

Aktivität

- -
- - - {#each monthLabels as label} - - {label.month} - - {/each} - - - {#each DAY_LABELS as label, i} - {#if label} - - {label} - - {/if} - {/each} - - - {#each weeks as week, weekIndex} - {#each week as day, dayIndex} - {#if day.date} - - {formatTooltip(day)} - - {:else} - - {/if} - {/each} - {/each} - -
- - -
- Weniger -
-
-
-
-
-
-
- Mehr -
-
- - diff --git a/apps/todo/apps/web/src/lib/components/statistics/PriorityDonutChart.svelte b/apps/todo/apps/web/src/lib/components/statistics/PriorityDonutChart.svelte deleted file mode 100644 index 1b95be66a..000000000 --- a/apps/todo/apps/web/src/lib/components/statistics/PriorityDonutChart.svelte +++ /dev/null @@ -1,248 +0,0 @@ - - -
-

Prioritäten

- - -
- - {#each arcs as arc} - (hoveredSegment = arc.priority)} - onmouseleave={() => (hoveredSegment = null)} - role="graphics-symbol" - aria-label="{PRIORITY_LABELS[arc.priority]}: {arc.count}" - > - {PRIORITY_LABELS[arc.priority]}: {arc.count} ({arc.percentage}%) - - {/each} - - - - {total} - - Aktiv - -
- - -
- {#each data as item} -
(hoveredSegment = item.priority)} - onmouseleave={() => (hoveredSegment = null)} - role="button" - tabindex="0" - > - - {PRIORITY_LABELS[item.priority]} - {item.count} -
- {/each} -
-
- - diff --git a/apps/todo/apps/web/src/lib/components/statistics/ProjectProgressBars.svelte b/apps/todo/apps/web/src/lib/components/statistics/ProjectProgressBars.svelte deleted file mode 100644 index c6060dac1..000000000 --- a/apps/todo/apps/web/src/lib/components/statistics/ProjectProgressBars.svelte +++ /dev/null @@ -1,192 +0,0 @@ - - -
-

Projekt-Fortschritt

- - {#if sortedData.length === 0} -

Keine Projekte mit Aufgaben

- {:else} -
- {#each sortedData as project} -
-
-
- - {project.projectName} -
- - {project.completed}/{project.total} - -
- -
-
- - {#if project.completed > 0} -
- {/if} - - - {#if project.inProgress > 0} -
- {/if} -
- - {project.percentage}% -
-
- {/each} -
- {/if} -
- - diff --git a/apps/todo/apps/web/src/lib/components/statistics/StatsOverview.svelte b/apps/todo/apps/web/src/lib/components/statistics/StatsOverview.svelte deleted file mode 100644 index 8d701f95c..000000000 --- a/apps/todo/apps/web/src/lib/components/statistics/StatsOverview.svelte +++ /dev/null @@ -1,196 +0,0 @@ - - -
-
-
- -
-
- {completedToday} - Heute erledigt -
-
- -
-
- -
-
- {completedThisWeek} - Diese Woche -
-
- -
-
- -
-
- {activeTasks} - Aktive Tasks -
-
- -
0} - class:stat-card-neutral={overdueTasks === 0} - > -
- -
-
- {overdueTasks} - Überfällig -
-
- -
-
- -
-
- {completionRate}% - Abschlussrate -
-
- - {#if storyPointsThisWeek > 0} -
-
- -
-
- {storyPointsThisWeek} - Story Points -
-
- {/if} -
- - diff --git a/apps/todo/apps/web/src/lib/components/statistics/WeeklyTrendChart.svelte b/apps/todo/apps/web/src/lib/components/statistics/WeeklyTrendChart.svelte deleted file mode 100644 index 7b6d0415f..000000000 --- a/apps/todo/apps/web/src/lib/components/statistics/WeeklyTrendChart.svelte +++ /dev/null @@ -1,214 +0,0 @@ - - -
-

Trend (letzte 4 Wochen)

- - - - {#each yTicks as tick} - - {/each} - - - - - - - - - - - - - - - - {#each data as point, i} - - {point.count} Aufgaben am {point.date} - - {/each} - - - {#each yTicks as tick} - - {tick} - - {/each} - - - {#each xLabels as label} - - {label.label} - - {/each} - -
- - diff --git a/apps/todo/apps/web/src/lib/data/demo-tasks.ts b/apps/todo/apps/web/src/lib/data/demo-tasks.ts new file mode 100644 index 000000000..767feecd8 --- /dev/null +++ b/apps/todo/apps/web/src/lib/data/demo-tasks.ts @@ -0,0 +1,187 @@ +/** + * Demo Tasks - Static sample tasks for unauthenticated users + * + * Shows a realistic task list with various task types to demonstrate + * the app's capabilities without requiring login. + */ + +import type { Task } from '@todo/shared'; +import { addDays, format, subDays } from 'date-fns'; + +/** + * Generate demo tasks relative to the current date + */ +export function generateDemoTasks(): Task[] { + const now = new Date(); + const today = format(now, 'yyyy-MM-dd'); + const tomorrow = format(addDays(now, 1), 'yyyy-MM-dd'); + const dayAfterTomorrow = format(addDays(now, 2), 'yyyy-MM-dd'); + const nextWeek = format(addDays(now, 7), 'yyyy-MM-dd'); + const yesterday = format(subDays(now, 1), 'yyyy-MM-dd'); + + const demoTasks: Task[] = [ + // Overdue task + { + id: 'demo_1', + userId: 'demo', + title: 'Steuererklärung abgeben', + description: 'Alle Unterlagen zusammenstellen und online einreichen', + dueDate: yesterday, + priority: 'urgent', + status: 'pending', + isCompleted: false, + order: 0, + subtasks: [ + { id: 'sub_1_1', title: 'Belege sammeln', isCompleted: true, order: 0 }, + { id: 'sub_1_2', title: 'Formulare ausfüllen', isCompleted: true, order: 1 }, + { id: 'sub_1_3', title: 'Online einreichen', isCompleted: false, order: 2 }, + ], + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Today - high priority + { + id: 'demo_2', + userId: 'demo', + title: 'Präsentation vorbereiten', + description: 'Slides für das Team-Meeting erstellen', + dueDate: today, + priority: 'high', + status: 'in_progress', + isCompleted: false, + order: 1, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Today - medium priority + { + id: 'demo_3', + userId: 'demo', + title: 'E-Mails beantworten', + dueDate: today, + priority: 'medium', + status: 'pending', + isCompleted: false, + order: 2, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Tomorrow + { + id: 'demo_4', + userId: 'demo', + title: 'Arzttermin', + description: 'Jährliche Vorsorgeuntersuchung - Praxis Dr. Müller', + dueDate: tomorrow, + dueTime: '10:00', + priority: 'high', + status: 'pending', + isCompleted: false, + order: 3, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Day after tomorrow + { + id: 'demo_5', + userId: 'demo', + title: 'Einkaufsliste', + priority: 'low', + status: 'pending', + isCompleted: false, + order: 4, + dueDate: dayAfterTomorrow, + subtasks: [ + { id: 'sub_5_1', title: 'Milch', isCompleted: false, order: 0 }, + { id: 'sub_5_2', title: 'Brot', isCompleted: false, order: 1 }, + { id: 'sub_5_3', title: 'Obst', isCompleted: false, order: 2 }, + { id: 'sub_5_4', title: 'Gemüse', isCompleted: false, order: 3 }, + ], + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Next week + { + id: 'demo_6', + userId: 'demo', + title: 'Wohnung aufräumen', + description: 'Frühjahrsputz - alle Zimmer gründlich reinigen', + dueDate: nextWeek, + priority: 'medium', + status: 'pending', + isCompleted: false, + order: 5, + subtasks: [ + { id: 'sub_6_1', title: 'Küche', isCompleted: false, order: 0 }, + { id: 'sub_6_2', title: 'Bad', isCompleted: false, order: 1 }, + { id: 'sub_6_3', title: 'Wohnzimmer', isCompleted: false, order: 2 }, + { id: 'sub_6_4', title: 'Schlafzimmer', isCompleted: false, order: 3 }, + ], + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // No due date (inbox) + { + id: 'demo_7', + userId: 'demo', + title: 'Buch "Atomic Habits" lesen', + description: 'Kapitel 1-5 diese Woche', + priority: 'low', + status: 'pending', + isCompleted: false, + order: 6, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Completed task + { + id: 'demo_8', + userId: 'demo', + title: 'Fitnessstudio anmelden', + priority: 'medium', + status: 'completed', + isCompleted: true, + completedAt: subDays(now, 2).toISOString(), + order: 7, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Another completed + { + id: 'demo_9', + userId: 'demo', + title: 'Geburtstagsgeschenk kaufen', + description: 'Für Lisa - sie mag Bücher und Tee', + priority: 'high', + status: 'completed', + isCompleted: true, + completedAt: subDays(now, 1).toISOString(), + order: 8, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + // Work task + { + id: 'demo_10', + userId: 'demo', + title: 'Code Review für PR #42', + description: 'Feature-Branch von Max reviewen', + dueDate: tomorrow, + priority: 'medium', + status: 'pending', + isCompleted: false, + order: 9, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }, + ]; + + return demoTasks; +} + +/** + * Check if a task ID is a demo task + */ +export function isDemoTask(id: string): boolean { + return id.startsWith('demo_'); +} diff --git a/apps/todo/apps/web/src/lib/stores/index.ts b/apps/todo/apps/web/src/lib/stores/index.ts index 91fc6e843..f0664ad7c 100644 --- a/apps/todo/apps/web/src/lib/stores/index.ts +++ b/apps/todo/apps/web/src/lib/stores/index.ts @@ -3,5 +3,4 @@ export { projectsStore } from './projects.svelte'; export { tasksStore } from './tasks.svelte'; export { labelsStore } from './labels.svelte'; export { viewStore } from './view.svelte'; -export { statisticsStore } from './statistics.svelte'; export type { ViewType, SortBy, SortOrder } from './view.svelte'; diff --git a/apps/todo/apps/web/src/lib/stores/kanban.svelte.ts b/apps/todo/apps/web/src/lib/stores/kanban.svelte.ts index 2cde6a356..1213ba838 100644 --- a/apps/todo/apps/web/src/lib/stores/kanban.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/kanban.svelte.ts @@ -5,6 +5,7 @@ import type { KanbanBoard, KanbanColumn, Task } from '@todo/shared'; import * as kanbanApi from '$lib/api/kanban'; import * as tasksApi from '$lib/api/tasks'; +import { authStore } from './auth.svelte'; // Board state let boards = $state([]); @@ -418,9 +419,16 @@ export const kanbanStore = { /** * Create a new task in a specific column + * Requires authentication - demo mode shows auth gate */ async createTaskInColumn(columnId: string, title: string, projectId?: string) { error = null; + + // Demo mode: require authentication + if (!authStore.isAuthenticated) { + return { error: 'auth_required' as const }; + } + try { // Find the column to get its default status const column = columns.find((c) => c.id === columnId); diff --git a/apps/todo/apps/web/src/lib/stores/network.svelte.ts b/apps/todo/apps/web/src/lib/stores/network.svelte.ts deleted file mode 100644 index 59d1ee396..000000000 --- a/apps/todo/apps/web/src/lib/stores/network.svelte.ts +++ /dev/null @@ -1,370 +0,0 @@ -/** - * Network Store - Manages network graph state with D3-force simulation - */ - -import { browser } from '$app/environment'; -import { networkApi } from '$lib/api/network'; -import type { NetworkNode, NetworkLink } from '$lib/api/network'; -import { - forceSimulation, - forceLink, - forceManyBody, - forceCenter, - forceCollide, - type Simulation, -} from 'd3-force'; -import type { - SimulationNode as SharedSimulationNode, - SimulationLink as SharedSimulationLink, -} from '@manacore/shared-ui'; - -// Re-export types from shared-ui for convenience -export type SimulationNode = SharedSimulationNode; -export type SimulationLink = SharedSimulationLink; - -// State -let nodes = $state([]); -let links = $state([]); -let loading = $state(false); -let error = $state(null); -let selectedNodeId = $state(null); -let simulation: Simulation | null = null; -let searchQuery = $state(''); -let filterTagId = $state(null); -let filterProject = $state(null); -let minStrength = $state(0); -let tickCounter = $state(0); -let simulationInitialized = false; -let dataLoaded = false; -let lastDimensions = { width: 0, height: 0 }; - -// Derived state for filtering -const filteredNodes = $derived.by(() => { - let result = nodes; - - // Search filter - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (node) => - node.name.toLowerCase().includes(query) || - node.subtitle?.toLowerCase().includes(query) || - node.tags.some((t) => t.name.toLowerCase().includes(query)) - ); - } - - // Tag filter - if (filterTagId) { - result = result.filter((node) => node.tags.some((t) => t.id === filterTagId)); - } - - // Project filter (uses subtitle field) - if (filterProject) { - result = result.filter((node) => node.subtitle === filterProject); - } - - return result; -}); - -const filteredLinks = $derived.by(() => { - const filteredNodeIds = new Set(filteredNodes.map((n) => n.id)); - return links.filter((link) => { - const sourceId = typeof link.source === 'string' ? link.source : link.source.id; - const targetId = typeof link.target === 'string' ? link.target : link.target.id; - // Check if both nodes are visible - if (!filteredNodeIds.has(sourceId) || !filteredNodeIds.has(targetId)) { - return false; - } - // Filter by minimum strength - if (minStrength > 0 && link.strength < minStrength) { - return false; - } - return true; - }); -}); - -// Get unique projects for filter dropdown -const uniqueProjects = $derived.by(() => { - const projects = new Set(); - for (const node of nodes) { - if (node.subtitle) { - projects.add(node.subtitle); - } - } - return Array.from(projects).sort(); -}); - -// Get unique tags for filter dropdown -const uniqueTags = $derived.by(() => { - const tagsMap = new Map(); - for (const node of nodes) { - for (const tag of node.tags) { - if (!tagsMap.has(tag.id)) { - tagsMap.set(tag.id, tag); - } - } - } - return Array.from(tagsMap.values()).sort((a, b) => a.name.localeCompare(b.name)); -}); - -export const networkStore = { - // Getters - get nodes() { - void tickCounter; - return filteredNodes; - }, - get allNodes() { - void tickCounter; - return nodes; - }, - get links() { - void tickCounter; - return filteredLinks; - }, - get allLinks() { - void tickCounter; - return links; - }, - get tick() { - return tickCounter; - }, - get loading() { - return loading; - }, - get error() { - return error; - }, - get selectedNodeId() { - return selectedNodeId; - }, - get selectedNode() { - return nodes.find((n) => n.id === selectedNodeId) || null; - }, - get searchQuery() { - return searchQuery; - }, - get filterTagId() { - return filterTagId; - }, - get filterProject() { - return filterProject; - }, - get minStrength() { - return minStrength; - }, - get uniqueProjects() { - return uniqueProjects; - }, - get uniqueTags() { - return uniqueTags; - }, - - /** - * Load network graph data from API - */ - async loadGraph(force = false) { - if (dataLoaded && !force) { - return; - } - - if (loading) { - return; - } - - loading = true; - error = null; - - if (simulation) { - simulation.stop(); - simulation = null; - } - simulationInitialized = false; - - try { - const response = await networkApi.getGraph(); - - // Convert to simulation nodes with subtitle for project - nodes = response.nodes.map((node) => ({ - ...node, - subtitle: node.company, // Map project name to subtitle - x: undefined, - y: undefined, - vx: undefined, - vy: undefined, - fx: null, - fy: null, - })); - - // Convert to simulation links - links = response.links.map((link) => ({ - source: link.source, - target: link.target, - type: link.type, - strength: link.strength, - sharedTags: link.sharedTags, - })); - - dataLoaded = true; - } catch (e) { - error = e instanceof Error ? e.message : 'Failed to load network graph'; - console.error('Failed to load network graph:', e); - } finally { - loading = false; - } - }, - - /** - * Initialize D3 force simulation - */ - initSimulation(width: number, height: number) { - if (!browser) return; - if (nodes.length === 0) return; - if (width <= 0 || height <= 0) return; - - if (simulationInitialized && simulation) { - if ( - Math.abs(lastDimensions.width - width) > 50 || - Math.abs(lastDimensions.height - height) > 50 - ) { - lastDimensions = { width, height }; - this.updateSimulationCenter(width, height); - } - return; - } - - if (simulation) { - simulation.stop(); - } - - lastDimensions = { width, height }; - - const centerX = width / 2; - const centerY = height / 2; - const radius = Math.min(width, height) / 3; - - nodes.forEach((node, i) => { - if (node.x === undefined || node.y === undefined) { - const angle = (i / nodes.length) * 2 * Math.PI; - const r = radius * (0.5 + Math.random() * 0.5); - node.x = centerX + r * Math.cos(angle); - node.y = centerY + r * Math.sin(angle); - } - }); - - simulation = forceSimulation(nodes) - .force( - 'link', - forceLink(links) - .id((d) => d.id) - .distance(100) - .strength(0.5) - ) - .force('charge', forceManyBody().strength(-300)) - .force('center', forceCenter(centerX, centerY)) - .force('collision', forceCollide().radius(50)) - .on('tick', () => { - tickCounter++; - }); - - simulationInitialized = true; - simulation.alpha(1).restart(); - }, - - updateSimulationCenter(width: number, height: number) { - if (simulation) { - simulation.force('center', forceCenter(width / 2, height / 2)); - simulation.alpha(0.3).restart(); - } - }, - - stopSimulation() { - if (simulation) { - simulation.stop(); - simulation = null; - } - simulationInitialized = false; - }, - - reset() { - this.stopSimulation(); - nodes = []; - links = []; - dataLoaded = false; - lastDimensions = { width: 0, height: 0 }; - tickCounter = 0; - }, - - reheatSimulation() { - if (simulation) { - simulation.alpha(0.3).restart(); - } - }, - - fixNode(nodeId: string, x: number, y: number) { - const node = nodes.find((n) => n.id === nodeId); - if (node) { - node.fx = x; - node.fy = y; - } - }, - - releaseNode(nodeId: string) { - const node = nodes.find((n) => n.id === nodeId); - if (node) { - node.fx = null; - node.fy = null; - } - }, - - selectNode(nodeId: string | null) { - selectedNodeId = nodeId; - }, - - setSearch(query: string) { - searchQuery = query; - }, - - setFilterTag(tagId: string | null) { - filterTagId = tagId; - }, - - setFilterProject(project: string | null) { - filterProject = project; - }, - - setMinStrength(strength: number) { - minStrength = strength; - }, - - clearFilters() { - searchQuery = ''; - filterTagId = null; - filterProject = null; - minStrength = 0; - }, - - getConnectedNodes(nodeId: string): SimulationNode[] { - const connectedIds = new Set(); - - for (const link of links) { - const sourceId = typeof link.source === 'string' ? link.source : link.source.id; - const targetId = typeof link.target === 'string' ? link.target : link.target.id; - - if (sourceId === nodeId) { - connectedIds.add(targetId); - } else if (targetId === nodeId) { - connectedIds.add(sourceId); - } - } - - return nodes.filter((n) => connectedIds.has(n.id)); - }, - - getNodeLinks(nodeId: string): SimulationLink[] { - return links.filter((link) => { - const sourceId = typeof link.source === 'string' ? link.source : link.source.id; - const targetId = typeof link.target === 'string' ? link.target : link.target.id; - return sourceId === nodeId || targetId === nodeId; - }); - }, -}; diff --git a/apps/todo/apps/web/src/lib/stores/session-tasks.svelte.ts b/apps/todo/apps/web/src/lib/stores/session-tasks.svelte.ts deleted file mode 100644 index 92d36ddbb..000000000 --- a/apps/todo/apps/web/src/lib/stores/session-tasks.svelte.ts +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Session Tasks Store - Temporary local tasks for guest users - * Tasks are stored in sessionStorage and lost when the browser tab is closed - */ - -import type { Task, TaskPriority, Subtask } from '@todo/shared'; -import { browser } from '$app/environment'; - -const STORAGE_KEY = 'todo-session-tasks'; - -// Generate a unique ID for session tasks -function generateSessionId(): string { - return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; -} - -// Load tasks from sessionStorage -function loadFromStorage(): Task[] { - if (!browser) return []; - try { - const stored = sessionStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : []; - } catch { - return []; - } -} - -// Save tasks to sessionStorage -function saveToStorage(tasks: Task[]) { - if (!browser) return; - try { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); - } catch (e) { - console.warn('Failed to save session tasks:', e); - } -} - -// State -let tasks = $state(loadFromStorage()); - -export const sessionTasksStore = { - get tasks() { - return tasks; - }, - - get hasTaskks() { - return tasks.length > 0; - }, - - /** - * Initialize from sessionStorage (call on mount) - */ - initialize() { - tasks = loadFromStorage(); - }, - - /** - * Create a new session task - */ - createTask(data: { - title: string; - description?: string; - projectId?: string; - dueDate?: string; - priority?: TaskPriority; - subtasks?: Subtask[]; - }): Task { - const newTask: Task = { - id: generateSessionId(), - projectId: data.projectId || 'session-inbox', - userId: 'guest', - title: data.title, - description: data.description || null, - dueDate: data.dueDate || null, - priority: data.priority || 'medium', - status: 'pending', - isCompleted: false, - order: tasks.length, - subtasks: data.subtasks || null, - labels: [], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - tasks = [...tasks, newTask]; - saveToStorage(tasks); - return newTask; - }, - - /** - * Update a session task - */ - updateTask(id: string, data: Partial): Task | null { - const index = tasks.findIndex((t) => t.id === id); - if (index === -1) return null; - - const updatedTask = { - ...tasks[index], - ...data, - updatedAt: new Date().toISOString(), - }; - - tasks = tasks.map((t) => (t.id === id ? updatedTask : t)); - saveToStorage(tasks); - return updatedTask; - }, - - /** - * Complete a session task - */ - completeTask(id: string): Task | null { - return this.updateTask(id, { - isCompleted: true, - status: 'completed', - completedAt: new Date().toISOString(), - }); - }, - - /** - * Uncomplete a session task - */ - uncompleteTask(id: string): Task | null { - return this.updateTask(id, { - isCompleted: false, - status: 'pending', - completedAt: null, - }); - }, - - /** - * Delete a session task - */ - deleteTask(id: string): boolean { - const hadTask = tasks.some((t) => t.id === id); - tasks = tasks.filter((t) => t.id !== id); - saveToStorage(tasks); - return hadTask; - }, - - /** - * Get task by ID - */ - getById(id: string): Task | undefined { - return tasks.find((t) => t.id === id); - }, - - /** - * Check if a task ID is a session task - */ - isSessionTask(id: string): boolean { - return id.startsWith('session_'); - }, - - /** - * Get all tasks (for migration to cloud on login) - */ - getAllTasks(): Task[] { - return [...tasks]; - }, - - /** - * Clear all session tasks (after migration or on explicit clear) - */ - clear() { - tasks = []; - if (browser) { - sessionStorage.removeItem(STORAGE_KEY); - } - }, - - /** - * Get count of session tasks - */ - get count() { - return tasks.length; - }, - - /** - * Get incomplete tasks - */ - get incompleteTasks() { - return tasks.filter((t) => !t.isCompleted); - }, - - /** - * Get completed tasks - */ - get completedTasks() { - return tasks.filter((t) => t.isCompleted); - }, -}; diff --git a/apps/todo/apps/web/src/lib/stores/statistics.svelte.ts b/apps/todo/apps/web/src/lib/stores/statistics.svelte.ts deleted file mode 100644 index f7fe7eaba..000000000 --- a/apps/todo/apps/web/src/lib/stores/statistics.svelte.ts +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Statistics Store - Calculates task statistics using Svelte 5 runes - */ - -import type { Task, TaskPriority, Project } from '@todo/shared'; -import { - startOfDay, - startOfWeek, - endOfWeek, - subDays, - subWeeks, - format, - differenceInDays, - isToday, - isSameDay, - parseISO, - eachDayOfInterval, -} from 'date-fns'; -import { de } from 'date-fns/locale'; - -// Types -export interface DailyCompletion { - date: string; - count: number; - dayOfWeek: number; -} - -export interface WeeklyData { - week: string; - weekStart: Date; - completedCount: number; - storyPoints: number; -} - -export interface PriorityBreakdown { - priority: TaskPriority; - count: number; - percentage: number; - color: string; -} - -export interface ProjectProgress { - projectId: string | null; - projectName: string; - projectColor: string; - total: number; - completed: number; - inProgress: number; - percentage: number; -} - -export interface DayProductivity { - day: string; - dayIndex: number; - avgCompletions: number; -} - -const PRIORITY_COLORS: Record = { - low: '#10B981', - medium: '#F59E0B', - high: '#F97316', - urgent: '#EF4444', -}; - -const DAY_NAMES = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; - -// State -let tasks = $state([]); -let projects = $state([]); - -export const statisticsStore = { - // Setters - setTasks(newTasks: Task[]) { - tasks = newTasks; - }, - - setProjects(newProjects: Project[]) { - projects = newProjects; - }, - - // Quick Stats - get totalTasks() { - return tasks.length; - }, - - get completedTasks() { - return tasks.filter((t) => t.isCompleted).length; - }, - - get activeTasks() { - return tasks.filter((t) => !t.isCompleted).length; - }, - - get overdueTasks() { - const today = startOfDay(new Date()); - return tasks.filter((t) => { - if (t.isCompleted || !t.dueDate) return false; - const dueDate = startOfDay(new Date(t.dueDate)); - return dueDate < today; - }).length; - }, - - get completedToday() { - return tasks.filter((t) => { - if (!t.isCompleted || !t.completedAt) return false; - return isToday(new Date(t.completedAt)); - }).length; - }, - - get completedThisWeek() { - const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 }); - const weekEnd = endOfWeek(new Date(), { weekStartsOn: 1 }); - return tasks.filter((t) => { - if (!t.isCompleted || !t.completedAt) return false; - const completedDate = new Date(t.completedAt); - return completedDate >= weekStart && completedDate <= weekEnd; - }).length; - }, - - get storyPointsThisWeek() { - const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 }); - const weekEnd = endOfWeek(new Date(), { weekStartsOn: 1 }); - return tasks - .filter((t) => { - if (!t.isCompleted || !t.completedAt) return false; - const completedDate = new Date(t.completedAt); - return completedDate >= weekStart && completedDate <= weekEnd; - }) - .reduce((sum, t) => sum + (t.metadata?.storyPoints || 0), 0); - }, - - get completionRate() { - if (tasks.length === 0) return 0; - return Math.round((this.completedTasks / tasks.length) * 100); - }, - - // Activity Heatmap (last 6 months) - get activityHeatmap(): DailyCompletion[] { - const endDate = new Date(); - const startDate = subDays(endDate, 180); // ~6 months - - // Count completions per day - const completionMap = new Map(); - - tasks.forEach((t) => { - if (t.isCompleted && t.completedAt) { - const dateKey = format(new Date(t.completedAt), 'yyyy-MM-dd'); - completionMap.set(dateKey, (completionMap.get(dateKey) || 0) + 1); - } - }); - - // Generate all days - const days = eachDayOfInterval({ start: startDate, end: endDate }); - - return days.map((day) => { - const dateKey = format(day, 'yyyy-MM-dd'); - return { - date: dateKey, - count: completionMap.get(dateKey) || 0, - dayOfWeek: day.getDay(), - }; - }); - }, - - // Weekly Trend (last 4 weeks) - get weeklyTrend(): { date: string; count: number; dayName: string }[] { - const endDate = new Date(); - const startDate = subDays(endDate, 27); // Last 4 weeks - - const completionMap = new Map(); - - tasks.forEach((t) => { - if (t.isCompleted && t.completedAt) { - const completedDate = new Date(t.completedAt); - if (completedDate >= startDate && completedDate <= endDate) { - const dateKey = format(completedDate, 'yyyy-MM-dd'); - completionMap.set(dateKey, (completionMap.get(dateKey) || 0) + 1); - } - } - }); - - const days = eachDayOfInterval({ start: startDate, end: endDate }); - - return days.map((day) => { - const dateKey = format(day, 'yyyy-MM-dd'); - return { - date: dateKey, - count: completionMap.get(dateKey) || 0, - dayName: DAY_NAMES[day.getDay()], - }; - }); - }, - - // Priority Breakdown - get priorityBreakdown(): PriorityBreakdown[] { - const activeTasks = tasks.filter((t) => !t.isCompleted); - const total = activeTasks.length; - - const counts: Record = { - low: 0, - medium: 0, - high: 0, - urgent: 0, - }; - - activeTasks.forEach((t) => { - const priority = (t.priority as TaskPriority) || 'medium'; - counts[priority]++; - }); - - return (['urgent', 'high', 'medium', 'low'] as TaskPriority[]).map((priority) => ({ - priority, - count: counts[priority], - percentage: total > 0 ? Math.round((counts[priority] / total) * 100) : 0, - color: PRIORITY_COLORS[priority], - })); - }, - - // Project Progress - get projectProgress(): ProjectProgress[] { - const projectMap = new Map< - string | null, - { total: number; completed: number; inProgress: number } - >(); - - // Initialize with inbox (null projectId) - projectMap.set(null, { total: 0, completed: 0, inProgress: 0 }); - - // Initialize all projects - projects.forEach((p) => { - projectMap.set(p.id, { total: 0, completed: 0, inProgress: 0 }); - }); - - // Count tasks - tasks.forEach((t) => { - const projectId = t.projectId || null; - const data = projectMap.get(projectId) || { total: 0, completed: 0, inProgress: 0 }; - - data.total++; - if (t.isCompleted) { - data.completed++; - } else if (t.status === 'in_progress') { - data.inProgress++; - } - - projectMap.set(projectId, data); - }); - - // Convert to array - const result: ProjectProgress[] = []; - - projectMap.forEach((data, projectId) => { - if (data.total === 0) return; // Skip empty projects - - const project = projectId ? projects.find((p) => p.id === projectId) : null; - - result.push({ - projectId, - projectName: project?.name || 'Inbox', - projectColor: project?.color || '#6B7280', - total: data.total, - completed: data.completed, - inProgress: data.inProgress, - percentage: data.total > 0 ? Math.round((data.completed / data.total) * 100) : 0, - }); - }); - - // Sort by total tasks descending - return result.sort((a, b) => b.total - a.total); - }, - - // Weekly Velocity (Story Points per week) - get weeklyVelocity(): WeeklyData[] { - const weeks: WeeklyData[] = []; - - for (let i = 11; i >= 0; i--) { - const weekStart = startOfWeek(subWeeks(new Date(), i), { weekStartsOn: 1 }); - const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 }); - - const weekTasks = tasks.filter((t) => { - if (!t.isCompleted || !t.completedAt) return false; - const completedDate = new Date(t.completedAt); - return completedDate >= weekStart && completedDate <= weekEnd; - }); - - weeks.push({ - week: format(weekStart, 'd. MMM', { locale: de }), - weekStart, - completedCount: weekTasks.length, - storyPoints: weekTasks.reduce((sum, t) => sum + (t.metadata?.storyPoints || 0), 0), - }); - } - - return weeks; - }, - - // Most Productive Days - get productiveDays(): DayProductivity[] { - const dayStats = new Map }>(); - - // Initialize all days - for (let i = 0; i < 7; i++) { - dayStats.set(i, { total: 0, weeks: new Set() }); - } - - // Count completions per day of week - tasks.forEach((t) => { - if (t.isCompleted && t.completedAt) { - const completedDate = new Date(t.completedAt); - const dayOfWeek = completedDate.getDay(); - const weekKey = format(completedDate, 'yyyy-ww'); - - const stats = dayStats.get(dayOfWeek)!; - stats.total++; - stats.weeks.add(weekKey); - } - }); - - // Calculate averages - return Array.from(dayStats.entries()).map(([dayIndex, stats]) => ({ - day: DAY_NAMES[dayIndex], - dayIndex, - avgCompletions: - stats.weeks.size > 0 ? Math.round((stats.total / stats.weeks.size) * 10) / 10 : 0, - })); - }, - - // Subtask Stats - get subtaskStats() { - let totalSubtasks = 0; - let completedSubtasks = 0; - - tasks.forEach((t) => { - if (t.subtasks && Array.isArray(t.subtasks)) { - totalSubtasks += t.subtasks.length; - completedSubtasks += t.subtasks.filter((s) => s.isCompleted).length; - } - }); - - return { - total: totalSubtasks, - completed: completedSubtasks, - percentage: totalSubtasks > 0 ? Math.round((completedSubtasks / totalSubtasks) * 100) : 0, - }; - }, - - // Average completion time (in days) - get averageCompletionTime() { - const completedWithDates = tasks.filter((t) => t.isCompleted && t.completedAt && t.createdAt); - - if (completedWithDates.length === 0) return 0; - - const totalDays = completedWithDates.reduce((sum, t) => { - const created = new Date(t.createdAt); - const completed = new Date(t.completedAt!); - return sum + differenceInDays(completed, created); - }, 0); - - return Math.round((totalDays / completedWithDates.length) * 10) / 10; - }, -}; diff --git a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts index 19f7df922..24c6a1144 100644 --- a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts @@ -1,13 +1,14 @@ /** * Tasks Store - Manages task state using Svelte 5 runes - * Supports both authenticated (cloud) and guest (session) modes + * Authenticated users: tasks from API + * Demo mode: static sample tasks to showcase the app */ import type { Task, TaskPriority, TaskStatus, Subtask } from '@todo/shared'; import * as tasksApi from '$lib/api/tasks'; import { isToday, isPast, isFuture, startOfDay, addDays } from 'date-fns'; -import { sessionTasksStore } from './session-tasks.svelte'; import { authStore } from './auth.svelte'; +import { generateDemoTasks, isDemoTask } from '$lib/data/demo-tasks'; // State let tasks = $state([]); @@ -117,16 +118,15 @@ export const tasksStore = { /** * Fetch all tasks (incomplete + completed) for unified view - * In guest mode, only shows session tasks + * In demo mode, shows static sample tasks */ async fetchAllTasks() { loading = true; error = null; - // Guest mode: load session tasks only + // Demo mode: load static demo tasks if (!authStore.isAuthenticated) { - sessionTasksStore.initialize(); - tasks = sessionTasksStore.tasks; + tasks = generateDemoTasks(); loading = false; return; } @@ -201,7 +201,7 @@ export const tasksStore = { /** * Create a new task - * If not authenticated, creates a session task (local only) + * Requires authentication - demo mode shows auth gate */ async createTask(data: { title: string; @@ -215,18 +215,9 @@ export const tasksStore = { }) { error = null; - // Guest mode: create session task + // Demo mode: require authentication if (!authStore.isAuthenticated) { - const sessionTask = sessionTasksStore.createTask({ - title: data.title, - description: data.description, - projectId: data.projectId || 'session-inbox', - dueDate: data.dueDate, - priority: data.priority, - subtasks: data.subtasks as Subtask[], - }); - tasks = [...tasks, sessionTask]; - return sessionTask; + return { error: 'auth_required' as const }; } // Authenticated: create via API @@ -243,7 +234,7 @@ export const tasksStore = { /** * Update an existing task - * Handles both session tasks (local) and cloud tasks + * Demo tasks require authentication */ async updateTask( id: string, @@ -268,14 +259,9 @@ export const tasksStore = { ) { error = null; - // Session task: update locally - if (sessionTasksStore.isSessionTask(id)) { - const updated = sessionTasksStore.updateTask(id, data); - if (updated) { - tasks = tasks.map((t) => (t.id === id ? updated : t)); - return updated; - } - throw new Error('Task not found'); + // Demo task: require authentication + if (isDemoTask(id)) { + return { error: 'auth_required' as const }; } // Cloud task: update via API @@ -293,6 +279,7 @@ export const tasksStore = { /** * Update task optimistically (for drag and drop) * Updates local state immediately, then syncs with server + * Demo tasks require authentication */ async updateTaskOptimistic( id: string, @@ -301,6 +288,11 @@ export const tasksStore = { isCompleted?: boolean; } ) { + // Demo task: require authentication + if (isDemoTask(id)) { + return { error: 'auth_required' as const }; + } + // Optimistic update - immediately update local state const originalTask = tasks.find((t) => t.id === id); if (!originalTask) return; @@ -333,16 +325,14 @@ export const tasksStore = { /** * Delete a task - * Handles both session tasks (local) and cloud tasks + * Demo tasks require authentication */ async deleteTask(id: string) { error = null; - // Session task: delete locally - if (sessionTasksStore.isSessionTask(id)) { - sessionTasksStore.deleteTask(id); - tasks = tasks.filter((t) => t.id !== id); - return; + // Demo task: require authentication + if (isDemoTask(id)) { + return { error: 'auth_required' as const }; } // Cloud task: delete via API @@ -358,19 +348,14 @@ export const tasksStore = { /** * Mark task as complete - * Handles both session tasks (local) and cloud tasks + * Demo tasks require authentication */ async completeTask(id: string) { error = null; - // Session task: complete locally - if (sessionTasksStore.isSessionTask(id)) { - const completed = sessionTasksStore.completeTask(id); - if (completed) { - tasks = tasks.map((t) => (t.id === id ? completed : t)); - return completed; - } - throw new Error('Task not found'); + // Demo task: require authentication + if (isDemoTask(id)) { + return { error: 'auth_required' as const }; } // Cloud task: complete via API @@ -387,19 +372,14 @@ export const tasksStore = { /** * Mark task as incomplete - * Handles both session tasks (local) and cloud tasks + * Demo tasks require authentication */ async uncompleteTask(id: string) { error = null; - // Session task: uncomplete locally - if (sessionTasksStore.isSessionTask(id)) { - const uncompleted = sessionTasksStore.uncompleteTask(id); - if (uncompleted) { - tasks = tasks.map((t) => (t.id === id ? uncompleted : t)); - return uncompleted; - } - throw new Error('Task not found'); + // Demo task: require authentication + if (isDemoTask(id)) { + return { error: 'auth_required' as const }; } // Cloud task: uncomplete via API @@ -491,63 +471,9 @@ export const tasksStore = { }, /** - * Check if a task is a session task (local only) + * Check if a task is a demo task (static sample data) */ - isSessionTask(taskId: string) { - return sessionTasksStore.isSessionTask(taskId); - }, - - /** - * Migrate session tasks to cloud after login - * Call this after successful authentication - */ - async migrateSessionTasks(defaultProjectId?: string) { - const sessionTasks = sessionTasksStore.getAllTasks(); - if (sessionTasks.length === 0) return { migrated: 0, failed: 0 }; - - let migrated = 0; - let failed = 0; - - for (const sessionTask of sessionTasks) { - try { - await tasksApi.createTask({ - title: sessionTask.title, - description: sessionTask.description || undefined, - projectId: defaultProjectId || undefined, - dueDate: sessionTask.dueDate ? String(sessionTask.dueDate) : undefined, - priority: sessionTask.priority, - subtasks: sessionTask.subtasks?.map((s) => ({ - title: s.title, - isCompleted: s.isCompleted, - order: s.order, - })), - }); - migrated++; - } catch { - failed++; - } - } - - // Clear session tasks after migration - if (migrated > 0) { - sessionTasksStore.clear(); - console.log(`Migrated ${migrated} tasks to cloud`); - } - - return { migrated, failed }; - }, - - /** - * Get count of pending session tasks - */ - get sessionTaskCount() { - return sessionTasksStore.count; - }, - - /** - * Check if there are pending session tasks to migrate - */ - get hasSessionTasks() { - return sessionTasksStore.count > 0; + isDemoTask(taskId: string) { + return isDemoTask(taskId); }, }; diff --git a/apps/todo/apps/web/src/routes/(app)/+layout.svelte b/apps/todo/apps/web/src/routes/(app)/+layout.svelte index 7a697ac90..76bf92aef 100644 --- a/apps/todo/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+layout.svelte @@ -40,8 +40,8 @@ import { getTasks } from '$lib/api/tasks'; import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser'; import AuthGateModal from '$lib/components/AuthGateModal.svelte'; - import { sessionTasksStore } from '$lib/stores/session-tasks.svelte'; import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui'; + import { browser } from '$app/environment'; // App switcher items const appItems = getPillAppItems('todo'); @@ -100,13 +100,18 @@ const parsed = parseTaskInput(query); const resolved = resolveTaskIds(parsed, projectsStore.projects, labelsStore.labels); - await tasksStore.createTask({ + const result = await tasksStore.createTask({ title: resolved.title, dueDate: resolved.dueDate, priority: resolved.priority, projectId: resolved.projectId, labelIds: resolved.labelIds, }); + + // Show auth gate if authentication required (demo mode) + if (result && 'error' in result && result.error === 'auth_required') { + showAuthGate('save'); + } } let isSidebarMode = $state(false); @@ -164,9 +169,7 @@ const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Aufgaben', icon: 'list' }, { href: '/kanban', label: 'Kanban', icon: 'columns' }, - { href: '/statistics', label: 'Statistiken', icon: 'chart' }, { href: '/tags', label: 'Tags', icon: 'tag' }, - { href: '/network', label: 'Netzwerk', icon: 'share-2' }, { href: '/settings', label: 'Einstellungen', icon: 'settings' }, { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; @@ -286,11 +289,17 @@ showAuthGateModal = true; } - // Session tasks indicator - let sessionTaskCount = $derived(sessionTasksStore.count); - - // Language for GuestWelcomeModal - let currentLocale = $derived($locale || 'de'); + // Listen for show-auth-gate events from child components + $effect(() => { + if (browser) { + const handler = (e: Event) => { + const customEvent = e as CustomEvent<{ action?: 'save' | 'sync' | 'feature' }>; + showAuthGate(customEvent.detail?.action || 'save'); + }; + window.addEventListener('show-auth-gate', handler); + return () => window.removeEventListener('show-auth-gate', handler); + } + }); onMount(async () => { // Initialize split-panel from URL/localStorage @@ -299,9 +308,6 @@ // Initialize todo settings todoSettings.initialize(); - // Initialize session tasks for guest mode - sessionTasksStore.initialize(); - // Show guest welcome modal for unauthenticated users if (!authStore.isAuthenticated && shouldShowGuestWelcome('todo')) { showGuestWelcome = true; @@ -314,12 +320,6 @@ if (authStore.isAuthenticated) { await Promise.all([labelsStore.fetchLabels(), userSettings.load()]); - // Check for session tasks to migrate after login - if (tasksStore.hasSessionTasks) { - const defaultProject = projectsStore.inboxProject; - await tasksStore.migrateSessionTasks(defaultProject?.id); - } - // Redirect to start page if on root and a custom start page is set const currentPath = window.location.pathname; if (currentPath === '/' && userSettings.startPage && userSettings.startPage !== '/') { @@ -391,7 +391,7 @@
- + {#if !authStore.isAuthenticated}
+ - Gast-Modus - {#if sessionTaskCount > 0} - - {sessionTaskCount} - {sessionTaskCount === 1 ? 'Aufgabe' : 'Aufgaben'} lokal gespeichert - {:else} - - Aufgaben werden nur in diesem Tab gespeichert - {/if} + Demo-Modus +
-
- {#if networkStore.selectedNode.subtitle} -

{networkStore.selectedNode.subtitle}

- {/if} - {#if networkStore.selectedNode.tags.length > 0} -
- {#each networkStore.selectedNode.tags as tag} - - {tag.name} - - {/each} -
- {/if} -
- {networkStore.selectedNode.connectionCount} Verbindungen -
- - {/if} - - - diff --git a/apps/todo/apps/web/src/routes/(app)/statistics/+page.svelte b/apps/todo/apps/web/src/routes/(app)/statistics/+page.svelte deleted file mode 100644 index 1f39e20e2..000000000 --- a/apps/todo/apps/web/src/routes/(app)/statistics/+page.svelte +++ /dev/null @@ -1,222 +0,0 @@ - - - - Statistiken - Todo - - -
- - - {#if loading} - - {:else} - -
- -
- - -
- -
- -
- - -
-
- -
- -
- -
-
- - -
- -
-
- - -
-
- Durchschn. Bearbeitungszeit - {statisticsStore.averageCompletionTime} Tage -
- - {#if statisticsStore.subtaskStats.total > 0} -
- Subtasks erledigt - - {statisticsStore.subtaskStats.completed}/{statisticsStore.subtaskStats.total} - ({statisticsStore.subtaskStats.percentage}%) - -
- {/if} - -
- Produktivster Tag - - {#if statisticsStore.productiveDays.length > 0} - {@const bestDay = statisticsStore.productiveDays.reduce((best, day) => - day.avgCompletions > best.avgCompletions ? day : best - )} - {bestDay.day} ({bestDay.avgCompletions} Aufg./Tag) - {:else} - - - {/if} - -
-
- {/if} -
- - diff --git a/apps/todo/docs/CLEANUP_PLAN.md b/apps/todo/docs/CLEANUP_PLAN.md new file mode 100644 index 000000000..63fdcda5e --- /dev/null +++ b/apps/todo/docs/CLEANUP_PLAN.md @@ -0,0 +1,116 @@ +# Todo App - Cleanup Plan + +Dieser Plan dokumentiert Features und Code, die überdurchschnittlich viel Komplexität erzeugen bei geringem Nutzen. Ziel ist eine schlankere, wartbarere Codebase. + +## Status-Legende + +- ✅ Erledigt +- 🔄 In Bearbeitung +- ⏳ Geplant +- ❌ Abgelehnt + +--- + +## Geplante Aufräumarbeiten + +### Priorität 1: Quick Wins (Hoher ROI) + +#### ✅ 1.1 Statistiken & Heatmap entfernen + +**Status:** Erledigt +**Geschätzte Ersparnis:** ~1.900 Zeilen +**Komplexität:** HOCH | **Nutzen:** NIEDRIG + +**Beschreibung:** +Umfangreiches Analytics-System mit Activity Heatmap, Weekly Trend Chart, Priority Donut, Project Progress, Weekly Velocity. Die meisten Nutzer verwenden diese Features nicht. + +**Zu entfernende Dateien:** +- `src/lib/stores/statistics.svelte.ts` (~361 Zeilen) +- `src/lib/components/statistics/ActivityHeatmap.svelte` +- `src/lib/components/statistics/WeeklyTrendChart.svelte` +- `src/lib/components/statistics/PriorityDonutChart.svelte` +- `src/lib/components/statistics/ProjectProgressBars.svelte` +- `src/lib/components/statistics/StatsOverview.svelte` +- `src/routes/(app)/statistics/+page.svelte` + +**Zu ändernde Dateien:** +- `src/routes/(app)/+layout.svelte` - Nav-Item entfernen + +--- + +#### ✅ 1.2 Network View entfernen + +**Status:** Erledigt +**Geschätzte Ersparnis:** ~800 Zeilen +**Komplexität:** HOCH | **Nutzen:** NIEDRIG + +**Beschreibung:** +D3.js Force-Directed Graph zur Visualisierung von Task-Beziehungen. Komplex (Force Simulation, Node Dragging, Filtering) aber wenig genutzt. + +**Zu entfernende Dateien:** +- `src/lib/stores/network.svelte.ts` (~370 Zeilen) +- `src/lib/api/network.ts` (~50 Zeilen) +- `src/routes/(app)/network/+page.svelte` + +**Zu ändernde Dateien:** +- `src/routes/(app)/+layout.svelte` - Nav-Item entfernen + +--- + +#### ✅ 1.3 Session Tasks → Demo-Modus + +**Status:** Erledigt +**Geschätzte Ersparnis:** ~100 Zeilen (netto) +**Komplexität:** MITTEL | **Nutzen:** HOCH (bessere UX) + +**Beschreibung:** +Wie bei der Calendar-App: Session-basiertes Task-Management durch statischen Demo-Modus ersetzen. Statt frustrierender UX (Tasks verschwinden bei Tab-Schließung) zeigt die App Beispiel-Tasks. + +**Zu entfernende Dateien:** +- `src/lib/stores/session-tasks.svelte.ts` (~190 Zeilen) + +**Neue Dateien:** +- `src/lib/data/demo-tasks.ts` (~100 Zeilen) + +**Zu ändernde Dateien:** +- `src/lib/stores/tasks.svelte.ts` - Session-Logik durch Demo-Tasks ersetzen +- `src/routes/(app)/+layout.svelte` - Demo-Banner, Auth-Gate +- `src/routes/(app)/+page.svelte` - Auth-Gate bei Task-Erstellung + +--- + +### Priorität 2: Mittlerer Aufwand + +#### ⏳ 2.1 Contacts Integration entfernen + +**Status:** Geplant +**Geschätzte Ersparnis:** ~200 Zeilen +**Komplexität:** MITTEL | **Nutzen:** NIEDRIG + +**Beschreibung:** +Cross-App Integration mit Contacts-App. Geringe Nutzung wenn Contacts-App nicht adoptiert. + +**Zu entfernende Dateien:** +- `src/lib/stores/contacts.svelte.ts` (~175 Zeilen) + +--- + +## Zusammenfassung + +| Phase | Features | LOC Ersparnis | Status | +|-------|----------|---------------|--------| +| ✅ Prio 1.1 | Statistiken/Heatmap | ~1.900 | Erledigt | +| ✅ Prio 1.2 | Network View | ~800 | Erledigt | +| ✅ Prio 1.3 | Sessions → Demo | ~100 | Erledigt | +| 🟡 Prio 2 | Contacts Integration | ~200 | Geplant | +| **Gesamt** | | **~3.000** | | + +**Ziel:** ~25% Code-Reduktion bei gleichem/besserem Nutzererlebnis + +--- + +## Changelog + +| Datum | Aktion | Commit | +|-------|--------|--------| +| 2026-01-28 | Statistiken, Network View, Session Tasks entfernt; Demo-Modus implementiert | f4d6201a |