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

@ -16,16 +16,16 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
ipcMain.handle("GET_BASE_URL", () => {
return getBaseURL();
});
});
// UPDATE Base URL
ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => {
// UPDATE Base URL
ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => {
setBaseURL(url);
return {
updated: true,
baseURL: url
};
});
});
ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(key);
@ -168,39 +168,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.data);
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 });

View File

@ -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,253 @@ export const useActiveAppTracker = (
onLimitExceeded?: () => void
) => {
const dispatch = useDispatch();
const { logsByProject } = useSelector((state: RootState) => state.logs);
const lastActiveRef = useRef<WindowInfo | null>(null);
const lastLogIdRef = useRef<string | null>(null);
const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<number | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
const limitCheckedRef = useRef<boolean>(false);
const isStoppingRef = useRef<boolean>(false); // ✅ Flag to prevent new requests
const lastAppNameRef = useRef<string | null>(null);
const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
const limitCheckedRef = useRef(false);
const isStoppingRef = useRef(false);
const checkCountRef = useRef(0);
const canSend = !!(activeProjectLogId && projectId && !isStoppingRef.current);
// ✅ 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;
}
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,
// Initialize logs when tracking starts
useEffect(() => {
if (isTracking && activeProjectLogId && projectId) {
// Initialize fresh logs for this tracking session
dispatch(
initializeProjectLogs({
projectId,
title,
appName,
};
logId: activeProjectLogId,
})
);
console.log(` Logs initialized for project: ${projectName} (${projectId})`);
if (!isNew && lastLogIdRef.current) {
payload.id = lastLogIdRef.current;
// Reset refs
lastActiveRef.current = null;
lastTitleRef.current = null;
lastAppNameRef.current = null;
checkCountRef.current = 0;
isStoppingRef.current = false;
}
}, [isTracking, activeProjectLogId, projectId, projectName, dispatch]);
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));
// Debug function to check logs state
const debugCurrentLogs = useCallback(() => {
if (projectId) {
dispatch(debugLogsState({ projectId }));
}
}, [projectId, dispatch]);
// ✅ STEP 1: Set stopping flag IMMEDIATELY
// 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;
console.log("🔒 Stopping flag set — no new requests allowed");
// ✅ STEP 2: Stop the interval IMMEDIATELY
stopInterval();
// Stop intervals immediately
stopIntervals();
if (!activeProjectLogId || !projectId) {
console.log("⚠️ Skipping final PATCH — no active project log ID");
return;
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 {
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}`);
const nowUTC = new Date().toISOString();
const result = await sendActivityLog(
activeProjectLogId,
// Finalize the last log entry
const allLogs = [...projectLogs.logs];
// if (allLogs.length > 0) {
// const lastLog = allLogs[allLogs.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 last window: ${lastLog.title} (${lastLog.totalTime}ms)`);
// }
// 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
});
console.log(`📊 Total logs: ${allLogs.length}, Valid logs (≥1s): ${validLogs.length}`);
if (validLogs.length === 0) {
console.log("ℹ️ No valid logs to send");
dispatch(clearProjectLogs({ projectId }));
return true;
}
// Prepare payload with all valid logs
const payload = {
vaId: vaId || "",
projectId,
lastTitleRef.current,
lastActiveRef.current.owner.name || "Unknown",
false // PATCH (update existing log)
);
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("✅ Final PATCH sent successfully!");
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}`);
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(),
}
} catch (error) {
console.error("❌ Final PATCH failed:", error);
throw error;
}
} else {
console.log("ℹ️ No active window to finalize");
: 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,
});
// ✅ STEP 4: Clean up all refs
console.log("🧹 Cleaning up tracking references...");
let res = window.electron.trackingTabsOpeningClosing(payload)
console.log("Tracking response:", res);
dispatch(clearProjectLogs({ projectId }));
// Cleanup refs
lastActiveRef.current = null;
lastTitleRef.current = null;
lastLogIdRef.current = null;
lastAppNameRef.current = null;
limitCheckedRef.current = false;
console.log("✅ Cleanup complete");
console.log("✅ Logs sent and cleared successfully");
console.log("=".repeat(60));
}, [activeProjectLogId, projectId, stopInterval]);
// ✅ Update ref whenever sendFinalPatch changes
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");
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,
]
);
if (!isTracking || !canSend) {
console.log("🟡 Tracking inactive — cleaning up");
stopInterval();
// Main tracking logic - runs every 5 seconds
useEffect(() => {
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 (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);
if (!details || !details.id || !details.owner?.name) {
return;
}
// POST new window log
console.log("🆕 New window detected:", title);
await sendActivityLog(activeProjectLogId!, projectId!, title, appName, true);
const currentTitle = details.title;
const currentAppName = details.owner.name;
// Update refs and Redux
checkCountRef.current += 1;
// Update Redux with current window activity
dispatch(
updateWindowLog({
projectId,
title: currentTitle,
appName: currentAppName,
url: null,
})
);
// Update refs
lastActiveRef.current = details;
lastTitleRef.current = title;
dispatch(setActiveWindow({ window: details, projectId, projectName }));
} else {
console.log("⏸️ Same window, skipping");
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,
};
};

View File

@ -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;
@ -173,7 +176,7 @@ const DashboardLayout = () => {
}, [selectedProject]);
// Initialize useActiveAppTracker and store finalizeTracking in ref
const { finalizeTracking, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker(
const { finalizeAndSendLogs, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker(
isTracking,
activeProjectLogId,
selectedProject?.projectId ?? null,
@ -184,106 +187,74 @@ 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;
// Set loading state
setIsStopping(true);
setLoadingStop(true);
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);
}
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,
projectId: projectId,
type: "LOGOUT",
time: currentStopTime
time: new Date().toISOString()
};
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);
if (projectListRes?.success && Array.isArray(projectListRes.data)) {
// Update projects data
dispatch(setProjectsDataSlice(projectListRes.data));
// Update project summary
dispatch(
setProjectSummarySlice({
dispatch(setProjectSummarySlice({
totalProjects: projectListRes.totalProjects || 0,
activeProjects: projectListRes.activeProjects || 0,
awayProjects: projectListRes.awayProjects || 0,
@ -291,80 +262,54 @@ const DashboardLayout = () => {
totalWorkMs: projectListRes.totalWorkMs || 0,
vaName: projectListRes.vaName || "",
vaId: projectListRes.vaId || "",
})
);
}));
// Update selected project with fresh data from server
// Update selected project
const updatedProject = projectListRes.data.find(
(p: Project) => p.projectId === projectId
);
if (updatedProject) {
dispatch(setSelectedProjectSlice(updatedProject));
// Sync client time with server time
const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime);
console.log("🔄 Syncing with server time:", {
serverTime: updatedProject.totalWorkTime,
serverSeconds: serverElapsedTime,
clientSeconds: elapsedTime,
difference: serverElapsedTime - elapsedTime
});
// Force update the tracking map with server time
dispatch(
updateProjectTracking({
projectId: updatedProject.projectId,
data: {
elapsedTime: serverElapsedTime,
accumulatedTime: serverElapsedTime,
isTracking: false,
isPaused: false,
startTime: null,
endTime: Date.now(),
pauseStartTime: null,
totalPausedTime: 0
}
})
);
console.log("✅ Project synced with server");
}
}
} catch (error) {
console.error("❌ Error refreshing project list:", error);
} finally {
// Clear loading states
}
// STEP 8: Reset logs for this project
if (logsSent && projectId) {
dispatch(clearProjectLogs({ projectId }));
}
// 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);
// ✅ Reset stopping flag for next session
if (setIsStoppingInTracker) {
setIsStoppingInTracker(false);
}
console.log("=".repeat(70));
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
console.log("=".repeat(70));
}
}, [
}, [
selectedProject,
projectSummary,
elapsedTime,
dispatch,
loadingStop,
loadingStart,
setIsStoppingInTracker
]);
setIsStoppingInTracker,
finalizeAndSendLogs,
]);
// Update the ref whenever finalizeTracking changes
useEffect(() => {
stopTrackingRef.current = finalizeTracking;
}, [finalizeTracking]);
// useEffect(() => {
// stopTrackingRef.current = finalizeTracking;
// }, [finalizeTracking]);
const socketRef = useRef<Socket | null>(null);
const intervalRef = useRef<number | null>(null);
@ -404,6 +349,7 @@ const DashboardLayout = () => {
const handleLogout = async () => {
try {
dispatch(clearAllLogs());
const res = await window.electron.logout();
if (res) {
dispatch(clearUser());
@ -459,6 +405,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);
@ -928,3 +879,5 @@ const DashboardLayout = () => {
};
export default DashboardLayout;

View File

@ -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,
},
});

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