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); const limitAlertShownRef = useRef(false); // NOW define handleLimitExceeded using the defined handleStopTracking const handleLimitExceeded = useCallback(async () => { console.log("🚨 LIMIT EXCEEDED - Showing alert (not stopping)"); if (!selectedProject) { console.error("❌ No selected project for limit exceeded"); return; } // Only show alert once per tracking session if (limitAlertShownRef.current) { console.log("⚠️ Alert already shown, skipping"); return; } limitAlertShownRef.current = true; console.log("⏰ Showing limit exceeded alert"); // Show alert to user alert( `⏰ Daily Limit Reached!\n\n` + `Project: ${selectedProject.projectName}\n` + `Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` + `You've reached the daily time limit for this project.\n` + `Timer will continue running. Please stop manually when ready.` ); console.log("βœ… Alert shown, timer continues running"); }, [selectedProject]); // Initialize useActiveAppTracker and store finalizeTracking in ref const { finalizeTracking, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker( isTracking, activeProjectLogId, selectedProject?.projectId ?? null, selectedProject?.projectName ?? null, handleLimitExceeded ); // Define handleStopTracking FIRST const handleStopTracking = useCallback(async () => { if (loadingStop || loadingStart) { console.log("⏸️ Already processing, skipping..."); return; } if (!selectedProject) { console.warn("⚠️ No selected project to stop"); return; } console.log("=".repeat(70)); console.log("πŸ›‘ STOP TRACKING INITIATED"); console.log("=".repeat(70)); console.log(`πŸ“¦ Project: ${selectedProject.projectName}`); console.log(`⏱️ Elapsed Time: ${elapsedTime}s`); // Reset alert flag when stopping limitAlertShownRef.current = false; const currentStopTime = new Date().toISOString(); const projectId = selectedProject.projectId; // βœ… CRITICAL: Set stopping flag IMMEDIATELY to block all new tab tracking console.log("πŸ”’ Setting stopping flag..."); if (setIsStoppingInTracker) { setIsStoppingInTracker(true); } setIsStopping(true); setLoadingStop(true); handleSocketDisconnect(); // STEP 1: Clear local timer to prevent further client-side updates console.log("⏹️ STEP 1: Clearing local timer..."); if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; console.log("βœ… Timer cleared"); } // STEP 2: Send final PATCH with last active window data console.log("πŸ“€ STEP 2: Sending final activity tracking..."); if (stopTrackingRef.current) { try { await stopTrackingRef.current(); // This calls sendFinalPatch() in the hook console.log("βœ… Final activity tracking completed"); } catch (error) { console.error("❌ Error during final tracking:", error); } } else { console.log("⚠️ No tracking ref available"); } // STEP 3: Send LOGOUT to backend console.log("πŸšͺ STEP 3: Sending LOGOUT request..."); try { const payload = { vaId: projectSummary.vaId, projectId: projectId, type: "LOGOUT", time: currentStopTime }; console.log("πŸ“‘ Logout payload:", payload); const res = await window.electron.stopProjectTracking(payload); console.log("βœ… LOGOUT successful:", res); } catch (error) { console.error("❌ LOGOUT failed:", error); } // STEP 4: Update Redux tracking state console.log("πŸ”„ STEP 4: Updating Redux state..."); dispatch( stopTrackingReducer({ projectId, }) ); // STEP 6: Update UI (restore window) console.log("πŸͺŸ STEP 6: Restoring window..."); await window.electron.toggleCloseBtn(true); await window.electron.resizeWindow(500, 600); toggleWindowSize.current = true; console.log("βœ… Window restored"); // 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 console.log("πŸ“Š STEP 5: Refreshing project list..."); try { const projectListRes = await window.electron.getProjectList(); console.log("βœ… Project list fetched:", 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 from server const updatedProject = projectListRes.data.find( (p: Project) => p.projectId === projectId ); if (updatedProject) { dispatch(setSelectedProjectSlice(updatedProject)); // Sync client time with server time const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime); console.log("πŸ”„ Syncing with server time:", { serverTime: updatedProject.totalWorkTime, serverSeconds: serverElapsedTime, clientSeconds: 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("βœ… Project synced with server"); } } } catch (error) { console.error("❌ Error refreshing project list:", error); } finally { // Clear loading states setLoadingStop(false); setIsStopping(false); // βœ… Reset stopping flag for next session if (setIsStoppingInTracker) { setIsStoppingInTracker(false); } console.log("=".repeat(70)); console.log("πŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); console.log("=".repeat(70)); } }, [ selectedProject, projectSummary, elapsedTime, dispatch, loadingStop, loadingStart, setIsStoppingInTracker ]); // Update the ref whenever finalizeTracking changes useEffect(() => { stopTrackingRef.current = finalizeTracking; }, [finalizeTracking]); const socketRef = useRef(null); const intervalRef = useRef(null); const lastAppsRef = 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]); 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}`); limitAlertShownRef.current = false; 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 && !limitAlertShownRef.current) { 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://quality-friendly-goldfish.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("startSendAppsDetails", async ({ apps }) => { // socketRef.current?.emit("getVisibleApps", { token: await token, apps }); // }); socketRef.current.on("startSendAppsDetails", async ({ projId }) => { if (projId !== selectedProject?.projectId) return; console.log("▢️ startSendAppsDetails received"); // avoid multiple intervals if (intervalRef.current) return; intervalRef.current = setInterval(async () => { const currentApps = await window.electron.getRunningApps(); // Compare with last sent data const hasChanged = JSON.stringify(currentApps) !== JSON.stringify(lastAppsRef.current); if (hasChanged) { lastAppsRef.current = currentApps; socketRef.current?.emit("getVisibleApps", { token: await token, apps: currentApps, }); } }, 5000); }); socketRef.current.on("stopSendAppsDetails", async ({ projId }) => { if (projId !== selectedProject?.projectId) return; console.log("⏹ stopSendAppsDetails received"); // clear the interval if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } // Send final stop event to server // socketRef.current?.emit("stopSendAppsDetails", { // token: await token, // }); }); 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 `; }; const [checkIp, setCheckIp] = useState(false) useEffect(() => { setTimeout(() => { fetch('https://api.ipify.org/?format=json').then((res) => console.log("IPPPPPPPPPPPPPP", res)) }, 5000) }, [checkIp]) 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;