From 0b51cd5afbecfc9d9012b1968a4298f35759b8cb Mon Sep 17 00:00:00 2001 From: Adam Jolicoeur Date: Wed, 14 Jan 2026 16:17:34 -0500 Subject: [PATCH] fix: Refactor TimeTrackingProvider usage and improve unsaved changes tracking Moved TimeTrackingProvider to wrap the entire app in App.tsx, removing redundant providers from individual pages. Improved unsaved changes tracking in TimeTrackingContext by setting hasUnsavedChanges in all relevant state-changing functions instead of using a global effect. Minor code formatting and indentation fixes applied. --- dev-dist/sw.js | 2 +- src/App.tsx | 43 ++++++++-------- src/contexts/TimeTrackingContext.tsx | 66 +++++++++++++----------- src/pages/Archive.tsx | 76 ++++++++++++++-------------- src/pages/Categories.tsx | 14 ++--- src/pages/Index.tsx | 74 +++++++++++++-------------- src/pages/ProjectList.tsx | 4 +- src/pages/Settings.tsx | 4 +- 8 files changed, 141 insertions(+), 142 deletions(-) diff --git a/dev-dist/sw.js b/dev-dist/sw.js index ae3c97b..b39aad6 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict'; */ workbox.precacheAndRoute([{ "url": "index.html", - "revision": "0.9ku5uov2tfg" + "revision": "0.r0dlh3r3ltk" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/App.tsx b/src/App.tsx index 6b2373a..17d0cda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { AuthProvider } from "@/contexts/AuthContext"; import { OfflineProvider } from "@/contexts/OfflineContext"; +import { TimeTrackingProvider } from "@/contexts/TimeTrackingContext"; import { Suspense, lazy } from "react"; import { InstallPrompt } from "@/components/InstallPrompt"; import { UpdateNotification } from "@/components/UpdateNotification"; @@ -27,26 +28,28 @@ const PageLoader = () => ( const App = () => ( - - - - - }> - - } /> - } /> - } /> - } /> - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - - - - - - + + + + + + }> + + } /> + } /> + } /> + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + + + + + ); diff --git a/src/contexts/TimeTrackingContext.tsx b/src/contexts/TimeTrackingContext.tsx index 6637df6..07f8a75 100644 --- a/src/contexts/TimeTrackingContext.tsx +++ b/src/contexts/TimeTrackingContext.tsx @@ -1,10 +1,10 @@ import React, { - createContext, - useContext, - useState, - useEffect, - useCallback, - useRef + createContext, + useContext, + useState, + useEffect, + useCallback, + useRef } from "react"; import { DEFAULT_CATEGORIES, TaskCategory } from "@/config/categories"; import { DEFAULT_PROJECTS, ProjectCategory } from "@/config/projects"; @@ -60,15 +60,15 @@ export interface TimeEntry { } export interface InvoiceData { - client: string; - period: { startDate: Date; endDate: Date }; - projects: { [key: string]: { hours: number; rate: number; amount: number } }; - summary: { - totalHours: number; - totalAmount: number; - }; - tasks: (Task & { dayId: string; dayDate: string; dailySummary: string })[]; - dailySummaries: { [dayId: string]: { date: string; summary: string } }; + client: string; + period: { startDate: Date; endDate: Date }; + projects: { [key: string]: { hours: number; rate: number; amount: number } }; + summary: { + totalHours: number; + totalAmount: number; + }; + tasks: (Task & { dayId: string; dayDate: string; dailySummary: string })[]; + dailySummaries: { [dayId: string]: { date: string; summary: string } }; } interface TimeTrackingContextType { @@ -462,15 +462,6 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ // 2. Window close (beforeunload) // 3. Manual sync button - // Track changes to mark as unsaved - useEffect(() => { - // Mark as having unsaved changes whenever state changes - // (but not during initial loading) - if (!loading && dataService) { - setHasUnsavedChanges(true); - } - }, [isDayStarted, dayStartTime, tasks, currentTask, archivedDays, projects, categories, loading, dataService]); - // Save on window close to prevent data loss useEffect(() => { const handleBeforeUnload = (event: BeforeUnloadEvent) => { @@ -498,6 +489,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ const now = startDateTime || new Date(); setIsDayStarted(true); setDayStartTime(now); + setHasUnsavedChanges(true); console.log('Day started at:', now); }; @@ -522,6 +514,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ setCurrentTask(null); } setIsDayStarted(false); + setHasUnsavedChanges(true); console.log('🔚 Day ended - saving state...'); // Save immediately since this is a critical action saveImmediately().then(() => { @@ -568,6 +561,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ setTasks((prev) => [...prev, newTask]); setCurrentTask(newTask); + setHasUnsavedChanges(true); console.log('New task started:', title, 'at', taskStartTime); // Save immediately since this is a critical action saveImmediately(); @@ -580,6 +574,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ if (currentTask?.id === taskId) { setCurrentTask((prev) => (prev ? { ...prev, ...updates } : null)); } + setHasUnsavedChanges(true); console.log('Task updated:', taskId, updates); }; @@ -588,6 +583,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ if (currentTask?.id === taskId) { setCurrentTask(null); } + setHasUnsavedChanges(true); console.log('Task deleted:', taskId); }; @@ -643,6 +639,8 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ }); console.log('✅ Cleared current day state saved'); + setHasUnsavedChanges(false); + // Show success notification to user toast({ title: "Day Archived Successfully", @@ -693,6 +691,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ id: Date.now().toString() }; setProjects((prev) => [...prev, newProject]); + setHasUnsavedChanges(true); console.log('📋 Project added (not saved automatically)'); }; @@ -702,17 +701,20 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ project.id === projectId ? { ...project, ...updates } : project ) ); + setHasUnsavedChanges(true); console.log('📋 Project updated (not saved automatically)'); }; const deleteProject = (projectId: string) => { setProjects((prev) => prev.filter((project) => project.id !== projectId)); + setHasUnsavedChanges(true); console.log('📋 Project deleted (not saved automatically)'); }; const resetProjectsToDefaults = () => { const defaultProjects = convertDefaultProjects(DEFAULT_PROJECTS); setProjects(defaultProjects); + setHasUnsavedChanges(true); }; // Archive management functions @@ -732,6 +734,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ // Then persist to database await dataService.updateArchivedDay(dayId, updates); + setHasUnsavedChanges(false); console.log('✅ Database update complete'); } catch (error) { console.error('❌ Error updating archived day:', error); @@ -778,6 +781,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ // Remove from archive setArchivedDays((prev) => prev.filter((day) => day.id !== dayId)); + setHasUnsavedChanges(true); console.log('Day restored from archive'); }; @@ -788,6 +792,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ id: Date.now().toString() }; setCategories((prev) => [...prev, newCategory]); + setHasUnsavedChanges(true); console.log('🏷️ Category added (not saved automatically)'); }; @@ -800,6 +805,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ category.id === categoryId ? { ...category, ...updates } : category ) ); + setHasUnsavedChanges(true); console.log('🏷️ Category updated (not saved automatically)'); }; @@ -807,6 +813,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ setCategories((prev) => prev.filter((category) => category.id !== categoryId) ); + setHasUnsavedChanges(true); console.log('🏷️ Category deleted (not saved automatically)'); }; @@ -845,13 +852,14 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ setCurrentTask((prev) => prev ? { - ...prev, - startTime: roundedStartTime, - endTime: roundedEndTime - } + ...prev, + startTime: roundedStartTime, + endTime: roundedEndTime + } : null ); } + setHasUnsavedChanges(true); }; const getTotalDayDuration = () => { @@ -959,7 +967,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ }); return Math.round(totalRevenue * 100) / 100; - }; const getBillableHoursForDay = (day: DayRecord): number => { + }; const getBillableHoursForDay = (day: DayRecord): number => { // Create lookup maps for O(1) access (performance optimization) const projectMap = new Map(projects.map(p => [p.name, p])); const categoryMap = new Map(categories.map(c => [c.id, c])); diff --git a/src/pages/Archive.tsx b/src/pages/Archive.tsx index 2f96e13..b475d89 100644 --- a/src/pages/Archive.tsx +++ b/src/pages/Archive.tsx @@ -165,42 +165,42 @@ const ArchiveContent: React.FC = () => { - - -
- {totalBillableHours.toFixed(1)}h -
-
Billable Hours
-
-
- - -
- {totalNonBillableHours.toFixed(1)}h -
-
Non-billable Hours
-
- {totalHoursWorked.toFixed(1)}h total work -
-
-
- - -
- ${totalRevenue.toFixed(2)} -
-
Total Revenue
-
-
- - -
- ${totalBillableHours > 0 ? (totalRevenue / totalBillableHours).toFixed(2) : '0.00'} -
-
Avg Billable Rate
-
-
- + + +
+ {totalBillableHours.toFixed(1)}h +
+
Billable Hours
+
+
+ + +
+ {totalNonBillableHours.toFixed(1)}h +
+
Non-billable Hours
+
+ {totalHoursWorked.toFixed(1)}h total work +
+
+
+ + +
+ ${totalRevenue.toFixed(2)} +
+
Total Revenue
+
+
+ + +
+ ${totalBillableHours > 0 ? (totalRevenue / totalBillableHours).toFixed(2) : '0.00'} +
+
Avg Billable Rate
+
+
+ {/* Archived Days */}
@@ -238,9 +238,7 @@ const ArchiveContent: React.FC = () => { const Archive: React.FC = () => { return ( - - - + ); }; diff --git a/src/pages/Categories.tsx b/src/pages/Categories.tsx index 3e13d5a..a06fc7f 100644 --- a/src/pages/Categories.tsx +++ b/src/pages/Categories.tsx @@ -200,11 +200,10 @@ const CategoryContent: React.FC = () => { onClick={() => setFormData((prev) => ({ ...prev, color })) } - className={`w-6 h-6 rounded-full border-2 hover:scale-110 transition-transform ${ - formData.color === color + className={`w-6 h-6 rounded-full border-2 hover:scale-110 transition-transform ${formData.color === color ? 'border-gray-800' : 'border-gray-300' - }`} + }`} style={{ backgroundColor: color }} /> ))} @@ -277,11 +276,10 @@ const CategoryContent: React.FC = () => {

{category.name}

- + }`}> {category.isBillable !== false ? 'Billable' : 'Non-billable'}
@@ -325,9 +323,7 @@ const CategoryContent: React.FC = () => { const Categories: React.FC = () => { return ( - - - + ); }; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index fd8f8ad..b8c177d 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -99,35 +99,35 @@ const TimeTrackerContent = () => { {/* Main Content */} {!isDayStarted ? ( -
-
-
-

- - Dashboard -

-
- {/* Summary Stats */} -
- - -
- {sortedDays.length} -
-
Days Tracked
-
-
- - -
- {totalHours}h -
-
Total Hours
-
-
+
+
+
+

+ + Dashboard +

+
+ {/* Summary Stats */} +
+ + +
+ {sortedDays.length} +
+
Days Tracked
+
+
+ + +
+ {totalHours}h +
+
Total Hours
+
+
+
-
) : null}
{ ))}
)} - + )}
@@ -199,9 +199,7 @@ const TimeTrackerContent = () => { const Index = () => { return ( - - - + ); }; diff --git a/src/pages/ProjectList.tsx b/src/pages/ProjectList.tsx index 899c486..a4c9313 100644 --- a/src/pages/ProjectList.tsx +++ b/src/pages/ProjectList.tsx @@ -357,9 +357,7 @@ const ProjectContent: React.FC = () => { const ProjectList: React.FC = () => { return ( - - - + ); }; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 2a337d8..37a00bf 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -226,9 +226,7 @@ const SettingsContent: React.FC = () => { const Settings: React.FC = () => { return ( - - - + ); };