diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index aab0b66..c5f36f5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ nextSteps.txt /playwright-report/ /blob-report/ /playwright/.cache/ +resources \ No newline at end of file diff --git a/resources/wireguard/mac/wg0.conf b/resources/wireguard/mac/wg0.conf index 5053efb..fa2a861 100644 --- a/resources/wireguard/mac/wg0.conf +++ b/resources/wireguard/mac/wg0.conf @@ -1,10 +1,10 @@ [Interface] PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I= -Address = 10.0.0.2/24 +Address = 10.0.0.2/32 DNS = 1.1.1.1 [Peer] PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U= Endpoint = 35.95.209.161:51820 -AllowedIPs = 10.0.0.0/24 +AllowedIPs = 10.0.0.0/32 PersistentKeepalive = 25 diff --git a/resources/wireguard/windows/wg0.conf b/resources/wireguard/windows/wg0.conf index 5053efb..88400b7 100644 --- a/resources/wireguard/windows/wg0.conf +++ b/resources/wireguard/windows/wg0.conf @@ -1,6 +1,7 @@ + [Interface] -PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I= -Address = 10.0.0.2/24 +PrivateKey = QBPeUFxS5EVEZT2QRhMgSW4gUXG2WoUTlUFg/p9ZEEk= +Address = 10.0.0.97/32 DNS = 1.1.1.1 [Peer] diff --git a/src/assets/splash.html b/src/assets/splash.html index ac2e51a..92a9805 100644 --- a/src/assets/splash.html +++ b/src/assets/splash.html @@ -1,5 +1,6 @@ + @@ -55,8 +60,266 @@ - + + \ No newline at end of file diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index 95d6d3e..3cd6426 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -4,15 +4,22 @@ import { getToken } from '../utils/tokenHelper.js'; import chalk from "chalk"; import { sendTokenExpired } from '../ipc/ipcHandler.js'; import https from 'https'; +import { getBaseURL } from './baseURL.js'; const TOKEN_KEY = 'ACCESS_TOKEN'; +const Base_URL = await getBaseURL() + const httpsAgent = new https.Agent({ rejectUnauthorized: false }); + +console.log('Base_URL$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' ,Base_URL) + const api: AxiosInstance = axios.create({ - baseURL: 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va', // πŸ” your backend + // baseURL: 'https://portal-sf.docva.com/api/v1/va', // πŸ” your backend + baseURL: `${Base_URL}/api/v1/va`, httpsAgent // timeout: 10000, }); @@ -29,7 +36,7 @@ api.interceptors.request.use( if (token) { config.headers['token'] = token; - config.headers['ngrok-skip-browser-warning'] = 'true'; + config.headers['ngrok-skip-browser-warning'] = 'true'; } return config; @@ -54,7 +61,7 @@ api.interceptors.response.use( (response: AxiosResponse) => response.data, async (error) => { - console.log(error , "Line 46") + console.log(error, "Line 46") const status = error?.response?.status; console.error('[Axios Error]', status); @@ -69,4 +76,4 @@ api.interceptors.response.use( } ); -export default api; +export default api; \ No newline at end of file diff --git a/src/electron/api/baseURL.ts b/src/electron/api/baseURL.ts new file mode 100644 index 0000000..2a33d5a --- /dev/null +++ b/src/electron/api/baseURL.ts @@ -0,0 +1,7 @@ +// let CURRENT_BASE_URL = "https://portal-sf.docva.com/api/v1/va"; +let CURRENT_BASE_URL = "https://portal-sf.docva.com"; + +export const getBaseURL = () => CURRENT_BASE_URL; +export const setBaseURL = (url: string) => { + CURRENT_BASE_URL = url; +}; diff --git a/src/electron/api/regionCheck.ts b/src/electron/api/regionCheck.ts new file mode 100644 index 0000000..719fec6 --- /dev/null +++ b/src/electron/api/regionCheck.ts @@ -0,0 +1,41 @@ + +import axios from "axios"; +import https from "https"; +import { getBaseURL } from "./baseURL.js"; + +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); + +export const checkRegion = async () => { + const baseURL = await getBaseURL(); + + console.log("Lin 11 BaseURL CHECK@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ,baseURL) + + try { + + const res = await axios.get(`${baseURL}/health`, { + headers: { "ngrok-skip-browser-warning": "true" }, + httpsAgent, + timeout: 4000 + }); + + console.log(res , "checkRegion") + + return { + success: true, + status: res.status, + data: res.data + }; + + } catch (error: any) { + console.log(error , "Line 29") + const status = error?.response?.status || 500; + + console.log("Region Check Error:", status); + + return { + success: false, + status, + message: "Region check failed" + }; + } +}; diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index 5ec569b..fe3cc2d 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -3,7 +3,9 @@ import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js"; import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js"; import api from "../api/axiosClient.js"; import { getMainWindow } from '../main.js'; +import { checkRegion } from "../api/regionCheck.js"; import { startVPN, stopVPN } from "../vpn.js"; +import { getBaseURL, setBaseURL } from "../api/baseURL.js"; @@ -11,6 +13,22 @@ import { startVPN, stopVPN } from "../vpn.js"; export const registerIPCHandlers = (mainWindow: BrowserWindow) => { console.log("βš™οΈ Registering IPC Handlers..."); + + ipcMain.handle("GET_BASE_URL", () => { + return getBaseURL(); + }); + + // UPDATE Base URL + ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => { + setBaseURL(url); + api.defaults.baseURL = `${url}/api/v1/va`; + console.log("Base URL updated to:", api.defaults.baseURL); + return { + updated: true, + baseURL: url + }; + }); + ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => { return await getToken(key); }); @@ -152,39 +170,30 @@ ipcMain.handle("stop-project-tracking", async (event, payload) => { // tabs opening and closing when and tracking start ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => { try { - console.log("🟦 IPC tracking-tabs-opening-and-closing:", payload); + console.log("🟦 IPC received payload:", payload); - let response; - if (!payload.id) { - // 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); - } else { - // PATCH existing log (same tab, update) - response = await api.patch("/log/project/track", { - id: payload.id, - logId: payload.logId, - projectId: payload.projectId, - title: payload.title, - appName: payload.appName, - }); - console.log("Log updated:", response); - } + const response = await api.post("/log/project/track", payload); - return response; + console.log("🟩 Log track API success:", response); + + return { + success: true, + data: response.data, + }; } catch (error) { - console.error("Error syncing project tab tracking:", error); - throw error; + console.error("❌ Log track API error:", error); + + return { + success: false, + message: "Something went wrong", + error, + }; } }); + ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => { try { console.log("πŸ“₯ get-project-logs called with:", { projectID, fromDate, toDate }); @@ -535,6 +544,11 @@ ipcMain.handle("update-payment", async (event, data: Record) => { +ipcMain.handle("check-region", async () => { + return await checkRegion(); +}); + + export const sendTokenExpired = () => { console.log("⚠️ Sending token-expired event to renderer"); diff --git a/src/electron/main.ts b/src/electron/main.ts index 9dbe4d0..46bd7aa 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -178,6 +178,12 @@ app.on("ready", async () => { // HANDLE CLOSE EVENTS // ------------------------------------ // handleCloseEvents(win); + mainWindow.on("close", () => { + if(mainWindow !== null) { + mainWindow.webContents.executeJavaScript(`window.__allowClose__ = true`); + } + }); + }); @@ -198,6 +204,7 @@ app.on('browser-window-focus', () => { // }); + // --------------------------- // CUSTOM CLOSE BEHAVIOR // --------------------------- diff --git a/src/electron/preload.cts b/src/electron/preload.cts index 1de0eea..2864150 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -65,9 +65,14 @@ contextBridge.exposeInMainWorld('electron', { getLeaveList: () => ipcRenderer.invoke("get-leave-list"), getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId), + checkRegion: () => ipcRenderer.invoke("check-region"), + connectVPN: () => ipcRenderer.invoke("vpn-connect"), disconnectVPN: () => ipcRenderer.invoke("vpn-disconnect"), + getBaseURL: () => ipcRenderer.invoke("GET_BASE_URL"), + updateBaseURL: (url: string) => ipcRenderer.invoke("UPDATE_BASE_URL", url), + } satisfies Window['electron']); // function ipcInvoke( diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts index 0a15c7d..d8523d5 100644 --- a/src/electron/utils/utilityFunctions.ts +++ b/src/electron/utils/utilityFunctions.ts @@ -8,6 +8,7 @@ import { saveToken } from "./tokenHelper.js"; import { activeWindow } from "get-windows"; import { exec } from "child_process"; import { getUIPath } from "../pathResolver.js"; +import { getBaseURL } from "../api/baseURL.js"; const httpsAgent = new https.Agent({ rejectUnauthorized: false @@ -61,14 +62,16 @@ export const handleLogin = async (credentials: { email: string, password: string // Call your login API console.log("🟑 login Request received", credentials); + let baseURL = getBaseURL() + console.log("app.getAppPath()", app.getAppPath()); console.log("UIPATH", getUIPath()); - console.log("Doom 1"); + console.log("Doom 1" , `${baseURL}/api/v1/va/login`); try { console.log("➑️ Calling API..."); const response = await axios.post( - 'https://daniela-nonprofessed-boastfully.ngrok-free.app/api/v1/va/login', + `${baseURL}/api/v1/va/login`, { userDetails: { ...credentials } }, { headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent } ); diff --git a/src/electron/vpn.ts b/src/electron/vpn.ts index f43275e..0a04be7 100644 --- a/src/electron/vpn.ts +++ b/src/electron/vpn.ts @@ -3,6 +3,7 @@ import { exec, execFile } from "child_process"; import fs from "fs"; import os from "os"; import path from "path"; +import axios from "axios"; // // ------------------ HELPERS ------------------ @@ -51,20 +52,72 @@ function getWireGuardInstallerPath(): string { // OS-specific wg0.conf -function getConfigPath(): string { +// function getConfigPath(): string { +// const base = isDev() +// ? path.join(process.cwd(), "resources", "wireguard") +// : path.join(process.resourcesPath, "wireguard"); + +// if (process.platform === "win32") { +// return path.join(base, "windows", "wg0.conf"); +// } + +// if (process.platform === "darwin") { +// return path.join(base, "macos", "wg0.conf"); +// } + +// throw new Error("Unsupported OS"); +// } + +export async function getConfigPath(): Promise { + if (process.platform !== "win32") { + throw new Error("Only Windows supported for dynamic WireGuard config"); + } + console.log("Calling http://35.95.209.161/api/v1/vpn/connect") + // 1️⃣ Call API to get dynamic config + let response = null; + try { + response = await axios.post( + "http://35.95.209.161/api/v1/vpn/connect", + {}, + { timeout: 15000 } + ); + } catch (err: any) { + console.error("VPN API error:", { + message: err.message, + code: err.code, + response: err.response?.data, + }); + throw err; + } + console.log("Called http://35.95.209.161/api/v1/vpn/connect", JSON.stringify(response.data)); + + if (!response.data?.success || !response.data.clientConf) { + throw new Error("Invalid VPN config response"); + } + + const clientConf: string = response.data.clientConf; + + // 2️⃣ Decide base directory (same idea as your existing logic) const base = isDev() - ? path.join(process.cwd(), "resources", "wireguard") - : path.join(process.resourcesPath, "wireguard"); + ? path.join(process.cwd(), "resources", "wireguard", "windows") + : path.join(process.resourcesPath, "wireguard", "windows"); - if (process.platform === "win32") { - return path.join(base, "windows", "wg0.conf"); - } + // Ensure directory exists + fs.mkdirSync(base, { recursive: true }); - if (process.platform === "darwin") { - return path.join(base, "mac", "wg0.conf"); - } + // 3️⃣ Create temp / runtime conf file + // IMPORTANT: name must stay stable (WireGuard service name depends on it) + const confPath = path.join(base, "wg0.conf"); - throw new Error("Unsupported OS"); + // 4️⃣ Write config to file + fs.writeFileSync(confPath, clientConf, { + encoding: "utf-8", + mode: 0o600, // secure permissions + }); + + console.log("VPN config written to:", confPath); + // 5️⃣ Return path (same contract as before) + return confPath; } // Detect installed WireGuard.exe on Windows @@ -145,16 +198,24 @@ async function ensureWireGuardInstalled(): Promise { async function connectWindows(): Promise { + + try { + const response = await axios.get('http://35.95.209.161') + console.log("Response from test API:", response.data); + } catch (error) { + console.error("Error connecting to test API:", error); + } + const cli = await ensureWireGuardInstalled(); - const conf = getConfigPath(); + const conf = await getConfigPath(); // Disable GUI popup and silent mode (ignore errors) - await execPromise(`reg add "HKCU\\Software\\WireGuard" /v HideTrayIcon /t REG_DWORD /d 1 /f`).catch(() => {}); - await execPromise(`reg add "HKCU\\Software\\WireGuard" /v SuppressTray /t REG_DWORD /d 1 /f`).catch(() => {}); - await execPromise(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v WireGuard /f`).catch(() => {}); + await execPromise(`reg add "HKCU\\Software\\WireGuard" /v HideTrayIcon /t REG_DWORD /d 1 /f`).catch(() => { }); + await execPromise(`reg add "HKCU\\Software\\WireGuard" /v SuppressTray /t REG_DWORD /d 1 /f`).catch(() => { }); + await execPromise(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v WireGuard /f`).catch(() => { }); // Start background service - await execPromise(`sc start WireGuardManager`).catch(() => {}); + await execPromise(`sc start WireGuardManager`).catch(() => { }); return new Promise((resolve, reject) => { execFile(cli, ["/installtunnelservice", conf], (err) => { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 7de1c12..b55a71b 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -97,22 +97,23 @@ import Vpn from "./pages/Vpn/Vpn"; import Schedule from "./pages/Schedule/Schedule"; import PaysSlips from "./pages/PaySlips/PaysSlips"; import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime"; +import VpnGuard from "./pages/Vpn/VpnGuard"; const App: React.FC = () => { - + const dispatch = useDispatch(); // βœ… Get user directly from Redux const user = useSelector((state: RootState) => state.user.user); - + useEffect(() => { dispatch(loadUserFromStorage()); }, [dispatch]); - console.log("App.tsx β†’ user:", user); + console.log("App.tsx β†’ user:", user); return ( @@ -122,7 +123,9 @@ const App: React.FC = () => { path="/login" element={ - + + + } /> @@ -132,20 +135,22 @@ const App: React.FC = () => { path="/dashboard" element={ + {/* */} + {/* */} } > } /> } /> } /> - } /> + } /> } /> } /> } /> } /> - } /> - } /> + } /> + } /> {/* Default Redirect */} diff --git a/src/ui/hooks/useActiveAppTracker.tsx b/src/ui/hooks/useActiveAppTracker.tsx index 3099d19..d66fb60 100644 --- a/src/ui/hooks/useActiveAppTracker.tsx +++ b/src/ui/hooks/useActiveAppTracker.tsx @@ -1,6 +1,14 @@ +// hooks/useActiveAppTracker.tsx import { useEffect, useRef, useCallback } from "react"; -import { useDispatch } from "react-redux"; -import { setActiveWindow, WindowInfo } from "../store/windowSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "../store"; +import { WindowInfo } from "../store/windowSlice"; +import { + initializeProjectLogs, + updateWindowLog, + clearProjectLogs, + debugLogsState, +} from "../store/logsSlice"; interface DailyHours { todayTotal: string; @@ -16,273 +24,223 @@ export const useActiveAppTracker = ( onLimitExceeded?: () => void ) => { const dispatch = useDispatch(); + const { logsByProject } = useSelector((state: RootState) => state.logs); const lastActiveRef = useRef(null); - const lastLogIdRef = useRef(null); const lastTitleRef = useRef(null); - const intervalIdRef = useRef(null); - const finalizeTrackingRef = useRef<(() => Promise) | null>(null); - const limitCheckedRef = useRef(false); - const isStoppingRef = useRef(false); // βœ… Flag to prevent new requests + const lastAppNameRef = useRef(null); + const intervalIdRef = useRef | null>(null); + const limitCheckedRef = useRef(false); + const isStoppingRef = useRef(false); + const checkCountRef = useRef(0); - const canSend = !!(activeProjectLogId && projectId && !isStoppingRef.current); + // Initialize logs when tracking starts + useEffect(() => { + if (isTracking && activeProjectLogId && projectId) { + // Initialize fresh logs for this tracking session + dispatch( + initializeProjectLogs({ + projectId, + logId: activeProjectLogId, + }) + ); + console.log(` Logs initialized for project: ${projectName} (${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 (remainingMins <= 0) { - return true; + // Reset refs + lastActiveRef.current = null; + lastTitleRef.current = null; + lastAppNameRef.current = null; + checkCountRef.current = 0; + isStoppingRef.current = false; } + }, [isTracking, activeProjectLogId, projectId, projectName, dispatch]); - return false; - }; - - // βœ… Send activity log (POST for new, PATCH for update) - const sendActivityLog = async ( - projectLogId: string, - projectId: string, - title: string, - appName: string, - isNew: boolean - ) => { - // βœ… CRITICAL: Block all requests when stopping - if (isStoppingRef.current) { - console.log("🚫 Stopping in progress β€” blocking activity log"); - return null; - } - - if (!canSend) { - console.log("🚫 Skipping API β€” missing required IDs"); - return null; - } - - try { - const payload: { - id?: string; - logId: string; - projectId: string; - title: string; - appName: string; - } = { - logId: projectLogId, - projectId, - title, - appName, - }; - - 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); - - console.log("πŸ“Š Activity log response:", res); - - // Store the log ID for future updates - if (isNew && res?.data?._id) { - lastLogIdRef.current = res.data._id; - console.log("πŸ’Ύ Stored log ID:", res.data._id); - } - - // βœ… Check daily limit after every API call - if (res?.dailyHours) { - const limitExceeded = checkDailyLimit(res.dailyHours); - - if (limitExceeded && !limitCheckedRef.current) { - limitCheckedRef.current = true; - console.warn("🚨 Daily limit exceeded!"); - - if (onLimitExceeded) { - setTimeout(() => { - onLimitExceeded(); - }, 500); - } - } - } - - return res; - } catch (error) { - console.error("❌ Activity log error:", error); - throw error; - } - }; - - // βœ… Stop the interval immediately - const stopInterval = useCallback(() => { - console.log("πŸ›‘ Stopping activity tracking interval..."); + // Stop tracking intervals + const stopIntervals = useCallback(() => { if (intervalIdRef.current) { clearInterval(intervalIdRef.current); intervalIdRef.current = null; - console.log("βœ… Interval cleared"); } }, []); - // βœ… Send final PATCH before stopping (called by handleStopTracking) - const sendFinalPatch = useCallback(async () => { - console.log("=".repeat(60)); - console.log("πŸ›‘ FINAL PATCH INITIATED"); - console.log("=".repeat(60)); - - // βœ… STEP 1: Set stopping flag IMMEDIATELY - isStoppingRef.current = true; - console.log("πŸ”’ Stopping flag set β€” no new requests allowed"); - - // βœ… STEP 2: Stop the interval IMMEDIATELY - stopInterval(); - - if (!activeProjectLogId || !projectId) { - console.log("⚠️ Skipping final PATCH β€” no active project log ID"); - return; + // Debug function to check logs state + const debugCurrentLogs = useCallback(() => { + if (projectId) { + dispatch(debugLogsState({ projectId })); } + }, [projectId, dispatch]); - // βœ… STEP 3: Send ONE final PATCH for the last active window - if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) { - try { - console.log("πŸ“Š Final Activity Details:"); - console.log(` β€’ Window: ${lastTitleRef.current}`); - console.log(` β€’ App: ${lastActiveRef.current.owner.name}`); - console.log(` β€’ Log ID: ${lastLogIdRef.current}`); - console.log(` β€’ Project: ${projectId}`); + // Finalize and send all logs when stopping tracking + // hooks/useActiveAppTracker.tsx - finalizeAndSendLogs function +const finalizeAndSendLogs = useCallback( + async (vaId: string): Promise => { + console.log(" FINALIZING AND SENDING ALL LOGS"); - const result = await sendActivityLog( - activeProjectLogId, - projectId, - lastTitleRef.current, - lastActiveRef.current.owner.name || "Unknown", - false // PATCH (update existing log) - ); + // Set stopping flag + isStoppingRef.current = true; - console.log("βœ… Final PATCH sent successfully!"); + // Stop intervals immediately + stopIntervals(); - if (result?.dailyHours) { - console.log("πŸ“ˆ Final Daily Hours:"); - console.log(` β€’ Today: ${result.dailyHours.todayTotal}`); - console.log(` β€’ Limit: ${result.dailyHours.dailyLimit}`); - console.log(` β€’ Remaining: ${result.dailyHours.remaining}`); + if (!activeProjectLogId || !projectId) { + console.log("⚠️ Skipping β€” no active project"); + return false; } - } catch (error) { - console.error("❌ Final PATCH failed:", error); - throw error; - } - } else { - console.log("ℹ️ No active window to finalize"); - } + const projectLogs = logsByProject[projectId]; + if (!projectLogs || projectLogs.logs.length === 0) { + console.log("ℹ️ No logs found for project", projectId); + return true; + } - // βœ… STEP 4: 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, stopInterval]); + try { + // Create a DEEP copy of the logs + const allLogs = JSON.parse(JSON.stringify(projectLogs.logs)); - // βœ… Update ref whenever sendFinalPatch changes + // Filter out logs with very short duration (less than 1000ms = 1 second) + const validLogs = allLogs.filter((log:any) => { + return log.totalTime >= 1000; // Minimum 1 second (1000ms) + }); + + console.log(`πŸ“Š Total logs: ${allLogs.length}, Valid logs (β‰₯1000ms): ${validLogs.length}`); + + if (validLogs.length === 0) { + console.log("ℹ️ No valid logs to send"); + dispatch(clearProjectLogs({ projectId })); + return true; + } + + // Prepare payload with all valid logs - send directly as stored + const payload = { + vaId: vaId || "", + projectId, + logId: activeProjectLogId, + logs: validLogs, // Already formatted correctly + }; + + console.log("πŸ“€ Payload to send:", payload); + // console.log("πŸ“Š Sending logs summary:", { + // projectId, + // projectName, + // logCount: validLogs.length, + // totalTimeMs: validLogs.reduce((sum, log) => sum + log.totalTime, 0), + // totalTimeSec: validLogs.reduce((sum, log) => sum + Math.round(log.totalTime / 1000), 0), + // sampleLog: validLogs[0] ? { + // title: validLogs[0].title.substring(0, 40), + // from: validLogs[0].from, + // to: validLogs[0].to, + // durationMs: validLogs[0].totalTime, + // } : null, + // }); + + let res = await window.electron.trackingTabsOpeningClosing(payload); + console.log("Tracking response:", res); + + dispatch(clearProjectLogs({ projectId })); + + // Cleanup refs + lastActiveRef.current = null; + lastTitleRef.current = null; + lastAppNameRef.current = null; + limitCheckedRef.current = false; + + console.log("βœ… Logs sent and cleared successfully"); + console.log("=".repeat(60)); + + return true; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : "Unknown error"; + console.error("❌ Error sending logs:", error); + console.log("=".repeat(60)); + return false; + } + }, + [ + activeProjectLogId, + projectId, + logsByProject, + stopIntervals, + dispatch, + ] +); + + // Main tracking logic - runs every 5 seconds useEffect(() => { - finalizeTrackingRef.current = sendFinalPatch; - }, [sendFinalPatch]); - - // βœ… Main tracking interval effect - useEffect(() => { - // Reset flags when tracking starts - if (isTracking) { - limitCheckedRef.current = false; - isStoppingRef.current = false; - console.log("🟒 Tracking started β€” flags reset"); - } - - if (!isTracking || !canSend) { - console.log("🟑 Tracking inactive β€” cleaning up"); - stopInterval(); + if (!isTracking || !projectId || !activeProjectLogId) { + console.log("⏸️ Tracking inactive β€” stopping interval"); + stopIntervals(); return; } - console.log(`🟒 Starting active tracker for: ${projectName}`); + console.log(`πŸš€ Starting tracker for: ${projectName}`); - const fetchActiveApp = async () => { - // βœ… Double-check stopping flag before each fetch + const checkActiveWindow = async () => { if (isStoppingRef.current) { - console.log("🚫 Stopping in progress β€” skipping fetch"); return; } try { const details = (await window.electron.getActiveAppDetails()) as WindowInfo; - if (!details || !details.id) return; - const title = details.title; - const appName = details.owner.name; - const prevTitle = lastTitleRef.current; - const hasChanged = !prevTitle || prevTitle !== title; + if (!details || !details.id || !details.owner?.name) { + return; + } - if (hasChanged) { - // PATCH previous window before creating new log - if (lastLogIdRef.current && prevTitle) { - console.log("πŸ”„ Updating previous window:", prevTitle); - await sendActivityLog(activeProjectLogId!, projectId!, prevTitle, appName, false); - } + const currentTitle = details.title; + const currentAppName = details.owner.name; - // POST new window log - console.log("πŸ†• New window detected:", title); - await sendActivityLog(activeProjectLogId!, projectId!, title, appName, true); + checkCountRef.current += 1; - // Update refs and Redux - lastActiveRef.current = details; - lastTitleRef.current = title; - dispatch(setActiveWindow({ window: details, projectId, projectName })); - } else { - console.log("⏸️ Same window, skipping"); + // Update Redux with current window activity + dispatch( + updateWindowLog({ + projectId, + title: currentTitle, + appName: currentAppName, + url: null, + }) + ); + + // Update refs + lastActiveRef.current = details; + lastTitleRef.current = currentTitle; + lastAppNameRef.current = currentAppName; + + // Debug every 10 checks (every 50 seconds) + if (checkCountRef.current % 10 === 0) { + debugCurrentLogs(); } } catch (error) { - console.error("❌ Failed to fetch active app:", error); + console.error("❌ Failed to check active window:", error); } }; - // Initial fetch - fetchActiveApp(); + // Initial check + checkActiveWindow(); // Set up interval (every 5 seconds) - intervalIdRef.current = setInterval(fetchActiveApp, 5000); + intervalIdRef.current = setInterval(checkActiveWindow, 5000); - // Cleanup on unmount + // Cleanup return () => { - console.log("🧹 Cleaning up tracker interval"); - stopInterval(); + console.log("🧹 Cleaning up tracker"); + stopIntervals(); }; - }, [isTracking, activeProjectLogId, projectId, projectName, dispatch, stopInterval]); + }, [ + isTracking, + activeProjectLogId, + projectId, + projectName, + stopIntervals, + dispatch, + debugCurrentLogs, + ]); return { - finalizeTracking: finalizeTrackingRef.current, - stopInterval, + finalizeAndSendLogs, + stopIntervals, setIsStopping: (value: boolean) => { isStoppingRef.current = value; - console.log(`πŸ”’ Stopping flag manually set to: ${value}`); - } + }, + debugCurrentLogs, }; }; \ No newline at end of file diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index fc02701..2c45e1e 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -39,6 +39,7 @@ import { canStartTracking } from "../utils/projectLimits"; import { useTheme } from "../helper/ThemeToggle"; import LogoutModal from "../modal/LogoutModal"; import { parseTimeStringToSeconds } from "../utils/timeParser"; +import { clearAllLogs, clearProjectLogs } from "../store/logsSlice"; interface User { id: string; @@ -100,6 +101,8 @@ const DashboardLayout = () => { const { projectsData, projectSummary, selectedProject, projectTrackingMap } = useSelector((state: any) => state.projects); + const { logsByProject } = useSelector((state: RootState) => state.logs); + const tracking = selectedProject ? projectTrackingMap[selectedProject.projectId] : null; @@ -116,7 +119,7 @@ const DashboardLayout = () => { { 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: "VPN", path: "/dashboard/vpn", icon: }, { label: "Help & Support", path: "/dashboard/help-support", icon: }, ]; @@ -167,11 +170,13 @@ const DashboardLayout = () => { `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( + const { finalizeAndSendLogs, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker( isTracking, activeProjectLogId, selectedProject?.projectId ?? null, @@ -182,95 +187,81 @@ const DashboardLayout = () => { // 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; + const vaId = projectSummary.vaId || ""; - // βœ… CRITICAL: Set stopping flag IMMEDIATELY to block all new tab tracking - console.log("πŸ”’ Setting stopping flag..."); + // STEP 1: Set stopping flag in tracker if (setIsStoppingInTracker) { setIsStoppingInTracker(true); } + // Set loading state setIsStopping(true); setLoadingStop(true); + + // STEP 2: Disconnect socket handleSocketDisconnect(); - // STEP 1: Clear local timer to prevent further client-side updates - console.log("⏹️ STEP 1: Clearing local timer..."); + // STEP 3: Clear 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) { + // STEP 4: Finalize and send logs + console.log("πŸ“€ STEP 4: Finalizing and sending logs..."); + let logsSent = false; + if (finalizeAndSendLogs) { try { - await stopTrackingRef.current(); // This calls sendFinalPatch() in the hook - console.log("βœ… Final activity tracking completed"); + logsSent = await finalizeAndSendLogs(vaId); // Pass vaId here + console.log(logsSent ? "βœ… Logs sent successfully" : "⚠️ Logs not sent"); } catch (error) { - console.error("❌ Error during final tracking:", error); + console.error("❌ Error sending logs:", error); } - } else { - console.log("⚠️ No tracking ref available"); } - // STEP 3: Send LOGOUT to backend - console.log("πŸšͺ STEP 3: Sending LOGOUT request..."); + // STEP 5: Send LOGOUT to backend + console.log("πŸšͺ STEP 5: Sending LOGOUT request..."); try { - const payload = { - vaId: projectSummary.vaId, + const logoutPayload = { + vaId: vaId, + logId: activeProjectLogId, projectId: projectId, type: "LOGOUT", time: currentStopTime }; - console.log("πŸ“‘ Logout payload:", payload); - const res = await window.electron.stopProjectTracking(payload); - console.log("βœ… LOGOUT successful:", res); + await window.electron.stopProjectTracking(logoutPayload); + console.log("βœ… LOGOUT successful"); } 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 Redux tracking 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..."); + // STEP 7: Refresh project list try { const projectListRes = await window.electron.getProjectList(); console.log("βœ… Project list fetched:", projectListRes); @@ -349,20 +340,27 @@ const DashboardLayout = () => { console.log("πŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); console.log("=".repeat(70)); } + + // STEP 8: Reset logs for this project + if (logsSent && projectId) { + dispatch(clearProjectLogs({ projectId })); + } + + console.log("πŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); }, [ selectedProject, projectSummary, - elapsedTime, dispatch, loadingStop, loadingStart, - setIsStoppingInTracker + setIsStoppingInTracker, + finalizeAndSendLogs, ]); // Update the ref whenever finalizeTracking changes - useEffect(() => { - stopTrackingRef.current = finalizeTracking; - }, [finalizeTracking]); + // useEffect(() => { + // stopTrackingRef.current = finalizeTracking; + // }, [finalizeTracking]); const socketRef = useRef(null); const intervalRef = useRef(null); @@ -402,6 +400,7 @@ const DashboardLayout = () => { const handleLogout = async () => { try { + dispatch(clearAllLogs()); const res = await window.electron.logout(); if (res) { dispatch(clearUser()); @@ -457,6 +456,11 @@ const DashboardLayout = () => { return; } + // Reset logs for this project if they exist + if (logsByProject[project.projectId]) { + dispatch(clearProjectLogs({ projectId: project.projectId })); + } + // CRITICAL FIX: Get the current accumulated time BEFORE starting timer const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime); @@ -588,7 +592,7 @@ const DashboardLayout = () => { if (!selectedProject?.projectId) return console.log(selectedProject?.projectId, "SelectedProject ID") - socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", { + socketRef.current = io("https://portal-sf.docva.com", { transports: ["websocket"], secure: true, rejectUnauthorized: false, @@ -724,15 +728,26 @@ const DashboardLayout = () => { // 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 ( <>
- @@ -741,9 +756,10 @@ const DashboardLayout = () => { onClick={() => { window.electron.disconnectVPN(); }} + className="text-red-400" > Disconnect VPN - + */} {/* Header */} {isTracking && @@ -913,4 +929,6 @@ const DashboardLayout = () => { ); }; -export default DashboardLayout; \ No newline at end of file +export default DashboardLayout; + + diff --git a/src/ui/main.tsx b/src/ui/main.tsx index 5a91adf..9d1b9fc 100644 --- a/src/ui/main.tsx +++ b/src/ui/main.tsx @@ -6,6 +6,36 @@ import { Provider } from "react-redux"; import { store } from "./store"; import { ToastProvider } from './helper/ToastContext'; +declare global { + interface Window { + __allowClose__: boolean; + } +} + + +// renderer only +window.addEventListener("online", (e) => { + console.log("Network online (ignored)"); + e.preventDefault(); +}); + +window.addEventListener("offline", (e) => { + console.log("Network offline (ignored)"); + e.preventDefault(); +}); + +// Extra safety +window.__allowClose__ = false; + +window.addEventListener("beforeunload", (e) => { + if (!window.__allowClose__) { + e.returnValue = false; + } +}); + + + + ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index 8130cbe..9216697 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -106,6 +106,7 @@ const Dashboard: React.FC = () => { const projectSummary = useSelector((state: any) => state.projects.projectSummary); const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); const [isRefreshing, setIsRefreshing] = useState(false); + const isLimitReached = selectedProject ? isProjectLimitReached(selectedProject) : false; console.log("Line 111", projectsData, selectedProject, projectTrackingMap) @@ -129,14 +130,14 @@ const Dashboard: React.FC = () => { }; const fetchProjects = async () => { - setIsRefreshing(true) + setIsRefreshing(true); try { console.log("πŸ“‘ Fetching projects...Dashboard"); const res = await window.electron.getProjectList(); - console.log(" Project list response in dashboard: ", res.data?.[0]); + console.log("Project list response in dashboard: ", res.data?.[0]); if (res?.success && Array.isArray(res.data)) { - // Store data redux store + // Store data in redux store dispatch(setProjectsDataSlice(res.data)); dispatch( @@ -151,40 +152,85 @@ const Dashboard: React.FC = () => { }) ); - // Auto-select first project - if (res.data.length > 0) { - const firstProject = res.data[0]; - - - console.log("Line 158" , firstProject) - // Select the project - dispatch(setSelectedProjectSlice(firstProject)); - - const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime); - - // Initialize tracking data for first project - dispatch( - initializeProjectTracking({ - projectId: firstProject.projectId, - initialTime: initialElapsed, - }) - ); + // FIX: Check if we have any projects + if (res.data.length === 0) { + // No projects available, clear selected project + dispatch(setSelectedProjectSlice(null)); + // fetchProfile(); + return; } - fetchProfile() + // Check if we already have a selected project that's being tracked + const currentlySelected = selectedProject; + const currentlyTracking = isTracking; + // Find if the currently selected project still exists in the fresh data + let projectToSelect = null; + + if (currentlySelected && currentlyTracking) { + // We're tracking a project - try to preserve it + const existingProject = res.data.find( + (p: Project) => p.projectId === currentlySelected.projectId + ); + + if (existingProject) { + projectToSelect = existingProject; + } + } + + // If no project is selected OR tracked project no longer exists, auto-select first project + if (!projectToSelect && res.data.length > 0) { + projectToSelect = res.data[0]; + } + + if (projectToSelect) { + console.log("Line 158", projectToSelect); + // Select the project + dispatch(setSelectedProjectSlice(projectToSelect)); + + // Only initialize tracking if it doesn't exist already + const trackingExists = projectTrackingMap[projectToSelect.projectId]; + if (!trackingExists) { + const initialElapsed = parseTimeStringToSeconds(projectToSelect.totalWorkTime); + dispatch( + initializeProjectTracking({ + projectId: projectToSelect.projectId, + initialTime: initialElapsed, + }) + ); + } else { + // Update project data but preserve tracking state + dispatch( + updateProjectTracking({ + projectId: projectToSelect.projectId, + data: { + ...trackingExists, + // Update elapsed time from server if needed + elapsedTime: parseTimeStringToSeconds(projectToSelect.totalWorkTime), + } + }) + ); + } + } + + fetchProfile(); } else { console.warn("⚠️ Unexpected response:", res); + // FIX: Also clear selected project on unexpected response + dispatch(setSelectedProjectSlice(null)); } } catch (err) { console.error("❌ Error fetching projects:", err); + // FIX: Clear selected project on error + dispatch(setSelectedProjectSlice(null)); } finally { - setIsRefreshing(false) + setIsRefreshing(false); } }; useEffect(() => { fetchProjects(); + fetchProfile() }, []); const handleRefresh = async () => { @@ -246,12 +292,13 @@ const Dashboard: React.FC = () => { }; return ( -
+
{/* Header */}

Dashboard

{/* Tracking Section */} + {/* Tracking Section */}
@@ -318,7 +365,9 @@ const Dashboard: React.FC = () => { ) : (

- Select a project below to start tracking. + {projectsData.length === 0 + ? "No projects available." + : "Select a project below to start tracking."}

)}
@@ -330,15 +379,16 @@ const Dashboard: React.FC = () => { {/* πŸ”Ή Sticky Header */}

Projects

- + {!selectedData?.isTracking && + }
{/* πŸ”Ή Table or Empty State or Skeleton */} @@ -362,7 +412,7 @@ const Dashboard: React.FC = () => { minHeight: "auto", }} > - +
diff --git a/src/ui/pages/HelpSupport/HelpSupport.tsx b/src/ui/pages/HelpSupport/HelpSupport.tsx index 3d28484..6210087 100644 --- a/src/ui/pages/HelpSupport/HelpSupport.tsx +++ b/src/ui/pages/HelpSupport/HelpSupport.tsx @@ -23,7 +23,7 @@ const HelpSupport: React.FC = () => {

Email

-

support@vatechcorp.com

+

support@docva.com

@@ -31,7 +31,7 @@ const HelpSupport: React.FC = () => {

Phone

-

+91 98765 43210

+

+XXXX XXXXX

diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx index 7e99b90..bd9c521 100644 --- a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx +++ b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx @@ -464,7 +464,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { console.log("Timesheet Created Successfully"); resetForm(); // Show success toast instead of alert - showToast("PTO Request submitted successfully!", "success"); + showToast("Timesheet Request submitted successfully!", "success"); // Navigate after a small delay setTimeout(() => { @@ -680,4 +680,5 @@ const AdjustmentTab: React.FC = ({ projects }) => { ); }; -export default AdjustmentTab; \ No newline at end of file +export default AdjustmentTab; + diff --git a/src/ui/pages/PaySlips/PaysSlips.tsx b/src/ui/pages/PaySlips/PaysSlips.tsx index d3d9ca5..0e1a51e 100644 --- a/src/ui/pages/PaySlips/PaysSlips.tsx +++ b/src/ui/pages/PaySlips/PaysSlips.tsx @@ -9,6 +9,8 @@ const PaysSlips = () => { const [error, setError] = useState(null); const [rows, setRows] = useState([]); + + useEffect(() => { if (!projectSummary?.vaId) return; @@ -84,7 +86,7 @@ const PaysSlips = () => {
- + {/* */} @@ -111,13 +113,13 @@ const PaysSlips = () => { - + */} ))} diff --git a/src/ui/pages/Settings/ChangePasswordModal.tsx b/src/ui/pages/Settings/ChangePasswordModal.tsx index 4d6c5bf..52cbd08 100644 --- a/src/ui/pages/Settings/ChangePasswordModal.tsx +++ b/src/ui/pages/Settings/ChangePasswordModal.tsx @@ -1,6 +1,7 @@ // src/ui/pages/Settings/ChangePasswordModal.tsx import React, { useState } from "react"; import { Eye, EyeOff } from "lucide-react"; +import { useToast } from "../../helper/ToastContext"; interface ChangePasswordModalProps { isOpen: boolean; @@ -19,12 +20,13 @@ const ChangePasswordModal: React.FC = ({ const [showOld, setShowOld] = useState(false); const [showNew, setShowNew] = useState(false); const [showConfirm, setShowConfirm] = useState(false); + const { showToast } = useToast(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (newPassword !== confirmPassword) { - alert("New passwords do not match!"); + showToast("New passwords do not match!" , "error"); return; } @@ -40,17 +42,17 @@ const ChangePasswordModal: React.FC = ({ const response = await window.electron.changePassword(payload); if (response.success) { - alert("Password changed successfully!"); + showToast("Password changed successfully!" , "success"); setOldPassword("") setNewPassword("") setConfirmPassword("") onClose(); } else { - alert(response.message || "Failed to change password."); + showToast((response.message || "Failed to change password.") , "error"); } } catch (error) { console.error("Password change error:", error); - alert("Error changing password."); + showToast("Error changing password." , "error"); } finally { setLoading(false); } diff --git a/src/ui/pages/Timesheets/ProjectLogsModal.tsx b/src/ui/pages/Timesheets/ProjectLogsModal.tsx index 7144932..99855b8 100644 --- a/src/ui/pages/Timesheets/ProjectLogsModal.tsx +++ b/src/ui/pages/Timesheets/ProjectLogsModal.tsx @@ -1,3 +1,4 @@ +import { DateTime } from "luxon"; import React from "react"; interface TabActivity { @@ -27,6 +28,21 @@ interface Props { const ProjectLogsModal: React.FC = ({ isOpen, onClose, projectName, logs, loading }) => { if (!isOpen) return null; + + const getTimeDifference = (from: string, to: string) => { + const start = DateTime.fromISO(from); + const end = DateTime.fromISO(to); + + const diff = end.diff(start, ["hours", "minutes", "seconds"]); + + return { + hours: diff.hours, + minutes: diff.minutes, + seconds: Math.floor(diff.seconds), + formatted: `${diff.hours}h ${diff.minutes}m ${Math.floor(diff.seconds)}s` + }; + }; + return (
@@ -46,7 +62,11 @@ const ProjectLogsModal: React.FC = ({ isOpen, onClose, projectName, logs,
- πŸ•’ Session {idx + 1} β€” {session.totalSessionTime} + πŸ•’ Session {idx + 1} β€” {getTimeDifference( + new Date(session.from).toISOString(), + new Date(session.to).toISOString() + ).formatted} + {new Date(session.from).toLocaleTimeString()} -{" "} @@ -69,10 +89,10 @@ const ProjectLogsModal: React.FC = ({ isOpen, onClose, projectName, logs,
- + ))} diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index b6cc269..49cdb60 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -52,10 +52,11 @@ const Timesheets: React.FC = () => { const { windowLogs } = useSelector((state: RootState) => state.window); + console.log(" Timesheet in component:", windowLogs); const [projectsData, setProjectsData] = useState([]); const [logs, setLogs] = useState([]); - const [selectedProject, setSelectedProject] = useState(null); + // const [selectedProject, setSelectedProject] = useState(null); const [selectedProjectName, setSelectedProjectName] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [loading, setLoading] = useState(false); @@ -63,6 +64,8 @@ const Timesheets: React.FC = () => { const [lastRefreshed, setLastRefreshed] = useState(null); const [initialLoading, setInitialLoading] = useState(true); const [tick, setTick] = useState(0); + const { selectedProject } = useSelector((state: any) => state.projects); + const selectedProjectId = selectedProject?.projectId ?? null; const activeProjects = projectsData.filter(p => p.status === "ACTIVE"); @@ -84,7 +87,6 @@ const Timesheets: React.FC = () => { }; - const fetchProjects = async () => { setInitialLoading(true); try { @@ -120,7 +122,7 @@ const Timesheets: React.FC = () => { try { setModalOpen(true); setLoading(true); - setSelectedProject(projectID); + // setSelectedProject(projectID); setSelectedProjectName(projectName); console.log("projectID", projectID) @@ -154,10 +156,6 @@ const Timesheets: React.FC = () => { return parts.slice(0, 2).join(" "); }; - - - - // Skeleton loader rows const renderSkeletonRows = () => { return Array.from({ length: 5 }).map((_, index) => ( @@ -225,13 +223,13 @@ const Timesheets: React.FC = () => {
#Date Total Hours Total PayActionAction
${row.totalPayAmount.toFixed(2)} + {/* -
{tabIdx + 1} - {tab.title}
- {tab.appName} + {tab.title}
+ {tab.appName}
{new Date(tab.from).toLocaleTimeString()}
{new Date(tab.to).toLocaleTimeString()}
{new Date(tab.from).toLocaleTimeString()}
{new Date(tab.to).toLocaleTimeString()}
{tab.durationFormatted}
{formatWorkTime(entry.totalWorkTime)} + + {/* Disconnect VPN (Disabled when NOT connected) */} + {/* */} + +
+

+ Need help? Contact IT support at{" "} + + support@docva.com + +

+ + + + + ); + } - useEffect(() => { - // checkVPN(); - }, []); - if (checking) { return ( -
- - Checking VPN... -
+ <> + {children} + ); - } - if (!vpnConnected) { - return ( -
-
-

VPN Required

-

Please connect to your company VPN to continue.

- - -
-
- ); - } - - return <>{children}; }; -export default VpnGuard; +export default VpnGuard; \ No newline at end of file diff --git a/src/ui/store/index.tsx b/src/ui/store/index.tsx index 18dc804..d7af0c6 100644 --- a/src/ui/store/index.tsx +++ b/src/ui/store/index.tsx @@ -4,6 +4,7 @@ import windowReducer from "./windowSlice"; import projectsReducer from "./projectSlice"; import userReducer from "./userSlice"; import profileReducer from "./profileSlice"; +import logsReducer from './logsSlice'; export const store = configureStore({ reducer: { @@ -11,6 +12,7 @@ export const store = configureStore({ projects: projectsReducer, user: userReducer, profile: profileReducer, + logs: logsReducer, }, }); diff --git a/src/ui/store/logsSlice.tsx b/src/ui/store/logsSlice.tsx new file mode 100644 index 0000000..d8c00fd --- /dev/null +++ b/src/ui/store/logsSlice.tsx @@ -0,0 +1,240 @@ +// store/logsSlice.ts +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface LogEntry { + id: string; // Unique ID for each log entry + title: string; + appName: string; + url: string | null; + from: string; // UTC ISO string with .000 milliseconds + to: string; // UTC ISO string with .000 milliseconds + totalTime: number; // milliseconds +} + +export interface ProjectLogs { + projectId: string; + logId: string | null; + logs: LogEntry[]; // All collected logs during tracking + trackingStartTime: string | null; + lastWindowTitle: string | null; // Track last window title + lastWindowApp: string | null; // Track last window app +} + +interface LogsState { + logsByProject: Record; +} + +const initialState: LogsState = { + logsByProject: {}, +}; + +// Helper function to get timestamp with .000 milliseconds +const getTimestampWithZeroMs = () => { + const now = new Date(); + // Format: YYYY-MM-DDTHH:mm:ss.000Z + const isoString = now.toISOString(); + // Replace milliseconds with .000 + return isoString.replace(/\.\d{3}Z$/, '.000Z'); +}; + +// Helper function to format any date string to have .000 milliseconds +const formatToZeroMs = (dateString: string): string => { + if (dateString.endsWith('.000Z')) { + return dateString; // Already formatted + } + const date = new Date(dateString); + const isoString = date.toISOString(); + return isoString.replace(/\.\d{3}Z$/, '.000Z'); +}; + +const logsSlice = createSlice({ + name: "logs", + initialState, + reducers: { + // Initialize logs for a project + initializeProjectLogs: ( + state, + action: PayloadAction<{ + projectId: string; + logId: string; + }> + ) => { + const { projectId, logId } = action.payload; + const nowUTC = getTimestampWithZeroMs(); + + // Always create fresh logs for new tracking session + state.logsByProject[projectId] = { + projectId, + logId, + logs: [], // Start with empty logs + trackingStartTime: nowUTC, + lastWindowTitle: null, + lastWindowApp: null, + }; + + console.log(`πŸ“ Initialized logs for project ${projectId} with logId: ${logId}`); + }, + + // Add or update window log + updateWindowLog: ( + state, + action: PayloadAction<{ + projectId: string; + title: string; + appName: string; + url?: string | null; + }> + ) => { + const { projectId, title, appName, url = null } = action.payload; + const projectLogs = state.logsByProject[projectId]; + + if (!projectLogs) { + console.warn(`Cannot update log: Project ${projectId} not initialized`); + return; + } + + const nowUTC = getTimestampWithZeroMs(); + const isSameWindow = + projectLogs.lastWindowTitle === title && + projectLogs.lastWindowApp === appName; + + // If same window as before, update the last log's end time + if (isSameWindow && projectLogs.logs.length > 0) { + const lastLog = projectLogs.logs[projectLogs.logs.length - 1]; + // Format the existing from time to ensure it has .000 milliseconds + lastLog.from = formatToZeroMs(lastLog.from); + lastLog.to = nowUTC; + + // Calculate total time in milliseconds + const startTime = new Date(lastLog.from).getTime(); + const endTime = new Date(lastLog.to).getTime(); + lastLog.totalTime = endTime - startTime; + + // console.log(`⏱️ Updated existing log: ${title} (${lastLog.totalTime}ms)`); + } + // If new window or first window + else { + // If there was a previous window, ensure it's properly ended + if (projectLogs.lastWindowTitle && projectLogs.logs.length > 0) { + const lastLog = projectLogs.logs[projectLogs.logs.length - 1]; + // Format the existing from time to ensure it has .000 milliseconds + lastLog.from = formatToZeroMs(lastLog.from); + lastLog.to = nowUTC; + + // Calculate total time in milliseconds + const startTime = new Date(lastLog.from).getTime(); + const endTime = new Date(lastLog.to).getTime(); + lastLog.totalTime = endTime - startTime; + + console.log( + `βœ… Finalized previous window: ${lastLog.title} (${lastLog.totalTime}ms)` + ); + } + + // Create new log entry with .000 milliseconds + const newLog: LogEntry = { + id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + title, + appName, + url, + from: nowUTC, + to: nowUTC, // Will be updated continuously + totalTime: 0, // Starts at 0 (in milliseconds) + }; + + projectLogs.logs.push(newLog); + console.log(`πŸ“ Started new window: ${title} on ${appName}`); + } + + // Update last window tracking + projectLogs.lastWindowTitle = title; + projectLogs.lastWindowApp = appName; + }, + + // Clear project logs + clearProjectLogs: ( + state, + action: PayloadAction<{ + projectId: string; + }> + ) => { + const { projectId } = action.payload; + delete state.logsByProject[projectId]; + console.log(`🧹 Cleared logs for project ${projectId}`); + }, + + // Clear all logs + clearAllLogs: (state) => { + state.logsByProject = {}; + console.log("🧹 Cleared all logs"); + }, + + // Debug: Get logs state + debugLogsState: ( + state, + action: PayloadAction<{ + projectId: string; + }> + ) => { + const { projectId } = action.payload; + const projectLogs = state.logsByProject[projectId]; + + if (!projectLogs) { + console.log(`πŸ“‹ No logs found for project ${projectId}`); + return; + } + + console.log(`πŸ“‹ Logs State for ${projectId}:`, { + totalLogs: projectLogs.logs.length, + lastWindow: projectLogs.lastWindowTitle, + logs: projectLogs.logs.map((log) => ({ + title: log.title.substring(0, 40) + (log.title.length > 40 ? "..." : ""), + appName: log.appName, + from: log.from, + to: log.to, + durationMs: log.totalTime, + durationSec: Math.round(log.totalTime / 1000), + })), + }); + }, + + // Optional: Add a reducer to fix all timestamps in existing logs + fixAllTimestamps: ( + state, + action: PayloadAction<{ + projectId: string; + }> + ) => { + const { projectId } = action.payload; + const projectLogs = state.logsByProject[projectId]; + + if (!projectLogs) { + console.warn(`Cannot fix timestamps: Project ${projectId} not found`); + return; + } + + // Fix all timestamps in existing logs + projectLogs.logs.forEach(log => { + log.from = formatToZeroMs(log.from); + log.to = formatToZeroMs(log.to); + // Recalculate totalTime based on formatted timestamps + const startTime = new Date(log.from).getTime(); + const endTime = new Date(log.to).getTime(); + log.totalTime = endTime - startTime; + }); + + console.log(`πŸ”§ Fixed timestamps for project ${projectId}`); + }, + }, +}); + +export const { + initializeProjectLogs, + updateWindowLog, + clearProjectLogs, + clearAllLogs, + debugLogsState, + fixAllTimestamps, +} = logsSlice.actions; + +export default logsSlice.reducer; \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 273cad3..bf8f151 100644 --- a/types.d.ts +++ b/types.d.ts @@ -133,6 +133,33 @@ interface PayslipResponse { } +interface RegionResponse { + success: boolean; + status: number; + data?: any; + retry?: boolean; + message?: string; +} + + +interface TrackingLog { + title: string; + appName: string; + url: string; + from: string; + to: string; + totalTime: number; +} + +interface TrackingPayload { + vaId: string; + projectId: string; + logId: string; + logs: TrackingLog[]; +} + + + interface Window { electron: { login: (credentials: { email: string; password: string }) => Promise; @@ -158,18 +185,12 @@ interface Window { }) => Promise; stopProjectTracking: (data: { vaId: string; + logId: string, projectId: string; type: string; time: string }) => Promise; - trackingTabsOpeningClosing: (data: { - id?: string; - logId: string; - projectId: string; - title: string; - appName: string; - }) => Promise; - + trackingTabsOpeningClosing: (data: TrackingPayload) => Promise; getProjectLogs: ( projectId: string, fromDate?: string, @@ -242,8 +263,14 @@ interface Window { data?: any; }>; + checkRegion: () => Promise; connectVPN: () => Promise; disconnectVPN: () => Promise; + getBaseURL: () => Promise; + updateBaseURL: (url: string) => Promise<{ + updated: boolean; + baseURL: string; + }>; }; } diff --git a/vite.config.ts.timestamp-1765963658478-f34f24a548cda.mjs b/vite.config.ts.timestamp-1765963658478-f34f24a548cda.mjs new file mode 100644 index 0000000..dcc3c93 --- /dev/null +++ b/vite.config.ts.timestamp-1765963658478-f34f24a548cda.mjs @@ -0,0 +1,19 @@ +// vite.config.ts +import { defineConfig } from "file:///C:/Users/user/OneDrive/Desktop/electron-main/node_modules/vite/dist/node/index.js"; +import react from "file:///C:/Users/user/OneDrive/Desktop/electron-main/node_modules/@vitejs/plugin-react/dist/index.js"; +import tailwindcss from "file:///C:/Users/user/OneDrive/Desktop/electron-main/node_modules/@tailwindcss/vite/dist/index.mjs"; +var vite_config_default = defineConfig({ + plugins: [react(), tailwindcss()], + base: "./", + build: { + outDir: "dist-react" + }, + server: { + port: 5123, + strictPort: true + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx1c2VyXFxcXE9uZURyaXZlXFxcXERlc2t0b3BcXFxcZWxlY3Ryb24tbWFpblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcVXNlcnNcXFxcdXNlclxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXGVsZWN0cm9uLW1haW5cXFxcdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L1VzZXJzL3VzZXIvT25lRHJpdmUvRGVza3RvcC9lbGVjdHJvbi1tYWluL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XHJcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XHJcbmltcG9ydCB0YWlsd2luZGNzcyBmcm9tICdAdGFpbHdpbmRjc3Mvdml0ZSdcclxuXHJcbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXHJcbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XHJcbiAgcGx1Z2luczogW3JlYWN0KCksdGFpbHdpbmRjc3MoKV0sXHJcbiAgYmFzZTogJy4vJyxcclxuICBidWlsZDoge1xyXG4gICAgb3V0RGlyOiAnZGlzdC1yZWFjdCcsXHJcbiAgfSxcclxuICBzZXJ2ZXI6IHtcclxuICAgIHBvcnQ6IDUxMjMsXHJcbiAgICBzdHJpY3RQb3J0OiB0cnVlLFxyXG4gIH0sXHJcbn0pO1xyXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWtVLFNBQVMsb0JBQW9CO0FBQy9WLE9BQU8sV0FBVztBQUNsQixPQUFPLGlCQUFpQjtBQUd4QixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxHQUFFLFlBQVksQ0FBQztBQUFBLEVBQy9CLE1BQU07QUFBQSxFQUNOLE9BQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxFQUNWO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsSUFDTixZQUFZO0FBQUEsRUFDZDtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==