Merge branch 'new-window-v2' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 3:22 - 17Dec

This commit is contained in:
BhagwanSingh 2025-12-17 15:23:02 +05:30
commit 05e706e150
6 changed files with 552 additions and 425 deletions

View File

@ -15,17 +15,17 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
ipcMain.handle("GET_BASE_URL", () => { ipcMain.handle("GET_BASE_URL", () => {
return getBaseURL(); return getBaseURL();
}); });
// UPDATE Base URL // UPDATE Base URL
ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => { ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => {
setBaseURL(url); setBaseURL(url);
return { return {
updated: true, updated: true,
baseURL: url baseURL: url
}; };
}); });
ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => { ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(key); return await getToken(key);
@ -168,39 +168,30 @@ ipcMain.handle("stop-project-tracking", async (event, payload) => {
// tabs opening and closing when and tracking start // tabs opening and closing when and tracking start
ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => { ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
try { try {
console.log("🟦 IPC tracking-tabs-opening-and-closing:", payload); console.log("🟦 IPC received payload:", payload);
let response; const response = await api.post("/log/project/track", payload);
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);
}
return response; console.log("🟩 Log track API success:", response.data);
return {
success: true,
data: response.data,
};
} catch (error) { } catch (error) {
console.error("Error syncing project tab tracking:", error); console.error("❌ Log track API error:", error);
throw error;
return {
success: false,
message: "Something went wrong",
error,
};
} }
}); });
ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => { ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => {
try { try {
console.log("📥 get-project-logs called with:", { projectID, fromDate, toDate }); console.log("📥 get-project-logs called with:", { projectID, fromDate, toDate });

View File

@ -1,6 +1,14 @@
// hooks/useActiveAppTracker.tsx
import { useEffect, useRef, useCallback } from "react"; import { useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice"; import { RootState } from "../store";
import { WindowInfo } from "../store/windowSlice";
import {
initializeProjectLogs,
updateWindowLog,
clearProjectLogs,
debugLogsState,
} from "../store/logsSlice";
interface DailyHours { interface DailyHours {
todayTotal: string; todayTotal: string;
@ -16,273 +24,253 @@ export const useActiveAppTracker = (
onLimitExceeded?: () => void onLimitExceeded?: () => void
) => { ) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { logsByProject } = useSelector((state: RootState) => state.logs);
const lastActiveRef = useRef<WindowInfo | null>(null); const lastActiveRef = useRef<WindowInfo | null>(null);
const lastLogIdRef = useRef<string | null>(null);
const lastTitleRef = useRef<string | null>(null); const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<number | null>(null); const lastAppNameRef = useRef<string | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null); const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
const limitCheckedRef = useRef<boolean>(false); const limitCheckedRef = useRef(false);
const isStoppingRef = useRef<boolean>(false); // ✅ Flag to prevent new requests 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 // Reset refs
const parseTimeToMinutes = (timeStr: string): number => { lastActiveRef.current = null;
const hourMatch = timeStr.match(/(\d+)h/); lastTitleRef.current = null;
const minMatch = timeStr.match(/(\d+)m/); lastAppNameRef.current = null;
checkCountRef.current = 0;
const hours = hourMatch ? parseInt(hourMatch[1]) : 0; isStoppingRef.current = false;
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;
} }
}, [isTracking, activeProjectLogId, projectId, projectName, dispatch]);
return false; // Stop tracking intervals
}; const stopIntervals = useCallback(() => {
// ✅ 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...");
if (intervalIdRef.current) { if (intervalIdRef.current) {
clearInterval(intervalIdRef.current); clearInterval(intervalIdRef.current);
intervalIdRef.current = null; intervalIdRef.current = null;
console.log("✅ Interval cleared");
} }
}, []); }, []);
// ✅ Send final PATCH before stopping (called by handleStopTracking) // Debug function to check logs state
const sendFinalPatch = useCallback(async () => { const debugCurrentLogs = useCallback(() => {
console.log("=".repeat(60)); if (projectId) {
console.log("🛑 FINAL PATCH INITIATED"); dispatch(debugLogsState({ projectId }));
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;
} }
}, [projectId, dispatch]);
// Finalize and send all logs when stopping tracking
const finalizeAndSendLogs = useCallback(
async (vaId: string): Promise<boolean> => {
// console.log("=".repeat(60));
console.log(" FINALIZING AND SENDING ALL LOGS");
// console.log("=".repeat(60));
// Set stopping flag
isStoppingRef.current = true;
// Stop intervals immediately
stopIntervals();
if (!activeProjectLogId || !projectId) {
console.log("⚠️ Skipping — no active project");
return false;
}
const projectLogs = logsByProject[projectId];
if (!projectLogs || projectLogs.logs.length === 0) {
console.log("ℹ️ No logs found for project", projectId);
return true;
}
// ✅ STEP 3: Send ONE final PATCH for the last active window
if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) {
try { try {
console.log("📊 Final Activity Details:"); const nowUTC = new Date().toISOString();
console.log(` • Window: ${lastTitleRef.current}`);
console.log(` • App: ${lastActiveRef.current.owner.name}`);
console.log(` • Log ID: ${lastLogIdRef.current}`);
console.log(` • Project: ${projectId}`);
const result = await sendActivityLog( // Finalize the last log entry
activeProjectLogId, const allLogs = [...projectLogs.logs];
projectId, // if (allLogs.length > 0) {
lastTitleRef.current, // const lastLog = allLogs[allLogs.length - 1];
lastActiveRef.current.owner.name || "Unknown", // const endTime = new Date(nowUTC);
false // PATCH (update existing log) // const startTime = new Date(lastLog.from);
); // lastLog.to = nowUTC;
// lastLog.totalTime = endTime.getTime() - startTime.getTime();
// console.log(`🛑 Finalized last window: ${lastLog.title} (${lastLog.totalTime}ms)`);
// }
console.log("✅ Final PATCH sent successfully!"); // Filter out logs with very short duration (less than 1 second)
const validLogs = allLogs.filter((log) => {
const duration = new Date(log.to).getTime() - new Date(log.from).getTime();
return duration >= 1000; // Minimum 1 second
});
if (result?.dailyHours) { console.log(`📊 Total logs: ${allLogs.length}, Valid logs (≥1s): ${validLogs.length}`);
console.log("📈 Final Daily Hours:");
console.log(` • Today: ${result.dailyHours.todayTotal}`); if (validLogs.length === 0) {
console.log(` • Limit: ${result.dailyHours.dailyLimit}`); console.log("ℹ️ No valid logs to send");
console.log(` • Remaining: ${result.dailyHours.remaining}`); dispatch(clearProjectLogs({ projectId }));
return true;
} }
// Prepare payload with all valid logs
const payload = {
vaId: vaId || "",
projectId,
logId: activeProjectLogId,
logs: allLogs.map((log) => ({
title: log.title,
appName: log.appName,
url: log.url ?? "",
from: log.from,
to: log.to,
totalTime: log.totalTime,
})),
};
console.log("📤 Payload to send:", payload);
console.log("📊 Sending logs summary:", {
projectId,
projectName,
logCount: validLogs.length,
totalTime: validLogs.reduce((sum, log) => sum + log.totalTime, 0),
firstLog: validLogs[0]
? {
title: validLogs[0].title.substring(0, 40),
appName: validLogs[0].appName,
duration: validLogs[0].totalTime,
from: new Date(validLogs[0].from).toLocaleTimeString(),
to: new Date(validLogs[0].to).toLocaleTimeString(),
}
: null,
lastLog: validLogs[validLogs.length - 1]
? {
title: validLogs[validLogs.length - 1].title.substring(0, 40),
appName: validLogs[validLogs.length - 1].appName,
duration: validLogs[validLogs.length - 1].totalTime,
from: new Date(validLogs[validLogs.length - 1].from).toLocaleTimeString(),
to: new Date(validLogs[validLogs.length - 1].to).toLocaleTimeString(),
}
: null,
});
let res = 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) { } catch (error) {
console.error("❌ Final PATCH failed:", error); const errorMsg = error instanceof Error ? error.message : "Unknown error";
throw error; console.error("❌ Error sending logs:", error);
console.log("=".repeat(60));
return false;
} }
} else { },
console.log("ℹ️ No active window to finalize"); [
} activeProjectLogId,
projectId,
logsByProject,
stopIntervals,
dispatch,
]
);
// ✅ STEP 4: Clean up all refs // Main tracking logic - runs every 5 seconds
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]);
// ✅ Update ref whenever sendFinalPatch changes
useEffect(() => { useEffect(() => {
finalizeTrackingRef.current = sendFinalPatch; if (!isTracking || !projectId || !activeProjectLogId) {
}, [sendFinalPatch]); console.log("⏸️ Tracking inactive — stopping interval");
stopIntervals();
// ✅ 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();
return; return;
} }
console.log(`🟢 Starting active tracker for: ${projectName}`); console.log(`🚀 Starting tracker for: ${projectName}`);
const fetchActiveApp = async () => { const checkActiveWindow = async () => {
// ✅ Double-check stopping flag before each fetch
if (isStoppingRef.current) { if (isStoppingRef.current) {
console.log("🚫 Stopping in progress — skipping fetch");
return; return;
} }
try { try {
const details = (await window.electron.getActiveAppDetails()) as WindowInfo; const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id) return;
const title = details.title; if (!details || !details.id || !details.owner?.name) {
const appName = details.owner.name; return;
const prevTitle = lastTitleRef.current; }
const hasChanged = !prevTitle || prevTitle !== title;
if (hasChanged) { const currentTitle = details.title;
// PATCH previous window before creating new log const currentAppName = details.owner.name;
if (lastLogIdRef.current && prevTitle) {
console.log("🔄 Updating previous window:", prevTitle);
await sendActivityLog(activeProjectLogId!, projectId!, prevTitle, appName, false);
}
// POST new window log checkCountRef.current += 1;
console.log("🆕 New window detected:", title);
await sendActivityLog(activeProjectLogId!, projectId!, title, appName, true);
// Update refs and Redux // Update Redux with current window activity
lastActiveRef.current = details; dispatch(
lastTitleRef.current = title; updateWindowLog({
dispatch(setActiveWindow({ window: details, projectId, projectName })); projectId,
} else { title: currentTitle,
console.log("⏸️ Same window, skipping"); 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) { } catch (error) {
console.error("❌ Failed to fetch active app:", error); console.error("❌ Failed to check active window:", error);
} }
}; };
// Initial fetch // Initial check
fetchActiveApp(); checkActiveWindow();
// Set up interval (every 5 seconds) // Set up interval (every 5 seconds)
intervalIdRef.current = setInterval(fetchActiveApp, 5000); intervalIdRef.current = setInterval(checkActiveWindow, 5000);
// Cleanup on unmount // Cleanup
return () => { return () => {
console.log("🧹 Cleaning up tracker interval"); console.log("🧹 Cleaning up tracker");
stopInterval(); stopIntervals();
}; };
}, [isTracking, activeProjectLogId, projectId, projectName, dispatch, stopInterval]); }, [
isTracking,
activeProjectLogId,
projectId,
projectName,
stopIntervals,
dispatch,
debugCurrentLogs,
]);
return { return {
finalizeTracking: finalizeTrackingRef.current, finalizeAndSendLogs,
stopInterval, stopIntervals,
setIsStopping: (value: boolean) => { setIsStopping: (value: boolean) => {
isStoppingRef.current = value; isStoppingRef.current = value;
console.log(`🔒 Stopping flag manually set to: ${value}`); },
} debugCurrentLogs,
}; };
}; };

View File

@ -39,6 +39,7 @@ import { canStartTracking } from "../utils/projectLimits";
import { useTheme } from "../helper/ThemeToggle"; import { useTheme } from "../helper/ThemeToggle";
import LogoutModal from "../modal/LogoutModal"; import LogoutModal from "../modal/LogoutModal";
import { parseTimeStringToSeconds } from "../utils/timeParser"; import { parseTimeStringToSeconds } from "../utils/timeParser";
import { clearAllLogs, clearProjectLogs } from "../store/logsSlice";
interface User { interface User {
id: string; id: string;
@ -100,6 +101,8 @@ const DashboardLayout = () => {
const { projectsData, projectSummary, selectedProject, projectTrackingMap } = const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
useSelector((state: any) => state.projects); useSelector((state: any) => state.projects);
const { logsByProject } = useSelector((state: RootState) => state.logs);
const tracking = selectedProject const tracking = selectedProject
? projectTrackingMap[selectedProject.projectId] ? projectTrackingMap[selectedProject.projectId]
: null; : null;
@ -173,7 +176,7 @@ const DashboardLayout = () => {
}, [selectedProject]); }, [selectedProject]);
// Initialize useActiveAppTracker and store finalizeTracking in ref // Initialize useActiveAppTracker and store finalizeTracking in ref
const { finalizeTracking, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker( const { finalizeAndSendLogs, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker(
isTracking, isTracking,
activeProjectLogId, activeProjectLogId,
selectedProject?.projectId ?? null, selectedProject?.projectId ?? null,
@ -182,189 +185,131 @@ const DashboardLayout = () => {
); );
// Define handleStopTracking FIRST // Define handleStopTracking FIRST
const handleStopTracking = useCallback(async () => { const handleStopTracking = useCallback(async () => {
if (loadingStop || loadingStart) { if (loadingStop || loadingStart) {
console.log("⏸️ Already processing, skipping..."); return;
return; }
}
if (!selectedProject) { if (!selectedProject) {
console.warn("⚠️ No selected project to stop"); return;
return; }
}
console.log("=".repeat(70)); console.log("🛑 STOP TRACKING INITIATED");
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 // Set loading state
limitAlertShownRef.current = false; setIsStopping(true);
setLoadingStop(true);
const currentStopTime = new Date().toISOString(); const projectId = selectedProject.projectId;
const projectId = selectedProject.projectId; const vaId = projectSummary.vaId || "";
// ✅ CRITICAL: Set stopping flag IMMEDIATELY to block all new tab tracking // STEP 1: Set stopping flag in tracker
console.log("🔒 Setting stopping flag..."); if (setIsStoppingInTracker) {
if (setIsStoppingInTracker) { setIsStoppingInTracker(true);
setIsStoppingInTracker(true); }
}
setIsStopping(true); // STEP 2: Disconnect socket
setLoadingStop(true); handleSocketDisconnect();
handleSocketDisconnect();
// STEP 1: Clear local timer to prevent further client-side updates // STEP 3: Clear local timer
console.log("⏹️ STEP 1: Clearing local timer..."); if (timerRef.current) {
if (timerRef.current) { clearInterval(timerRef.current);
clearInterval(timerRef.current); timerRef.current = null;
timerRef.current = null; }
console.log("✅ Timer cleared");
}
// STEP 2: Send final PATCH with last active window data // STEP 4: Finalize and send logs
console.log("📤 STEP 2: Sending final activity tracking..."); console.log("📤 STEP 4: Finalizing and sending logs...");
if (stopTrackingRef.current) { let logsSent = false;
try { if (finalizeAndSendLogs) {
await stopTrackingRef.current(); // This calls sendFinalPatch() in the hook
console.log("✅ Final activity tracking completed");
} catch (error) {
console.error("❌ Error during final tracking:", error);
}
} else {
console.log("⚠️ No tracking ref available");
}
// STEP 3: Send LOGOUT to backend
console.log("🚪 STEP 3: Sending LOGOUT request...");
try { try {
const payload = { logsSent = await finalizeAndSendLogs(vaId); // Pass vaId here
vaId: projectSummary.vaId, console.log(logsSent ? "✅ Logs sent successfully" : "⚠️ Logs not sent");
projectId: projectId,
type: "LOGOUT",
time: currentStopTime
};
console.log("📡 Logout payload:", payload);
const res = await window.electron.stopProjectTracking(payload);
console.log("✅ LOGOUT successful:", res);
} catch (error) { } catch (error) {
console.error("❌ LOGOUT failed:", error); console.error("❌ Error sending logs:", error);
} }
}
// STEP 4: Update Redux tracking state // STEP 5: Send LOGOUT to backend
console.log("🔄 STEP 4: Updating Redux state..."); console.log("🚪 STEP 5: Sending LOGOUT request...");
dispatch( try {
stopTrackingReducer({ const logoutPayload = {
projectId, vaId: vaId,
}) projectId: projectId,
); type: "LOGOUT",
time: new Date().toISOString()
};
// STEP 6: Update UI (restore window) await window.electron.stopProjectTracking(logoutPayload);
console.log("🪟 STEP 6: Restoring window..."); console.log("✅ LOGOUT successful");
await window.electron.toggleCloseBtn(true); } catch (error) {
await window.electron.resizeWindow(500, 600); console.error("❌ LOGOUT failed:", error);
toggleWindowSize.current = true; }
console.log("✅ Window restored");
// Allow React to process the state update // STEP 6: Update Redux tracking state
await new Promise(resolve => setTimeout(resolve, 50)); dispatch(stopTrackingReducer({ projectId }));
// STEP 5: Refresh project list to get updated work hours from server // STEP 7: Refresh project list
console.log("📊 STEP 5: Refreshing project list..."); try {
try { const projectListRes = await window.electron.getProjectList();
const projectListRes = await window.electron.getProjectList(); if (projectListRes?.success && Array.isArray(projectListRes.data)) {
console.log("✅ Project list fetched:", projectListRes); dispatch(setProjectsDataSlice(projectListRes.data));
if (projectListRes?.success && Array.isArray(projectListRes.data)) { dispatch(setProjectSummarySlice({
// Update projects data totalProjects: projectListRes.totalProjects || 0,
dispatch(setProjectsDataSlice(projectListRes.data)); activeProjects: projectListRes.activeProjects || 0,
awayProjects: projectListRes.awayProjects || 0,
totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s",
totalWorkMs: projectListRes.totalWorkMs || 0,
vaName: projectListRes.vaName || "",
vaId: projectListRes.vaId || "",
}));
// Update project summary // Update selected project
dispatch( const updatedProject = projectListRes.data.find(
setProjectSummarySlice({ (p: Project) => p.projectId === projectId
totalProjects: projectListRes.totalProjects || 0, );
activeProjects: projectListRes.activeProjects || 0,
awayProjects: projectListRes.awayProjects || 0,
totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s",
totalWorkMs: projectListRes.totalWorkMs || 0,
vaName: projectListRes.vaName || "",
vaId: projectListRes.vaId || "",
})
);
// Update selected project with fresh data from server if (updatedProject) {
const updatedProject = projectListRes.data.find( dispatch(setSelectedProjectSlice(updatedProject));
(p: Project) => p.projectId === projectId
);
if (updatedProject) {
dispatch(setSelectedProjectSlice(updatedProject));
// Sync client time with server time
const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime);
console.log("🔄 Syncing with server time:", {
serverTime: updatedProject.totalWorkTime,
serverSeconds: serverElapsedTime,
clientSeconds: elapsedTime,
difference: serverElapsedTime - elapsedTime
});
// Force update the tracking map with server time
dispatch(
updateProjectTracking({
projectId: updatedProject.projectId,
data: {
elapsedTime: serverElapsedTime,
accumulatedTime: serverElapsedTime,
isTracking: false,
isPaused: false,
startTime: null,
endTime: Date.now(),
pauseStartTime: null,
totalPausedTime: 0
}
})
);
console.log("✅ Project synced with server");
}
} }
} catch (error) {
console.error("❌ Error refreshing project list:", error);
} finally {
// Clear loading states
setLoadingStop(false);
setIsStopping(false);
// ✅ Reset stopping flag for next session
if (setIsStoppingInTracker) {
setIsStoppingInTracker(false);
}
console.log("=".repeat(70));
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
console.log("=".repeat(70));
} }
}, [ } catch (error) {
selectedProject, console.error("❌ Error refreshing project list:", error);
projectSummary, }
elapsedTime,
dispatch, // STEP 8: Reset logs for this project
loadingStop, if (logsSent && projectId) {
loadingStart, dispatch(clearProjectLogs({ projectId }));
setIsStoppingInTracker }
]);
// STEP 9: Restore window
await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
// STEP 10: Final cleanup
setLoadingStop(false);
setIsStopping(false);
if (setIsStoppingInTracker) {
setIsStoppingInTracker(false);
}
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
}, [
selectedProject,
projectSummary,
dispatch,
loadingStop,
loadingStart,
setIsStoppingInTracker,
finalizeAndSendLogs,
]);
// Update the ref whenever finalizeTracking changes // Update the ref whenever finalizeTracking changes
useEffect(() => { // useEffect(() => {
stopTrackingRef.current = finalizeTracking; // stopTrackingRef.current = finalizeTracking;
}, [finalizeTracking]); // }, [finalizeTracking]);
const socketRef = useRef<Socket | null>(null); const socketRef = useRef<Socket | null>(null);
const intervalRef = useRef<number | null>(null); const intervalRef = useRef<number | null>(null);
@ -404,6 +349,7 @@ const DashboardLayout = () => {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
dispatch(clearAllLogs());
const res = await window.electron.logout(); const res = await window.electron.logout();
if (res) { if (res) {
dispatch(clearUser()); dispatch(clearUser());
@ -459,6 +405,11 @@ const DashboardLayout = () => {
return; 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 // CRITICAL FIX: Get the current accumulated time BEFORE starting timer
const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime); const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime);
@ -928,3 +879,5 @@ const DashboardLayout = () => {
}; };
export default DashboardLayout; export default DashboardLayout;

View File

@ -4,6 +4,7 @@ import windowReducer from "./windowSlice";
import projectsReducer from "./projectSlice"; import projectsReducer from "./projectSlice";
import userReducer from "./userSlice"; import userReducer from "./userSlice";
import profileReducer from "./profileSlice"; import profileReducer from "./profileSlice";
import logsReducer from './logsSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -11,6 +12,7 @@ export const store = configureStore({
projects: projectsReducer, projects: projectsReducer,
user: userReducer, user: userReducer,
profile: profileReducer, profile: profileReducer,
logs: logsReducer,
}, },
}); });

182
src/ui/store/logsSlice.tsx Normal file
View File

@ -0,0 +1,182 @@
// 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
to: string; // UTC ISO string
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<string, ProjectLogs>;
}
const initialState: LogsState = {
logsByProject: {},
};
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 = new Date().toISOString();
// 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 = new Date().toISOString();
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];
const startTime = new Date(lastLog.from);
const endTime = new Date(nowUTC);
lastLog.to = nowUTC;
lastLog.totalTime = endTime.getTime() - startTime.getTime();
// 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];
const endTime = new Date(nowUTC);
const startTime = new Date(lastLog.from);
lastLog.to = nowUTC;
lastLog.totalTime = endTime.getTime() - startTime.getTime();
console.log(
`✅ Finalized previous window: ${lastLog.title} (${lastLog.totalTime}ms)`
);
}
// Create new log entry
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
};
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: new Date(log.from).toLocaleTimeString(),
to: new Date(log.to).toLocaleTimeString(),
duration: log.totalTime,
})),
});
},
},
});
export const {
initializeProjectLogs,
updateWindowLog,
clearProjectLogs,
clearAllLogs,
debugLogsState,
} = logsSlice.actions;
export default logsSlice.reducer;

27
types.d.ts vendored
View File

@ -142,6 +142,24 @@ interface RegionResponse {
} }
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 { interface Window {
electron: { electron: {
login: (credentials: { email: string; password: string }) => Promise<any>; login: (credentials: { email: string; password: string }) => Promise<any>;
@ -171,14 +189,7 @@ interface Window {
type: string; type: string;
time: string time: string
}) => Promise<any>; }) => Promise<any>;
trackingTabsOpeningClosing: (data: { trackingTabsOpeningClosing: (data: TrackingPayload) => Promise<any>;
id?: string;
logId: string;
projectId: string;
title: string;
appName: string;
}) => Promise<any>;
getProjectLogs: ( getProjectLogs: (
projectId: string, projectId: string,
fromDate?: string, fromDate?: string,