diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index 5fedd1c..d6b37c9 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -35,7 +35,7 @@ api.interceptors.request.use( api.interceptors.response.use( (response: AxiosResponse) => response.data, async (error) => { - console.error('[Axios Error]', error.message); + // console.error('[Axios Error]', error.message); if (error.response?.status === 401) { console.warn('Token expired β€” trigger refresh logic if needed'); sendTokenExpired(); diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index 3eaedfe..64a9fb5 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -110,14 +110,29 @@ ipcMain.handle("start-project-tracking", async (event, payload) => { const response = await api.post("/log/project", payload); console.log(" Tracking start synced:", response); - return response; - } catch (error) { - console.error(" Error syncing project start:", error); - throw error; + return { success: true, data: response }; + } catch (error:any) { + console.error("❌ Error syncing project start:", error); + + // If API sent a response (like 400) + if (error.response) { + return { + success: false, + message: error.response.data?.message || "Request failed", + status: error.response.status + }; + } + + return { + success: false, + message: "Something went wrong", + status: 500 + }; } }); + // When project tracking stop ipcMain.handle("stop-project-tracking", async (event, payload) => { try { @@ -128,7 +143,7 @@ ipcMain.handle("stop-project-tracking", async (event, payload) => { console.log(" Tracking stop synced:", response); return response; } catch (error) { - console.error(" Error syncing project start:", error); + console.error(" Error syncing project stop:", error); throw error; } }); @@ -140,16 +155,16 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => { let response; if (!payload.id) { - // 🟒 POST new log (new tab opened) + // POST new log (new tab opened) response = await api.post("/log/project/track", { logId: payload.logId, projectId: payload.projectId, title: payload.title, appName: payload.appName, }); - console.log("βœ… New log created:", response.data); + console.log(" New log created:", response); } else { - // 🟑 PATCH existing log (same tab, update) + // PATCH existing log (same tab, update) response = await api.patch("/log/project/track", { id: payload.id, logId: payload.logId, @@ -157,12 +172,12 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => { title: payload.title, appName: payload.appName, }); - console.log("🟨 Log updated:", response.data); + console.log("Log updated:", response); } - return response.data; + return response; } catch (error) { - console.error("❌ Error syncing project tab tracking:", error); + console.error("Error syncing project tab tracking:", error); throw error; } }); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 11a4799..a792f6c 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -94,6 +94,8 @@ import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments import Timesheets from "./pages/Timesheets/Timesheets"; import Settings from "./pages/Settings/Settings"; import HelpSupport from "./pages/HelpSupport/HelpSupport"; +import Vpn from "./pages/Vpn/Vpn"; + const App: React.FC = () => { const theme = ThemeToggle(); @@ -139,6 +141,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> diff --git a/src/ui/hooks/useActiveAppTracker.tsx b/src/ui/hooks/useActiveAppTracker.tsx index 0727117..5dc48b8 100644 --- a/src/ui/hooks/useActiveAppTracker.tsx +++ b/src/ui/hooks/useActiveAppTracker.tsx @@ -2,11 +2,20 @@ import { useEffect, useRef, useCallback } from "react"; import { useDispatch } from "react-redux"; import { setActiveWindow, WindowInfo } from "../store/windowSlice"; +interface DailyHours { + todayTotal: string; + dailyLimit: string; + remaining: string; +} + + + export const useActiveAppTracker = ( isTracking: boolean, activeProjectLogId: string | null, projectId: string | null, - projectName: string | null + projectName: string | null, + onLimitExceeded?: () => void // Callback when limit is reached ) => { const dispatch = useDispatch(); const lastActiveRef = useRef(null); @@ -14,10 +23,44 @@ export const useActiveAppTracker = ( const lastTitleRef = useRef(null); const intervalIdRef = useRef(null); const finalizeTrackingRef = useRef<(() => Promise) | null>(null); + const limitCheckedRef = useRef(false); - // βœ… Common guard to check if we can send const canSend = !!(activeProjectLogId && projectId); + // βœ… Parse time string to minutes + const parseTimeToMinutes = (timeStr: string): number => { + const hourMatch = timeStr.match(/(\d+)h/); + const minMatch = timeStr.match(/(\d+)m/); + + const hours = hourMatch ? parseInt(hourMatch[1]) : 0; + const minutes = minMatch ? parseInt(minMatch[1]) : 0; + + return hours * 60 + minutes; + }; + + // βœ… Check if daily limit is exceeded + const checkDailyLimit = (dailyHours?: DailyHours): boolean => { + if (!dailyHours) return false; + + const todayTotalMins = parseTimeToMinutes(dailyHours.todayTotal); + const dailyLimitMins = parseTimeToMinutes(dailyHours.dailyLimit); + const remainingMins = parseTimeToMinutes(dailyHours.remaining); + + console.log("⏰ Daily Limit Check:", { + todayTotal: `${todayTotalMins} mins`, + dailyLimit: `${dailyLimitMins} mins`, + remaining: `${remainingMins} mins`, + exceeded: remainingMins <= 0 + }); + + // If remaining time is 0 or negative, limit is exceeded + if (remainingMins <= 0) { + return true; + } + + return false; + }; + const sendActivityLog = async ( projectLogId: string, projectId: string, @@ -44,17 +87,40 @@ export const useActiveAppTracker = ( appName, }; - // βœ… only attach `id` if it’s non-null and not undefined if (!isNew && lastLogIdRef.current) { payload.id = lastLogIdRef.current; } - console.log(isNew ? "➑️ POST new log" : "✏️ PATCH update log", payload); const res = await window.electron.trackingTabsOpeningClosing(payload); - if (isNew && res?._id) lastLogIdRef.current = res._id; + console.log("πŸ“Š Activity log response:", res); + + // Store the log ID for future updates + if (isNew && res?.data?._id) { + console.log("res?.data?._id", res?.data?._id) + lastLogIdRef.current = res.data._id; + } + + // βœ… Check daily limit after every API call + if (res?.dailyHours) { + console.log("res?.data?.dailyHours", res?.dailyHours) + const limitExceeded = checkDailyLimit(res?.dailyHours); + + if (limitExceeded && !limitCheckedRef.current) { + limitCheckedRef.current = true; + console.warn("🚨 Daily limit exceeded! Stopping tracking..."); + + // Trigger stop tracking callback + if (onLimitExceeded) { + setTimeout(() => { + onLimitExceeded(); + }, 500); + } + } + } + return res; } catch (error) { console.error("❌ Activity log error:", error); @@ -69,27 +135,54 @@ export const useActiveAppTracker = ( return; } - console.log("πŸ›‘ Sending final PATCH before stopping..."); + console.log("=".repeat(60)); + console.log("πŸ›‘ FINAL PATCH INITIATED - Recording last activity"); + console.log("=".repeat(60)); + if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) { try { - await sendActivityLog( + console.log("πŸ“Š Final Activity Details:"); + console.log(` β€’ Window Title: ${lastTitleRef.current}`); + console.log(` β€’ App Name: ${lastActiveRef.current.owner.name}`); + console.log(` β€’ Log ID: ${lastLogIdRef.current}`); + console.log(` β€’ Project ID: ${projectId}`); + + const result = await sendActivityLog( activeProjectLogId!, projectId!, lastTitleRef.current, lastActiveRef.current.owner.name || "Unknown", - false + false // This is a PATCH (update), not a POST (new) ); - console.log("βœ… Final PATCH sent for", lastTitleRef.current); + + console.log("βœ… Final PATCH sent successfully!"); + + if (result?.dailyHours) { + console.log("πŸ“ˆ Final Daily Hours Summary:"); + console.log(` β€’ Today Total: ${result.dailyHours.todayTotal}`); + console.log(` β€’ Daily Limit: ${result.dailyHours.dailyLimit}`); + console.log(` β€’ Remaining: ${result.dailyHours.remaining}`); + } + } catch (error) { console.error("❌ Final PATCH failed:", error); + throw error; // Re-throw to let caller handle } } else { - console.log("ℹ️ No active log to finalize"); + console.log("ℹ️ No active window to finalize"); + console.log(" β€’ Last Log ID:", lastLogIdRef.current || "None"); + console.log(" β€’ Last Title:", lastTitleRef.current || "None"); + console.log(" β€’ Last Active Window:", lastActiveRef.current ? "Yes" : "No"); } + // Clean up all refs + console.log("🧹 Cleaning up tracking references..."); lastActiveRef.current = null; lastTitleRef.current = null; lastLogIdRef.current = null; + limitCheckedRef.current = false; + console.log("βœ… Cleanup complete"); + console.log("=".repeat(60)); }, [activeProjectLogId, projectId, canSend]); useEffect(() => { @@ -97,7 +190,11 @@ export const useActiveAppTracker = ( }, [sendFinalPatch]); useEffect(() => { - // 🚫 If not tracking OR missing IDs, skip everything + // Reset limit check when tracking starts + if (isTracking) { + limitCheckedRef.current = false; + } + if (!isTracking || !canSend) { console.log("🟑 Tracking inactive or missing IDs β€” skipping tracker setup"); return; @@ -150,4 +247,4 @@ export const useActiveAppTracker = ( return { finalizeTracking: finalizeTrackingRef.current, }; -}; +}; \ No newline at end of file diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 75f2b71..7d18d01 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import io, { Socket } from 'socket.io-client'; import { @@ -25,18 +25,15 @@ import { ChevronDown, Play, ChevronUp, + GlobeLock, } 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 { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime } from "../store/projectSlice"; import { clearUser } from "../store/userSlice"; import { setError, setLoading, setProfile } from "../store/profileSlice"; - -// interface User { -// email: string; -// password: string; -// } +import { canStartTracking } from "../utils/projectLimits"; interface User { id: string; @@ -44,7 +41,6 @@ interface User { email: string; } -// Updated Project interface to match the JSON structure interface Project { projectId: string; projectName: string; @@ -52,6 +48,7 @@ interface Project { totalWorkTime: string; totalWorkMs: number; totalLogs: number; + dedicatedHours: string; } interface ProjectTrackingData { @@ -67,7 +64,6 @@ interface ProjectTrackingData { export interface TrackingState { selectedProject: Project | null; - projectTrackingMap: Map; isTracking: boolean; isPaused: boolean; elapsedTime: number; @@ -78,6 +74,8 @@ export interface TrackingState { handleStopTracking: () => void; handleProjectSelect: (projectId: string) => void; projectsData: Project[]; + loadingStart: boolean, + loadingStop: boolean } const DashboardLayout = () => { @@ -87,395 +85,332 @@ const DashboardLayout = () => { const location = useLocation(); const dispatch = useDispatch(); - const { windowLogs } = useSelector((state: RootState) => state.window); + const { projectsData, projectSummary, selectedProject, projectTrackingMap } = + useSelector((state: any) => state.projects); - console.log("🧾 windowLogs in component:", windowLogs); + 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: "Projects", path: "/dashboard/projects", icon: }, { label: "Timesheet", path: "/dashboard/timesheet", icon: }, { label: "PTO", path: "/dashboard/leave-adjustments", icon: }, { label: "Settings", path: "/dashboard/settings", icon: }, + { label: "VPN", path: "/dashboard/vpn", icon: }, { label: "Help & Support", path: "/dashboard/help-support", icon: }, ]; - // Tracking states - const [selectedProject, setSelectedProject] = useState(null); - const [isTracking, setIsTracking] = useState(false); const [isPaused, setIsPaused] = useState(false); - const [elapsedTime, setElapsedTime] = useState(0); const [startTime, setStartTime] = useState(null); const [endTime, setEndTime] = useState(null); const timerRef = useRef | 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 [activeProjectLogId, setActiveProjectLogId] = useState(""); + const toggleWindowSize = useRef(true); + const [loadingStart, setLoadingStart] = useState(false); + const [loadingStop, setLoadingStop] = useState(false); + + // βœ… 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; + } + + const projectId = selectedProject.projectId; + + console.log("πŸ›‘ Stop tracking initiated for:", selectedProject.projectName); + console.log("πŸ“Š Current elapsed time:", elapsedTime); + + setLoadingStop(true); + + // STEP 1: Send final PATCH with last active window data + // This ensures the last window activity is recorded before logout + 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); + // Continue with stop even if PATCH fails + } + } else { + console.warn("⚠️ No finalizeTracking function available"); + } + + // STEP 2: Stop timer everywhere + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + console.log("⏹️ Timer cleared"); + } else { + console.log("ℹ️ No active timer to clear"); + } + + // STEP 3: Send LOGOUT to backend + try { + const payload = { + vaId: projectSummary.vaId, + projectId: projectId, + type: "LOGOUT", + }; + + 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 UI (window restore) + try { + await window.electron.toggleCloseBtn(true); + await window.electron.resizeWindow(500, 600); + toggleWindowSize.current = true; + console.log("βœ… Window UI restored"); + } catch (error) { + console.error("❌ Error restoring window:", error); + } + + // STEP 5: Update Redux tracking state β†’ THIS TRIGGERS UI UPDATE + 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)); + console.log("βœ… Redux state updated - isTracking is now false"); + + // STEP 6: Clear active project log ID + setActiveProjectLogId(""); + setLoadingStop(false); + console.log("🧹 Active project log ID cleared"); + + console.log("=".repeat(50)); + console.log("πŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); + console.log(`Project: ${selectedProject.projectName}`); + console.log(`Final Time: ${formatTime(elapsedTime)}`); + console.log("UI Updates:"); + console.log(" βœ“ Header should be hidden"); + console.log(" βœ“ Start button should be visible"); + console.log(" βœ“ Layout should expand to full height"); + console.log("=".repeat(50)); + }, [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; + } + + // 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.` + ); + + // Stop tracking + await handleStopTracking(); + }, [selectedProject, handleStopTracking]); + + // βœ… Initialize useActiveAppTracker and store finalizeTracking in ref const { finalizeTracking } = useActiveAppTracker( isTracking, activeProjectLogId, selectedProject?.projectId ?? null, - selectedProject?.projectName ?? 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); - // const [seclectedScreen, setSelectedScreen] = useState(null) - - const [projectsData, setProjectsData] = useState([]); useEffect(() => { if (!window.electron?.onTokenExpired) return; console.log("πŸ” Token expiry listener registered"); - window.electron.onTokenExpired(() => { - console.log("πŸ”₯ Token expired β€” logging out..."); - handleLogout(); + 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 fetchProfile = async () => { - try { - dispatch(setLoading(true)); - const res = await window.electron.getProfile(); - console.log("Profile", res) - if (res.success && res.data) { - dispatch(setProfile(res.data)); - } else { - dispatch(setError("Failed to fetch profile")); - } - } catch (err: any) { - dispatch(setError(err.message)); - } finally { - dispatch(setLoading(false)); - } - }; - - // 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); - } - fetchProfile() - - } else { - console.warn("⚠️ Unexpected response:", res); - // if (res?.status) { - // // handleLogout() - // } - } - } catch (err) { - console.error("❌ Error fetching projects:", err); - } - }; - - fetchProjects(); - }, []); - - const [projectTrackingMap, setProjectTrackingMap] = useState< - Map - >(new Map()); - const handleLogout = async () => { try { const res = await window.electron.logout(); if (res) { - dispatch(clearUser()); // βœ… Removes from Redux + localStorage + dispatch(clearUser()); navigate("/login"); } - } catch (error) { + } 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]); + const selectedData = selectedProject + ? projectTrackingMap[selectedProject.projectId] + : null; - // 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; + + 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; } - const now = new Date(); + console.log(`⏰ Starting tracking. Remaining time: ${limitCheck.remaining}`); + + setLoadingStart(true); + + const now = Date.now(); + 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; - }); + const payload = { + vaId: projectSummary.vaId, + projectId: project.projectId, + type: "LOGIN", + }; try { - const payload = { - vaId: projectSummary.vaId, - projectId: project.projectId, - type: "LOGIN", + const res = await window.electron.startProjectTracking(payload); + + if (!res.success) { + alert(res.message); + await window.electron.toggleCloseBtn(true); + return; + } + + // ⬇️ REDUX UPDATE: Start Tracking + dispatch( + startTracking({ + projectId: project.projectId, + startTime: now, + }) + ); + + setActiveProjectLogId(res?.data?.logId); + console.log("βœ… Tracking started. Log ID:", res?.data?.logId); + + // ⬇️ Calculate time limit in seconds + const parseTimeToSeconds = (timeStr: string): number => { + const hourMatch = timeStr.match(/(\d+)h/); + const minMatch = timeStr.match(/(\d+)m/); + const hours = hourMatch ? parseInt(hourMatch[1]) : 0; + const minutes = minMatch ? parseInt(minMatch[1]) : 0; + return (hours * 3600) + (minutes * 60); }; - // console.log("πŸ“‘ Sending start tracking payload:", payload); - const res = await window.electron.startProjectTracking(payload); - console.log(" Synced start tracking:", res); - setActiveProjectLogId(res?.logId) + const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours); + const alreadyWorkedSeconds = Math.floor((project.totalWorkMs || 0) / 1000); + 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) + }); + + // ⬇️ Start global timer loop with limit check + timerRef.current = setInterval(() => { + const currentElapsed = Math.floor((Date.now() - now) / 1000) + + (selectedData?.accumulatedTime || 0); + + dispatch( + updateElapsedTime({ + projectId: project.projectId, + elapsedTime: currentElapsed, + }) + ); + + // βœ… Check if time limit reached + const totalTimeWorked = alreadyWorkedSeconds + Math.floor((Date.now() - now) / 1000); + + 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(); + } + }, 1000); + } catch (error) { - console.error(" Failed to sync project start:", error); + console.error("❌ Failed to sync project start:", error); + alert("Something went wrong!"); + await window.electron.toggleCloseBtn(true); + } finally { + setLoadingStart(false); } }; - // 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); + // Implement pause/resume logic if needed }; // Cleanup timer on unmount @@ -487,58 +422,24 @@ const DashboardLayout = () => { }; }, []); - // Handle project selection const handleProjectSelect = (projectId: string) => { - if (isTracking) return; // prevent switching while tracking - - const project = projectsData.find((p) => p.projectId === projectId); + const project = projectsData.find((p: any) => p.projectId === projectId); if (!project) return; - setSelectedProject(project); dispatch(setSelectedProjectSlice(project)); - const existingData = projectTrackingMap.get(projectId); + const existing = projectTrackingMap[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; - }); + if (!existing) { + dispatch(initializeProjectTracking({ + projectId, + initialTime: Math.floor((project.totalWorkMs || 0) / 1000) + })); + } }; - const trackingState: TrackingState = { selectedProject, - projectTrackingMap, isTracking, isPaused, elapsedTime, @@ -549,48 +450,37 @@ const DashboardLayout = () => { handleStopTracking, projectsData, handleProjectSelect, + loadingStart, + loadingStop }; 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`; }; - const toggleWindowSize = useRef(true); - return ( <> {/* Header */} {isTracking && -
- {/* Left Section */} - {/* {!isConnected ? ( - - ) : ( - - )} */} -
+
+
{selectedProject ? ( isTracking ? ( + ) : (
- {/* Right Section */}
- {/* Timer */}
{formatTime(elapsedTime || 0)}
- {/* Alert Icon */} - {/* Dropdown */} } + {!isTracking && ( + + )} {/* Main Content */} diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index d90aacd..08d0a60 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -2,6 +2,10 @@ import React, { useEffect, useRef, useState } from "react"; import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react"; import { useOutletContext } from "react-router-dom"; import NoData from "./NoData"; +import { useDispatch, useSelector } from "react-redux"; +import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice"; +import { setError, setLoading, setProfile } from "../store/profileSlice"; +import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits'; // Updated Project interface to match JSON structure interface Project { @@ -11,6 +15,7 @@ interface Project { totalWorkTime: string; totalWorkMs: number; totalLogs: number; + status: string } interface ProjectTrackingData { @@ -23,7 +28,7 @@ interface ProjectTrackingData { interface TrackingState { selectedProject: Project | null; - projectTrackingMap: Map; + // projectTrackingMap: Map; isTracking: boolean; isPaused: boolean; elapsedTime: number; @@ -34,6 +39,8 @@ interface TrackingState { handleStopTracking: () => void; handleProjectSelect: (projectId: string) => void; projectsData: Project[]; + loadingStart: boolean, + loadingStop: boolean } // Helper function @@ -79,8 +86,8 @@ const Widget: React.FC<{ // Dashboard Component const Dashboard: React.FC = () => { const { - selectedProject, - projectTrackingMap, + // selectedProject, + // projectTrackingMap, isTracking, isPaused, elapsedTime, @@ -89,7 +96,9 @@ const Dashboard: React.FC = () => { handlePauseResume, handleStopTracking, handleProjectSelect, - projectsData, + // projectsData, + loadingStart, + loadingStop } = useOutletContext(); // interface AppsListInt { @@ -100,10 +109,112 @@ const Dashboard: React.FC = () => { // const [runningApps, setRunningApps] = useState([]); + const projectsData = useSelector((state: any) => state.projects.projectsData); + const selectedProject = useSelector((state: any) => state.projects.selectedProject); + const projectSummary = useSelector((state: any) => state.projects.projectSummary); + const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); + + + console.log("Line 111", projectsData, selectedProject, projectTrackingMap) + + + const dispatch = useDispatch(); + + + const fetchProfile = async () => { + try { + dispatch(setLoading(true)); + const res = await window.electron.getProfile(); + console.log("Profile", res) + if (res.success && res.data) { + dispatch(setProfile(res.data)); + } else { + dispatch(setError("Failed to fetch profile")); + } + } catch (err: any) { + dispatch(setError(err.message)); + } finally { + dispatch(setLoading(false)); + } + }; + + useEffect(() => { + const fetchProjects = async () => { + try { + console.log("πŸ“‘ Fetching projects...Dashboard"); + const res = await window.electron.getProjectList(); + console.log(" Project list response in dashboard:", 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]; + + // Select the project + dispatch(setSelectedProjectSlice(firstProject)); + + // Convert total work ms β†’ seconds + const initialElapsed = Math.floor((firstProject.totalWorkMs || 0) / 1000); + + // Initialize tracking data for first project (your slice matches this) + dispatch( + initializeProjectTracking({ + projectId: firstProject.projectId, + initialTime: initialElapsed, + }) + ); + } + + fetchProfile() + + } else { + console.warn("⚠️ Unexpected response:", res); + // if (res?.status) { + // // handleLogout() + // } + } + } catch (err) { + console.error("❌ Error fetching projects:", err); + } + }; + + fetchProjects(); + }, []); + const selectedData = selectedProject - ? projectTrackingMap.get(selectedProject.projectId) + ? projectTrackingMap[selectedProject.projectId] : null; + const formatWorkTime = (time: string) => { if (!time) return "0h 0m"; @@ -115,49 +226,40 @@ const Dashboard: React.FC = () => { }; + const handleProjectSelectLocal = (projectId: string) => { + + const project = projectsData.find((p: any) => p.projectId === projectId); + if (!project) return; + + dispatch(setSelectedProjectSlice(project)); + + const existingData = projectTrackingMap[projectId]; + + if (!existingData) { + const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000); + + dispatch(initializeProjectTracking({ + projectId, + initialTime: initialElapsed + })); + } else { + // Restore saved tracking state + dispatch(updateProjectTracking({ + projectId, + data: existingData + })); + } + }; + + return (
{/* Header */}

Dashboard

- {/* */}
- - {/* Widgets */} - {/*
- } - color="primary" - /> - } - color="success" - /> - } - color="warning" - /> -
*/} - {/* Tracking Section */}
- {/*
- Tracking -
*/} -
{selectedProject ? ( @@ -183,60 +285,33 @@ const Dashboard: React.FC = () => { ) : ( )} +
- - {/*
-
- Status:{" "} - - {selectedProject.activityStatus} - -
-
- Tracking:{" "} - - {selectedData?.isPaused - ? "Paused" - : selectedData?.isTracking - ? "Active" - : "Ready"} - -
-
- Start:{" "} - - {selectedData?.startTime - ? selectedData.startTime.toLocaleTimeString() - : "β€”"} - -
-
*/} ) : (

@@ -257,7 +332,7 @@ const Dashboard: React.FC = () => { {/* πŸ”Ή Table or Empty State */} {projectsData.length > 0 ? (

{ # Name - {/* Logs Hours */} + Logs Hours Action - {projectsData.map((project, index) => { + {projectsData.map((project: any, index: any) => { const isCurrentlySelected = selectedProject?.projectId === project.projectId; const isDisabled = isTracking && !isCurrentlySelected; + const limitReached = isProjectLimitReached(project); + const remainingTime = getFormattedRemainingTime(project); return ( - + {index + 1} - {project.projectName} - {/* - {formatWorkTime(project.totalWorkTime)} - */} +
+ {project.projectName} + {limitReached && ( + Daily limit reached + )} +
+ +
+ + {formatWorkTime(project.totalWorkTime)} + + + of {project.dedicatedHours} + +
+ - +
+ + {!limitReached && ( + + {remainingTime} left + + )} +
); diff --git a/src/ui/pages/Login.tsx b/src/ui/pages/Login.tsx index 3f0f16f..138c08f 100644 --- a/src/ui/pages/Login.tsx +++ b/src/ui/pages/Login.tsx @@ -5,6 +5,7 @@ import { setUserSlice } from '../store/userSlice'; import DocVaLogoLight from '../../assets/logo-colored-vertical.svg' import DocVaLogoDark from '../../assets/logo-white-vertical.svg' import { ThemeToggle } from '../helper/ThemeToggle'; +import { KeyRound, Mail } from 'lucide-react'; const Login = () => { const theme = ThemeToggle(); @@ -41,7 +42,8 @@ const Login = () => { email: response.user.email, })); navigate("/") - } else if (response.success == false) { + } + else if (response.success == false) { console.log(response.error.message) setErrorMessage(response.error.message) } @@ -112,15 +114,8 @@ const Login = () => { -