Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - Window-Build Merge 10 Dec

This commit is contained in:
BhagwanSingh 2025-12-10 18:53:11 +05:30
commit 9172534f31
19 changed files with 371 additions and 100 deletions

0
.env Normal file
View File

View File

@ -1,10 +1,10 @@
[Interface] [Interface]
PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I= PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I=
Address = 10.0.0.2/24 Address = 10.0.0.2/32
DNS = 1.1.1.1 DNS = 1.1.1.1
[Peer] [Peer]
PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U= PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U=
Endpoint = 35.95.209.161:51820 Endpoint = 35.95.209.161:51820
AllowedIPs = 10.0.0.0/24 AllowedIPs = 10.0.0.0/32 0.0.0.0/0
PersistentKeepalive = 25 PersistentKeepalive = 25

View File

@ -69,4 +69,4 @@ api.interceptors.response.use(
} }
); );
export default api; export default api;

View File

@ -0,0 +1,6 @@
let CURRENT_BASE_URL = "https://quality-friendly-goldfish.ngrok-free.app/api/v1/va";
export const getBaseURL = () => CURRENT_BASE_URL;
export const setBaseURL = (url: string) => {
CURRENT_BASE_URL = url;
};

View File

@ -0,0 +1,40 @@
import axios from "axios";
import https from "https";
import { getBaseURL } from "./baseURL.js";
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
export const checkRegion = async () => {
const baseURL = await getBaseURL();
console.log("checkRegion@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ,baseURL)
try {
const res = await axios.get(`${baseURL}/health`, {
headers: { "ngrok-skip-browser-warning": "true" },
httpsAgent
});
console.log(res , "checkRegion")
return {
success: true,
status: res.status,
data: res.data
};
} catch (error: any) {
console.log(error , "Line 29")
const status = error?.response?.status || 500;
console.log("Region Check Error:", status);
return {
success: false,
status,
message: "Region check failed"
};
}
};

View File

@ -3,7 +3,9 @@ import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js";
import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js"; import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js";
import api from "../api/axiosClient.js"; import api from "../api/axiosClient.js";
import { getMainWindow } from '../main.js'; import { getMainWindow } from '../main.js';
import { checkRegion } from "../api/regionCheck.js";
import { startVPN, stopVPN } from "../vpn.js"; import { startVPN, stopVPN } from "../vpn.js";
import { getBaseURL, setBaseURL } from "../api/baseURL.js";
@ -11,6 +13,20 @@ import { startVPN, stopVPN } from "../vpn.js";
export const registerIPCHandlers = (mainWindow: BrowserWindow) => { export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
console.log("⚙️ Registering IPC Handlers..."); console.log("⚙️ Registering IPC Handlers...");
ipcMain.handle("GET_BASE_URL", () => {
return getBaseURL();
});
// UPDATE Base URL
ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => {
setBaseURL(url);
return {
updated: true,
baseURL: url
};
});
ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => { ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(key); return await getToken(key);
}); });
@ -535,6 +551,11 @@ ipcMain.handle("update-payment", async (event, data: Record<string, any>) => {
ipcMain.handle("check-region", async () => {
return await checkRegion();
});
export const sendTokenExpired = () => { export const sendTokenExpired = () => {
console.log("⚠️ Sending token-expired event to renderer"); console.log("⚠️ Sending token-expired event to renderer");

View File

@ -65,9 +65,14 @@ contextBridge.exposeInMainWorld('electron', {
getLeaveList: () => ipcRenderer.invoke("get-leave-list"), getLeaveList: () => ipcRenderer.invoke("get-leave-list"),
getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId), getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId),
checkRegion: () => ipcRenderer.invoke("check-region"),
connectVPN: () => ipcRenderer.invoke("vpn-connect"), connectVPN: () => ipcRenderer.invoke("vpn-connect"),
disconnectVPN: () => ipcRenderer.invoke("vpn-disconnect"), disconnectVPN: () => ipcRenderer.invoke("vpn-disconnect"),
getBaseURL: () => ipcRenderer.invoke("GET_BASE_URL"),
updateBaseURL: (url: string) => ipcRenderer.invoke("UPDATE_BASE_URL", url),
} satisfies Window['electron']); } satisfies Window['electron']);
// function ipcInvoke<Key extends keyof EventPayloadMapping>( // function ipcInvoke<Key extends keyof EventPayloadMapping>(

View File

@ -8,6 +8,7 @@ import { saveToken } from "./tokenHelper.js";
import { activeWindow } from "get-windows"; import { activeWindow } from "get-windows";
import { exec } from "child_process"; import { exec } from "child_process";
import { getUIPath } from "../pathResolver.js"; import { getUIPath } from "../pathResolver.js";
import { getBaseURL } from "../api/baseURL.js";
const httpsAgent = new https.Agent({ const httpsAgent = new https.Agent({
rejectUnauthorized: false rejectUnauthorized: false
@ -61,14 +62,16 @@ export const handleLogin = async (credentials: { email: string, password: string
// Call your login API // Call your login API
console.log("🟡 login Request received", credentials); console.log("🟡 login Request received", credentials);
let baseURL = getBaseURL()
console.log("app.getAppPath()", app.getAppPath()); console.log("app.getAppPath()", app.getAppPath());
console.log("UIPATH", getUIPath()); console.log("UIPATH", getUIPath());
console.log("Doom 1"); console.log("Doom 1" , `${baseURL}/api/v1/va/login`);
try { try {
console.log("➡️ Calling API..."); console.log("➡️ Calling API...");
const response = await axios.post( const response = await axios.post(
'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login', `https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login`,
{ userDetails: { ...credentials } }, { userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent } { headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent }
); );

View File

@ -97,22 +97,23 @@ import Vpn from "./pages/Vpn/Vpn";
import Schedule from "./pages/Schedule/Schedule"; import Schedule from "./pages/Schedule/Schedule";
import PaysSlips from "./pages/PaySlips/PaysSlips"; import PaysSlips from "./pages/PaySlips/PaysSlips";
import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime"; import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime";
import VpnGuard from "./pages/Vpn/VpnGuard";
const App: React.FC = () => { const App: React.FC = () => {
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
// ✅ Get user directly from Redux // ✅ Get user directly from Redux
const user = useSelector((state: RootState) => state.user.user); const user = useSelector((state: RootState) => state.user.user);
useEffect(() => { useEffect(() => {
dispatch(loadUserFromStorage()); dispatch(loadUserFromStorage());
}, [dispatch]); }, [dispatch]);
console.log("App.tsx → user:", user); console.log("App.tsx → user:", user);
return ( return (
<Router> <Router>
@ -122,7 +123,9 @@ const App: React.FC = () => {
path="/login" path="/login"
element={ element={
<PublicRoute user={user}> <PublicRoute user={user}>
<Login /> {/* <VpnGuard> */}
<Login />
{/* </VpnGuard> */}
</PublicRoute> </PublicRoute>
} }
/> />
@ -132,20 +135,22 @@ const App: React.FC = () => {
path="/dashboard" path="/dashboard"
element={ element={
<ProtectedRoute user={user}> <ProtectedRoute user={user}>
{/* <VpnGuard> */}
<DashboardLayout /> <DashboardLayout />
{/* </VpnGuard> */}
</ProtectedRoute> </ProtectedRoute>
} }
> >
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="projects" element={<Projects />} /> <Route path="projects" element={<Projects />} />
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} /> <Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="leave-adjustments/apply" element={<ApplyLeaveAndAdjustmentsTime/>} /> <Route path="leave-adjustments/apply" element={<ApplyLeaveAndAdjustmentsTime />} />
<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="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} /> <Route path="help-support" element={<HelpSupport />} />
<Route path="schedule" element={<Schedule/>} /> <Route path="schedule" element={<Schedule />} />
<Route path="pays-slips" element={<PaysSlips/>} /> <Route path="pays-slips" element={<PaysSlips />} />
</Route> </Route>
{/* Default Redirect */} {/* Default Redirect */}

View File

@ -167,6 +167,8 @@ const DashboardLayout = () => {
`Timer will continue running. Please stop manually when ready.` `Timer will continue running. Please stop manually when ready.`
); );
console.log("✅ Alert shown, timer continues running"); console.log("✅ Alert shown, timer continues running");
}, [selectedProject]); }, [selectedProject]);
@ -724,15 +726,26 @@ const DashboardLayout = () => {
// return `${hrs}h ${mins}m `; // 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 ( return (
<> <>
<div className="flex flex-col h-screen overflow-hidden"> <div className="flex flex-col h-screen overflow-hidden">
{/* <button {/* <button
onClick={() => { onClick={() => {
setCheckIp((prev) => !prev)
window.electron.connectVPN().then((msg) => { window.electron.connectVPN().then((msg) => {
console.log(msg); console.log(msg);
}); });
}} }}
className="text-purple-400"
> >
Connect VPN Connect VPN
</button> </button>
@ -741,6 +754,7 @@ const DashboardLayout = () => {
onClick={() => { onClick={() => {
window.electron.disconnectVPN(); window.electron.disconnectVPN();
}} }}
className="text-red-400"
> >
Disconnect VPN Disconnect VPN
</button> */} </button> */}

View File

@ -106,6 +106,7 @@ const Dashboard: React.FC = () => {
const projectSummary = useSelector((state: any) => state.projects.projectSummary); const projectSummary = useSelector((state: any) => state.projects.projectSummary);
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const isLimitReached = selectedProject ? isProjectLimitReached(selectedProject) : false;
console.log("Line 111", projectsData, selectedProject, projectTrackingMap) console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
@ -129,14 +130,14 @@ const Dashboard: React.FC = () => {
}; };
const fetchProjects = async () => { const fetchProjects = async () => {
setIsRefreshing(true) setIsRefreshing(true);
try { try {
console.log("📡 Fetching projects...Dashboard"); console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList(); const res = await window.electron.getProjectList();
console.log(" Project list response in dashboard: ", res.data?.[0]); console.log("Project list response in dashboard: ", res.data?.[0]);
if (res?.success && Array.isArray(res.data)) { if (res?.success && Array.isArray(res.data)) {
// Store data redux store // Store data in redux store
dispatch(setProjectsDataSlice(res.data)); dispatch(setProjectsDataSlice(res.data));
dispatch( dispatch(
@ -151,35 +152,67 @@ const Dashboard: React.FC = () => {
}) })
); );
// Auto-select first project // Check if we already have a selected project that's being tracked
if (res.data.length > 0) { const currentlySelected = selectedProject;
const firstProject = res.data[0]; const currentlyTracking = isTracking;
// Find if the currently selected project still exists in the fresh data
let projectToSelect = null;
console.log("Line 158" , firstProject) if (currentlySelected && currentlyTracking) {
// Select the project // We're tracking a project - try to preserve it
dispatch(setSelectedProjectSlice(firstProject)); const existingProject = res.data.find(
(p: Project) => p.projectId === currentlySelected.projectId
const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime);
// Initialize tracking data for first project
dispatch(
initializeProjectTracking({
projectId: firstProject.projectId,
initialTime: initialElapsed,
})
); );
if (existingProject) {
projectToSelect = existingProject;
}
} }
fetchProfile() // If no project is selected OR tracked project no longer exists, auto-select first project
if (!projectToSelect && res.data.length > 0) {
projectToSelect = res.data[0];
}
if (projectToSelect) {
console.log("Line 158", projectToSelect);
// Select the project
dispatch(setSelectedProjectSlice(projectToSelect));
// Only initialize tracking if it doesn't exist already
const trackingExists = projectTrackingMap[projectToSelect.projectId];
if (!trackingExists) {
const initialElapsed = parseTimeStringToSeconds(projectToSelect.totalWorkTime);
dispatch(
initializeProjectTracking({
projectId: projectToSelect.projectId,
initialTime: initialElapsed,
})
);
} else {
// Update project data but preserve tracking state
dispatch(
updateProjectTracking({
projectId: projectToSelect.projectId,
data: {
...trackingExists,
// Update elapsed time from server if needed
elapsedTime: parseTimeStringToSeconds(projectToSelect.totalWorkTime),
}
})
);
}
}
fetchProfile();
} else { } else {
console.warn("⚠️ Unexpected response:", res); console.warn("⚠️ Unexpected response:", res);
} }
} catch (err) { } catch (err) {
console.error("❌ Error fetching projects:", err); console.error("❌ Error fetching projects:", err);
} finally { } finally {
setIsRefreshing(false) setIsRefreshing(false);
} }
}; };
@ -246,7 +279,7 @@ const Dashboard: React.FC = () => {
}; };
return ( return (
<main className="flex-1 overflow-hidden no-scrollbar max-h-[85vh]"> <main className="flex-1 overflow-hidden no-scrollbar ">
{/* 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>
@ -272,6 +305,11 @@ const Dashboard: React.FC = () => {
<p className="text-lg font-extrabold text-success"> <p className="text-lg font-extrabold text-success">
{formatTime(displayTime)} {formatTime(displayTime)}
</p> </p>
{/* <p className={`text-lg font-extrabold ${isLimitReached ? 'text-error' : 'text-success'}`}>
{formatTime(displayTime)}
{isLimitReached && " ⚠️"}
</p> */}
<div> <div>
<span className="text-base-content/70">Project:</span>{" "} <span className="text-base-content/70">Project:</span>{" "}
<span className="font-semibold"> <span className="font-semibold">
@ -330,15 +368,16 @@ const Dashboard: React.FC = () => {
{/* 🔹 Sticky Header */} {/* 🔹 Sticky Header */}
<div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center"> <div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
<p className="text-md font-bold">Projects</p> <p className="text-md font-bold">Projects</p>
{!selectedData?.isTracking &&
<button <button
onClick={handleRefresh} onClick={handleRefresh}
disabled={isRefreshing} disabled={isRefreshing }
className="btn btn-xs btn-ghost gap-1 text-warning" className="btn btn-xs btn-ghost gap-1 text-warning"
title="Refresh projects" title="Refresh projects"
> >
<RefreshCw className={`w-3 h-3 text-warning ${isRefreshing ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-3 h-3 text-warning ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh Refresh
</button> </button>}
</div> </div>
{/* 🔹 Table or Empty State or Skeleton */} {/* 🔹 Table or Empty State or Skeleton */}
@ -362,7 +401,7 @@ const Dashboard: React.FC = () => {
minHeight: "auto", minHeight: "auto",
}} }}
> >
<table className="table table-zebra w-full text-[12px]"> <table className="table table-zebra w-full text-[12px] ">
<thead className="sticky top-0 bg-base-200 z-10"> <thead className="sticky top-0 bg-base-200 z-10">
<tr> <tr>
<th>#</th> <th>#</th>

View File

@ -23,7 +23,7 @@ const HelpSupport: React.FC = () => {
<Mail size={18} className="text-primary" /> <Mail size={18} className="text-primary" />
<div> <div>
<p className="text-xs text-base-content/70">Email</p> <p className="text-xs text-base-content/70">Email</p>
<p className="font-medium text-sm">support@vatechcorp.com</p> <p className="font-medium text-sm">support@docva.com</p>
</div> </div>
</div> </div>
@ -31,7 +31,7 @@ const HelpSupport: React.FC = () => {
<Phone size={18} className="text-primary" /> <Phone size={18} className="text-primary" />
<div> <div>
<p className="text-xs text-base-content/70">Phone</p> <p className="text-xs text-base-content/70">Phone</p>
<p className="font-medium text-sm">+91 98765 43210</p> <p className="font-medium text-sm">+XXXX XXXXX</p>
</div> </div>
</div> </div>

View File

@ -464,7 +464,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
console.log("Timesheet Created Successfully"); console.log("Timesheet Created Successfully");
resetForm(); resetForm();
// Show success toast instead of alert // Show success toast instead of alert
showToast("PTO Request submitted successfully!", "success"); showToast("Timesheet Request submitted successfully!", "success");
// Navigate after a small delay // Navigate after a small delay
setTimeout(() => { setTimeout(() => {
@ -680,4 +680,5 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
); );
}; };
export default AdjustmentTab; export default AdjustmentTab;

View File

@ -84,7 +84,7 @@ const PaysSlips = () => {
<th>Date</th> <th>Date</th>
<th>Total Hours</th> <th>Total Hours</th>
<th>Total Pay</th> <th>Total Pay</th>
<th>Action</th> {/* <th>Action</th> */}
</tr> </tr>
</thead> </thead>
@ -111,13 +111,13 @@ const PaysSlips = () => {
<td>${row.totalPayAmount.toFixed(2)}</td> <td>${row.totalPayAmount.toFixed(2)}</td>
<td> {/* <td>
<button <button
className="btn btn-xs btn-primary" className="btn btn-xs btn-primary"
> >
View View
</button> </button>
</td> </td> */}
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@ -1,6 +1,7 @@
// src/ui/pages/Settings/ChangePasswordModal.tsx // src/ui/pages/Settings/ChangePasswordModal.tsx
import React, { useState } from "react"; import React, { useState } from "react";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { useToast } from "../../helper/ToastContext";
interface ChangePasswordModalProps { interface ChangePasswordModalProps {
isOpen: boolean; isOpen: boolean;
@ -19,12 +20,13 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
const [showOld, setShowOld] = useState(false); const [showOld, setShowOld] = useState(false);
const [showNew, setShowNew] = useState(false); const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const { showToast } = useToast();
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!"); showToast("New passwords do not match!" , "error");
return; return;
} }
@ -40,17 +42,17 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
const response = await window.electron.changePassword(payload); const response = await window.electron.changePassword(payload);
if (response.success) { if (response.success) {
alert("Password changed successfully!"); showToast("Password changed successfully!" , "success");
setOldPassword("") setOldPassword("")
setNewPassword("") setNewPassword("")
setConfirmPassword("") setConfirmPassword("")
onClose(); onClose();
} else { } else {
alert(response.message || "Failed to change password."); showToast((response.message || "Failed to change password.") , "error");
} }
} catch (error) { } catch (error) {
console.error("Password change error:", error); console.error("Password change error:", error);
alert("Error changing password."); showToast("Error changing password." , "error");
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@ -1,3 +1,4 @@
import { DateTime } from "luxon";
import React from "react"; import React from "react";
interface TabActivity { interface TabActivity {
@ -27,6 +28,21 @@ interface Props {
const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs, loading }) => { const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs, loading }) => {
if (!isOpen) return null; if (!isOpen) return null;
const getTimeDifference = (from: string, to: string) => {
const start = DateTime.fromISO(from);
const end = DateTime.fromISO(to);
const diff = end.diff(start, ["hours", "minutes", "seconds"]);
return {
hours: diff.hours,
minutes: diff.minutes,
seconds: Math.floor(diff.seconds),
formatted: `${diff.hours}h ${diff.minutes}m ${Math.floor(diff.seconds)}s`
};
};
return ( return (
<dialog className="modal modal-open"> <dialog className="modal modal-open">
<div className="modal-box max-h-[97vh] max-w-4xl bg-base-100 flex flex-col"> <div className="modal-box max-h-[97vh] max-w-4xl bg-base-100 flex flex-col">
@ -46,7 +62,11 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
<div key={idx} className="border border-base-300 overflow-hidden"> <div key={idx} className="border border-base-300 overflow-hidden">
<div className="bg-base-200 p-2 font-semibold flex justify-between items-center"> <div className="bg-base-200 p-2 font-semibold flex justify-between items-center">
<span className="text-[12px]"> <span className="text-[12px]">
🕒 Session {idx + 1} — {session.totalSessionTime} 🕒 Session {idx + 1} — {getTimeDifference(
new Date(session.from).toISOString(),
new Date(session.to).toISOString()
).formatted}
</span> </span>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{new Date(session.from).toLocaleTimeString()} -{" "} {new Date(session.from).toLocaleTimeString()} -{" "}
@ -69,10 +89,10 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
<tr key={tabIdx} className="text-[10px]"> <tr key={tabIdx} className="text-[10px]">
<td>{tabIdx + 1}</td> <td>{tabIdx + 1}</td>
<td className="truncate max-w-[150px]" title={tab.title}> <td className="truncate max-w-[150px]" title={tab.title}>
{tab.title} <br/> {tab.title} <br />
<span className="text-slate-500">{tab.appName}</span> <span className="text-slate-500">{tab.appName}</span>
</td> </td>
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <br/><span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td> <td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <br /><span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
<td>{tab.durationFormatted}</td> <td>{tab.durationFormatted}</td>
</tr> </tr>
))} ))}

View File

@ -52,10 +52,11 @@ const Timesheets: React.FC = () => {
const { windowLogs } = useSelector((state: RootState) => state.window); const { windowLogs } = useSelector((state: RootState) => state.window);
console.log(" Timesheet in component:", windowLogs); console.log(" Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]); const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [selectedProject, setSelectedProject] = useState<string | null>(null); // const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>(""); const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -63,9 +64,13 @@ const Timesheets: React.FC = () => {
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null); const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
const { selectedProject } =
useSelector((state: any) => state.projects);
const activeProjects = projectsData.filter(p => p.status === "ACTIVE"); const activeProjects = projectsData.filter(p => p.status === "ACTIVE");
console.log("selectedProject", selectedProject.projectId)
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
@ -84,7 +89,6 @@ const Timesheets: React.FC = () => {
}; };
const fetchProjects = async () => { const fetchProjects = async () => {
setInitialLoading(true); setInitialLoading(true);
try { try {
@ -120,7 +124,7 @@ const Timesheets: React.FC = () => {
try { try {
setModalOpen(true); setModalOpen(true);
setLoading(true); setLoading(true);
setSelectedProject(projectID); // setSelectedProject(projectID);
setSelectedProjectName(projectName); setSelectedProjectName(projectName);
console.log("projectID", projectID) console.log("projectID", projectID)
@ -154,10 +158,6 @@ const Timesheets: React.FC = () => {
return parts.slice(0, 2).join(" "); return parts.slice(0, 2).join(" ");
}; };
// Skeleton loader rows // Skeleton loader rows
const renderSkeletonRows = () => { const renderSkeletonRows = () => {
return Array.from({ length: 5 }).map((_, index) => ( return Array.from({ length: 5 }).map((_, index) => (
@ -225,13 +225,13 @@ const Timesheets: React.FC = () => {
<td>{formatWorkTime(entry.totalWorkTime)}</td> <td>{formatWorkTime(entry.totalWorkTime)}</td>
<td> <td>
<button <button
disabled={formatWorkTime(entry.totalWorkTime) === "0h 0m"} disabled={(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProject.projectId))}
onClick={() => onClick={() =>
handleViewLogs(entry.projectId, entry.projectName) handleViewLogs(entry.projectId, entry.projectName)
} }
className="btn btn-ghost btn-xs" className="btn btn-ghost btn-xs"
> >
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? ( {(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProject.projectId)) ? (
<EyeOff size={16} /> <EyeOff size={16} />
) : ( ) : (
<Eye size={16} /> <Eye size={16} />

View File

@ -1,54 +1,154 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Shield, ShieldAlert, RefreshCw, Wifi } from "lucide-react";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
} }
const VpnGuard: React.FC<Props> = ({ children }) => { const VpnGuard: React.FC<Props> = ({ children }) => {
const [checking, setChecking] = useState(true);
const [vpnConnected, setVpnConnected] = useState(true);
// const checkVPN = async () => { const VPN_API = "http://10.0.0.2:3000";
// try {
// const res = await window.electron.checkVpnConnection();
// setVpnConnected(res?.success);
// } catch {
// setVpnConnected(false);
// } finally {
// setChecking(false);
// }
// };
useEffect(() => { const [checking, setChecking] = useState(true);
// checkVPN(); const [vpnConnected, setVpnConnected] = useState(true);
}, []); const [isRetrying, setIsRetrying] = useState(false);
if (checking) { const checkVPN = async () => {
return ( try {
<div className="min-h-screen flex justify-center items-center"> setChecking(true);
<span className="loading loading-spinner loading-lg"></span>
<span className="ml-3">Checking VPN...</span>
</div>
);
}
if (!vpnConnected) { const res = await window.electron?.checkRegion();
return ( console.log("VpnGuard", res);
<div className="min-h-screen flex justify-center items-center text-center">
<div className="card shadow-xl bg-base-100 p-6">
<h2 className="text-xl font-bold text-red-600">VPN Required</h2>
<p className="mt-2">Please connect to your company VPN to continue.</p>
<button className="btn btn-primary mt-4" setVpnConnected(res?.success === true);
onClick={() => window.location.reload()}> } catch (error) {
Retry console.error("VPN check failed:", error);
</button> setVpnConnected(false);
</div> } finally {
</div> setChecking(false);
); setIsRetrying(false);
} }
};
useEffect(() => {
checkVPN();
}, []);
const connectVPN = async () => {
setChecking(true);
try {
const res = await window.electron.connectVPN();
console.log("connectVPN result → ", res);
// Only proceed if VPN actually connected
if (res === "Windows VPN connected silently") {
const vpnRes = await window.electron.updateBaseURL(VPN_API);
console.log("vpnRes", vpnRes);
if (vpnRes) {
// Run VPN check again AFTER URL changes
await checkVPN();
} else {
console.warn("Base URL updated but health check failed");
}
}
} catch (error) {
console.log(error);
} finally {
setChecking(false);
}
};
// Checking state
if (checking && !isRetrying) {
return (
<div className="min-h-screen flex flex-col justify-center items-center bg-base-200">
<div className="relative">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-24 h-24 border-4 border-primary opacity-20 rounded-full animate-ping"></div>
</div>
<div className="relative bg-base-100 rounded-full p-6 shadow-xl">
<Shield className="w-12 h-12 text-primary animate-pulse" />
</div>
</div>
<div className="mt-8 text-center">
<h2 className="text-2xl font-semibold mb-2">
Verifying Connection
</h2>
<p className="text-base-content opacity-70">Checking VPN status...</p>
<div className="flex justify-center mt-4 gap-1">
<span className="loading loading-dots loading-md text-primary"></span>
</div>
</div>
</div>
);
}
// VPN not connected state
if (!vpnConnected) {
return (
<div className="min-h-screen flex flex-col justify-center items-center bg-base-200 p-4">
<div className="max-w-md w-full">
<div className="card bg-base-100 shadow-2xl">
<div className="card-body items-center text-center">
{/* Icon with animation */}
<div className="relative mb-4">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-20 h-20 border-4 border-error opacity-20 rounded-full animate-pulse"></div>
</div>
<div className="relative bg-error bg-opacity-10 rounded-full p-4">
<ShieldAlert className="w-12 h-12" />
</div>
</div>
{/* Title */}
<h2 className="card-title text-3xl mb-3">
VPN Required
</h2>
{/* Description */}
<p className="mb-6 opacity-80">
Please connect to your company VPN to access this application.
</p>
{/* Retry button */}
<button
className="btn btn-primary btn-wide gap-2"
onClick={connectVPN}
disabled={isRetrying}
>
{isRetrying ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Checking Connection...
</>
) : (
<>
<RefreshCw className="w-5 h-5" />
Connect VPN
</>
)}
</button>
{/* Help text */}
<div className="divider"></div>
<p className="text-xs opacity-60">
Need help? Contact IT support at{" "}
<a href="mailto:support@company.com" className="link link-primary">
support@company.com
</a>
</p>
</div>
</div>
</div>
</div>
);
}
return <>{children}</>;
return <>{children}</>;
}; };
export default VpnGuard; export default VpnGuard;

15
types.d.ts vendored
View File

@ -133,6 +133,15 @@ interface PayslipResponse {
} }
interface RegionResponse {
success: boolean;
status: number;
data?: any;
retry?: boolean;
message?: string;
}
interface Window { interface Window {
electron: { electron: {
login: (credentials: { email: string; password: string }) => Promise<any>; login: (credentials: { email: string; password: string }) => Promise<any>;
@ -242,8 +251,14 @@ interface Window {
data?: any; data?: any;
}>; }>;
checkRegion: () => Promise<RegionResponse>;
connectVPN: () => Promise<string>; connectVPN: () => Promise<string>;
disconnectVPN: () => Promise<void>; disconnectVPN: () => Promise<void>;
getBaseURL: () => Promise<string>;
updateBaseURL: (url: string) => Promise<{
updated: boolean;
baseURL: string;
}>;
}; };
} }