diff --git a/package.json b/package.json index d675f28..bbebd38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.38.0", + "version": "1.39.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/scripts/backup-db.sh b/scripts/backup-db.sh new file mode 100755 index 0000000..667ecdd --- /dev/null +++ b/scripts/backup-db.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Automated PostgreSQL backup script +# Run via cron on the Supabase LXC: 0 */6 * * * /root/backup-db.sh +# Keeps 7 days of backups + +BACKUP_DIR="/root/backups" +CONTAINER="supabase-db" +DB_NAME="postgres" +DB_USER="postgres" +KEEP_DAYS=7 + +mkdir -p "$BACKUP_DIR" + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/todo_backup_${TIMESTAMP}.sql.gz" + +echo "[$(date)] Starting backup..." + +docker exec "$CONTAINER" pg_dump -U "$DB_USER" "$DB_NAME" --clean --if-exists | gzip > "$BACKUP_FILE" + +if [ $? -eq 0 ] && [ -s "$BACKUP_FILE" ]; then + echo "[$(date)] Backup saved: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))" +else + echo "[$(date)] ERROR: Backup failed!" + rm -f "$BACKUP_FILE" + exit 1 +fi + +# Remove backups older than KEEP_DAYS +find "$BACKUP_DIR" -name "todo_backup_*.sql.gz" -mtime +$KEEP_DAYS -delete +echo "[$(date)] Cleanup done. Current backups:" +ls -lh "$BACKUP_DIR"/todo_backup_*.sql.gz 2>/dev/null diff --git a/src/app/api/tasks/recurring/route.ts b/src/app/api/tasks/recurring/route.ts new file mode 100644 index 0000000..aa86ceb --- /dev/null +++ b/src/app/api/tasks/recurring/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +// PATCH - Update all tasks in a recurring series +export async function PATCH(request: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ where: { email: session.user.email } }); + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + try { + const body = await request.json(); + const { + // Identify the series by the representative task's current values + oldTitle, + oldRecurrenceInterval, + oldRecurrenceUnit, + oldRecurrenceTime, + // New values to apply + newTitle, + newRecurrenceInterval, + newRecurrenceUnit, + newRecurrenceTime, + newRecurrenceEndDate, + newRecurrenceDays, + } = body; + + if (!oldTitle) { + return NextResponse.json({ error: 'oldTitle is required to identify the series' }, { status: 400 }); + } + + // Find all tasks in this recurring series (matching signature) + const seriesTasks = await prisma.task.findMany({ + where: { + userId: user.id, + isRecurring: true, + title: oldTitle, + recurrenceInterval: oldRecurrenceInterval || 1, + recurrenceUnit: oldRecurrenceUnit || 'weeks', + ...(oldRecurrenceTime ? { recurrenceTime: oldRecurrenceTime } : {}), + }, + }); + + if (seriesTasks.length === 0) { + return NextResponse.json({ error: 'No recurring tasks found matching this series' }, { status: 404 }); + } + + // Build update data — only include fields that were provided + const updateData: any = {}; + if (newTitle !== undefined) updateData.title = newTitle; + if (newRecurrenceInterval !== undefined) updateData.recurrenceInterval = parseInt(newRecurrenceInterval) || 1; + if (newRecurrenceUnit !== undefined) updateData.recurrenceUnit = newRecurrenceUnit; + if (newRecurrenceTime !== undefined) updateData.recurrenceTime = newRecurrenceTime || null; + if (newRecurrenceEndDate !== undefined) updateData.recurrenceEndDate = newRecurrenceEndDate ? new Date(newRecurrenceEndDate) : null; + if (newRecurrenceDays !== undefined) updateData.recurrenceDays = newRecurrenceDays; + + if (Object.keys(updateData).length === 0) { + return NextResponse.json({ error: 'No update fields provided' }, { status: 400 }); + } + + // Update all tasks in the series + const taskIds = seriesTasks.map(t => t.id); + await prisma.task.updateMany({ + where: { id: { in: taskIds } }, + data: updateData, + }); + + return NextResponse.json({ + success: true, + updatedCount: taskIds.length, + }); + } catch (error) { + console.error('Error updating recurring series:', error); + return NextResponse.json({ error: 'Failed to update recurring series' }, { status: 500 }); + } +} diff --git a/src/app/api/tasks/sync/route.ts b/src/app/api/tasks/sync/route.ts index c755e23..13a8cad 100644 --- a/src/app/api/tasks/sync/route.ts +++ b/src/app/api/tasks/sync/route.ts @@ -660,6 +660,39 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: true, task: updatedTask }); } + if (provider === 'synology') { + const { createSynologyTask } = await import('@/lib/synology-tasks'); + const synoConnection = await prisma.calendarConnection.findFirst({ + where: { userId: task.userId, provider: 'synology' } + }); + if (!synoConnection?.accessToken || !synoConnection?.refreshToken) { + return NextResponse.json({ error: 'Synology credentials not available' }, { status: 400 }); + } + const [synoUsername, synoPassword] = synoConnection.accessToken.split(':'); + const synoServerUrl = synoConnection.refreshToken; + if (!synoUsername || !synoPassword || !synoServerUrl) { + return NextResponse.json({ error: 'Synology credentials incomplete' }, { status: 400 }); + } + + const created = await createSynologyTask(synoServerUrl, synoUsername, synoPassword, listExternalId, { + title: task.title, + notes: task.description || undefined, + due: task.scheduledDate ? task.scheduledDate.toISOString() : undefined, + }); + + const updatedTask = await prisma.task.update({ + where: { id: taskId }, + data: { + externalId: created.id, + externalProvider: 'synology', + externalListId: listExternalId, + lastSyncedAt: new Date(), + } + }); + + return NextResponse.json({ success: true, task: updatedTask }); + } + return NextResponse.json({ error: `Provider "${provider}" creation sync not supported yet` }, { status: 400 }); } catch (error: unknown) { diff --git a/src/app/globals.css b/src/app/globals.css index ddb6e1e..362fcd8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2652,6 +2652,33 @@ h3 { background: var(--weekly-bg); border-top: 1px solid var(--weekly-border); padding: 2px 0; + position: relative; +} + +/* Resize handle for draggable section borders */ +.resize-handle { + height: 8px; + cursor: ns-resize; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + touch-action: none; + position: relative; + z-index: 10; +} +.resize-handle:hover .resize-handle-bar, +.resize-handle:active .resize-handle-bar { + background: var(--weekly-accent, #3b82f6); + opacity: 0.6; +} +.resize-handle-bar { + width: 40px; + height: 3px; + border-radius: 2px; + background: var(--weekly-border); + opacity: 0.4; + transition: background 0.15s, opacity 0.15s; } .all-day-events-header { diff --git a/src/components/RecurringTasksManager.tsx b/src/components/RecurringTasksManager.tsx index abc2d0c..739cc01 100644 --- a/src/components/RecurringTasksManager.tsx +++ b/src/components/RecurringTasksManager.tsx @@ -1,38 +1,94 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import { Repeat, X, Trash2 } from 'lucide-react'; +import React, { useState } from 'react'; +import { Repeat, X, Trash2, Pencil, Check } from 'lucide-react'; interface RecurringTasksManagerProps { isOpen: boolean; onClose: () => void; tasks: any[]; onStopRecurring: (task: any) => void; + onSeriesUpdated?: () => void; } -export default function RecurringTasksManager({ isOpen, onClose, tasks, onStopRecurring }: RecurringTasksManagerProps) { +export default function RecurringTasksManager({ isOpen, onClose, tasks, onStopRecurring, onSeriesUpdated }: RecurringTasksManagerProps) { + const [editingSignature, setEditingSignature] = useState(null); + const [editTitle, setEditTitle] = useState(''); + const [editInterval, setEditInterval] = useState(1); + const [editUnit, setEditUnit] = useState('weeks'); + const [editTime, setEditTime] = useState(''); + const [isSaving, setIsSaving] = useState(false); + if (!isOpen) return null; // Filter and group recurring tasks by series signature - // We use title + recurrence settings as the identity of a "series" - const seriesGroups = new Map(); + const seriesGroups = new Map(); tasks.filter(t => t.isRecurring).forEach(t => { - // Only include those that aren't already ended if (t.recurrenceEndDate && new Date(t.recurrenceEndDate) < new Date()) { return; } const signature = `${t.title}-${t.recurrenceInterval || 1}-${t.recurrenceUnit || 'weeks'}-${t.recurrenceTime || ''}`; - // Keep the latest instance to represent the series const existing = seriesGroups.get(signature); - if (!existing || (t.scheduledDate && new Date(t.scheduledDate) > new Date(existing.scheduledDate || 0))) { - seriesGroups.set(signature, t); + if (!existing || (t.scheduledDate && new Date(t.scheduledDate) > new Date(existing.task.scheduledDate || 0))) { + seriesGroups.set(signature, { task: t, signature }); } }); const groupedTasks = Array.from(seriesGroups.values()); + const startEditing = (task: any, signature: string) => { + setEditingSignature(signature); + setEditTitle(task.title); + setEditInterval(task.recurrenceInterval || 1); + setEditUnit(task.recurrenceUnit || 'weeks'); + setEditTime(task.recurrenceTime || ''); + }; + + const cancelEditing = () => { + setEditingSignature(null); + }; + + const saveEditing = async (task: any) => { + setIsSaving(true); + try { + const res = await fetch('/api/tasks/recurring', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + oldTitle: task.title, + oldRecurrenceInterval: task.recurrenceInterval || 1, + oldRecurrenceUnit: task.recurrenceUnit || 'weeks', + oldRecurrenceTime: task.recurrenceTime || '', + newTitle: editTitle, + newRecurrenceInterval: editInterval, + newRecurrenceUnit: editUnit, + newRecurrenceTime: editTime || null, + }), + }); + + if (res.ok) { + setEditingSignature(null); + onSeriesUpdated?.(); + } else { + const err = await res.json(); + alert(err.error || 'Failed to update series'); + } + } catch (e) { + console.error('Failed to update recurring series:', e); + } finally { + setIsSaving(false); + } + }; + + const unitLabel = (unit: string, interval: number) => { + if (unit === 'days') return interval === 1 ? 'Tag' : 'Tage'; + if (unit === 'weeks') return interval === 1 ? 'Woche' : 'Wochen'; + if (unit === 'months') return interval === 1 ? 'Monat' : 'Monate'; + return interval === 1 ? 'Jahr' : 'Jahre'; + }; + return (
e.stopPropagation()}> @@ -60,38 +116,103 @@ export default function RecurringTasksManager({ isOpen, onClose, tasks, onStopRe
) : (
- {groupedTasks.map(task => ( -
-
-
- {task.title} + {groupedTasks.map(({ task, signature }) => ( +
+ {editingSignature === signature ? ( + /* Edit mode */ +
+ setEditTitle(e.target.value)} + className="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm font-semibold outline-none focus:border-teal-400" + placeholder="Aufgabentitel" + /> +
+ Alle + setEditInterval(parseInt(e.target.value) || 1)} + className="w-14 px-2 py-1 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-xs text-center outline-none" + /> + + um + setEditTime(e.target.value)} + className="px-2 py-1 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-xs outline-none" + /> +
+
+ + +
-
- - Alle {task.recurrenceInterval === 1 ? '' : task.recurrenceInterval} { - task.recurrenceUnit === 'days' ? 'Tage' : - task.recurrenceUnit === 'weeks' ? 'Wochen' : - task.recurrenceUnit === 'months' ? 'Monate' : 'Jahre' - } - - {task.recurrenceTime && ( - - um {task.recurrenceTime} Uhr - - )} + ) : ( + /* View mode */ +
+
+
+ {task.title} +
+
+ + Alle {task.recurrenceInterval === 1 ? '' : `${task.recurrenceInterval} `}{unitLabel(task.recurrenceUnit || 'weeks', task.recurrenceInterval || 1)} + + {task.recurrenceTime && ( + + um {task.recurrenceTime} Uhr + + )} +
+
+
+ + +
-
- + )}
))}
diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index f620aaf..bb5a261 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -2,17 +2,24 @@ import React, { useState, useEffect, useRef } from 'react'; import { format } from 'date-fns'; +interface SomedayList { + id: string; + title: string; + tasks: any[]; +} + interface SearchModalProps { isOpen: boolean; onClose: () => void; tasks: any[]; events: any[]; + somedayLists?: SomedayList[]; onSelectTask: (date: Date) => void; } -export default function SearchModal({ isOpen, onClose, tasks, events, onSelectTask }: SearchModalProps) { +export default function SearchModal({ isOpen, onClose, tasks, events, somedayLists = [], onSelectTask }: SearchModalProps) { const [query, setQuery] = useState(''); - const [results, setResults] = useState<{ type: 'task' | 'event', item: any }[]>([]); + const [results, setResults] = useState<{ type: 'task' | 'event' | 'someday', item: any }[]>([]); const inputRef = useRef(null); useEffect(() => { @@ -33,18 +40,25 @@ export default function SearchModal({ isOpen, onClose, tasks, events, onSelectTa t.title.toLowerCase().includes(lowerQuery) && !t.somedayListId ).map(t => ({ type: 'task' as const, item: t })); + // Include someday tasks from all someday lists + const somedayTasks = somedayLists.flatMap(list => + list.tasks + .filter(t => t.title.toLowerCase().includes(lowerQuery)) + .map(t => ({ type: 'someday' as const, item: { ...t, somedayListTitle: list.title } })) + ); + const filteredEvents = events.filter(e => e.title.toLowerCase().includes(lowerQuery) ).map(e => ({ type: 'event' as const, item: e })); - setResults([...filteredTasks, ...filteredEvents].slice(0, 10)); - }, [query, tasks, events]); + setResults([...filteredTasks, ...somedayTasks, ...filteredEvents].slice(0, 15)); + }, [query, tasks, events, somedayLists]); if (!isOpen) return null; return (
-
e.stopPropagation()}> +
e.stopPropagation()}>
@@ -72,6 +86,10 @@ export default function SearchModal({ isOpen, onClose, tasks, events, onSelectTa key={`${result.type}-${result.item.id}-${idx}`} className="px-4 py-3 hover:bg-gray-50 cursor-pointer flex items-center gap-3 border-b border-gray-50 last:border-0" onClick={() => { + if (result.type === 'someday') { + onClose(); + return; + } const date = result.type === 'task' ? (result.item.scheduledDate ? new Date(result.item.scheduledDate) : new Date()) : (new Date(result.item.startTime)); @@ -80,8 +98,8 @@ export default function SearchModal({ isOpen, onClose, tasks, events, onSelectTa onClose(); }} > -
- {result.type === 'task' ? ( +
+ {result.type === 'task' || result.type === 'someday' ? ( ) : ( @@ -90,15 +108,18 @@ export default function SearchModal({ isOpen, onClose, tasks, events, onSelectTa
{result.item.title}
- {format( - result.type === 'task' - ? (result.item.scheduledDate ? new Date(result.item.scheduledDate) : new Date()) - : new Date(result.item.startTime), - 'PPP' - )} + {result.type === 'someday' + ? `Someday \u2022 ${result.item.somedayListTitle}` + : format( + result.type === 'task' + ? (result.item.scheduledDate ? new Date(result.item.scheduledDate) : new Date()) + : new Date(result.item.startTime), + 'PPP' + ) + }
- {result.type === 'task' && result.item.completed && ( + {(result.type === 'task' || result.type === 'someday') && result.item.completed && ( Completed )}
diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 293c693..7af0f04 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -1623,6 +1623,57 @@ export default function WeeklyView() { const [somedayExpanded, setSomedayExpanded] = useState(true); const [isAllDayExpanded, setIsAllDayExpanded] = useState(true); + const [somedayHeight, setSomedayHeight] = useState(() => { + if (typeof document !== 'undefined') { + const c = document.cookie.match(/somedayHeight=(\d+)/); + return c ? parseInt(c[1]) : null; + } + return null; + }); + const [allDayHeight, setAllDayHeight] = useState(() => { + if (typeof document !== 'undefined') { + const c = document.cookie.match(/allDayHeight=(\d+)/); + return c ? parseInt(c[1]) : null; + } + return null; + }); + const resizingRef = useRef<{ target: 'someday' | 'allday'; startY: number; startHeight: number } | null>(null); + + const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday') => { + e.preventDefault(); + const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; + const section = target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement); + if (!section) return; + resizingRef.current = { target, startY: clientY, startHeight: section.getBoundingClientRect().height }; + + const onMove = (ev: MouseEvent | TouchEvent) => { + if (!resizingRef.current) return; + const y = 'touches' in ev ? ev.touches[0].clientY : ev.clientY; + const delta = y - resizingRef.current.startY; + const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta)); + if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight); + else setAllDayHeight(newHeight); + }; + const onEnd = () => { + if (resizingRef.current) { + const section2 = resizingRef.current.target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement); + if (section2) { + const h = Math.round(section2.getBoundingClientRect().height); + document.cookie = `${resizingRef.current.target === 'someday' ? 'somedayHeight' : 'allDayHeight'}=${h};path=/;max-age=31536000`; + } + } + resizingRef.current = null; + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onEnd); + window.removeEventListener('touchmove', onMove); + window.removeEventListener('touchend', onEnd); + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onEnd); + window.addEventListener('touchmove', onMove); + window.addEventListener('touchend', onEnd); + }, []); + const [somedayLists, setSomedayLists] = useState([]); const [projects, setProjects] = useState<{ id: string; name: string; icon?: string | null; color?: string | null }[]>([]); const [editingTaskId, setEditingTaskId] = useState(null); @@ -2316,15 +2367,19 @@ export default function WeeklyView() { if (data.event) { // Transform API shape (start.dateTime/end.dateTime) to frontend shape (startTime/endTime) const ev = data.event; + // Find calendar info from connections to fill in missing color/title + const calInfo = connections.flatMap((c: any) => + (c.calendars || []).map((cal: any) => ({ ...cal, provider: c.provider })) + ).find((c: any) => c.id === (ev.calendarId || eventData.calendarId)); const frontendEvent: CalendarEvent = { id: ev.id, title: ev.title, startTime: ev.start?.dateTime || ev.start?.date || ev.startTime || '', endTime: ev.end?.dateTime || ev.end?.date || ev.endTime || '', - source: ev.source, - calendarId: ev.calendarId, - calendarTitle: ev.calendarTitle, - calendarColor: ev.backgroundColor || ev.calendarColor, + source: ev.source || calInfo?.provider || 'google', + calendarId: ev.calendarId || eventData.calendarId, + calendarTitle: ev.calendarTitle || calInfo?.summary || calInfo?.title || '', + calendarColor: ev.backgroundColor || ev.calendarColor || calInfo?.backgroundColor || calInfo?.color || '#3b82f6', }; setRawCalendarEvents(prev => { if (eventData.id) { @@ -2333,10 +2388,9 @@ export default function WeeklyView() { return [...prev, frontendEvent]; }); } - // Re-read from cache (not a force-refresh from provider, which could - // overwrite the optimistic update if the provider hasn't propagated yet). - // The backend already cached the event via upsertCachedEvent. - setTimeout(() => fetchCalendarEvents(false), 2000); + // Re-read from cache to get the canonical version + // The backend already cached the event via upsertCachedEvent + await fetchCalendarEvents(false); } catch (error: any) { console.error("Error saving event:", error); if (error.name === "AbortError") { @@ -2986,7 +3040,28 @@ export default function WeeklyView() { const res = await fetch("/api/projects"); if (res.ok) { const data = await res.json(); - setProjects(data.projects || []); + const updatedProjects = data.projects || []; + setProjects(updatedProjects); + + // Update project references on tasks so color changes take effect immediately + const projectMap = new Map( + updatedProjects.map((p: any) => [p.id, p]) + ); + setTasks(prev => prev.map(t => { + if (t.projectId && projectMap.has(t.projectId)) { + return { ...t, project: projectMap.get(t.projectId) || null }; + } + return t; + })); + setSomedayLists(prev => prev.map(list => ({ + ...list, + tasks: list.tasks.map(t => { + if (t.projectId && projectMap.has(t.projectId)) { + return { ...t, project: projectMap.get(t.projectId) || null }; + } + return t; + }), + }))); } } catch (error) { console.error("Error fetching projects:", error); @@ -4873,6 +4948,47 @@ export default function WeeklyView() { startTime: null, }), }); + + // If the target list is synced to an external provider and + // the task doesn't already exist at that provider/list, create it there + const targetList = somedayLists.find((l) => l.id === listId); + const needsSync = targetList?.externalId && targetList?.externalProvider && ( + !draggedTask.externalId || + draggedTask.externalProvider !== targetList.externalProvider || + draggedTask.externalListId !== targetList.externalId + ); + if (needsSync) { + try { + const syncRes = await fetch("/api/tasks/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ taskId: draggedTask.id }), + }); + if (syncRes.ok) { + const syncData = await syncRes.json(); + // Update local state with external IDs + if (syncData.task) { + setSomedayLists((prev) => + prev.map((l) => ({ + ...l, + tasks: l.tasks.map((t) => + t.id === draggedTask.id + ? { + ...t, + externalId: syncData.task.externalId, + externalProvider: syncData.task.externalProvider, + externalListId: syncData.task.externalListId, + } + : t + ), + })) + ); + } + } + } catch (syncError) { + console.error("Failed to sync task to external provider:", syncError); + } + } } catch (error) { console.error("Error moving task to someday slot:", error); } @@ -5094,6 +5210,7 @@ export default function WeeklyView() { return (
{showTimeGrid && ( @@ -5240,6 +5357,16 @@ export default function WeeklyView() {
)}
+ {/* Resize handle */} + {isAllDayExpanded && ( +
startResize(e, 'allday')} + onTouchStart={(e) => startResize(e, 'allday')} + > +
+
+ )} ); })(); @@ -6947,6 +7074,7 @@ export default function WeeklyView() {
{/* Someday tabs bar */}
@@ -7820,6 +7948,16 @@ export default function WeeklyView() {
{/* close flex row */} + {/* Resize handle */} + {somedayExpanded && ( +
startResize(e, 'someday')} + onTouchStart={(e) => startResize(e, 'someday')} + > +
+
+ )} ) } @@ -7830,6 +7968,7 @@ export default function WeeklyView() { onClose={() => setIsSearchOpen(false)} tasks={tasks} events={calendarEvents} + somedayLists={somedayLists} onSelectTask={(date) => { setCurrentWeekStart(getStartOfWeek(date)); }} @@ -7884,6 +8023,7 @@ export default function WeeklyView() { console.error("Failed to stop recurring series:", error); } }} + onSeriesUpdated={() => fetchTasks()} /> {/* Recurring Task Delete Confirmation Modal */}