Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker
This commit is contained in:
commit
558ea81fc2
|
|
@ -41,7 +41,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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -150,4 +247,4 @@ export const useActiveAppTracker = (
|
||||||
return {
|
return {
|
||||||
finalizeTracking: finalizeTrackingRef.current,
|
finalizeTracking: finalizeTrackingRef.current,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -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,399 +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)
|
|
||||||
|
|
||||||
handleConnect();
|
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);
|
|
||||||
handleDisconnect();
|
|
||||||
} 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
|
||||||
|
|
@ -491,145 +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 handleConnect = async () => {
|
|
||||||
|
|
||||||
const token = await window.electron.getToken("ACCESS_TOKEN");
|
|
||||||
|
|
||||||
if (socketRef.current) {
|
|
||||||
console.warn("Already connected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedProject?.projectId) return
|
|
||||||
|
|
||||||
console.log(selectedProject?.projectId, "SelectedProject ID")
|
|
||||||
socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", {
|
|
||||||
transports: ["websocket"],
|
|
||||||
secure: true,
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
reconnection: true,
|
|
||||||
withCredentials: true,
|
|
||||||
forceNew: true,
|
|
||||||
extraHeaders: {
|
|
||||||
"Access-Control-Allow-Origin": "*"
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
token: token,
|
|
||||||
projectId: selectedProject?.projectId,
|
|
||||||
userType: "va"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current.on("connect", () => {
|
|
||||||
console.log("🟢 Connected:", socketRef.current?.id);
|
|
||||||
setIsConnected(true);
|
|
||||||
|
|
||||||
// Start sending every 5 seconds
|
|
||||||
intervalRef.current = setInterval(async () => {
|
|
||||||
const apps = await window.electron.getRunningApps()
|
|
||||||
socketRef.current?.emit("getVisibleApps", { token: await token, apps });
|
|
||||||
|
|
||||||
// const base64String = await window.electron.captureAppScreenShot("Screen 1");
|
|
||||||
// if (base64String) {
|
|
||||||
// socketRef.current?.emit("sendImage", {token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5MGIzMjhkMDkxMDFmNzRjZjUyNGMwNyIsImVtYWlsIjoiYmhhZ3dhbi5yQGhpY2dsb2JhbHNvbHV0aW9ucy5jb20iLCJuYW1lIjoiUm9oaXQiLCJpYXQiOjE3NjI4NTgxOTMsImV4cCI6MTc2Mjk0NDU5M30.UlHy3RU9W5LOid7smG7WzyNT7dnqhQT4qqzRrN1axmU", image: base64String, title: "Screen 1"});
|
|
||||||
// }
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current.on("appToCapture", async ({ appTitle }) => {
|
|
||||||
console.log("📸 Capture requested for:", appTitle);
|
|
||||||
|
|
||||||
const base64String = await window.electron.captureAppScreenShot(appTitle);
|
|
||||||
if (base64String && socketRef.current) {
|
|
||||||
socketRef.current.emit("sendImage", {
|
|
||||||
token: token,
|
|
||||||
image: base64String,
|
|
||||||
title: appTitle,
|
|
||||||
projectId: selectedProject?.projectId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socketRef.current.on("disconnect", () => {
|
|
||||||
console.log("🔴 Disconnected");
|
|
||||||
setIsConnected(false);
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketRef.current) {
|
|
||||||
socketRef.current.disconnect();
|
|
||||||
socketRef.current = null;
|
|
||||||
setIsConnected(false);
|
|
||||||
console.log("🔌 Disconnected manually");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const trackingState: TrackingState = {
|
const trackingState: TrackingState = {
|
||||||
selectedProject,
|
selectedProject,
|
||||||
projectTrackingMap,
|
|
||||||
isTracking,
|
isTracking,
|
||||||
isPaused,
|
isPaused,
|
||||||
elapsedTime,
|
elapsedTime,
|
||||||
|
|
@ -640,48 +450,39 @@ 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;
|
const secs = seconds % 60;
|
||||||
return `${hrs}h ${mins}m`;
|
return `${hrs}h ${mins}m ${secs}s`;
|
||||||
|
// 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 */}
|
|
||||||
{/* {!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">
|
<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"
|
||||||
|
|
@ -712,19 +513,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) {
|
||||||
|
|
@ -744,13 +541,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 }) =>
|
||||||
|
|
@ -765,13 +561,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 */}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -41,8 +48,8 @@ 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;
|
const secs = seconds % 60;
|
||||||
// return `${hrs}h ${mins}m ${secs}s`;
|
return `${hrs}h ${mins}m ${secs}s`;
|
||||||
return `${hrs}h ${mins}m `;
|
// return `${hrs}h ${mins}m `;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Widget Component
|
// Widget Component
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -535,7 +535,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className=" ">
|
<div className=" ">
|
||||||
<h1 className="text-mb font-bold mb-2">Adjust Timesheet</h1>
|
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* General Error Alert */}
|
{/* General Error Alert */}
|
||||||
|
|
@ -545,6 +545,9 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<div className="rounded-lg bg-base-100 overflow-hidden">
|
<div className="rounded-lg bg-base-100 overflow-hidden">
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue