va-tracker/src/ui/layout/DashboardLayout.tsx

466 lines
14 KiB
TypeScript

import React, { useState, useEffect, useRef } from "react";
import {
Outlet,
NavLink,
useNavigate,
useLocation,
} from "react-router-dom";
import {
Bell,
Menu,
LogOut,
User,
Settings,
Home,
FolderKanban,
X,
CalendarDays,
SettingsIcon,
TimerIcon,
HelpCircleIcon,
Pause,
ShieldBan,
ChevronDown,
Play,
} from "lucide-react";
interface User {
email: string;
password: string;
}
interface Project {
id: number;
projectName: string;
clientName: string;
}
interface ProjectTrackingData {
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
isTracking: boolean;
isPaused: boolean;
pauseStartTime: Date | null;
totalPausedTime: number;
accumulatedTime: number; // ✅ NEW: Stores time from previous sessions
}
interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<number, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
startTime: Date | null;
endTime: Date | null;
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (id: number) => void;
projectsData: Project[];
}
interface DashboardLayoutProps {
user: User | null;
setUser: React.Dispatch<React.SetStateAction<User | null>>;
}
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const [isSidebarOpen, setSidebarOpen] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const navigate = useNavigate();
const location = useLocation();
// Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
const [startTime, setStartTime] = useState<Date | null>(null);
const [endTime, setEndTime] = useState<Date | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [projectsData, setProjectsData] = useState<Project[]>([
{ id: 1, projectName: "CRM Redesign", clientName: "Salesforce" },
{ id: 2, projectName: "AI Assistant", clientName: "OpenAI" },
{ id: 3, projectName: "Marketing Dashboard", clientName: "Google" },
]);
const [projectTrackingMap, setProjectTrackingMap] = useState<
Map<number, ProjectTrackingData>
>(new Map());
const handleLogout = async () => {
try {
const res = await window.electron.logout()
if (res && res.success) {
localStorage.removeItem("user");
setUser(null);
navigate("/login");
}
} catch (error) {
console.error("Logout error:", error);
}
};
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
const closeSidebarOnMobile = () => {
if (isMobile) setSidebarOpen(false);
};
const menuItems = [
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
{ label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
];
// ✅ Sync global states with selected project tracking data
useEffect(() => {
if (selectedProject) {
const projectData = projectTrackingMap.get(selectedProject.id);
if (projectData) {
setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused);
setElapsedTime(projectData.elapsedTime);
setStartTime(projectData.startTime);
setEndTime(projectData.endTime);
}
}
}, [selectedProject, projectTrackingMap]);
// ✅ Timer effect - calculates elapsed time based on timestamps + accumulated time
useEffect(() => {
if (isTracking && !isPaused && selectedProject) {
timerRef.current = setInterval(() => {
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
if (current && current.isTracking && !current.isPaused && current.startTime) {
const now = Date.now();
const startTimestamp = current.startTime.getTime();
const currentSessionTime = Math.floor((now - startTimestamp) / 1000);
const totalPausedTime = current.totalPausedTime || 0;
// ✅ FIX: Add accumulated time from previous sessions
const calculatedElapsed =
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
current.elapsedTime = calculatedElapsed;
updated.set(selectedProject.id, { ...current });
// Update global elapsedTime in sync
setElapsedTime(calculatedElapsed);
}
return updated;
});
}, 1000);
} else {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [isTracking, isPaused, selectedProject]);
// ✅ Start tracking
const handleStartTracking = (project: Project) => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
const now = new Date();
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(project.id) || {
elapsedTime: 0,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: 0,
};
// ✅ Keep accumulated time from previous sessions
current.startTime = now;
current.isTracking = true;
current.isPaused = false;
current.pauseStartTime = null;
current.totalPausedTime = 0; // Reset pause time for new session
updated.set(project.id, current);
// Update global tracking states
setIsTracking(true);
setIsPaused(false);
setStartTime(now);
setElapsedTime(current.accumulatedTime); // Start from accumulated time
return updated;
});
};
// ✅ Pause/Resume tracking with pause duration tracking
const handlePauseResume = () => {
if (!selectedProject) return;
const currentProjectData = projectTrackingMap.get(selectedProject.id);
if (!currentProjectData || !currentProjectData.isTracking) return;
const now = new Date();
const newPausedState = !currentProjectData.isPaused;
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
if (!current) return updated;
if (newPausedState) {
// Starting pause
current.isPaused = true;
current.pauseStartTime = now;
} else {
// Resuming from pause
current.isPaused = false;
if (current.pauseStartTime) {
const pauseDuration = Math.floor(
(now.getTime() - current.pauseStartTime.getTime()) / 1000
);
current.totalPausedTime = (current.totalPausedTime || 0) + pauseDuration;
current.pauseStartTime = null;
}
}
updated.set(selectedProject.id, { ...current });
setIsPaused(newPausedState);
return updated;
});
};
// ✅ Stop tracking - PRESERVES elapsed time
const handleStopTracking = () => {
if (!selectedProject) return;
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
const now = new Date();
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
if (!current) return updated;
// ✅ FIX: Store current elapsed time as accumulated time
current.accumulatedTime = current.elapsedTime;
current.isTracking = false;
current.isPaused = false;
current.endTime = now;
current.startTime = null; // Clear startTime for stopped state
updated.set(selectedProject.id, { ...current });
// Update global states - keep elapsed time
setIsTracking(false);
setIsPaused(false);
setEndTime(now);
setElapsedTime(current.elapsedTime); // ✅ Keep the elapsed time
return updated;
});
console.log("Tracking stopped. Time preserved:", elapsedTime);
};
// Cleanup timer on unmount
useEffect(() => {
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
// ✅ Handle project selection
const handleProjectSelect = (id: number) => {
if (isTracking) return;
const project = projectsData.find((p) => p.id === id);
if (project) {
setSelectedProject(project);
const projectData = projectTrackingMap.get(id);
if (projectData) {
setElapsedTime(projectData.elapsedTime);
setStartTime(projectData.startTime);
setEndTime(projectData.endTime);
setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused);
} else {
setElapsedTime(0);
setStartTime(null);
setEndTime(null);
setIsTracking(false);
setIsPaused(false);
}
}
};
const trackingState: TrackingState = {
selectedProject,
projectTrackingMap,
isTracking,
isPaused,
elapsedTime,
startTime,
endTime,
handleStartTracking,
handlePauseResume,
handleStopTracking,
projectsData,
handleProjectSelect,
};
const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hrs}h ${mins}m ${secs}s`;
};
return (
<>
{/* Header */}
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
{/* Left Section */}
<div className="flex items-center space-x-3">
{selectedProject ? (
isTracking ? (
<button
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
onClick={handleStopTracking}
title="Stop Tracking"
>
<Pause size={16} />
</button>
) : (
<button
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
onClick={() => handleStartTracking(selectedProject)}
title="Start Tracking"
>
<Play size={16} />
</button>
)
) : (
<button
className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed"
disabled
title="Select a project to start"
>
<Play size={16} />
</button>
)}
{selectedProject ? (
<span className="ml-2 text-sm text-base-content/70">
{selectedProject.projectName}
</span>
) : (
<span className="ml-2 text-sm text-base-content/40 italic">
No Project Selected
</span>
)}
</div>
{/* Right Section */}
<div className="flex items-center space-x-3">
{/* Timer */}
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
{formatTime(elapsedTime || 0)}
</div>
{/* Alert Icon */}
<span className="text-error">
<ShieldBan size={18} />
</span>
{/* Dropdown */}
<div className="dropdown dropdown-end">
<div
tabIndex={0}
role="button"
className="btn btn-sm btn-outline btn-primary p-1.5 rounded-lg"
>
<ChevronDown />
</div>
<ul
tabIndex={0}
className="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-40 mt-2"
>
<li><a>View Logs</a></li>
<li><a>Settings</a></li>
</ul>
</div>
</div>
</header>
{/* Layout Body */}
<div className="flex h-[calc(100vh-3rem)] bg-base-200 overflow-hidden">
{/* Sidebar - Always visible */}
<aside
className="w-20 bg-base-100 border-r border-base-300 flex-shrink-0
flex flex-col items-center py-4 space-y-4"
>
{menuItems.map((item) => (
<NavLink
key={item.label}
to={item.path}
end={item.exact}
className={({ isActive }) =>
`flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${
isActive
? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
}`
}
>
{item.icon}
</NavLink>
))}
<button
onClick={handleLogout}
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer"
title="Logout"
>
<LogOut size={18} />
</button>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
<Outlet context={trackingState} />
</main>
</div>
</>
);
};
export default DashboardLayout;