From 1040b540586bd0282618e5d7f7f887e7f5507084 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:34:25 +0100 Subject: [PATCH] fix(calendar): fix Todo integration and show all tasks in sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix /tasks/upcoming endpoint 500 error (Invalid time value) - Use inArray instead of or() for batch label loading - Show all todos in sidebar (overdue + today + upcoming) - Make todo list scrollable with max-height - Improve error handling: don't mark service unavailable if today tasks loaded 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../apps/web/src/lib/api/base-client.ts | 1 - apps/calendar/apps/web/src/lib/api/todos.ts | 17 ++++- .../calendar/TodoSidebarSection.svelte | 41 ++---------- .../apps/web/src/lib/stores/todos.svelte.ts | 62 ++++++++++++++----- .../apps/backend/src/task/task.service.ts | 50 +++++++-------- 5 files changed, 90 insertions(+), 81 deletions(-) diff --git a/apps/calendar/apps/web/src/lib/api/base-client.ts b/apps/calendar/apps/web/src/lib/api/base-client.ts index f2d3ff1ff..06f9ba7c7 100644 --- a/apps/calendar/apps/web/src/lib/api/base-client.ts +++ b/apps/calendar/apps/web/src/lib/api/base-client.ts @@ -84,7 +84,6 @@ export function createApiClient(config: ApiClientConfig) { return { data, error: null }; } catch (error) { clearTimeout(timeoutId); - console.error('[BaseClient] Fetch error:', error); if (error instanceof Error && error.name === 'AbortError') { return { diff --git a/apps/calendar/apps/web/src/lib/api/todos.ts b/apps/calendar/apps/web/src/lib/api/todos.ts index f356b8d6f..db72aac5d 100644 --- a/apps/calendar/apps/web/src/lib/api/todos.ts +++ b/apps/calendar/apps/web/src/lib/api/todos.ts @@ -73,6 +73,11 @@ export interface Task { dueDate?: string | null; dueTime?: string | null; startDate?: string | null; + // Time-Blocking (for calendar integration) + scheduledDate?: string | null; + scheduledStartTime?: string | null; // HH:mm format + scheduledEndTime?: string | null; // HH:mm format + estimatedDuration?: number | null; // Duration in minutes priority: TaskPriority; status: TaskStatus; isCompleted: boolean; @@ -97,6 +102,11 @@ export interface CreateTaskInput { projectId?: string | null; dueDate?: string | null; dueTime?: string | null; + // Time-Blocking + scheduledDate?: string | null; + scheduledStartTime?: string | null; + scheduledEndTime?: string | null; + estimatedDuration?: number | null; priority?: TaskPriority; labelIds?: string[]; subtasks?: Omit[]; @@ -110,6 +120,11 @@ export interface UpdateTaskInput { projectId?: string | null; dueDate?: string | null; dueTime?: string | null; + // Time-Blocking + scheduledDate?: string | null; + scheduledStartTime?: string | null; + scheduledEndTime?: string | null; + estimatedDuration?: number | null; priority?: TaskPriority; status?: TaskStatus; isCompleted?: boolean; @@ -242,9 +257,7 @@ export async function uncompleteTask( } export async function getTodayTasks(): Promise<{ data: Task[] | null; error: Error | null }> { - console.log('[TodoAPI] Fetching /tasks/today from:', TODO_API_BASE); const result = await fetchTodoApi('/tasks/today'); - console.log('[TodoAPI] Response:', result); return { data: result.data?.tasks || null, error: result.error, diff --git a/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte b/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte index 8ebcc0dc2..88cc23a6e 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte @@ -5,21 +5,14 @@ import TodoDetailModal from '$lib/components/todo/TodoDetailModal.svelte'; import QuickAddTodo from '$lib/components/todo/QuickAddTodo.svelte'; import { ChevronDown, ChevronRight, Plus, CheckSquare, AlertTriangle } from 'lucide-svelte'; - import { goto } from '$app/navigation'; import { onMount } from 'svelte'; - interface Props { - maxItems?: number; - } - - let { maxItems = 5 }: Props = $props(); - let isExpanded = $state(true); let showQuickAdd = $state(false); let selectedTask = $state(null); - // Derived: combined overdue + today todos - const displayTodos = $derived(todosStore.getSidebarTodos(maxItems)); + // Derived: all active todos (overdue + today + upcoming) + const displayTodos = $derived(todosStore.getSidebarTodos()); const overdueCount = $derived(todosStore.overdueTodos.length); const totalActiveCount = $derived(todosStore.activeTodosCount); @@ -53,10 +46,6 @@ function handleQuickAddCancel() { showQuickAdd = false; } - - function goToAllTasks() { - goto('/tasks'); - }
@@ -118,12 +107,6 @@ /> {/each}
- - {#if totalActiveCount > maxItems} - - {/if} {/if} @@ -236,6 +219,8 @@ display: flex; flex-direction: column; gap: 0.25rem; + max-height: 300px; + overflow-y: auto; } .service-unavailable, @@ -269,24 +254,6 @@ } } - .show-all-button { - width: 100%; - padding: 0.5rem; - margin-top: 0.5rem; - border: none; - background: transparent; - color: hsl(var(--color-primary)); - font-size: 0.8125rem; - font-weight: 500; - cursor: pointer; - border-radius: var(--radius-md); - transition: background 150ms ease; - } - - .show-all-button:hover { - background: hsl(var(--color-primary) / 0.1); - } - .quick-add-wrapper { margin-top: 0.5rem; padding: 0 0.25rem; diff --git a/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts b/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts index 768b4d4e9..21d510939 100644 --- a/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts @@ -61,7 +61,7 @@ export const todosStore = { // ========== Derived Getters ========== /** - * Get todos for a specific day + * Get todos for a specific day (by dueDate) */ getTodosForDay(date: Date): Task[] { const currentTodos = todos ?? []; @@ -74,6 +74,48 @@ export const todosStore = { }); }, + /** + * Get scheduled tasks for a specific day (by scheduledDate - for time-blocking) + */ + getScheduledTasksForDay(date: Date): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => { + if (!task.scheduledDate || task.isCompleted) return false; + const scheduledDate = + typeof task.scheduledDate === 'string' ? parseISO(task.scheduledDate) : task.scheduledDate; + return isSameDay(scheduledDate, date); + }); + }, + + /** + * Get scheduled tasks within a date range (for time-blocking) + */ + getScheduledTasksInRange(start: Date, end: Date): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => { + if (!task.scheduledDate) return false; + const scheduledDate = + typeof task.scheduledDate === 'string' ? parseISO(task.scheduledDate) : task.scheduledDate; + return isWithinInterval(scheduledDate, { start, end }); + }); + }, + + /** + * Get unscheduled tasks (no scheduledDate - for sidebar drag source) + */ + get unscheduledForTimeBlocking(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos + .filter((task) => !task.isCompleted && !task.scheduledDate) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + /** * Get todos within a date range */ @@ -175,17 +217,15 @@ export const todosStore = { }, /** - * Get combined sidebar todos (overdue + today, sorted by priority) - * Limited to show in sidebar + * Get combined sidebar todos (overdue + today + upcoming, sorted by date then priority) */ - getSidebarTodos(limit = 5): Task[] { + getSidebarTodos(): Task[] { const overdue = this.overdueTodos; const today = this.todaysTodos; + const upcoming = this.upcomingTodos; - // Combine and sort: overdue first, then today, both by priority - const combined = [...overdue, ...today]; - - return combined.slice(0, limit); + // Combine: overdue first, then today, then upcoming + return [...overdue, ...today, ...upcoming]; }, /** @@ -242,12 +282,9 @@ export const todosStore = { loading = true; error = null; - console.log('[TodoStore] Fetching today todos...'); const result = await api.getTodayTasks(); - console.log('[TodoStore] Result:', result); if (result.error) { - console.error('[TodoStore] Error fetching todos:', result.error); error = result.error.message; serviceAvailable = false; } else { @@ -270,12 +307,9 @@ export const todosStore = { loading = true; error = null; - console.log('[TodoStore] Fetching upcoming todos...'); const result = await api.getUpcomingTasks(); - console.log('[TodoStore] Upcoming result:', result); if (result.error) { - console.error('[TodoStore] Error fetching upcoming:', result.error); error = result.error.message; // Only set serviceAvailable to false if we have no todos yet // (if fetchTodayTodos succeeded, we should still show the service as available) diff --git a/apps/todo/apps/backend/src/task/task.service.ts b/apps/todo/apps/backend/src/task/task.service.ts index def2e9b3d..940c6e89d 100644 --- a/apps/todo/apps/backend/src/task/task.service.ts +++ b/apps/todo/apps/backend/src/task/task.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { eq, and, or, gte, lte, ilike, asc, desc, isNull, SQL, sql } from 'drizzle-orm'; +import { eq, and, or, gte, lte, ilike, asc, desc, isNull, SQL, sql, inArray } from 'drizzle-orm'; import { RRule, RRuleSet, rrulestr } from 'rrule'; import { DATABASE_CONNECTION } from '../db/database.module'; import { type Database } from '../db/connection'; @@ -125,6 +125,11 @@ export class TaskService { dueDate: dto.dueDate ? new Date(dto.dueDate) : null, dueTime: dto.dueTime, startDate: dto.startDate ? new Date(dto.startDate) : null, + // Time-Blocking fields + scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null, + scheduledStartTime: dto.scheduledStartTime, + scheduledEndTime: dto.scheduledEndTime, + estimatedDuration: dto.estimatedDuration, priority: dto.priority ?? 'medium', recurrenceRule: dto.recurrenceRule, recurrenceEndDate: dto.recurrenceEndDate ? new Date(dto.recurrenceEndDate) : null, @@ -159,6 +164,12 @@ export class TaskService { : dto.startDate === null ? null : undefined, + // Time-Blocking fields + scheduledDate: dto.scheduledDate + ? new Date(dto.scheduledDate) + : dto.scheduledDate === null + ? null + : undefined, recurrenceEndDate: dto.recurrenceEndDate ? new Date(dto.recurrenceEndDate) : dto.recurrenceEndDate === null @@ -434,21 +445,20 @@ export class TaskService { } async getUpcomingTasks(userId: string, days: number = 7): Promise { + // Ensure days is a valid number + const daysNum = typeof days === 'number' && !isNaN(days) ? days : 7; + const today = new Date(); today.setHours(0, 0, 0, 0); - const endDate = new Date(today); - endDate.setDate(endDate.getDate() + days); - - // Use SQL date strings for more reliable comparison - const todayStr = today.toISOString(); - const endDateStr = endDate.toISOString(); + const endDate = new Date(today.getTime()); + endDate.setDate(endDate.getDate() + daysNum); const result = await this.db.query.tasks.findMany({ where: and( eq(tasks.userId, userId), eq(tasks.isCompleted, false), - gte(tasks.dueDate, todayStr), - lte(tasks.dueDate, endDateStr) + gte(tasks.dueDate, today), + lte(tasks.dueDate, endDate) ), orderBy: [asc(tasks.dueDate), asc(tasks.order)], }); @@ -529,17 +539,11 @@ export class TaskService { const taskIds = taskList.map((t) => t.id); - // Single query to get all task-label relationships - // Use inArray for better performance with multiple IDs + // Single query to get all task-label relationships using inArray const allTaskLabels = await this.db .select() .from(taskLabels) - .where( - sql`${taskLabels.taskId} IN (${sql.join( - taskIds.map((id) => sql`${id}`), - sql`, ` - )})` - ); + .where(inArray(taskLabels.taskId, taskIds)); if (allTaskLabels.length === 0) { // No labels for any task - return tasks with empty labels array @@ -549,16 +553,8 @@ export class TaskService { // Get unique label IDs const uniqueLabelIds = [...new Set(allTaskLabels.map((tl) => tl.labelId))]; - // Single query to get all labels using IN clause - const allLabels = await this.db - .select() - .from(labels) - .where( - sql`${labels.id} IN (${sql.join( - uniqueLabelIds.map((id) => sql`${id}`), - sql`, ` - )})` - ); + // Single query to get all labels using inArray + const allLabels = await this.db.select().from(labels).where(inArray(labels.id, uniqueLabelIds)); // Create a map of labelId -> label for fast lookup const labelMap = new Map(allLabels.map((l) => [l.id, l]));