change the log login and logout sync with the redux

This commit is contained in:
JaiPandey425 2025-11-19 20:03:57 +05:30
parent df92e68a0a
commit 2212359c96
11 changed files with 997 additions and 607 deletions

View File

@ -35,7 +35,7 @@ api.interceptors.request.use(
api.interceptors.response.use( api.interceptors.response.use(
(response: AxiosResponse) => response.data, (response: AxiosResponse) => response.data,
async (error) => { async (error) => {
console.error('[Axios Error]', error.message); // console.error('[Axios Error]', error.message);
if (error.response?.status === 401) { if (error.response?.status === 401) {
console.warn('Token expired — trigger refresh logic if needed'); console.warn('Token expired — trigger refresh logic if needed');
sendTokenExpired(); sendTokenExpired();

View File

@ -110,14 +110,29 @@ ipcMain.handle("start-project-tracking", async (event, payload) => {
const response = await api.post("/log/project", payload); const response = await api.post("/log/project", payload);
console.log(" Tracking start synced:", response); console.log(" Tracking start synced:", response);
return response; return { success: true, data: response };
} catch (error) { } catch (error:any) {
console.error(" Error syncing project start:", error); console.error("❌ Error syncing project start:", error);
throw error;
// If API sent a response (like 400)
if (error.response) {
return {
success: false,
message: error.response.data?.message || "Request failed",
status: error.response.status
};
}
return {
success: false,
message: "Something went wrong",
status: 500
};
} }
}); });
// When project tracking stop // When project tracking stop
ipcMain.handle("stop-project-tracking", async (event, payload) => { ipcMain.handle("stop-project-tracking", async (event, payload) => {
try { try {
@ -128,7 +143,7 @@ ipcMain.handle("stop-project-tracking", async (event, payload) => {
console.log(" Tracking stop synced:", response); console.log(" Tracking stop synced:", response);
return response; return response;
} catch (error) { } catch (error) {
console.error(" Error syncing project start:", error); console.error(" Error syncing project stop:", error);
throw error; throw error;
} }
}); });
@ -140,16 +155,16 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
let response; let response;
if (!payload.id) { if (!payload.id) {
// 🟢 POST new log (new tab opened) // POST new log (new tab opened)
response = await api.post("/log/project/track", { response = await api.post("/log/project/track", {
logId: payload.logId, logId: payload.logId,
projectId: payload.projectId, projectId: payload.projectId,
title: payload.title, title: payload.title,
appName: payload.appName, appName: payload.appName,
}); });
console.log("✅ New log created:", response.data); console.log(" New log created:", response);
} else { } else {
// 🟡 PATCH existing log (same tab, update) // PATCH existing log (same tab, update)
response = await api.patch("/log/project/track", { response = await api.patch("/log/project/track", {
id: payload.id, id: payload.id,
logId: payload.logId, logId: payload.logId,
@ -157,12 +172,12 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
title: payload.title, title: payload.title,
appName: payload.appName, appName: payload.appName,
}); });
console.log("🟨 Log updated:", response.data); console.log("Log updated:", response);
} }
return response.data; return response;
} catch (error) { } catch (error) {
console.error("❌ Error syncing project tab tracking:", error); console.error("Error syncing project tab tracking:", error);
throw error; throw error;
} }
}); });

View File

@ -94,6 +94,8 @@ import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments
import Timesheets from "./pages/Timesheets/Timesheets"; import Timesheets from "./pages/Timesheets/Timesheets";
import Settings from "./pages/Settings/Settings"; import Settings from "./pages/Settings/Settings";
import HelpSupport from "./pages/HelpSupport/HelpSupport"; import HelpSupport from "./pages/HelpSupport/HelpSupport";
import Vpn from "./pages/Vpn/Vpn";
const App: React.FC = () => { const App: React.FC = () => {
const theme = ThemeToggle(); const theme = ThemeToggle();
@ -139,6 +141,7 @@ const App: React.FC = () => {
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} /> <Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="timesheet" element={<Timesheets />} /> <Route path="timesheet" element={<Timesheets />} />
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
<Route path="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} /> <Route path="help-support" element={<HelpSupport />} />
</Route> </Route>

View File

@ -2,11 +2,20 @@ import { useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice"; import { setActiveWindow, WindowInfo } from "../store/windowSlice";
interface DailyHours {
todayTotal: string;
dailyLimit: string;
remaining: string;
}
export const useActiveAppTracker = ( export const useActiveAppTracker = (
isTracking: boolean, isTracking: boolean,
activeProjectLogId: string | null, activeProjectLogId: string | null,
projectId: string | null, projectId: string | null,
projectName: string | null projectName: string | null,
onLimitExceeded?: () => void // Callback when limit is reached
) => { ) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const lastActiveRef = useRef<WindowInfo | null>(null); const lastActiveRef = useRef<WindowInfo | null>(null);
@ -14,10 +23,44 @@ export const useActiveAppTracker = (
const lastTitleRef = useRef<string | null>(null); const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<number | null>(null); const intervalIdRef = useRef<number | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null); const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
const limitCheckedRef = useRef<boolean>(false);
// ✅ Common guard to check if we can send
const canSend = !!(activeProjectLogId && projectId); const canSend = !!(activeProjectLogId && projectId);
// ✅ Parse time string to minutes
const parseTimeToMinutes = (timeStr: string): number => {
const hourMatch = timeStr.match(/(\d+)h/);
const minMatch = timeStr.match(/(\d+)m/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
return hours * 60 + minutes;
};
// ✅ Check if daily limit is exceeded
const checkDailyLimit = (dailyHours?: DailyHours): boolean => {
if (!dailyHours) return false;
const todayTotalMins = parseTimeToMinutes(dailyHours.todayTotal);
const dailyLimitMins = parseTimeToMinutes(dailyHours.dailyLimit);
const remainingMins = parseTimeToMinutes(dailyHours.remaining);
console.log("⏰ Daily Limit Check:", {
todayTotal: `${todayTotalMins} mins`,
dailyLimit: `${dailyLimitMins} mins`,
remaining: `${remainingMins} mins`,
exceeded: remainingMins <= 0
});
// If remaining time is 0 or negative, limit is exceeded
if (remainingMins <= 0) {
return true;
}
return false;
};
const sendActivityLog = async ( const sendActivityLog = async (
projectLogId: string, projectLogId: string,
projectId: string, projectId: string,
@ -44,17 +87,40 @@ export const useActiveAppTracker = (
appName, appName,
}; };
// ✅ only attach `id` if it’s non-null and not undefined
if (!isNew && lastLogIdRef.current) { if (!isNew && lastLogIdRef.current) {
payload.id = lastLogIdRef.current; payload.id = lastLogIdRef.current;
} }
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload); console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
const res = await window.electron.trackingTabsOpeningClosing(payload); const res = await window.electron.trackingTabsOpeningClosing(payload);
if (isNew && res?._id) lastLogIdRef.current = res._id; console.log("📊 Activity log response:", res);
// Store the log ID for future updates
if (isNew && res?.data?._id) {
console.log("res?.data?._id", res?.data?._id)
lastLogIdRef.current = res.data._id;
}
// ✅ Check daily limit after every API call
if (res?.dailyHours) {
console.log("res?.data?.dailyHours", res?.dailyHours)
const limitExceeded = checkDailyLimit(res?.dailyHours);
if (limitExceeded && !limitCheckedRef.current) {
limitCheckedRef.current = true;
console.warn("🚨 Daily limit exceeded! Stopping tracking...");
// Trigger stop tracking callback
if (onLimitExceeded) {
setTimeout(() => {
onLimitExceeded();
}, 500);
}
}
}
return res; return res;
} catch (error) { } catch (error) {
console.error("❌ Activity log error:", error); console.error("❌ Activity log error:", error);
@ -69,27 +135,54 @@ export const useActiveAppTracker = (
return; return;
} }
console.log("🛑 Sending final PATCH before stopping..."); console.log("=".repeat(60));
console.log("🛑 FINAL PATCH INITIATED - Recording last activity");
console.log("=".repeat(60));
if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) { if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) {
try { try {
await sendActivityLog( console.log("📊 Final Activity Details:");
console.log(` • Window Title: ${lastTitleRef.current}`);
console.log(` • App Name: ${lastActiveRef.current.owner.name}`);
console.log(` • Log ID: ${lastLogIdRef.current}`);
console.log(` • Project ID: ${projectId}`);
const result = await sendActivityLog(
activeProjectLogId!, activeProjectLogId!,
projectId!, projectId!,
lastTitleRef.current, lastTitleRef.current,
lastActiveRef.current.owner.name || "Unknown", lastActiveRef.current.owner.name || "Unknown",
false false // This is a PATCH (update), not a POST (new)
); );
console.log("✅ Final PATCH sent for", lastTitleRef.current);
console.log("✅ Final PATCH sent successfully!");
if (result?.dailyHours) {
console.log("📈 Final Daily Hours Summary:");
console.log(` • Today Total: ${result.dailyHours.todayTotal}`);
console.log(` • Daily Limit: ${result.dailyHours.dailyLimit}`);
console.log(` • Remaining: ${result.dailyHours.remaining}`);
}
} catch (error) { } catch (error) {
console.error("❌ Final PATCH failed:", error); console.error("❌ Final PATCH failed:", error);
throw error; // Re-throw to let caller handle
} }
} else { } else {
console.log("ℹ️ No active log to finalize"); console.log("ℹ️ No active window to finalize");
console.log(" • Last Log ID:", lastLogIdRef.current || "None");
console.log(" • Last Title:", lastTitleRef.current || "None");
console.log(" • Last Active Window:", lastActiveRef.current ? "Yes" : "No");
} }
// Clean up all refs
console.log("🧹 Cleaning up tracking references...");
lastActiveRef.current = null; lastActiveRef.current = null;
lastTitleRef.current = null; lastTitleRef.current = null;
lastLogIdRef.current = null; lastLogIdRef.current = null;
limitCheckedRef.current = false;
console.log("✅ Cleanup complete");
console.log("=".repeat(60));
}, [activeProjectLogId, projectId, canSend]); }, [activeProjectLogId, projectId, canSend]);
useEffect(() => { useEffect(() => {
@ -97,7 +190,11 @@ export const useActiveAppTracker = (
}, [sendFinalPatch]); }, [sendFinalPatch]);
useEffect(() => { useEffect(() => {
// 🚫 If not tracking OR missing IDs, skip everything // Reset limit check when tracking starts
if (isTracking) {
limitCheckedRef.current = false;
}
if (!isTracking || !canSend) { if (!isTracking || !canSend) {
console.log("🟡 Tracking inactive or missing IDs — skipping tracker setup"); console.log("🟡 Tracking inactive or missing IDs — skipping tracker setup");
return; return;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import io, { Socket } from 'socket.io-client'; import io, { Socket } from 'socket.io-client';
import { import {
@ -25,18 +25,15 @@ import {
ChevronDown, ChevronDown,
Play, Play,
ChevronUp, ChevronUp,
GlobeLock,
} from "lucide-react"; } from "lucide-react";
import { useActiveAppTracker } from "../hooks/useActiveAppTracker"; import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../store"; import { RootState } from "../store";
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice"; import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime } from "../store/projectSlice";
import { clearUser } from "../store/userSlice"; import { clearUser } from "../store/userSlice";
import { setError, setLoading, setProfile } from "../store/profileSlice"; import { setError, setLoading, setProfile } from "../store/profileSlice";
import { canStartTracking } from "../utils/projectLimits";
// interface User {
// email: string;
// password: string;
// }
interface User { interface User {
id: string; id: string;
@ -44,7 +41,6 @@ interface User {
email: string; email: string;
} }
// Updated Project interface to match the JSON structure
interface Project { interface Project {
projectId: string; projectId: string;
projectName: string; projectName: string;
@ -52,6 +48,7 @@ interface Project {
totalWorkTime: string; totalWorkTime: string;
totalWorkMs: number; totalWorkMs: number;
totalLogs: number; totalLogs: number;
dedicatedHours: string;
} }
interface ProjectTrackingData { interface ProjectTrackingData {
@ -67,7 +64,6 @@ interface ProjectTrackingData {
export interface TrackingState { export interface TrackingState {
selectedProject: Project | null; selectedProject: Project | null;
projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean; isTracking: boolean;
isPaused: boolean; isPaused: boolean;
elapsedTime: number; elapsedTime: number;
@ -78,6 +74,8 @@ export interface TrackingState {
handleStopTracking: () => void; handleStopTracking: () => void;
handleProjectSelect: (projectId: string) => void; handleProjectSelect: (projectId: string) => void;
projectsData: Project[]; projectsData: Project[];
loadingStart: boolean,
loadingStop: boolean
} }
const DashboardLayout = () => { const DashboardLayout = () => {
@ -87,395 +85,332 @@ const DashboardLayout = () => {
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { windowLogs } = useSelector((state: RootState) => state.window); const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
useSelector((state: any) => state.projects);
console.log("🧾 windowLogs in component:", windowLogs); const tracking = selectedProject
? projectTrackingMap[selectedProject.projectId]
: null;
const isTracking = tracking?.isTracking;
const elapsedTime = tracking?.elapsedTime || 0;
const { windowLogs } = useSelector((state: RootState) => state.window);
const menuItems = [ const menuItems = [
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true }, { label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
// { label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> }, { label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> }, { label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> }, { label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> }, { label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
]; ];
// Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false);
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);
const [startTime, setStartTime] = useState<Date | null>(null); const [startTime, setStartTime] = useState<Date | null>(null);
const [endTime, setEndTime] = useState<Date | null>(null); const [endTime, setEndTime] = useState<Date | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null); const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [projectSummary, setProjectSummary] = useState({ const [activeProjectLogId, setActiveProjectLogId] = useState("");
totalProjects: 0, const toggleWindowSize = useRef(true);
activeProjects: 0, const [loadingStart, setLoadingStart] = useState(false);
awayProjects: 0, const [loadingStop, setLoadingStop] = useState(false);
totalWorkTime: "0h 0m 0s",
totalWorkMs: 0,
vaName: "",
vaId: "",
});
const [activeProjectLogId, setActiveProjectLogId] = useState("")
// ✅ Create a ref to store the stop function so it can be called from the hook
const stopTrackingRef = useRef<(() => Promise<void>) | null>(null);
// ✅ Define handleStopTracking FIRST
const handleStopTracking = useCallback(async () => {
if (loadingStop || loadingStart) return;
if (!selectedProject) {
console.warn("⚠️ No selected project to stop");
return;
}
const projectId = selectedProject.projectId;
console.log("🛑 Stop tracking initiated for:", selectedProject.projectName);
console.log("📊 Current elapsed time:", elapsedTime);
setLoadingStop(true);
// STEP 1: Send final PATCH with last active window data
// This ensures the last window activity is recorded before logout
if (stopTrackingRef.current) {
try {
console.log("📤 Sending final PATCH to record last activity...");
await stopTrackingRef.current();
console.log("✅ Final tracking data sent successfully");
} catch (error) {
console.error("❌ Error sending final tracking data:", error);
// Continue with stop even if PATCH fails
}
} else {
console.warn("⚠️ No finalizeTracking function available");
}
// STEP 2: Stop timer everywhere
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
console.log("⏹️ Timer cleared");
} else {
console.log("ℹ️ No active timer to clear");
}
// STEP 3: Send LOGOUT to backend
try {
const payload = {
vaId: projectSummary.vaId,
projectId: projectId,
type: "LOGOUT",
};
console.log("📡 Sending LOGOUT request:", payload);
const res = await window.electron.stopProjectTracking(payload);
console.log("✅ LOGOUT successful:", res);
} catch (error) {
console.error("❌ Failed to send LOGOUT:", error);
}
// STEP 4: Update UI (window restore)
try {
await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
console.log("✅ Window UI restored");
} catch (error) {
console.error("❌ Error restoring window:", error);
}
// STEP 5: Update Redux tracking state → THIS TRIGGERS UI UPDATE
console.log("🔄 Updating Redux state to stop tracking...");
dispatch(
stopTrackingReducer({
projectId,
})
);
// Allow React to process the state update
await new Promise(resolve => setTimeout(resolve, 50));
console.log("✅ Redux state updated - isTracking is now false");
// STEP 6: Clear active project log ID
setActiveProjectLogId("");
setLoadingStop(false);
console.log("🧹 Active project log ID cleared");
console.log("=".repeat(50));
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
console.log(`Project: ${selectedProject.projectName}`);
console.log(`Final Time: ${formatTime(elapsedTime)}`);
console.log("UI Updates:");
console.log(" ✓ Header should be hidden");
console.log(" ✓ Start button should be visible");
console.log(" ✓ Layout should expand to full height");
console.log("=".repeat(50));
}, [selectedProject, projectSummary, elapsedTime, dispatch]);
// ✅ NOW define handleLimitExceeded using the defined handleStopTracking
const handleLimitExceeded = useCallback(async () => {
console.log("🚨 LIMIT EXCEEDED - Auto-stopping tracking");
if (!selectedProject) {
console.error("❌ No selected project for limit exceeded");
return;
}
// Show alert to user
alert(
`⏰ Daily Limit Reached!\n\n` +
`Project: ${selectedProject.projectName}\n` +
`Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` +
`Your tracking has been stopped automatically because you've reached the daily time limit for this project.`
);
// Stop tracking
await handleStopTracking();
}, [selectedProject, handleStopTracking]);
// ✅ Initialize useActiveAppTracker and store finalizeTracking in ref
const { finalizeTracking } = useActiveAppTracker( const { finalizeTracking } = useActiveAppTracker(
isTracking, isTracking,
activeProjectLogId, activeProjectLogId,
selectedProject?.projectId ?? null, selectedProject?.projectId ?? null,
selectedProject?.projectName ?? null selectedProject?.projectName ?? null,
handleLimitExceeded
); );
// ✅ Update the ref whenever finalizeTracking changes
useEffect(() => {
stopTrackingRef.current = 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);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
// const [seclectedScreen, setSelectedScreen] = useState<string | null>(null)
const [projectsData, setProjectsData] = useState<Project[]>([]);
useEffect(() => { useEffect(() => {
if (!window.electron?.onTokenExpired) return; if (!window.electron?.onTokenExpired) return;
console.log("🔐 Token expiry listener registered"); console.log("🔐 Token expiry listener registered");
window.electron.onTokenExpired(() => { window.electron.onTokenExpired(async () => {
console.log("🔥 Token expired — logging out..."); console.log("🔥 Token expired — checking tracking status...");
handleLogout();
// If user is tracking → FIRST stop tracking
if (isTracking && handleStopTracking) {
console.log("⛔ Token expired DURING tracking → stopping tracking first...");
try {
await handleStopTracking(); // Wait until stop is completed
console.log("🛑 Tracking stopped successfully before logout");
} catch (err) {
console.error("❌ Error while stopping tracking after token expiry:", err);
}
}
// Then perform logout
console.log("🚪 Proceeding to logout due to token expiry...");
await handleLogout();
}); });
return () => { return () => {
console.log("🧹 Cleaning token-expired listener"); console.log("🧹 Cleaning token-expired listener");
}; };
}, []); }, [isTracking, handleStopTracking]);
const fetchProfile = async () => {
try {
dispatch(setLoading(true));
const res = await window.electron.getProfile();
console.log("Profile", res)
if (res.success && res.data) {
dispatch(setProfile(res.data));
} else {
dispatch(setError("Failed to fetch profile"));
}
} catch (err: any) {
dispatch(setError(err.message));
} finally {
dispatch(setLoading(false));
}
};
// fetch the project list of VA
useEffect(() => {
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...");
const res = await window.electron.getProjectList();
console.log(" Project list response:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
setProjectsData(res.data);
// Store project summary info
setProjectSummary({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
});
// Store data redux store
dispatch(setProjectsDataSlice(res.data));
dispatch(
setProjectSummarySlice({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
})
);
// Auto-select first project
if (res.data.length > 0 && !selectedProject) {
const firstProject = res.data[0];
setSelectedProject(firstProject);
dispatch(setSelectedProjectSlice(firstProject));
// Convert totalWorkMs to seconds for elapsedTime
const initialElapsedTime = Math.floor(firstProject.totalWorkMs / 1000);
// Initialize tracking map for that project
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
if (!updated.has(firstProject.projectId)) {
updated.set(firstProject.projectId, {
elapsedTime: initialElapsedTime,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: initialElapsedTime,
});
}
return updated;
});
// Set the initial elapsed time in the component state
setElapsedTime(initialElapsedTime);
}
fetchProfile()
} else {
console.warn("⚠️ Unexpected response:", res);
// if (res?.status) {
// // handleLogout()
// }
}
} catch (err) {
console.error("❌ Error fetching projects:", err);
}
};
fetchProjects();
}, []);
const [projectTrackingMap, setProjectTrackingMap] = useState<
Map<string, ProjectTrackingData>
>(new Map());
const handleLogout = async () => { const handleLogout = async () => {
try { try {
const res = await window.electron.logout(); const res = await window.electron.logout();
if (res) { if (res) {
dispatch(clearUser()); // ✅ Removes from Redux + localStorage dispatch(clearUser());
navigate("/login"); navigate("/login");
} }
} catch (error) { } catch (error) {
console.error("Logout error:", error); console.error("Logout error:", error);
} }
}; };
// Sync global states with selected project tracking data const selectedData = selectedProject
useEffect(() => { ? projectTrackingMap[selectedProject.projectId]
if (selectedProject) { : null;
const projectData = projectTrackingMap.get(selectedProject.projectId);
if (projectData) {
setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused);
setElapsedTime(projectData.elapsedTime);
setStartTime(projectData.startTime);
setEndTime(projectData.endTime);
}
}
}, [selectedProject, projectTrackingMap]);
// Timer effect - calculates elapsed time based on timestamps + accumulated time
useEffect(() => {
if (isTracking && !isPaused && selectedProject) {
timerRef.current = setInterval(() => {
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.projectId);
if (current && current.isTracking && !current.isPaused && current.startTime) {
const now = Date.now();
const startTimestamp = current.startTime.getTime();
const currentSessionTime = Math.floor((now - startTimestamp) / 1000);
const totalPausedTime = current.totalPausedTime || 0;
const calculatedElapsed =
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
current.elapsedTime = calculatedElapsed;
updated.set(selectedProject.projectId, { ...current });
setElapsedTime(calculatedElapsed);
}
return updated;
});
}, 1000);
} else {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [isTracking, isPaused, selectedProject]);
// Start tracking
const handleStartTracking = async (project: Project) => { const handleStartTracking = async (project: Project) => {
if (timerRef.current) {
clearInterval(timerRef.current); if (loadingStart || loadingStop) return;
timerRef.current = null;
// ✅ Check limit before starting
const limitCheck = canStartTracking(project);
if (!limitCheck.canStart) {
alert(
`⚠️ Cannot Start Tracking!\n\n` +
`Project: ${project.projectName}\n` +
`Reason: ${limitCheck.reason}\n\n` +
`You've already completed your daily hours for this project.`
);
return;
} }
const now = new Date(); console.log(`⏰ Starting tracking. Remaining time: ${limitCheck.remaining}`);
setLoadingStart(true);
const now = Date.now();
window.electron.toggleCloseBtn(false); window.electron.toggleCloseBtn(false);
setProjectTrackingMap((prev) => { const payload = {
const updated = new Map(prev); vaId: projectSummary.vaId,
const current = updated.get(project.projectId) || { projectId: project.projectId,
elapsedTime: 0, type: "LOGIN",
startTime: null, };
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: 0,
};
current.startTime = now;
current.isTracking = true;
current.isPaused = false;
current.pauseStartTime = null;
current.totalPausedTime = 0;
updated.set(project.projectId, current);
setIsTracking(true);
setIsPaused(false);
setStartTime(now);
setElapsedTime(current.accumulatedTime);
return updated;
});
try { try {
const payload = { const res = await window.electron.startProjectTracking(payload);
vaId: projectSummary.vaId,
projectId: project.projectId, if (!res.success) {
type: "LOGIN", alert(res.message);
await window.electron.toggleCloseBtn(true);
return;
}
// ⬇️ REDUX UPDATE: Start Tracking
dispatch(
startTracking({
projectId: project.projectId,
startTime: now,
})
);
setActiveProjectLogId(res?.data?.logId);
console.log("✅ Tracking started. Log ID:", res?.data?.logId);
// ⬇️ Calculate time limit in seconds
const parseTimeToSeconds = (timeStr: string): number => {
const hourMatch = timeStr.match(/(\d+)h/);
const minMatch = timeStr.match(/(\d+)m/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
return (hours * 3600) + (minutes * 60);
}; };
// console.log("📡 Sending start tracking payload:", payload); const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
const res = await window.electron.startProjectTracking(payload); const alreadyWorkedSeconds = Math.floor((project.totalWorkMs || 0) / 1000);
console.log(" Synced start tracking:", res); const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds;
setActiveProjectLogId(res?.logId)
console.log("⏱️ Time Limits:", {
dedicatedHours: project.dedicatedHours,
dedicatedSeconds,
alreadyWorked: `${Math.floor(alreadyWorkedSeconds / 60)}m ${alreadyWorkedSeconds % 60}s`,
remainingSeconds,
remainingMinutes: Math.floor(remainingSeconds / 60)
});
// ⬇️ Start global timer loop with limit check
timerRef.current = setInterval(() => {
const currentElapsed = Math.floor((Date.now() - now) / 1000) +
(selectedData?.accumulatedTime || 0);
dispatch(
updateElapsedTime({
projectId: project.projectId,
elapsedTime: currentElapsed,
})
);
// ✅ Check if time limit reached
const totalTimeWorked = alreadyWorkedSeconds + Math.floor((Date.now() - now) / 1000);
if (totalTimeWorked >= dedicatedSeconds) {
console.warn("🚨 CLIENT-SIDE: Time limit reached!");
console.log(`Total worked: ${Math.floor(totalTimeWorked / 60)}m vs Limit: ${Math.floor(dedicatedSeconds / 60)}m`);
// Clear timer first to prevent multiple triggers
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// Trigger auto-stop
handleLimitExceeded();
}
}, 1000);
} catch (error) { } catch (error) {
console.error(" Failed to sync project start:", error); console.error("❌ Failed to sync project start:", error);
alert("Something went wrong!");
await window.electron.toggleCloseBtn(true);
} finally {
setLoadingStart(false);
} }
}; };
// Pause/Resume tracking with pause duration tracking
const handlePauseResume = () => { const handlePauseResume = () => {
if (!selectedProject) return; if (!selectedProject) return;
// Implement pause/resume logic if needed
const currentProjectData = projectTrackingMap.get(selectedProject.projectId);
if (!currentProjectData || !currentProjectData.isTracking) return;
const now = new Date();
const newPausedState = !currentProjectData.isPaused;
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.projectId);
if (!current) return updated;
if (newPausedState) {
current.isPaused = true;
current.pauseStartTime = now;
} else {
current.isPaused = false;
if (current.pauseStartTime) {
const pauseDuration = Math.floor(
(now.getTime() - current.pauseStartTime.getTime()) / 1000
);
current.totalPausedTime = (current.totalPausedTime || 0) + pauseDuration;
current.pauseStartTime = null;
}
}
updated.set(selectedProject.projectId, { ...current });
setIsPaused(newPausedState);
return updated;
});
};
// Stop tracking - PRESERVES elapsed time
const handleStopTracking = async () => {
if (!selectedProject) return;
console.log("🛑 Stop tracking initiated...");
// STEP 1: Send final PATCH first
if (finalizeTracking) {
try {
await finalizeTracking();
console.log(" Final tracking data sent");
} catch (error) {
console.error("❌ Error sending final tracking data:", error);
}
}
// STEP 2: Clear timer
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// STEP 3: Send LOGOUT request
try {
const payload = {
vaId: projectSummary.vaId,
projectId: selectedProject.projectId,
type: "LOGOUT",
};
console.log("📡 Sending stop tracking payload:", payload);
const res = await window.electron.stopProjectTracking(payload);
console.log(" Synced stop tracking:", res);
} catch (error) {
console.error("❌ Failed to sync project stop:", error);
}
// STEP 4: Update UI and state
const now = new Date();
await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.projectId);
if (!current) return updated;
current.accumulatedTime = current.elapsedTime;
current.isTracking = false;
current.isPaused = false;
current.endTime = now;
current.startTime = null;
updated.set(selectedProject.projectId, { ...current });
setIsTracking(false);
setIsPaused(false);
setEndTime(now);
setElapsedTime(current.elapsedTime);
return updated;
});
console.log(" Tracking stopped successfully. Time preserved:", elapsedTime);
}; };
// Cleanup timer on unmount // Cleanup timer on unmount
@ -487,58 +422,24 @@ const DashboardLayout = () => {
}; };
}, []); }, []);
// Handle project selection
const handleProjectSelect = (projectId: string) => { const handleProjectSelect = (projectId: string) => {
if (isTracking) return; // prevent switching while tracking const project = projectsData.find((p: any) => p.projectId === projectId);
const project = projectsData.find((p) => p.projectId === projectId);
if (!project) return; if (!project) return;
setSelectedProject(project);
dispatch(setSelectedProjectSlice(project)); dispatch(setSelectedProjectSlice(project));
const existingData = projectTrackingMap.get(projectId); const existing = projectTrackingMap[projectId];
setProjectTrackingMap((prev) => { if (!existing) {
const updated = new Map(prev); dispatch(initializeProjectTracking({
projectId,
// If project is not yet tracked, initialize it with totalWorkMs initialTime: Math.floor((project.totalWorkMs || 0) / 1000)
if (!existingData) { }));
const initialElapsedTime = Math.floor((project.totalWorkMs || 0) / 1000); }
updated.set(projectId, {
elapsedTime: initialElapsedTime,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: initialElapsedTime,
});
// Update component-level state
setElapsedTime(initialElapsedTime);
setStartTime(null);
setEndTime(null);
setIsTracking(false);
setIsPaused(false);
} else {
// If already tracked, restore its state
setElapsedTime(existingData.elapsedTime);
setStartTime(existingData.startTime);
setEndTime(existingData.endTime);
setIsTracking(existingData.isTracking);
setIsPaused(existingData.isPaused);
}
return updated;
});
}; };
const trackingState: TrackingState = { const trackingState: TrackingState = {
selectedProject, selectedProject,
projectTrackingMap,
isTracking, isTracking,
isPaused, isPaused,
elapsedTime, elapsedTime,
@ -549,48 +450,37 @@ const DashboardLayout = () => {
handleStopTracking, handleStopTracking,
projectsData, projectsData,
handleProjectSelect, handleProjectSelect,
loadingStart,
loadingStop
}; };
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600); const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60); const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hrs}h ${mins}m`; return `${hrs}h ${mins}m`;
}; };
const toggleWindowSize = useRef(true);
return ( return (
<> <>
{/* Header */} {/* Header */}
{isTracking && {isTracking &&
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30"> <header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
{/* Left Section */} <div className="flex items-center space-x-3">
{/* {!isConnected ? (
<button
onClick={handleConnect}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded"
>
Connect
</button>
) : (
<button
onClick={handleDisconnect}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"
>
Disconnect
</button>
)} */}
<div className="flex items-center space-x-3">
{selectedProject ? ( {selectedProject ? (
isTracking ? ( isTracking ? (
<button <button
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer" className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
onClick={handleStopTracking} onClick={handleStopTracking}
disabled={loadingStop}
title="Stop Tracking" title="Stop Tracking"
> >
<Pause size={16} /> {loadingStop ? (
<span className="loading loading-spinner loading-xs text-white"></span>
) : (
<Pause size={16} />
)}
</button> </button>
) : ( ) : (
<button <button
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer" className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
@ -621,19 +511,15 @@ const DashboardLayout = () => {
)} )}
</div> </div>
{/* Right Section */}
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{/* Timer */}
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm"> <div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
{formatTime(elapsedTime || 0)} {formatTime(elapsedTime || 0)}
</div> </div>
{/* Alert Icon */}
<span className="text-error"> <span className="text-error">
<ShieldBan size={18} /> <ShieldBan size={18} />
</span> </span>
{/* Dropdown */}
<button <button
onClick={() => { onClick={() => {
if (toggleWindowSize.current) { if (toggleWindowSize.current) {
@ -653,13 +539,12 @@ const DashboardLayout = () => {
} }
{/* Layout Body */} {/* Layout Body */}
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : " h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}> <div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : "h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
{/* Sidebar */} {/* Sidebar */}
<aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4"> <aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4">
{menuItems.map((item , index) => ( {menuItems.map((item) => (
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}> <div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}>
<NavLink <NavLink
key={item.label}
to={item.path} to={item.path}
end={item.exact} end={item.exact}
className={({ isActive }) => className={({ isActive }) =>
@ -674,13 +559,15 @@ const DashboardLayout = () => {
</div> </div>
))} ))}
{!isTracking && <button {!isTracking && (
onClick={handleLogout} <button
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right " onClick={handleLogout}
data-tip={"Logout"} className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right"
> data-tip={"Logout"}
<LogOut size={18} /> >
</button>} <LogOut size={18} />
</button>
)}
</aside> </aside>
{/* Main Content */} {/* Main Content */}

View File

@ -2,6 +2,10 @@ import React, { useEffect, useRef, useState } from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react"; import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import { useOutletContext } from "react-router-dom"; import { useOutletContext } from "react-router-dom";
import NoData from "./NoData"; import NoData from "./NoData";
import { useDispatch, useSelector } from "react-redux";
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice";
import { setError, setLoading, setProfile } from "../store/profileSlice";
import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits';
// Updated Project interface to match JSON structure // Updated Project interface to match JSON structure
interface Project { interface Project {
@ -11,6 +15,7 @@ interface Project {
totalWorkTime: string; totalWorkTime: string;
totalWorkMs: number; totalWorkMs: number;
totalLogs: number; totalLogs: number;
status: string
} }
interface ProjectTrackingData { interface ProjectTrackingData {
@ -23,7 +28,7 @@ interface ProjectTrackingData {
interface TrackingState { interface TrackingState {
selectedProject: Project | null; selectedProject: Project | null;
projectTrackingMap: Map<string, ProjectTrackingData>; // projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean; isTracking: boolean;
isPaused: boolean; isPaused: boolean;
elapsedTime: number; elapsedTime: number;
@ -34,6 +39,8 @@ interface TrackingState {
handleStopTracking: () => void; handleStopTracking: () => void;
handleProjectSelect: (projectId: string) => void; handleProjectSelect: (projectId: string) => void;
projectsData: Project[]; projectsData: Project[];
loadingStart: boolean,
loadingStop: boolean
} }
// Helper function // Helper function
@ -79,8 +86,8 @@ const Widget: React.FC<{
// Dashboard Component // Dashboard Component
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const { const {
selectedProject, // selectedProject,
projectTrackingMap, // projectTrackingMap,
isTracking, isTracking,
isPaused, isPaused,
elapsedTime, elapsedTime,
@ -89,7 +96,9 @@ const Dashboard: React.FC = () => {
handlePauseResume, handlePauseResume,
handleStopTracking, handleStopTracking,
handleProjectSelect, handleProjectSelect,
projectsData, // projectsData,
loadingStart,
loadingStop
} = useOutletContext<TrackingState>(); } = useOutletContext<TrackingState>();
// interface AppsListInt { // interface AppsListInt {
@ -100,10 +109,112 @@ const Dashboard: React.FC = () => {
// const [runningApps, setRunningApps] = useState<AppsListInt[]>([]); // const [runningApps, setRunningApps] = useState<AppsListInt[]>([]);
const projectsData = useSelector((state: any) => state.projects.projectsData);
const selectedProject = useSelector((state: any) => state.projects.selectedProject);
const projectSummary = useSelector((state: any) => state.projects.projectSummary);
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
const dispatch = useDispatch();
const fetchProfile = async () => {
try {
dispatch(setLoading(true));
const res = await window.electron.getProfile();
console.log("Profile", res)
if (res.success && res.data) {
dispatch(setProfile(res.data));
} else {
dispatch(setError("Failed to fetch profile"));
}
} catch (err: any) {
dispatch(setError(err.message));
} finally {
dispatch(setLoading(false));
}
};
useEffect(() => {
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList();
console.log(" Project list response in dashboard:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
// setProjectsData(res.data);
// Store project summary info
// setProjectSummary({
// totalProjects: res.totalProjects || 0,
// activeProjects: res.activeProjects || 0,
// awayProjects: res.awayProjects || 0,
// totalWorkTime: res.totalWorkTime || "0h 0m 0s",
// totalWorkMs: res.totalWorkMs || 0,
// vaName: res.vaName || "",
// vaId: res.vaId || "",
// });
// Store data redux store
dispatch(setProjectsDataSlice(res.data));
dispatch(
setProjectSummarySlice({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
})
);
// Auto-select first project
if (res.data.length > 0 && !selectedProject) {
const firstProject = res.data[0];
// Select the project
dispatch(setSelectedProjectSlice(firstProject));
// Convert total work ms → seconds
const initialElapsed = Math.floor((firstProject.totalWorkMs || 0) / 1000);
// Initialize tracking data for first project (your slice matches this)
dispatch(
initializeProjectTracking({
projectId: firstProject.projectId,
initialTime: initialElapsed,
})
);
}
fetchProfile()
} else {
console.warn("⚠️ Unexpected response:", res);
// if (res?.status) {
// // handleLogout()
// }
}
} catch (err) {
console.error("❌ Error fetching projects:", err);
}
};
fetchProjects();
}, []);
const selectedData = selectedProject const selectedData = selectedProject
? projectTrackingMap.get(selectedProject.projectId) ? projectTrackingMap[selectedProject.projectId]
: null; : null;
const formatWorkTime = (time: string) => { const formatWorkTime = (time: string) => {
if (!time) return "0h 0m"; if (!time) return "0h 0m";
@ -115,49 +226,40 @@ const Dashboard: React.FC = () => {
}; };
const handleProjectSelectLocal = (projectId: string) => {
const project = projectsData.find((p: any) => p.projectId === projectId);
if (!project) return;
dispatch(setSelectedProjectSlice(project));
const existingData = projectTrackingMap[projectId];
if (!existingData) {
const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000);
dispatch(initializeProjectTracking({
projectId,
initialTime: initialElapsed
}));
} else {
// Restore saved tracking state
dispatch(updateProjectTracking({
projectId,
data: existingData
}));
}
};
return ( return (
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen"> <main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
{/* Header */} {/* Header */}
<div className="mb-2"> <div className="mb-2">
<h1 className="text-md font-bold">Dashboard</h1> <h1 className="text-md font-bold">Dashboard</h1>
{/* <button
onClick={async () => {
await window.electron.uploadScreenshot();
}}
className="btn btn-xs btn-outline mt-2"
>
Screenshot Test
</button> */}
</div> </div>
{/* Widgets */}
{/* <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Widget
title="Logged Hours Today"
value="6h 24m"
icon={<Clock size={24} />}
color="primary"
/>
<Widget
title="Logged Hours This Week"
value="4h 48m"
icon={<TrendingUp size={24} />}
color="success"
/>
<Widget
title="Active Project"
value={selectedProject ? selectedProject.projectName : "—"}
icon={<ProjectorIcon size={24} />}
color="warning"
/>
</div> */}
{/* Tracking Section */} {/* Tracking Section */}
<div className="w-full mb-4"> <div className="w-full mb-4">
{/* <div className="flex justify-between items-center mb-2">
<span className="text-md font-bold">Tracking</span>
</div> */}
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm"> <div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body"> <div className="card-body">
{selectedProject ? ( {selectedProject ? (
@ -183,60 +285,33 @@ const Dashboard: React.FC = () => {
<button <button
onClick={() => handleStartTracking(selectedProject)} onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto" className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
disabled={loadingStart}
> >
<Play className="w-3 h-3" /> Start Tracking {loadingStart ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
<Play className="w-3 h-3" />
)}
{loadingStart ? "Starting..." : "Start Tracking"}
</button> </button>
) : ( ) : (
<button <button
className="btn btn-xs btn-error rounded-full" className="btn btn-xs btn-error rounded-full"
onClick={handleStopTracking} onClick={handleStopTracking}
disabled={loadingStop}
> >
<Square className="w-3 h-3" /> {loadingStop ? (
Stop Tracking <span className="loading loading-spinner loading-xs"></span>
) : (
<Square className="w-3 h-3" />
)}
{loadingStop ? "Stopping..." : "Stop Tracking"}
</button> </button>
)} )}
</div> </div>
<div className="divider my-1" /> <div className="divider my-1" />
{/* <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
<div>
<span className="text-base-content/70">Status:</span>{" "}
<span
className={`badge ${selectedProject.activityStatus === "ACTIVE"
? "badge-success"
: "badge-warning"
}`}
>
{selectedProject.activityStatus}
</span>
</div>
<div>
<span className="text-base-content/70">Tracking:</span>{" "}
<span
className={`badge ${selectedData?.isPaused
? "badge-warning"
: selectedData?.isTracking
? "badge-success"
: "badge-neutral"
}`}
>
{selectedData?.isPaused
? "Paused"
: selectedData?.isTracking
? "Active"
: "Ready"}
</span>
</div>
<div>
<span className="text-base-content/70">Start:</span>{" "}
<span>
{selectedData?.startTime
? selectedData.startTime.toLocaleTimeString()
: "—"}
</span>
</div>
</div> */}
</> </>
) : ( ) : (
<p className="text-sm text-base-content/60 text-center"> <p className="text-sm text-base-content/60 text-center">
@ -257,7 +332,7 @@ const Dashboard: React.FC = () => {
{/* 🔹 Table or Empty State */} {/* 🔹 Table or Empty State */}
{projectsData.length > 0 ? ( {projectsData.length > 0 ? (
<div <div
className="overflow-y-auto" className="overflow-y-auto no-scrollbar"
style={{ style={{
maxHeight: "50vh", // scroll only after this height maxHeight: "50vh", // scroll only after this height
minHeight: "auto", // auto adjusts if few rows minHeight: "auto", // auto adjusts if few rows
@ -268,32 +343,53 @@ const Dashboard: React.FC = () => {
<tr> <tr>
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
{/* <th>Logs Hours</th> */} <th>Logs Hours</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{projectsData.map((project, index) => { {projectsData.map((project: any, index: any) => {
const isCurrentlySelected = const isCurrentlySelected =
selectedProject?.projectId === project.projectId; selectedProject?.projectId === project.projectId;
const isDisabled = isTracking && !isCurrentlySelected; const isDisabled = isTracking && !isCurrentlySelected;
const limitReached = isProjectLimitReached(project);
const remainingTime = getFormattedRemainingTime(project);
return ( return (
<tr key={project.projectId}> <tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
<td className="text-xs opacity-70">{index + 1}</td> <td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium">{project.projectName}</td> <td className="font-medium"><div className="flex flex-col">
{/* <td className="font-mono text-xs"> <span className="font-medium">{project.projectName}</span>
{formatWorkTime(project.totalWorkTime)} {limitReached && (
</td> */} <span className="text-xs text-error">Daily limit reached</span>
)}
</div></td>
<td className="font-mono text-xs">
<div className="flex flex-col">
<span className="font-mono text-xs">
{formatWorkTime(project.totalWorkTime)}
</span>
<span className="text-xs opacity-60">
of {project.dedicatedHours}
</span>
</div>
</td>
<td> <td>
<button <div className="flex flex-col gap-1">
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary" <button
}`} className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
onClick={() => handleProjectSelect(project.projectId)} }`}
disabled={isDisabled} onClick={() => handleProjectSelectLocal(project.projectId)}
> disabled={isDisabled || limitReached}
{isCurrentlySelected ? "Selected" : "Select"} >
</button> {isCurrentlySelected ? "Selected" : "Select"}
</button>
{!limitReached && (
<span className="text-xs text-success">
{remainingTime} left
</span>
)}
</div>
</td> </td>
</tr> </tr>
); );

View File

@ -5,6 +5,7 @@ import { setUserSlice } from '../store/userSlice';
import DocVaLogoLight from '../../assets/logo-colored-vertical.svg' import DocVaLogoLight from '../../assets/logo-colored-vertical.svg'
import DocVaLogoDark from '../../assets/logo-white-vertical.svg' import DocVaLogoDark from '../../assets/logo-white-vertical.svg'
import { ThemeToggle } from '../helper/ThemeToggle'; import { ThemeToggle } from '../helper/ThemeToggle';
import { KeyRound, Mail } from 'lucide-react';
const Login = () => { const Login = () => {
const theme = ThemeToggle(); const theme = ThemeToggle();
@ -41,7 +42,8 @@ const Login = () => {
email: response.user.email, email: response.user.email,
})); }));
navigate("/") navigate("/")
} else if (response.success == false) { }
else if (response.success == false) {
console.log(response.error.message) console.log(response.error.message)
setErrorMessage(response.error.message) setErrorMessage(response.error.message)
} }
@ -112,15 +114,8 @@ const Login = () => {
<label className="label"> <label className="label">
<span className="label-text font-medium">Email Address</span> <span className="label-text font-medium">Email Address</span>
</label> </label>
<label className="input input-bordered flex items-center gap-2"> <label className="input input-bordered flex items-center gap-2" tabIndex={0}>
<svg <Mail size={18} />
viewBox="0 0 16 16"
fill="currentColor"
className="w-4 h-4 opacity-70"
>
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<input <input
type="email" type="email"
name="email" name="email"
@ -137,18 +132,8 @@ const Login = () => {
<label className="label"> <label className="label">
<span className="label-text font-medium">Password</span> <span className="label-text font-medium">Password</span>
</label> </label>
<label className="input input-bordered flex items-center gap-2"> <label className="input input-bordered flex items-center gap-2" tabIndex={0}>
<svg <KeyRound size={18} />
viewBox="0 0 16 16"
fill="currentColor"
className="w-4 h-4 opacity-70"
>
<path
fillRule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clipRule="evenodd"
/>
</svg>
<input <input
type="password" type="password"
name="password" name="password"

View File

@ -16,38 +16,45 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showOld, setShowOld] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
alert("New passwords do not match!"); alert("New passwords do not match!");
return; return;
}
setLoading(true);
try {
const payload = {
oldPassword,
password: newPassword,
cPassword: confirmPassword,
};
// 🔗 Call the Electron bridge function
const response = await window.electron.changePassword(payload);
if (response.success) {
alert("Password changed successfully!");
onClose();
} else {
alert(response.message || "Failed to change password.");
} }
} catch (error) {
console.error("Password change error:", error); setLoading(true);
alert("Error changing password."); try {
} finally { const payload = {
setLoading(false); oldPassword,
} password: newPassword,
}; cPassword: confirmPassword,
};
// 🔗 Call the Electron bridge function
const response = await window.electron.changePassword(payload);
if (response.success) {
alert("Password changed successfully!");
setOldPassword("")
setNewPassword("")
setConfirmPassword("")
onClose();
} else {
alert(response.message || "Failed to change password.");
}
} catch (error) {
console.error("Password change error:", error);
alert("Error changing password.");
} finally {
setLoading(false);
}
};
if (!isOpen) return null; if (!isOpen) return null;
@ -60,21 +67,31 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
{/* Old Password */} {/* Old Password */}
<div> <div>
<label className="label text-xs font-medium">Current Password</label> <label className="label text-xs font-medium">Current Password</label>
<input <div className="relative">
type={showPassword ? "text" : "password"} <input
className="input input-bordered w-full" type={showOld ? "text" : "password"}
value={oldPassword} className="input input-bordered w-full pr-10"
onChange={(e) => setOldPassword(e.target.value)} value={oldPassword}
required onChange={(e) => setOldPassword(e.target.value)}
/> required
/>
<button
type="button"
className="absolute right-2 top-3 text-gray-500"
onClick={() => setShowOld(!showOld)}
>
{showOld ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div> </div>
{/* New Password */} {/* New Password */}
<div> <div>
<label className="label text-xs font-medium">New Password</label> <label className="label text-xs font-medium">New Password</label>
<div className="relative"> <div className="relative">
<input <input
type={showPassword ? "text" : "password"} type={showNew ? "text" : "password"}
className="input input-bordered w-full pr-10" className="input input-bordered w-full pr-10"
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
@ -82,26 +99,37 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
/> />
<button <button
type="button" type="button"
className="absolute right-2 top-2 text-gray-500" className="absolute right-2 top-3 text-gray-500"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowNew(!showNew)}
> >
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />} {showNew ? <EyeOff size={16} /> : <Eye size={16} />}
</button> </button>
</div> </div>
</div> </div>
{/* Confirm Password */} {/* Confirm Password */}
<div> <div>
<label className="label text-xs font-medium">Confirm New Password</label> <label className="label text-xs font-medium">Confirm New Password</label>
<input <div className="relative">
type={showPassword ? "text" : "password"} <input
className="input input-bordered w-full" type={showConfirm ? "text" : "password"}
value={confirmPassword} className="input input-bordered w-full pr-10"
onChange={(e) => setConfirmPassword(e.target.value)} value={confirmPassword}
required onChange={(e) => setConfirmPassword(e.target.value)}
/> required
/>
<button
type="button"
className="absolute right-2 top-3 text-gray-500"
onClick={() => setShowConfirm(!showConfirm)}
>
{showConfirm ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div> </div>
<div className="modal-action"> <div className="modal-action">
<button <button
type="button" type="button"
@ -121,9 +149,14 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
</form> </form>
</div> </div>
<form method="dialog" className="modal-backdrop" onClick={onClose}> <form
<button>close</button> method="dialog"
className="modal-backdrop"
onClick={!loading ? onClose : undefined}
>
<button disabled={loading}>close</button>
</form> </form>
</dialog> </dialog>
); );
}; };

50
src/ui/pages/Vpn/Vpn.tsx Normal file
View File

@ -0,0 +1,50 @@
import React from 'react'
const Vpn = () => {
return (
<>
<div className="max-h-screen">
<div className="w-full mx-auto">
{/* Header */}
<div className=" ">
<h1 className="text-mb font-bold mb-2">VPN Servers</h1>
</div>
<div className="overflow-x-auto rounded-box border border-base-content/5 bg-base-100 text-sm">
<table className="table">
{/* head */}
<thead>
<tr>
<th>#</th>
<th>Server name</th>
<th>Connect</th>
</tr>
</thead>
<tbody>
{/* row 1 */}
<tr>
<th>1</th>
<td>Los Angeles [Fixed]</td>
<td><button className="btn btn-xs btn-secondary">Connect</button></td>
</tr>
{/* row 2 */}
<tr>
<th>2</th>
<td>Dallas 1</td>
<td><button className="btn btn-xs btn-secondary">Connect</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</>
)
}
export default Vpn

View File

@ -15,17 +15,29 @@ interface ProjectSummary {
vaName: string; vaName: string;
vaId: string; vaId: string;
} }
interface TrackingData {
elapsedTime: number;
startTime: number | null;
endTime: number | null;
isTracking: boolean;
isPaused: boolean;
pauseStartTime: number | null;
totalPausedTime: number;
accumulatedTime: number;
}
interface ProjectsState { interface ProjectsState {
projectsData: Project[]; projectsData: Project[];
projectSummary: ProjectSummary | null; projectSummary: ProjectSummary | null;
selectedProject: Project | null; selectedProject: Project | null;
projectTrackingMap: Record<string, TrackingData>;
} }
const initialState: ProjectsState = { const initialState: ProjectsState = {
projectsData: [], projectsData: [],
projectSummary: null, projectSummary: null,
selectedProject: null, selectedProject: null,
projectTrackingMap: {},
}; };
const projectsSlice = createSlice({ const projectsSlice = createSlice({
@ -41,10 +53,84 @@ const projectsSlice = createSlice({
setSelectedProjectSlice(state, action: PayloadAction<Project | null>) { setSelectedProjectSlice(state, action: PayloadAction<Project | null>) {
state.selectedProject = action.payload; state.selectedProject = action.payload;
}, },
initializeProjectTracking(state, action: PayloadAction<{ projectId: string; initialTime: number }>) {
const { projectId, initialTime } = action.payload;
if (!state.projectTrackingMap[projectId]) {
state.projectTrackingMap[projectId] = {
elapsedTime: initialTime,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: initialTime,
};
}
},
updateProjectTracking(state, action: PayloadAction<{ projectId: string; data: Partial<TrackingData> }>) {
const { projectId, data } = action.payload;
if (state.projectTrackingMap[projectId]) {
state.projectTrackingMap[projectId] = {
...state.projectTrackingMap[projectId],
...data,
};
}
},
startTracking(state, action: PayloadAction<{ projectId: string; startTime: number }>) {
const { projectId, startTime } = action.payload;
const project = state.projectTrackingMap[projectId];
if (project) {
project.startTime = startTime;
project.isTracking = true;
project.isPaused = false;
project.pauseStartTime = null;
project.totalPausedTime = 0;
}
},
updateElapsedTime(state, action: PayloadAction<{ projectId: string; elapsedTime: number }>) {
const { projectId, elapsedTime } = action.payload;
const project = state.projectTrackingMap[projectId];
if (project) {
project.elapsedTime = elapsedTime;
project.accumulatedTime = elapsedTime;
}
},
stopTracking(state, action: PayloadAction<{ projectId: string }>) {
const { projectId } = action.payload;
const project = state.projectTrackingMap[projectId];
if (project) {
project.isTracking = false;
project.startTime = null;
}
},
stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) {
const p = state.projectTrackingMap[action.payload.projectId];
if (p) {
p.accumulatedTime = p.elapsedTime;
p.isTracking = false;
p.isPaused = false;
p.startTime = null;
p.pauseStartTime = null;
p.endTime = Date.now();
}
}
}, },
}); });
export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } = export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, initializeProjectTracking,
updateProjectTracking,startTracking , updateElapsedTime , stopTracking , stopTrackingReducer} =
projectsSlice.actions; projectsSlice.actions;
export default projectsSlice.reducer; export default projectsSlice.reducer;

View File

@ -0,0 +1,138 @@
// utils/projectLimits.ts
interface Project {
projectId: string;
projectName: string;
totalWorkTime: string;
totalWorkMs: number;
dedicatedHours: string;
[key: string]: any;
}
/**
* Parse time string (e.g., "10h", "30m", "2h 15m", "0h 5m 30s") to total minutes
*/
export const parseTimeToMinutes = (timeStr: string): number => {
if (!timeStr) return 0;
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;
};
/**
* Parse time string to total seconds
*/
export const parseTimeToSeconds = (timeStr: string): number => {
if (!timeStr) return 0;
const hourMatch = timeStr.match(/(\d+)h/);
const minMatch = timeStr.match(/(\d+)m/);
const secMatch = timeStr.match(/(\d+)s/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
const seconds = secMatch ? parseInt(secMatch[1]) : 0;
return (hours * 3600) + (minutes * 60) + seconds;
};
/**
* Format minutes to readable time string
*/
export const formatMinutesToTime = (minutes: number): string => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0) {
return `${hours}h ${mins}m`;
}
return `${mins}m`;
};
/**
* Format seconds to readable time string
*/
export const formatSecondsToTime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
}
return `${secs}s`;
};
/**
* Check if project has reached its daily limit
*/
export const isProjectLimitReached = (project: Project): boolean => {
const totalWorkedSeconds = parseTimeToSeconds(project.totalWorkTime);
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
return totalWorkedSeconds >= dedicatedSeconds;
};
/**
* Get remaining time for a project in seconds
*/
export const getRemainingTimeInSeconds = (project: Project): number => {
const totalWorkedSeconds = parseTimeToSeconds(project.totalWorkTime);
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
const remaining = dedicatedSeconds - totalWorkedSeconds;
return Math.max(0, remaining); // Never return negative
};
/**
* Get remaining time for a project (legacy - in minutes)
*/
export const getRemainingTime = (project: Project): number => {
const totalWorked = parseTimeToMinutes(project.totalWorkTime);
const dedicatedLimit = parseTimeToMinutes(project.dedicatedHours);
const remaining = dedicatedLimit - totalWorked;
return Math.max(0, remaining); // Never return negative
};
/**
* Get formatted remaining time string
*/
export const getFormattedRemainingTime = (project: Project): string => {
const remainingSeconds = getRemainingTimeInSeconds(project);
return formatSecondsToTime(remainingSeconds);
};
/**
* Check if project can start tracking
*/
export const canStartTracking = (project: Project): {
canStart: boolean;
reason?: string;
remaining?: string;
remainingSeconds?: number;
} => {
if (isProjectLimitReached(project)) {
return {
canStart: false,
reason: "Daily limit reached for this project",
remaining: "0m",
remainingSeconds: 0
};
}
const remainingSeconds = getRemainingTimeInSeconds(project);
return {
canStart: true,
remaining: getFormattedRemainingTime(project),
remainingSeconds
};
};