mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 06:39:41 +02:00
fix(calendar): fix Todo integration and show all tasks in sidebar
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
812a6f5090
commit
1040b54058
5 changed files with 90 additions and 81 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<Subtask, 'id'>[];
|
||||
|
|
@ -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<TasksResponse>('/tasks/today');
|
||||
console.log('[TodoAPI] Response:', result);
|
||||
return {
|
||||
data: result.data?.tasks || null,
|
||||
error: result.error,
|
||||
|
|
|
|||
|
|
@ -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<Task | null>(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');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="todo-sidebar-section">
|
||||
|
|
@ -118,12 +107,6 @@
|
|||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if totalActiveCount > maxItems}
|
||||
<button type="button" class="show-all-button" onclick={goToAllTasks}>
|
||||
Alle {totalActiveCount} anzeigen
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Quick Add -->
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<TaskWithLabels[]> {
|
||||
// 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]));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue