631 lines
19 KiB
TypeScript
631 lines
19 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,
|
|
ChevronUp,
|
|
} from "lucide-react";
|
|
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
import { RootState } from "../store";
|
|
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice";
|
|
import { clearUser } from "../store/userSlice";
|
|
|
|
// interface User {
|
|
// email: string;
|
|
// password: string;
|
|
// }
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
}
|
|
|
|
// Updated Project interface to match the JSON structure
|
|
interface Project {
|
|
projectId: string;
|
|
projectName: string;
|
|
activityStatus: "ACTIVE" | "AWAY";
|
|
totalWorkTime: string;
|
|
totalWorkMs: number;
|
|
totalLogs: number;
|
|
}
|
|
|
|
interface ProjectTrackingData {
|
|
elapsedTime: number;
|
|
startTime: Date | null;
|
|
endTime: Date | null;
|
|
isTracking: boolean;
|
|
isPaused: boolean;
|
|
pauseStartTime: Date | null;
|
|
totalPausedTime: number;
|
|
accumulatedTime: number;
|
|
}
|
|
|
|
export interface TrackingState {
|
|
selectedProject: Project | null;
|
|
projectTrackingMap: Map<string, ProjectTrackingData>;
|
|
isTracking: boolean;
|
|
isPaused: boolean;
|
|
elapsedTime: number;
|
|
startTime: Date | null;
|
|
endTime: Date | null;
|
|
handleStartTracking: (project: Project) => void;
|
|
handlePauseResume: () => void;
|
|
handleStopTracking: () => void;
|
|
handleProjectSelect: (projectId: string) => void;
|
|
projectsData: Project[];
|
|
}
|
|
|
|
const DashboardLayout = () => {
|
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const dispatch = useDispatch();
|
|
|
|
const { windowLogs } = useSelector((state: RootState) => state.window);
|
|
|
|
console.log("🧾 windowLogs in component:", windowLogs);
|
|
|
|
const menuItems = [
|
|
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
|
|
// { label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
|
|
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
|
|
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
|
|
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
|
|
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
|
|
];
|
|
|
|
// 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 [projectSummary, setProjectSummary] = useState({
|
|
totalProjects: 0,
|
|
activeProjects: 0,
|
|
awayProjects: 0,
|
|
totalWorkTime: "0h 0m 0s",
|
|
totalWorkMs: 0,
|
|
vaName: "",
|
|
vaId: "",
|
|
});
|
|
const [activeProjectLogId, setActiveProjectLogId] = useState("")
|
|
|
|
const { finalizeTracking } = useActiveAppTracker(
|
|
isTracking,
|
|
activeProjectLogId,
|
|
selectedProject?.projectId ?? null,
|
|
selectedProject?.projectName ?? null
|
|
);
|
|
|
|
const [projectsData, setProjectsData] = useState<Project[]>([]);
|
|
|
|
// fetch the project list of VA
|
|
useEffect(() => {
|
|
const fetchProjects = async () => {
|
|
try {
|
|
console.log("📡 Fetching projects...");
|
|
const res = await window.electron.getProjectList();
|
|
console.log(" Project list response:", res);
|
|
|
|
if (res?.success && Array.isArray(res.data)) {
|
|
// Store project list
|
|
setProjectsData(res.data);
|
|
|
|
// Store project summary info
|
|
setProjectSummary({
|
|
totalProjects: res.totalProjects || 0,
|
|
activeProjects: res.activeProjects || 0,
|
|
awayProjects: res.awayProjects || 0,
|
|
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
|
|
totalWorkMs: res.totalWorkMs || 0,
|
|
vaName: res.vaName || "",
|
|
vaId: res.vaId || "",
|
|
});
|
|
|
|
// Store data redux store
|
|
dispatch(setProjectsDataSlice(res.data));
|
|
|
|
dispatch(
|
|
setProjectSummarySlice({
|
|
totalProjects: res.totalProjects || 0,
|
|
activeProjects: res.activeProjects || 0,
|
|
awayProjects: res.awayProjects || 0,
|
|
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
|
|
totalWorkMs: res.totalWorkMs || 0,
|
|
vaName: res.vaName || "",
|
|
vaId: res.vaId || "",
|
|
})
|
|
);
|
|
|
|
// Auto-select first project
|
|
if (res.data.length > 0 && !selectedProject) {
|
|
const firstProject = res.data[0];
|
|
setSelectedProject(firstProject);
|
|
dispatch(setSelectedProjectSlice(firstProject));
|
|
|
|
// Convert totalWorkMs to seconds for elapsedTime
|
|
const initialElapsedTime = Math.floor(firstProject.totalWorkMs / 1000);
|
|
|
|
// Initialize tracking map for that project
|
|
setProjectTrackingMap((prev) => {
|
|
const updated = new Map(prev);
|
|
if (!updated.has(firstProject.projectId)) {
|
|
updated.set(firstProject.projectId, {
|
|
elapsedTime:initialElapsedTime,
|
|
startTime: null,
|
|
endTime: null,
|
|
isTracking: false,
|
|
isPaused: false,
|
|
pauseStartTime: null,
|
|
totalPausedTime: 0,
|
|
accumulatedTime: initialElapsedTime,
|
|
});
|
|
}
|
|
return updated;
|
|
});
|
|
// Set the initial elapsed time in the component state
|
|
setElapsedTime(initialElapsedTime);
|
|
}
|
|
} else {
|
|
console.warn("⚠️ Unexpected response:", res);
|
|
}
|
|
} catch (err) {
|
|
console.error("❌ Error fetching projects:", err);
|
|
}
|
|
};
|
|
|
|
fetchProjects();
|
|
}, []);
|
|
|
|
const [projectTrackingMap, setProjectTrackingMap] = useState<
|
|
Map<string, ProjectTrackingData>
|
|
>(new Map());
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
const res = await window.electron.logout();
|
|
if (res) {
|
|
dispatch(clearUser()); // ✅ Removes from Redux + localStorage
|
|
navigate("/login");
|
|
}
|
|
} catch (error) {
|
|
console.error("Logout error:", error);
|
|
}
|
|
|
|
};
|
|
|
|
// Sync global states with selected project tracking data
|
|
useEffect(() => {
|
|
if (selectedProject) {
|
|
const projectData = projectTrackingMap.get(selectedProject.projectId);
|
|
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.projectId);
|
|
|
|
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;
|
|
|
|
const calculatedElapsed =
|
|
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
|
|
|
|
current.elapsedTime = calculatedElapsed;
|
|
updated.set(selectedProject.projectId, { ...current });
|
|
|
|
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 = async (project: Project) => {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
|
|
const now = new Date();
|
|
window.electron.toggleCloseBtn(false);
|
|
|
|
setProjectTrackingMap((prev) => {
|
|
const updated = new Map(prev);
|
|
const current = updated.get(project.projectId) || {
|
|
elapsedTime: 0,
|
|
startTime: null,
|
|
endTime: null,
|
|
isTracking: false,
|
|
isPaused: false,
|
|
pauseStartTime: null,
|
|
totalPausedTime: 0,
|
|
accumulatedTime: 0,
|
|
};
|
|
|
|
current.startTime = now;
|
|
current.isTracking = true;
|
|
current.isPaused = false;
|
|
current.pauseStartTime = null;
|
|
current.totalPausedTime = 0;
|
|
updated.set(project.projectId, current);
|
|
|
|
setIsTracking(true);
|
|
setIsPaused(false);
|
|
setStartTime(now);
|
|
setElapsedTime(current.accumulatedTime);
|
|
|
|
return updated;
|
|
});
|
|
|
|
try {
|
|
const payload = {
|
|
vaId: projectSummary.vaId,
|
|
projectId: project.projectId,
|
|
type: "LOGIN",
|
|
};
|
|
|
|
// console.log("📡 Sending start tracking payload:", payload);
|
|
const res = await window.electron.startProjectTracking(payload);
|
|
console.log(" Synced start tracking:", res);
|
|
setActiveProjectLogId(res?.logId)
|
|
} catch (error) {
|
|
console.error(" Failed to sync project start:", error);
|
|
}
|
|
};
|
|
|
|
// Pause/Resume tracking with pause duration tracking
|
|
const handlePauseResume = () => {
|
|
if (!selectedProject) return;
|
|
|
|
const currentProjectData = projectTrackingMap.get(selectedProject.projectId);
|
|
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.projectId);
|
|
if (!current) return updated;
|
|
|
|
if (newPausedState) {
|
|
current.isPaused = true;
|
|
current.pauseStartTime = now;
|
|
} else {
|
|
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.projectId, { ...current });
|
|
setIsPaused(newPausedState);
|
|
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
// Stop tracking - PRESERVES elapsed time
|
|
const handleStopTracking = async () => {
|
|
if (!selectedProject) return;
|
|
|
|
console.log("🛑 Stop tracking initiated...");
|
|
|
|
// STEP 1: Send final PATCH first
|
|
if (finalizeTracking) {
|
|
try {
|
|
await finalizeTracking();
|
|
console.log(" Final tracking data sent");
|
|
} catch (error) {
|
|
console.error("❌ Error sending final tracking data:", error);
|
|
}
|
|
}
|
|
|
|
// STEP 2: Clear timer
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
|
|
// STEP 3: Send LOGOUT request
|
|
try {
|
|
const payload = {
|
|
vaId: projectSummary.vaId,
|
|
projectId: selectedProject.projectId,
|
|
type: "LOGOUT",
|
|
};
|
|
|
|
console.log("📡 Sending stop tracking payload:", payload);
|
|
const res = await window.electron.stopProjectTracking(payload);
|
|
console.log(" Synced stop tracking:", res);
|
|
} catch (error) {
|
|
console.error("❌ Failed to sync project stop:", error);
|
|
}
|
|
|
|
// STEP 4: Update UI and state
|
|
const now = new Date();
|
|
await window.electron.toggleCloseBtn(true);
|
|
await window.electron.resizeWindow(500, 600);
|
|
toggleWindowSize.current = true;
|
|
|
|
setProjectTrackingMap((prev) => {
|
|
const updated = new Map(prev);
|
|
const current = updated.get(selectedProject.projectId);
|
|
if (!current) return updated;
|
|
|
|
current.accumulatedTime = current.elapsedTime;
|
|
current.isTracking = false;
|
|
current.isPaused = false;
|
|
current.endTime = now;
|
|
current.startTime = null;
|
|
updated.set(selectedProject.projectId, { ...current });
|
|
|
|
setIsTracking(false);
|
|
setIsPaused(false);
|
|
setEndTime(now);
|
|
setElapsedTime(current.elapsedTime);
|
|
|
|
return updated;
|
|
});
|
|
|
|
console.log(" Tracking stopped successfully. Time preserved:", elapsedTime);
|
|
};
|
|
|
|
// Cleanup timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Handle project selection
|
|
const handleProjectSelect = (projectId: string) => {
|
|
if (isTracking) return; // prevent switching while tracking
|
|
|
|
const project = projectsData.find((p) => p.projectId === projectId);
|
|
if (!project) return;
|
|
|
|
setSelectedProject(project);
|
|
dispatch(setSelectedProjectSlice(project));
|
|
|
|
const existingData = projectTrackingMap.get(projectId);
|
|
|
|
setProjectTrackingMap((prev) => {
|
|
const updated = new Map(prev);
|
|
|
|
// If project is not yet tracked, initialize it with totalWorkMs
|
|
if (!existingData) {
|
|
const initialElapsedTime = Math.floor((project.totalWorkMs || 0) / 1000);
|
|
updated.set(projectId, {
|
|
elapsedTime: initialElapsedTime,
|
|
startTime: null,
|
|
endTime: null,
|
|
isTracking: false,
|
|
isPaused: false,
|
|
pauseStartTime: null,
|
|
totalPausedTime: 0,
|
|
accumulatedTime: initialElapsedTime,
|
|
});
|
|
|
|
// Update component-level state
|
|
setElapsedTime(initialElapsedTime);
|
|
setStartTime(null);
|
|
setEndTime(null);
|
|
setIsTracking(false);
|
|
setIsPaused(false);
|
|
} else {
|
|
// If already tracked, restore its state
|
|
setElapsedTime(existingData.elapsedTime);
|
|
setStartTime(existingData.startTime);
|
|
setEndTime(existingData.endTime);
|
|
setIsTracking(existingData.isTracking);
|
|
setIsPaused(existingData.isPaused);
|
|
}
|
|
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
|
|
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`;
|
|
};
|
|
|
|
const toggleWindowSize = useRef(true);
|
|
|
|
return (
|
|
<>
|
|
{/* Header */}
|
|
{isTracking &&
|
|
<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 */}
|
|
<button
|
|
onClick={() => {
|
|
if (toggleWindowSize.current) {
|
|
window.electron.resizeWindow(500, 85);
|
|
toggleWindowSize.current = false;
|
|
} else {
|
|
window.electron.resizeWindow(500, 600);
|
|
toggleWindowSize.current = true;
|
|
}
|
|
}}
|
|
className="btn btn-xs btn-outline btn-primary"
|
|
>
|
|
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
}
|
|
|
|
{/* Layout Body */}
|
|
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : " h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
|
{/* Sidebar */}
|
|
<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 , index) => (
|
|
<div className="tooltip z-100 tooltip-right" data-tip={item.label} key={index}>
|
|
<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>
|
|
</div>
|
|
))}
|
|
<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 tooltip tooltip-right "
|
|
data-tip={"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; |