930 lines
30 KiB
TypeScript
930 lines
30 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
import io, { Socket } from 'socket.io-client';
|
|
|
|
import {
|
|
Outlet,
|
|
NavLink,
|
|
useNavigate,
|
|
useLocation,
|
|
} from "react-router-dom";
|
|
import {
|
|
Bell,
|
|
Menu,
|
|
LogOut,
|
|
User,
|
|
Settings,
|
|
Home,
|
|
FolderKanban,
|
|
X,
|
|
CalendarDays,
|
|
SettingsIcon,
|
|
TimerIcon,
|
|
HelpCircleIcon,
|
|
Pause,
|
|
ShieldBan,
|
|
ChevronDown,
|
|
Play,
|
|
ChevronUp,
|
|
GlobeLock,
|
|
ClipboardClock,
|
|
CircleDollarSign,
|
|
} from "lucide-react";
|
|
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
import { RootState } from "../store";
|
|
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime, updateProjectTracking } from "../store/projectSlice";
|
|
import { clearUser } from "../store/userSlice";
|
|
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
|
import { canStartTracking } from "../utils/projectLimits";
|
|
import { useTheme } from "../helper/ThemeToggle";
|
|
import LogoutModal from "../modal/LogoutModal";
|
|
import { parseTimeStringToSeconds } from "../utils/timeParser";
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
}
|
|
|
|
interface Project {
|
|
projectId: string;
|
|
projectName: string;
|
|
activityStatus: "ACTIVE" | "AWAY";
|
|
totalWorkTime: string;
|
|
totalWorkMs: number;
|
|
totalLogs: number;
|
|
dedicatedHours: string;
|
|
}
|
|
|
|
interface ProjectTrackingData {
|
|
elapsedTime: number;
|
|
startTime: Date | null;
|
|
endTime: Date | null;
|
|
isTracking: boolean;
|
|
isPaused: boolean;
|
|
pauseStartTime: Date | null;
|
|
totalPausedTime: number;
|
|
accumulatedTime: number;
|
|
}
|
|
|
|
export interface TrackingState {
|
|
selectedProject: Project | null;
|
|
isTracking: boolean;
|
|
isPaused: boolean;
|
|
elapsedTime: number;
|
|
startTime: Date | null;
|
|
endTime: Date | null;
|
|
handleStartTracking: (project: Project) => void;
|
|
handlePauseResume: () => void;
|
|
handleStopTracking: () => void;
|
|
handleProjectSelect: (projectId: string) => void;
|
|
projectsData: Project[];
|
|
loadingStart: boolean,
|
|
loadingStop: boolean,
|
|
isStopping: boolean
|
|
}
|
|
|
|
const DashboardLayout = () => {
|
|
const { theme, setTheme } = useTheme();
|
|
|
|
const toggleTheme = () => {
|
|
setTheme(theme === "dark" ? "light" : "dark");
|
|
};
|
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const dispatch = useDispatch();
|
|
const [isStopping, setIsStopping] = useState(false);
|
|
|
|
const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
|
|
useSelector((state: any) => state.projects);
|
|
|
|
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 = [
|
|
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
|
|
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
|
|
{ label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
|
|
{ label: "Schedule", path: "/dashboard/schedule", icon: <ClipboardClock size={18} /> },
|
|
{ label: "Pays Slips", path: "/dashboard/pays-slips", icon: <CircleDollarSign 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} /> },
|
|
];
|
|
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const [startTime, setStartTime] = useState<Date | null>(null);
|
|
const [endTime, setEndTime] = useState<Date | null>(null);
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
const [activeProjectLogId, setActiveProjectLogId] = useState("");
|
|
const toggleWindowSize = useRef(true);
|
|
const [loadingStart, setLoadingStart] = useState(false);
|
|
const [loadingStop, setLoadingStop] = useState(false);
|
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
|
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute("data-theme", theme);
|
|
}, [theme]);
|
|
|
|
// Create a ref to store the stop function so it can be called from the hook
|
|
const stopTrackingRef = useRef<(() => Promise<void>) | null>(null);
|
|
const limitAlertShownRef = useRef(false);
|
|
|
|
// NOW define handleLimitExceeded using the defined handleStopTracking
|
|
const handleLimitExceeded = useCallback(async () => {
|
|
console.log("🚨 LIMIT EXCEEDED - Showing alert (not stopping)");
|
|
|
|
if (!selectedProject) {
|
|
console.error("❌ No selected project for limit exceeded");
|
|
return;
|
|
}
|
|
|
|
// Only show alert once per tracking session
|
|
if (limitAlertShownRef.current) {
|
|
console.log("⚠️ Alert already shown, skipping");
|
|
return;
|
|
}
|
|
|
|
limitAlertShownRef.current = true;
|
|
|
|
console.log("⏰ Showing limit exceeded alert");
|
|
|
|
// Show alert to user
|
|
alert(
|
|
`⏰ Daily Limit Reached!\n\n` +
|
|
`Project: ${selectedProject.projectName}\n` +
|
|
`Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` +
|
|
`You've reached the daily time limit for this project.\n` +
|
|
`Timer will continue running. Please stop manually when ready.`
|
|
);
|
|
|
|
|
|
|
|
console.log("✅ Alert shown, timer continues running");
|
|
}, [selectedProject]);
|
|
|
|
// Initialize useActiveAppTracker and store finalizeTracking in ref
|
|
const { finalizeTracking, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker(
|
|
isTracking,
|
|
activeProjectLogId,
|
|
selectedProject?.projectId ?? null,
|
|
selectedProject?.projectName ?? null,
|
|
handleLimitExceeded
|
|
);
|
|
|
|
// Define handleStopTracking FIRST
|
|
const handleStopTracking = useCallback(async () => {
|
|
if (loadingStop || loadingStart) {
|
|
console.log("⏸️ Already processing, skipping...");
|
|
return;
|
|
}
|
|
|
|
if (!selectedProject) {
|
|
console.warn("⚠️ No selected project to stop");
|
|
return;
|
|
}
|
|
|
|
console.log("=".repeat(70));
|
|
console.log("🛑 STOP TRACKING INITIATED");
|
|
console.log("=".repeat(70));
|
|
console.log(`📦 Project: ${selectedProject.projectName}`);
|
|
console.log(`⏱️ Elapsed Time: ${elapsedTime}s`);
|
|
|
|
// Reset alert flag when stopping
|
|
limitAlertShownRef.current = false;
|
|
|
|
const currentStopTime = new Date().toISOString();
|
|
const projectId = selectedProject.projectId;
|
|
|
|
// ✅ CRITICAL: Set stopping flag IMMEDIATELY to block all new tab tracking
|
|
console.log("🔒 Setting stopping flag...");
|
|
if (setIsStoppingInTracker) {
|
|
setIsStoppingInTracker(true);
|
|
}
|
|
|
|
setIsStopping(true);
|
|
setLoadingStop(true);
|
|
handleSocketDisconnect();
|
|
|
|
// STEP 1: Clear local timer to prevent further client-side updates
|
|
console.log("⏹️ STEP 1: Clearing local timer...");
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
console.log("✅ Timer cleared");
|
|
}
|
|
|
|
// STEP 2: Send final PATCH with last active window data
|
|
console.log("📤 STEP 2: Sending final activity tracking...");
|
|
if (stopTrackingRef.current) {
|
|
try {
|
|
await stopTrackingRef.current(); // This calls sendFinalPatch() in the hook
|
|
console.log("✅ Final activity tracking completed");
|
|
} catch (error) {
|
|
console.error("❌ Error during final tracking:", error);
|
|
}
|
|
} else {
|
|
console.log("⚠️ No tracking ref available");
|
|
}
|
|
|
|
// STEP 3: Send LOGOUT to backend
|
|
console.log("🚪 STEP 3: Sending LOGOUT request...");
|
|
try {
|
|
const payload = {
|
|
vaId: projectSummary.vaId,
|
|
projectId: projectId,
|
|
type: "LOGOUT",
|
|
time: currentStopTime
|
|
};
|
|
|
|
console.log("📡 Logout payload:", payload);
|
|
const res = await window.electron.stopProjectTracking(payload);
|
|
console.log("✅ LOGOUT successful:", res);
|
|
} catch (error) {
|
|
console.error("❌ LOGOUT failed:", error);
|
|
}
|
|
|
|
// STEP 4: Update Redux tracking state
|
|
console.log("🔄 STEP 4: Updating Redux state...");
|
|
dispatch(
|
|
stopTrackingReducer({
|
|
projectId,
|
|
})
|
|
);
|
|
|
|
// STEP 6: Update UI (restore window)
|
|
console.log("🪟 STEP 6: Restoring window...");
|
|
await window.electron.toggleCloseBtn(true);
|
|
await window.electron.resizeWindow(500, 600);
|
|
toggleWindowSize.current = true;
|
|
console.log("✅ Window restored");
|
|
|
|
// Allow React to process the state update
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
// STEP 5: Refresh project list to get updated work hours from server
|
|
console.log("📊 STEP 5: Refreshing project list...");
|
|
try {
|
|
const projectListRes = await window.electron.getProjectList();
|
|
console.log("✅ Project list fetched:", projectListRes);
|
|
|
|
if (projectListRes?.success && Array.isArray(projectListRes.data)) {
|
|
// Update projects data
|
|
dispatch(setProjectsDataSlice(projectListRes.data));
|
|
|
|
// Update project summary
|
|
dispatch(
|
|
setProjectSummarySlice({
|
|
totalProjects: projectListRes.totalProjects || 0,
|
|
activeProjects: projectListRes.activeProjects || 0,
|
|
awayProjects: projectListRes.awayProjects || 0,
|
|
totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s",
|
|
totalWorkMs: projectListRes.totalWorkMs || 0,
|
|
vaName: projectListRes.vaName || "",
|
|
vaId: projectListRes.vaId || "",
|
|
})
|
|
);
|
|
|
|
// Update selected project with fresh data from server
|
|
const updatedProject = projectListRes.data.find(
|
|
(p: Project) => p.projectId === projectId
|
|
);
|
|
|
|
if (updatedProject) {
|
|
dispatch(setSelectedProjectSlice(updatedProject));
|
|
|
|
// Sync client time with server time
|
|
const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime);
|
|
|
|
console.log("🔄 Syncing with server time:", {
|
|
serverTime: updatedProject.totalWorkTime,
|
|
serverSeconds: serverElapsedTime,
|
|
clientSeconds: elapsedTime,
|
|
difference: serverElapsedTime - elapsedTime
|
|
});
|
|
|
|
// Force update the tracking map with server time
|
|
dispatch(
|
|
updateProjectTracking({
|
|
projectId: updatedProject.projectId,
|
|
data: {
|
|
elapsedTime: serverElapsedTime,
|
|
accumulatedTime: serverElapsedTime,
|
|
isTracking: false,
|
|
isPaused: false,
|
|
startTime: null,
|
|
endTime: Date.now(),
|
|
pauseStartTime: null,
|
|
totalPausedTime: 0
|
|
}
|
|
})
|
|
);
|
|
|
|
console.log("✅ Project synced with server");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
console.error("❌ Error refreshing project list:", error);
|
|
} finally {
|
|
// Clear loading states
|
|
setLoadingStop(false);
|
|
setIsStopping(false);
|
|
|
|
// ✅ Reset stopping flag for next session
|
|
if (setIsStoppingInTracker) {
|
|
setIsStoppingInTracker(false);
|
|
}
|
|
|
|
console.log("=".repeat(70));
|
|
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
|
|
console.log("=".repeat(70));
|
|
}
|
|
}, [
|
|
selectedProject,
|
|
projectSummary,
|
|
elapsedTime,
|
|
dispatch,
|
|
loadingStop,
|
|
loadingStart,
|
|
setIsStoppingInTracker
|
|
]);
|
|
|
|
// Update the ref whenever finalizeTracking changes
|
|
useEffect(() => {
|
|
stopTrackingRef.current = finalizeTracking;
|
|
}, [finalizeTracking]);
|
|
|
|
const socketRef = useRef<Socket | null>(null);
|
|
const intervalRef = useRef<number | null>(null);
|
|
const lastAppsRef = useRef<Object | null>(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!window.electron?.onTokenExpired) return;
|
|
|
|
console.log("🔐 Token expiry listener registered");
|
|
|
|
window.electron.onTokenExpired(async () => {
|
|
console.log("🔥 Token expired — checking tracking status...");
|
|
|
|
// 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 () => {
|
|
console.log("🧹 Cleaning token-expired listener");
|
|
};
|
|
}, [isTracking]);
|
|
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
const res = await window.electron.logout();
|
|
if (res) {
|
|
dispatch(clearUser());
|
|
navigate("/login");
|
|
}
|
|
} catch (error) {
|
|
console.error("Logout error:", error);
|
|
}
|
|
};
|
|
|
|
const selectedData = selectedProject
|
|
? projectTrackingMap[selectedProject.projectId]
|
|
: null;
|
|
|
|
const handleStartTracking = async (project: Project) => {
|
|
if (loadingStart || loadingStop) return;
|
|
|
|
// 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;
|
|
}
|
|
|
|
console.log(`⏰ Starting tracking. Remaining time: ${limitCheck.remaining}`);
|
|
|
|
limitAlertShownRef.current = false;
|
|
|
|
setLoadingStart(true);
|
|
|
|
const now = Date.now();
|
|
let currentStartTime = new Date().toISOString();
|
|
window.electron.toggleCloseBtn(false);
|
|
|
|
const payload = {
|
|
vaId: projectSummary.vaId,
|
|
projectId: project.projectId,
|
|
type: "LOGIN",
|
|
time: currentStartTime
|
|
};
|
|
|
|
try {
|
|
const res = await window.electron.startProjectTracking(payload);
|
|
|
|
if (!res.success) {
|
|
alert(res.message);
|
|
await window.electron.toggleCloseBtn(true);
|
|
return;
|
|
}
|
|
|
|
// CRITICAL FIX: Get the current accumulated time BEFORE starting timer
|
|
const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime);
|
|
|
|
console.log("📊 Starting with accumulated time:", {
|
|
accumulatedTime: currentAccumulatedTime,
|
|
projectTotalWorkTime: project.totalWorkTime,
|
|
parsedTime: parseTimeStringToSeconds(project.totalWorkTime)
|
|
});
|
|
|
|
// ⬇️ REDUX UPDATE: Start Tracking
|
|
dispatch(
|
|
startTracking({
|
|
projectId: project.projectId,
|
|
startTime: now,
|
|
})
|
|
);
|
|
|
|
handleSocketConnect();
|
|
setActiveProjectLogId(res?.data?.logId);
|
|
console.log(" Tracking started. Log ID:", res?.data?.logId);
|
|
|
|
// ⬇️ Calculate time limit in seconds
|
|
const parseTimeToSeconds = (timeStr: string): number => {
|
|
return parseTimeStringToSeconds(timeStr);
|
|
};
|
|
|
|
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
|
|
const alreadyWorkedSeconds = parseTimeStringToSeconds(project.totalWorkTime);
|
|
const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds;
|
|
|
|
console.log("⏱️ Time Limits:", {
|
|
dedicatedHours: project.dedicatedHours,
|
|
dedicatedSeconds,
|
|
alreadyWorked: `${Math.floor(alreadyWorkedSeconds / 60)}m ${alreadyWorkedSeconds % 60}s`,
|
|
remainingSeconds,
|
|
remainingMinutes: Math.floor(remainingSeconds / 60)
|
|
});
|
|
|
|
// ⬇️ FIXED: Start timer with proper initial time
|
|
// Set initial elapsed time immediately to avoid jump
|
|
dispatch(
|
|
updateElapsedTime({
|
|
projectId: project.projectId,
|
|
elapsedTime: currentAccumulatedTime,
|
|
})
|
|
);
|
|
|
|
// Start global timer loop with limit check
|
|
timerRef.current = setInterval(() => {
|
|
const timeSinceStart = Math.floor((Date.now() - now) / 1000);
|
|
const currentElapsed = currentAccumulatedTime + timeSinceStart;
|
|
|
|
dispatch(
|
|
updateElapsedTime({
|
|
projectId: project.projectId,
|
|
elapsedTime: currentElapsed,
|
|
})
|
|
);
|
|
|
|
// Check if time limit reached
|
|
const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart;
|
|
|
|
if (totalTimeWorked >= dedicatedSeconds && !limitAlertShownRef.current) {
|
|
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();
|
|
handleSocketDisconnect();
|
|
}
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
console.error(" Failed to sync project start:", error);
|
|
alert("Something went wrong!");
|
|
await window.electron.toggleCloseBtn(true);
|
|
} finally {
|
|
setLoadingStart(false);
|
|
}
|
|
};
|
|
|
|
const handlePauseResume = () => {
|
|
if (!selectedProject) return;
|
|
// Implement pause/resume logic if needed
|
|
};
|
|
|
|
// Cleanup timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleProjectSelect = (projectId: string) => {
|
|
const project = projectsData.find((p: any) => p.projectId === projectId);
|
|
if (!project) return;
|
|
|
|
dispatch(setSelectedProjectSlice(project));
|
|
|
|
const existing = projectTrackingMap[projectId];
|
|
|
|
if (!existing) {
|
|
// Use parsed totalWorkTime instead of totalWorkMs
|
|
const initialTime = parseTimeStringToSeconds(project.totalWorkTime);
|
|
dispatch(initializeProjectTracking({
|
|
projectId,
|
|
initialTime: initialTime
|
|
}));
|
|
}
|
|
};
|
|
|
|
|
|
const handleSocketConnect = 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 });
|
|
// }, 5000);
|
|
});
|
|
|
|
// socketRef.current.on("startSendAppsDetails", async ({ apps }) => {
|
|
// socketRef.current?.emit("getVisibleApps", { token: await token, apps });
|
|
// });
|
|
|
|
socketRef.current.on("startSendAppsDetails", async ({ projId }) => {
|
|
if (projId !== selectedProject?.projectId) return;
|
|
console.log("▶️ startSendAppsDetails received");
|
|
|
|
// avoid multiple intervals
|
|
if (intervalRef.current) return;
|
|
|
|
intervalRef.current = setInterval(async () => {
|
|
const currentApps = await window.electron.getRunningApps();
|
|
|
|
// Compare with last sent data
|
|
const hasChanged = JSON.stringify(currentApps) !== JSON.stringify(lastAppsRef.current);
|
|
|
|
if (hasChanged) {
|
|
lastAppsRef.current = currentApps;
|
|
|
|
socketRef.current?.emit("getVisibleApps", {
|
|
token: await token,
|
|
apps: currentApps,
|
|
});
|
|
}
|
|
}, 5000);
|
|
});
|
|
|
|
socketRef.current.on("stopSendAppsDetails", async ({ projId }) => {
|
|
if (projId !== selectedProject?.projectId) return;
|
|
console.log("⏹ stopSendAppsDetails received");
|
|
|
|
// clear the interval
|
|
if (intervalRef.current) {
|
|
clearInterval(intervalRef.current);
|
|
intervalRef.current = null;
|
|
}
|
|
|
|
// Send final stop event to server
|
|
// socketRef.current?.emit("stopSendAppsDetails", {
|
|
// token: await token,
|
|
// });
|
|
});
|
|
|
|
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 handleSocketDisconnect = () => {
|
|
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 = {
|
|
selectedProject,
|
|
isTracking,
|
|
isPaused,
|
|
elapsedTime,
|
|
startTime,
|
|
endTime,
|
|
handleStartTracking,
|
|
handlePauseResume,
|
|
handleStopTracking,
|
|
projectsData,
|
|
handleProjectSelect,
|
|
loadingStart,
|
|
loadingStop,
|
|
isStopping
|
|
};
|
|
|
|
const formatTime = (seconds: number) => {
|
|
const hrs = Math.floor(seconds / 3600);
|
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
const secs = seconds % 60;
|
|
return `${hrs}h ${mins}m ${secs}s`;
|
|
// return `${hrs}h ${mins}m `;
|
|
};
|
|
|
|
|
|
const [checkIp, setCheckIp] = useState(false)
|
|
useEffect(() => {
|
|
setTimeout(() => {
|
|
fetch('https://api.ipify.org/?format=json').then((res) => console.log("IPPPPPPPPPPPPPP", res))
|
|
}, 5000)
|
|
}, [checkIp])
|
|
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col h-screen overflow-hidden">
|
|
{/* <button
|
|
onClick={() => {
|
|
setCheckIp((prev) => !prev)
|
|
window.electron.connectVPN().then((msg) => {
|
|
console.log(msg);
|
|
});
|
|
}}
|
|
className="text-purple-400"
|
|
>
|
|
Connect VPN
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
window.electron.disconnectVPN();
|
|
}}
|
|
className="text-red-400"
|
|
>
|
|
Disconnect VPN
|
|
</button> */}
|
|
|
|
{/* Header */}
|
|
{isTracking &&
|
|
<header className="w-full flex justify-between items-center px-4 py-2 bg-base-100 border-b border-base-300 sticky top-0 z-30">
|
|
<div className="flex items-center space-x-3">
|
|
{isStopping ? (
|
|
// Skeleton for left side during stop
|
|
<div className="flex items-center space-x-3">
|
|
<div className="skeleton w-8 h-8 rounded-full"></div>
|
|
<div className="skeleton w-32 h-4"></div>
|
|
</div>
|
|
) : selectedProject ? (
|
|
isTracking ? (
|
|
<button
|
|
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
|
onClick={handleStopTracking}
|
|
disabled={loadingStop}
|
|
title="Stop Tracking"
|
|
>
|
|
{loadingStop ? (
|
|
<span className="loading loading-spinner loading-xs text-white"></span>
|
|
) : (
|
|
<Pause size={16} />
|
|
)}
|
|
</button>
|
|
) : (
|
|
<button
|
|
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
|
onClick={() => handleStartTracking(selectedProject)}
|
|
title="Start Tracking"
|
|
>
|
|
<Play size={16} />
|
|
</button>
|
|
)
|
|
) : (
|
|
<button
|
|
className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed"
|
|
disabled
|
|
title="Select a project to start"
|
|
>
|
|
<Play size={16} />
|
|
</button>
|
|
)}
|
|
|
|
{!isStopping && selectedProject ? (
|
|
<span className="ml-2 text-sm text-base-content/70">
|
|
{selectedProject.projectName}
|
|
</span>
|
|
) : !isStopping ? (
|
|
<span className="ml-2 text-sm text-base-content/40 italic">
|
|
No Project Selected
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
{isStopping ? (
|
|
// Skeleton for right side during stop
|
|
<div className="flex items-center space-x-3">
|
|
<div className="skeleton w-20 h-6 rounded"></div>
|
|
<div className="skeleton w-6 h-6 rounded"></div>
|
|
<div className="skeleton w-8 h-8 rounded"></div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
|
|
{formatTime(
|
|
isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s")
|
|
)}
|
|
</div>
|
|
|
|
{/* <span className="text-error">
|
|
<ShieldBan size={18} />
|
|
</span> */}
|
|
|
|
<button
|
|
onClick={() => {
|
|
if (toggleWindowSize.current) {
|
|
window.electron.resizeWindow(500, 85);
|
|
toggleWindowSize.current = false;
|
|
} else {
|
|
window.electron.resizeWindow(500, 600);
|
|
toggleWindowSize.current = true;
|
|
}
|
|
}}
|
|
className="btn btn-xs btn-outline btn-primary"
|
|
>
|
|
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</header>
|
|
}
|
|
|
|
{/* Layout Body */}
|
|
<div className={`flex flex-1 bg-base-200 overflow-hidden`}>
|
|
{/* Sidebar */}
|
|
<aside className="flex-none w-16 border-r border-base-300 bg-base-100 flex flex-col items-center py-4 gap-2">
|
|
{menuItems.map((item) => (
|
|
<div className="tooltip z-100 tooltip-right " key={item.label} data-tip={item.label}>
|
|
<NavLink
|
|
to={item.path}
|
|
end={item.exact}
|
|
className={({ isActive }) =>
|
|
`flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-150 cursor-pointer ${isActive
|
|
? "bg-primary text-primary-content"
|
|
: "hover:bg-base-300 text-base-content"
|
|
}`
|
|
}
|
|
>
|
|
{item.icon}
|
|
</NavLink>
|
|
</div>
|
|
))}
|
|
|
|
<div className={`absolute ${!isTracking ? "bottom-14" : "bottom-5"} `}>
|
|
<label className="swap swap-rotate p-2.5">
|
|
{/* this hidden checkbox controls the state */}
|
|
<input type="checkbox" className="theme-controller" onChange={toggleTheme}
|
|
checked={theme === "dark"} />
|
|
|
|
{/* sun icon */}
|
|
<svg
|
|
className="swap-off h-5 w-5 fill-current"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24">
|
|
<path
|
|
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
|
</svg>
|
|
{/* moon icon */}
|
|
<svg
|
|
className="swap-on h-5 w-5 fill-current"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24">
|
|
<path
|
|
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
|
</svg>
|
|
</label>
|
|
</div>
|
|
|
|
{!isTracking && (
|
|
<button
|
|
onClick={() => setShowLogoutModal(true)}
|
|
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>
|
|
)}
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
|
|
<Outlet context={trackingState} />
|
|
</main>
|
|
|
|
<LogoutModal
|
|
open={showLogoutModal}
|
|
onClose={() => setShowLogoutModal(false)}
|
|
onConfirm={handleLogout}
|
|
/>
|
|
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default DashboardLayout; |