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
2 changes: 1 addition & 1 deletion dev-dist/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"), {
Expand Down
43 changes: 23 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,26 +28,28 @@ const PageLoader = () => (
const App = () => (
<OfflineProvider>
<AuthProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/projectlist" element={<ProjectList />} />
<Route path="/categories" element={<Categories />} />
<Route path="/archive" element={<Archive />} />
<Route path="/settings" element={<Settings />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
<MobileNav />
</Suspense>
</BrowserRouter>
<InstallPrompt />
<UpdateNotification />
</TooltipProvider>
<TimeTrackingProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/projectlist" element={<ProjectList />} />
<Route path="/categories" element={<Categories />} />
<Route path="/archive" element={<Archive />} />
<Route path="/settings" element={<Settings />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
<MobileNav />
</Suspense>
</BrowserRouter>
<InstallPrompt />
<UpdateNotification />
</TooltipProvider>
</TimeTrackingProvider>
</AuthProvider>
</OfflineProvider>
);
Expand Down
66 changes: 37 additions & 29 deletions src/contexts/TimeTrackingContext.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
};

Expand All @@ -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(() => {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
};

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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)');
};

Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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');
};

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

Expand All @@ -800,13 +805,15 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
category.id === categoryId ? { ...category, ...updates } : category
)
);
setHasUnsavedChanges(true);
console.log('🏷️ Category updated (not saved automatically)');
};

const deleteCategory = (categoryId: string) => {
setCategories((prev) =>
prev.filter((category) => category.id !== categoryId)
);
setHasUnsavedChanges(true);
console.log('🏷️ Category deleted (not saved automatically)');
};

Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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]));
Expand Down
76 changes: 37 additions & 39 deletions src/pages/Archive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,42 +165,42 @@ const ArchiveContent: React.FC = () => {
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-green-600">
{totalBillableHours.toFixed(1)}h
</div>
<div className="text-sm text-gray-600">Billable Hours</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-gray-600">
{totalNonBillableHours.toFixed(1)}h
</div>
<div className="text-sm text-gray-600">Non-billable Hours</div>
<div className="text-xs text-gray-500">
{totalHoursWorked.toFixed(1)}h total work
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-purple-600">
${totalRevenue.toFixed(2)}
</div>
<div className="text-sm text-gray-600">Total Revenue</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-orange-600">
${totalBillableHours > 0 ? (totalRevenue / totalBillableHours).toFixed(2) : '0.00'}
</div>
<div className="text-sm text-gray-600">Avg Billable Rate</div>
</CardContent>
</Card>
</div>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-green-600">
{totalBillableHours.toFixed(1)}h
</div>
<div className="text-sm text-gray-600">Billable Hours</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-gray-600">
{totalNonBillableHours.toFixed(1)}h
</div>
<div className="text-sm text-gray-600">Non-billable Hours</div>
<div className="text-xs text-gray-500">
{totalHoursWorked.toFixed(1)}h total work
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-purple-600">
${totalRevenue.toFixed(2)}
</div>
<div className="text-sm text-gray-600">Total Revenue</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-orange-600">
${totalBillableHours > 0 ? (totalRevenue / totalBillableHours).toFixed(2) : '0.00'}
</div>
<div className="text-sm text-gray-600">Avg Billable Rate</div>
</CardContent>
</Card>
</div>

{/* Archived Days */}
<div className="space-y-4">
Expand Down Expand Up @@ -238,9 +238,7 @@ const ArchiveContent: React.FC = () => {

const Archive: React.FC = () => {
return (
<TimeTrackingProvider>
<ArchiveContent />
</TimeTrackingProvider>
<ArchiveContent />
);
};

Expand Down
14 changes: 5 additions & 9 deletions src/pages/Categories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
/>
))}
Expand Down Expand Up @@ -277,11 +276,10 @@ const CategoryContent: React.FC = () => {
<h4 className="font-semibold text-gray-900">
{category.name}
</h4>
<span className={`px-2 py-1 text-xs rounded-full ${
category.isBillable !== false
<span className={`px-2 py-1 text-xs rounded-full ${category.isBillable !== false
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
}`}>
{category.isBillable !== false ? 'Billable' : 'Non-billable'}
</span>
</div>
Expand Down Expand Up @@ -325,9 +323,7 @@ const CategoryContent: React.FC = () => {

const Categories: React.FC = () => {
return (
<TimeTrackingProvider>
<CategoryContent />
</TimeTrackingProvider>
<CategoryContent />
);
};

Expand Down
Loading
Loading