diff --git a/src/contexts/TimeTrackingContext.tsx b/src/contexts/TimeTrackingContext.tsx index d0dfce8..71da2cd 100644 --- a/src/contexts/TimeTrackingContext.tsx +++ b/src/contexts/TimeTrackingContext.tsx @@ -272,9 +272,11 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ // Ref-based access to dataService for todo save effect (avoids adding dataService // to todo callback deps, which would invalidate them on every auth change). const dataServiceRef = useRef(null); - // Guards against saving todos during the initial data load. + // Guards against saving todos/planned tasks during the initial data load. const todoLoadedRef = useRef(false); const plannedLoadedRef = useRef(false); + // Tracks current plannedTasks for use in callbacks without closing over state. + const plannedTasksRef = useRef([]); // Initialize data service when auth state changes useEffect(() => { @@ -1059,10 +1061,9 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ dataServiceRef.current.saveTodos(todoItems); }, [todoItems]); - // Persist planned tasks whenever plannedTasks changes due to a user action. + // Keep ref in sync so planned task callbacks can read current state without closing over it. useEffect(() => { - if (!plannedLoadedRef.current || !dataServiceRef.current) return; - dataServiceRef.current.savePlannedTasks(plannedTasks); + plannedTasksRef.current = plannedTasks; }, [plannedTasks]); // Stable callbacks — no todoItems in deps. Functional updates ensure each @@ -1107,24 +1108,32 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ updatedAt: now }; setPlannedTasks(prev => [...prev, newTask]); + if (plannedLoadedRef.current) void dataServiceRef.current?.upsertPlannedTask(newTask); successNotify(); }, [successNotify]); const updatePlannedTask = useCallback((id: string, updates: Partial) => { - setPlannedTasks(prev => prev.map(t => - t.id === id ? { ...t, ...updates, updatedAt: new Date().toISOString() } : t - )); + const now = new Date().toISOString(); + setPlannedTasks(prev => prev.map(t => t.id === id ? { ...t, ...updates, updatedAt: now } : t)); + if (plannedLoadedRef.current) { + const current = plannedTasksRef.current.find(t => t.id === id); + if (current) void dataServiceRef.current?.upsertPlannedTask({ ...current, ...updates, updatedAt: now }); + } }, []); const deletePlannedTask = useCallback((id: string) => { setPlannedTasks(prev => prev.filter(t => t.id !== id)); + if (plannedLoadedRef.current) void dataServiceRef.current?.deletePlannedTask(id); mediumImpact(); }, [mediumImpact]); const movePlannedTask = useCallback((id: string, status: PlannedTaskStatus) => { - setPlannedTasks(prev => prev.map(t => - t.id === id ? { ...t, status, updatedAt: new Date().toISOString() } : t - )); + const now = new Date().toISOString(); + setPlannedTasks(prev => prev.map(t => t.id === id ? { ...t, status, updatedAt: now } : t)); + if (plannedLoadedRef.current) { + const current = plannedTasksRef.current.find(t => t.id === id); + if (current) void dataServiceRef.current?.upsertPlannedTask({ ...current, status, updatedAt: now }); + } lightImpact(); }, [lightImpact]); @@ -1137,12 +1146,13 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ toast({ title: "Resolve your previous day first", description: "Your previous work day is still open. Please end or discard it." }); return; } - const task = plannedTasks.find(t => t.id === id); + const task = plannedTasksRef.current.find(t => t.id === id); if (!task) return; const newTaskId = startNewTask(task.title, task.description, task.project, task.client, task.category); - setPlannedTasks(prev => prev.map(t => - t.id === id ? { ...t, status: "in_progress" as PlannedTaskStatus, linkedTaskId: newTaskId, updatedAt: new Date().toISOString() } : t - )); + const now = new Date().toISOString(); + const updated: PlannedTask = { ...task, status: "in_progress" as PlannedTaskStatus, linkedTaskId: newTaskId, updatedAt: now }; + setPlannedTasks(prev => prev.map(t => t.id === id ? updated : t)); + if (plannedLoadedRef.current) void dataServiceRef.current?.upsertPlannedTask(updated); toast({ title: `Task started: ${task.title}` }); }; diff --git a/src/services/dataService.ts b/src/services/dataService.ts index 9540b39..20e6497 100644 --- a/src/services/dataService.ts +++ b/src/services/dataService.ts @@ -41,6 +41,8 @@ export interface DataService { // Planned tasks operations savePlannedTasks: (tasks: PlannedTask[]) => Promise; getPlannedTasks: () => Promise; + upsertPlannedTask: (task: PlannedTask) => Promise; + deletePlannedTask: (id: string) => Promise; // Migration operations migrateFromLocalStorage: () => Promise; diff --git a/src/services/localStorageService/index.ts b/src/services/localStorageService/index.ts index 4f3be51..6998632 100644 --- a/src/services/localStorageService/index.ts +++ b/src/services/localStorageService/index.ts @@ -11,7 +11,7 @@ import { import { saveProjects, getProjects } from "./projects"; import { saveCategories, getCategories } from "./categories"; import { saveTodos, getTodos } from "./todos"; -import { savePlannedTasks, getPlannedTasks } from "./plannedTasks"; +import { savePlannedTasks, getPlannedTasks, upsertPlannedTask, deletePlannedTask } from "./plannedTasks"; export { STORAGE_KEYS, SCHEMA_VERSION } from "./constants"; @@ -72,6 +72,14 @@ export class LocalStorageService implements DataService { return getPlannedTasks(); } + upsertPlannedTask(task: PlannedTask): Promise { + return upsertPlannedTask(task); + } + + deletePlannedTask(id: string): Promise { + return deletePlannedTask(id); + } + async migrateFromLocalStorage(): Promise { // No-op for localStorage service } diff --git a/src/services/localStorageService/plannedTasks.ts b/src/services/localStorageService/plannedTasks.ts index 0001144..8654b08 100644 --- a/src/services/localStorageService/plannedTasks.ts +++ b/src/services/localStorageService/plannedTasks.ts @@ -13,6 +13,26 @@ export async function savePlannedTasks(tasks: PlannedTask[]): Promise { } } +export async function upsertPlannedTask(task: PlannedTask): Promise { + try { + const current = await getPlannedTasks(); + const exists = current.some(t => t.id === task.id); + const next = exists ? current.map(t => t.id === task.id ? task : t) : [...current, task]; + await savePlannedTasks(next); + } catch (error) { + console.warn("Failed to upsert planned task in localStorage:", error); + } +} + +export async function deletePlannedTask(id: string): Promise { + try { + const current = await getPlannedTasks(); + await savePlannedTasks(current.filter(t => t.id !== id)); + } catch (error) { + console.warn("Failed to delete planned task from localStorage:", error); + } +} + export async function getPlannedTasks(): Promise { try { return readVersioned(STORAGE_KEYS.PLANNED_TASKS, "data"); diff --git a/src/services/supabaseService.ts b/src/services/supabaseService.ts index 804481e..915dc5d 100644 --- a/src/services/supabaseService.ts +++ b/src/services/supabaseService.ts @@ -926,6 +926,36 @@ export class SupabaseService implements DataService { if (error) throw error; } + async upsertPlannedTask(task: PlannedTask): Promise { + const user = await this.requireUser(); + const { error } = await supabase.from("planned_tasks").upsert({ + id: task.id, + user_id: user.id, + title: task.title, + description: task.description ?? null, + status: task.status, + project_name: task.project ?? null, + client: task.client ?? null, + category_id: task.category ?? null, + priority: task.priority, + linked_task_id: task.linkedTaskId ?? null, + created_at: task.createdAt + }, { onConflict: "id" }); + trackDbCall("upsert", "planned_tasks"); + if (error) throw error; + } + + async deletePlannedTask(id: string): Promise { + const user = await this.requireUser(); + const { error } = await supabase + .from("planned_tasks") + .delete() + .eq("user_id", user.id) + .eq("id", id); + trackDbCall("delete", "planned_tasks"); + if (error) throw error; + } + async getPlannedTasks(): Promise { const user = await this.requireUser();