import React, { useState, useEffect, useRef, useCallback } from "react"; import io, { Socket } from 'socket.io-client'; 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, GlobeLock, ClipboardClock, CircleDollarSign, } from "lucide-react"; import { useActiveAppTracker } from "../hooks/useActiveAppTracker"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../store"; import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime, updateProjectTracking } from "../store/projectSlice"; import { clearUser } from "../store/userSlice"; import { setError, setLoading, setProfile } from "../store/profileSlice"; import { canStartTracking } from "../utils/projectLimits"; import { useTheme } from "../helper/ThemeToggle"; import LogoutModal from "../modal/LogoutModal"; import { parseTimeStringToSeconds } from "../utils/timeParser"; interface User { id: string; name: string; email: string; } interface Project { projectId: string; projectName: string; activityStatus: "ACTIVE" | "AWAY"; totalWorkTime: string; totalWorkMs: number; totalLogs: number; dedicatedHours: string; } 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; 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[]; loadingStart: boolean, loadingStop: boolean, isStopping: boolean } const DashboardLayout = () => { const { theme, setTheme } = useTheme(); const toggleTheme = () => { setTheme(theme === "dark" ? "light" : "dark"); }; const [isSidebarOpen, setSidebarOpen] = useState(true); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); const [isStopping, setIsStopping] = useState(false); const { projectsData, projectSummary, selectedProject, projectTrackingMap } = useSelector((state: any) => state.projects); const tracking = selectedProject ? projectTrackingMap[selectedProject.projectId] : null; const isTracking = tracking?.isTracking; const elapsedTime = tracking?.elapsedTime || 0; const { windowLogs } = useSelector((state: RootState) => state.window); const menuItems = [ { label: "Dashboard", path: "/dashboard", icon: , exact: true }, { label: "Timesheet", path: "/dashboard/timesheet", icon: }, { label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: }, { label: "Schedule", path: "/dashboard/schedule", icon: }, { label: "Pays Slips", path: "/dashboard/pays-slips", icon: }, { label: "Settings", path: "/dashboard/settings", icon: }, { label: "VPN", path: "/dashboard/vpn", icon: }, { label: "Help & Support", path: "/dashboard/help-support", icon: }, ]; const [isPaused, setIsPaused] = useState(false); const [startTime, setStartTime] = useState(null); const [endTime, setEndTime] = useState(null); const timerRef = useRef | null>(null); const [activeProjectLogId, setActiveProjectLogId] = useState(""); const toggleWindowSize = useRef(true); const [loadingStart, setLoadingStart] = useState(false); const [loadingStop, setLoadingStop] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); // Create a ref to store the stop function so it can be called from the hook const stopTrackingRef = useRef<(() => Promise) | null>(null); // Define handleStopTracking FIRST const handleStopTracking = useCallback(async () => { if (loadingStop || loadingStart) return; if (!selectedProject) { console.warn("No selected project to stop"); return; } let currentStopTime = new Date().toISOString(); const projectId = selectedProject.projectId; console.log(" Stop tracking initiated for:", selectedProject.projectName); console.log(" Current elapsed time:", elapsedTime); setIsStopping(true); setLoadingStop(true); handleSocketDisconnect(); // STEP 1: Clear timer FIRST to prevent further client-side updates if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; console.log("Timer cleared"); } // STEP 2: Send final PATCH with last active window data if (stopTrackingRef.current) { try { console.log(" Sending final PATCH to record last activity..."); await stopTrackingRef.current(); console.log(" Final tracking data sent successfully"); } catch (error) { console.error(" Error sending final tracking data:", error); } } // STEP 3: Send LOGOUT to backend try { const payload = { vaId: projectSummary.vaId, projectId: projectId, type: "LOGOUT", time: currentStopTime }; console.log("๐Ÿ“ก Sending LOGOUT request:", payload); const res = await window.electron.stopProjectTracking(payload); console.log(" LOGOUT successful:", res); } catch (error) { console.error(" Failed to send LOGOUT:", error); } // STEP 4: Update Redux tracking state to stop tracking console.log("๐Ÿ”„ Updating Redux state to stop tracking..."); dispatch( stopTrackingReducer({ projectId, }) ); // Allow React to process the state update await new Promise(resolve => setTimeout(resolve, 50)); // STEP 5: Refresh project list to get updated work hours from server try { console.log("๐Ÿ”„ Refreshing project list..."); const projectListRes = await window.electron.getProjectList(); console.log("๐Ÿ“Š Updated project list:", projectListRes); if (projectListRes?.success && Array.isArray(projectListRes.data)) { // Update projects data dispatch(setProjectsDataSlice(projectListRes.data)); // Update project summary dispatch( setProjectSummarySlice({ totalProjects: projectListRes.totalProjects || 0, activeProjects: projectListRes.activeProjects || 0, awayProjects: projectListRes.awayProjects || 0, totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s", totalWorkMs: projectListRes.totalWorkMs || 0, vaName: projectListRes.vaName || "", vaId: projectListRes.vaId || "", }) ); // Update selected project with fresh data const updatedProject = projectListRes.data.find( (p: Project) => p.projectId === projectId ); if (updatedProject) { dispatch(setSelectedProjectSlice(updatedProject)); // CRITICAL FIX: Use server time instead of client elapsedTime const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime); console.log("๐Ÿ”„ Syncing client time with server time:", { serverTime: updatedProject.totalWorkTime, serverElapsedSeconds: serverElapsedTime, previousClientTime: elapsedTime, difference: serverElapsedTime - elapsedTime }); // Force update the tracking map with server time dispatch( updateProjectTracking({ projectId: updatedProject.projectId, data: { elapsedTime: serverElapsedTime, accumulatedTime: serverElapsedTime, isTracking: false, isPaused: false, startTime: null, endTime: Date.now(), pauseStartTime: null, totalPausedTime: 0 } }) ); console.log("โœ… Selected project updated with server time"); } } // STEP 6: Update UI (window restore) await window.electron.toggleCloseBtn(true); await window.electron.resizeWindow(500, 600); toggleWindowSize.current = true; } catch (error) { console.error("โŒ Error refreshing project list:", error); } finally { // Clear loading states setLoadingStop(false); setIsStopping(false); console.log("๐ŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); } }, [selectedProject, projectSummary, elapsedTime, dispatch]); // NOW define handleLimitExceeded using the defined handleStopTracking const handleLimitExceeded = useCallback(async () => { console.log("๐Ÿšจ LIMIT EXCEEDED - Auto-stopping tracking"); if (!selectedProject) { console.error(" No selected project for limit exceeded"); return; } console.log("Alert Message 1") // Show alert to user alert( `โฐ Daily Limit Reached!\n\n` + `Project: ${selectedProject.projectName}\n` + `Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` + `Your tracking has been stopped automatically because you've reached the daily time limit for this project.` ); console.log("Alert Message 2") // Stop tracking // await handleStopTracking(); }, [selectedProject, handleStopTracking]); // Initialize useActiveAppTracker and store finalizeTracking in ref const { finalizeTracking } = useActiveAppTracker( isTracking, activeProjectLogId, selectedProject?.projectId ?? null, selectedProject?.projectName ?? null, handleLimitExceeded ); // Update the ref whenever finalizeTracking changes useEffect(() => { stopTrackingRef.current = finalizeTracking; }, [finalizeTracking]); const socketRef = useRef(null); const intervalRef = useRef(null); const [isConnected, setIsConnected] = useState(false); useEffect(() => { if (!window.electron?.onTokenExpired) return; console.log("๐Ÿ” Token expiry listener registered"); window.electron.onTokenExpired(async () => { console.log("๐Ÿ”ฅ Token expired โ€” checking tracking status..."); // If user is tracking โ†’ FIRST stop tracking if (isTracking && handleStopTracking) { console.log("โ›” Token expired DURING tracking โ†’ stopping tracking first..."); try { await handleStopTracking(); // Wait until stop is completed console.log("๐Ÿ›‘ Tracking stopped successfully before logout"); } catch (err) { console.error(" Error while stopping tracking after token expiry:", err); } } // Then perform logout console.log("๐Ÿšช Proceeding to logout due to token expiry..."); await handleLogout(); }); return () => { console.log("๐Ÿงน Cleaning token-expired listener"); }; }, [isTracking, handleStopTracking]); const handleLogout = async () => { try { const res = await window.electron.logout(); if (res) { dispatch(clearUser()); navigate("/login"); } } catch (error) { console.error("Logout error:", error); } }; const selectedData = selectedProject ? projectTrackingMap[selectedProject.projectId] : null; const handleStartTracking = async (project: Project) => { if (loadingStart || loadingStop) return; // Check limit before starting const limitCheck = canStartTracking(project); if (!limitCheck.canStart) { alert( `โš ๏ธ Cannot Start Tracking!\n\n` + `Project: ${project.projectName}\n` + `Reason: ${limitCheck.reason}\n\n` + `You've already completed your daily hours for this project.` ); return; } console.log(`โฐ Starting tracking. Remaining time: ${limitCheck.remaining}`); setLoadingStart(true); const now = Date.now(); let currentStartTime = new Date().toISOString(); window.electron.toggleCloseBtn(false); const payload = { vaId: projectSummary.vaId, projectId: project.projectId, type: "LOGIN", time: currentStartTime }; try { const res = await window.electron.startProjectTracking(payload); if (!res.success) { alert(res.message); await window.electron.toggleCloseBtn(true); return; } // CRITICAL FIX: Get the current accumulated time BEFORE starting timer const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime); console.log("๐Ÿ“Š Starting with accumulated time:", { accumulatedTime: currentAccumulatedTime, projectTotalWorkTime: project.totalWorkTime, parsedTime: parseTimeStringToSeconds(project.totalWorkTime) }); // โฌ‡๏ธ REDUX UPDATE: Start Tracking dispatch( startTracking({ projectId: project.projectId, startTime: now, }) ); handleSocketConnect(); setActiveProjectLogId(res?.data?.logId); console.log(" Tracking started. Log ID:", res?.data?.logId); // โฌ‡๏ธ Calculate time limit in seconds const parseTimeToSeconds = (timeStr: string): number => { return parseTimeStringToSeconds(timeStr); }; const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours); const alreadyWorkedSeconds = parseTimeStringToSeconds(project.totalWorkTime); const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds; console.log("โฑ๏ธ Time Limits:", { dedicatedHours: project.dedicatedHours, dedicatedSeconds, alreadyWorked: `${Math.floor(alreadyWorkedSeconds / 60)}m ${alreadyWorkedSeconds % 60}s`, remainingSeconds, remainingMinutes: Math.floor(remainingSeconds / 60) }); // โฌ‡๏ธ FIXED: Start timer with proper initial time // Set initial elapsed time immediately to avoid jump dispatch( updateElapsedTime({ projectId: project.projectId, elapsedTime: currentAccumulatedTime, }) ); // Start global timer loop with limit check timerRef.current = setInterval(() => { const timeSinceStart = Math.floor((Date.now() - now) / 1000); const currentElapsed = currentAccumulatedTime + timeSinceStart; dispatch( updateElapsedTime({ projectId: project.projectId, elapsedTime: currentElapsed, }) ); // Check if time limit reached const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart; if (totalTimeWorked >= dedicatedSeconds) { console.warn("๐Ÿšจ CLIENT-SIDE: Time limit reached!"); console.log(`Total worked: ${Math.floor(totalTimeWorked / 60)}m vs Limit: ${Math.floor(dedicatedSeconds / 60)}m`); // Clear timer first to prevent multiple triggers if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } // Trigger auto-stop handleLimitExceeded(); handleSocketDisconnect(); } }, 1000); } catch (error) { console.error(" Failed to sync project start:", error); alert("Something went wrong!"); await window.electron.toggleCloseBtn(true); } finally { setLoadingStart(false); } }; const handlePauseResume = () => { if (!selectedProject) return; // Implement pause/resume logic if needed }; // Cleanup timer on unmount useEffect(() => { return () => { if (timerRef.current) { clearInterval(timerRef.current); } }; }, []); const handleProjectSelect = (projectId: string) => { const project = projectsData.find((p: any) => p.projectId === projectId); if (!project) return; dispatch(setSelectedProjectSlice(project)); const existing = projectTrackingMap[projectId]; if (!existing) { // Use parsed totalWorkTime instead of totalWorkMs const initialTime = parseTimeStringToSeconds(project.totalWorkTime); dispatch(initializeProjectTracking({ projectId, initialTime: initialTime })); } }; const handleSocketConnect = async () => { const token = await window.electron.getToken("ACCESS_TOKEN"); if (socketRef.current) { console.warn("Already connected"); return; } if (!selectedProject?.projectId) return console.log(selectedProject?.projectId, "SelectedProject ID") socketRef.current = io("https://daniela-nonprofessed-boastfully.ngrok-free.app", { transports: ["websocket"], secure: true, rejectUnauthorized: false, reconnection: true, withCredentials: true, forceNew: true, extraHeaders: { "Access-Control-Allow-Origin": "*" }, auth: { token: token, projectId: selectedProject?.projectId, userType: "va" }, }); socketRef.current.on("connect", () => { console.log("๐ŸŸข Connected:", socketRef.current?.id); setIsConnected(true); // Start sending every 5 seconds intervalRef.current = setInterval(async () => { const apps = await window.electron.getRunningApps() socketRef.current?.emit("getVisibleApps", { token: await token, apps }); }, 5000); }); socketRef.current.on("appToCapture", async ({ appTitle }) => { console.log("๐Ÿ“ธ Capture requested for:", appTitle); const base64String = await window.electron.captureAppScreenShot(appTitle); if (base64String && socketRef.current) { socketRef.current.emit("sendImage", { token: token, image: base64String, title: appTitle, projectId: selectedProject?.projectId, }); } }); socketRef.current.on("disconnect", () => { console.log("๐Ÿ”ด Disconnected"); setIsConnected(false); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }); }; const handleSocketDisconnect = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } if (socketRef.current) { socketRef.current.disconnect(); socketRef.current = null; setIsConnected(false); console.log("๐Ÿ”Œ Disconnected manually"); } }; const trackingState: TrackingState = { selectedProject, isTracking, isPaused, elapsedTime, startTime, endTime, handleStartTracking, handlePauseResume, handleStopTracking, projectsData, handleProjectSelect, loadingStart, loadingStop, isStopping }; 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 `${hrs}h ${mins}m `; }; return ( <>
{/* Header */} {isTracking &&
{isStopping ? ( // Skeleton for left side during stop
) : selectedProject ? ( isTracking ? ( ) : ( ) ) : ( )} {!isStopping && selectedProject ? ( {selectedProject.projectName} ) : !isStopping ? ( No Project Selected ) : null}
{isStopping ? ( // Skeleton for right side during stop
) : ( <>
{formatTime( isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s") )}
{/* */} )}
} {/* Layout Body */}
{/* Sidebar */} {/* Main Content */}
setShowLogoutModal(false)} onConfirm={handleLogout} />
); }; export default DashboardLayout;