Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions src/contexts/TimeTrackingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataService | null>(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<PlannedTask[]>([]);

// Initialize data service when auth state changes
useEffect(() => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PlannedTask>) => {
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]);

Expand All @@ -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}` });
};

Expand Down
2 changes: 2 additions & 0 deletions src/services/dataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface DataService {
// Planned tasks operations
savePlannedTasks: (tasks: PlannedTask[]) => Promise<void>;
getPlannedTasks: () => Promise<PlannedTask[]>;
upsertPlannedTask: (task: PlannedTask) => Promise<void>;
deletePlannedTask: (id: string) => Promise<void>;

// Migration operations
migrateFromLocalStorage: () => Promise<void>;
Expand Down
10 changes: 9 additions & 1 deletion src/services/localStorageService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -72,6 +72,14 @@ export class LocalStorageService implements DataService {
return getPlannedTasks();
}

upsertPlannedTask(task: PlannedTask): Promise<void> {
return upsertPlannedTask(task);
}

deletePlannedTask(id: string): Promise<void> {
return deletePlannedTask(id);
}

async migrateFromLocalStorage(): Promise<void> {
// No-op for localStorage service
}
Expand Down
20 changes: 20 additions & 0 deletions src/services/localStorageService/plannedTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ export async function savePlannedTasks(tasks: PlannedTask[]): Promise<void> {
}
}

export async function upsertPlannedTask(task: PlannedTask): Promise<void> {
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<void> {
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<PlannedTask[]> {
try {
return readVersioned<PlannedTask>(STORAGE_KEYS.PLANNED_TASKS, "data");
Expand Down
30 changes: 30 additions & 0 deletions src/services/supabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,36 @@ export class SupabaseService implements DataService {
if (error) throw error;
}

async upsertPlannedTask(task: PlannedTask): Promise<void> {
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<void> {
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<PlannedTask[]> {
const user = await this.requireUser();

Expand Down
Loading