diff --git a/package-lock.json b/package-lock.json index 306e388..d0069ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "electron-store": "^11.0.2", "get-windows": "^9.2.3", "lucide-react": "^0.552.0", + "luxon": "^3.7.2", "os-utils": "^0.0.14", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -29,6 +30,7 @@ }, "devDependencies": { "@playwright/test": "^1.47.2", + "@types/luxon": "^3.7.1", "@types/node": "^22.19.0", "@types/os-utils": "^0.0.4", "@types/react": "^18.3.8", @@ -2812,6 +2814,12 @@ "@types/node": "*" } }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -8303,25 +8311,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/lzma-native": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", - "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.1.0", - "node-gyp-build": "^4.2.1", - "readable-stream": "^3.6.0" - }, - "bin": { - "lzmajs": "bin/lzmajs" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index c2c3c3b..1f2778b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "electron-store": "^11.0.2", "get-windows": "^9.2.3", "lucide-react": "^0.552.0", + "luxon": "^3.7.2", "os-utils": "^0.0.14", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -40,6 +41,7 @@ }, "devDependencies": { "@playwright/test": "^1.47.2", + "@types/luxon": "^3.7.1", "@types/node": "^22.19.0", "@types/os-utils": "^0.0.4", "@types/react": "^18.3.8", diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index 18d5805..5fedd1c 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { getToken } from '../utils/tokenHelper.js'; import chalk from "chalk"; +import { sendTokenExpired } from '../ipc/ipcHandler.js'; const TOKEN_KEY = 'ACCESS_TOKEN'; @@ -37,6 +38,7 @@ api.interceptors.response.use( console.error('[Axios Error]', error.message); if (error.response?.status === 401) { console.warn('Token expired โ€” trigger refresh logic if needed'); + sendTokenExpired(); } return Promise.reject(error); } diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index f7265e7..c4ba763 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -3,6 +3,10 @@ import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js"; import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js"; import api from "../api/axiosClient.js"; import { getMainWindow } from '../main.js'; + + + + export const registerIPCHandlers = (mainWindow: BrowserWindow) => { console.log("โš™๏ธ Registering IPC Handlers..."); @@ -42,6 +46,10 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => { return { success: false, message: "Logout failed" }; } }); + + + + ipcMain.handle("get-active-app", async () => { return await getActiveApp(); @@ -84,10 +92,11 @@ ipcMain.handle("get-project-list", async () => { // Return only serializable data return JSON.parse(JSON.stringify(res)); } catch (error: any) { - console.error(" get-project-list error:", error); + console.error(" get-project-list error:", error.status); return { success: false, - message: error?.message || String(error) || "Unknown error" + message: error?.message || String(error) || "Unknown error", + status:error?.status }; } }); @@ -271,3 +280,35 @@ ipcMain.handle("change-password", async (event, data) => { }; } }); + + +ipcMain.handle("get-project-summary", async (_, { projectId, date }) => { + try { + const url = `/timesheet/summary/${projectId}/${date}`; + console.log("๐Ÿ“ก Fetching summary:", url); + + const res = await api.get(url); + return { + success: true, + data: res.data, + }; + } catch (error: any) { + console.error("โŒ Error fetching summary:", error.message); + return { + success: false, + message: error.response?.data?.message || "Something went wrong", + }; + } +}); + + + +export const sendTokenExpired = () => { + console.log("โš ๏ธ Sending token-expired event to renderer"); + + BrowserWindow.getAllWindows().forEach((win) => { + if (!win.isDestroyed()) { + win.webContents.send("token-expired"); + } + }); +}; diff --git a/src/electron/main.ts b/src/electron/main.ts index 5d3a0ce..da22710 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -101,4 +101,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) { export function getMainWindow() { return mainWindow; -} \ No newline at end of file +} + diff --git a/src/electron/preload.cts b/src/electron/preload.cts index fb72e80..a4c818b 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -17,22 +17,38 @@ contextBridge.exposeInMainWorld('electron', { onWindowUpdate: (callback) => ipcRenderer.on("window:updated", (_, data) => callback(data)), + startProjectTracking: (data) => ipcRenderer.invoke("start-project-tracking", data), stopProjectTracking: (data) => ipcRenderer.invoke("stop-project-tracking", data), trackingTabsOpeningClosing: (data) => ipcRenderer.invoke("tracking-tabs-opening-and-closing", data), getProjectLogs: ( - projectID: string, - fromDate?: string, - toDate?: string -) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }), + projectID: string, + fromDate?: string, + toDate?: string + ) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }), -getProfile: () => ipcRenderer.invoke("get-profile"), + getProfile: () => ipcRenderer.invoke("get-profile"), -changePassword: (data: { oldPassword: string; password: string; cPassword: string }) => + changePassword: (data: { oldPassword: string; password: string; cPassword: string }) => ipcRenderer.invoke("change-password", data), + onTokenExpired: (callback) => { + // Remove old listeners + ipcRenderer.removeAllListeners("token-expired"); + + // Add single-use listener + ipcRenderer.once("token-expired", () => { + console.log("โš ๏ธ Token expired event received in PRELOAD (ONE TIME)"); + callback(); + }); + }, + + + getProjectSummary: (projectId: string, date: string) => + ipcRenderer.invoke("get-project-summary", { projectId, date }), + } satisfies Window['electron']); // function ipcInvoke( diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 3f56ce2..83c82d4 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -99,6 +99,9 @@ const App: React.FC = () => { const theme = ThemeToggle(); const dispatch = useDispatch(); + + + // โœ… Get user directly from Redux const user = useSelector((state: RootState) => state.user.user); useEffect(() => { diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index f15e6e8..75f2b71 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -133,6 +133,24 @@ const DashboardLayout = () => { const [projectsData, setProjectsData] = useState([]); + useEffect(() => { + if (!window.electron?.onTokenExpired) return; + + console.log("๐Ÿ” Token expiry listener registered"); + + window.electron.onTokenExpired(() => { + console.log("๐Ÿ”ฅ Token expired โ€” logging out..."); + handleLogout(); + }); + + return () => { + console.log("๐Ÿงน Cleaning token-expired listener"); + }; + }, []); + + + + const fetchProfile = async () => { try { dispatch(setLoading(true)); @@ -217,10 +235,13 @@ const DashboardLayout = () => { // Set the initial elapsed time in the component state setElapsedTime(initialElapsedTime); } - fetchProfile() + fetchProfile() } else { console.warn("โš ๏ธ Unexpected response:", res); + // if (res?.status) { + // // handleLogout() + // } } } catch (err) { console.error("โŒ Error fetching projects:", err); @@ -534,7 +555,7 @@ const DashboardLayout = () => { 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 toggleWindowSize = useRef(true); @@ -652,13 +673,14 @@ const DashboardLayout = () => { ))} - + } {/* Main Content */} diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index fa753a8..0c8474b 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -41,7 +41,8 @@ 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 ${secs}s`; + return `${hrs}h ${mins}m `; }; // Widget Component @@ -103,6 +104,17 @@ const Dashboard: React.FC = () => { ? projectTrackingMap.get(selectedProject.projectId) : null; + const formatWorkTime = (time: string) => { + if (!time) return "0h 0m"; + + // Example input: "0h 2m 29s" + const parts = time.split(" "); + + // Always return first two parts: ["0h", "2m"] + return parts.slice(0, 2).join(" "); + }; + + return (
{/* Header */} @@ -271,7 +283,7 @@ const Dashboard: React.FC = () => { {index + 1} {project.projectName} - {project.totalWorkTime || "0h 0m 0s"} + {formatWorkTime(project.totalWorkTime)} - - {/* */} {/* Tab Content */} @@ -253,10 +295,6 @@ const LeaveAndAdjustments: React.FC = () => { {activeTab === 'create_and_adjust_timesheet' && (
- {/*
-

Paid Time Off Request

-
*/} -
@@ -279,7 +317,6 @@ const LeaveAndAdjustments: React.FC = () => { ))} - {ptoErrors.fromDate && (
- -
handlePtoChange('fromDate', e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`} + type="text" + value={projectSummary ? `${projectSummary.totalWorkedHoursDecimal} hrs` : "0 hrs"} + readOnly + className="input input-bordered input-sm w-full bg-base-200" /> - {ptoErrors.fromDate && ( - - )}
@@ -322,15 +353,11 @@ const LeaveAndAdjustments: React.FC = () => { Current pending time handlePtoChange('toDate', e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`} + type="text" + value={projectSummary ? `${projectSummary.pendingHours} hrs` : "0 hrs"} + readOnly + className="input input-bordered input-sm w-full bg-base-200" /> - {ptoErrors.toDate && ( - - )}
@@ -339,11 +366,9 @@ const LeaveAndAdjustments: React.FC = () => { - setPtoLeave((prev) => ({ ...prev, fromDate: e.target.value })) - } - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`} + value={startTime} + onChange={(e) => setStartTime(e.target.value)} + className="input input-bordered input-sm w-full focus:input-primary transition-all" />
@@ -353,30 +378,22 @@ const LeaveAndAdjustments: React.FC = () => { - setPtoLeave((prev) => ({ ...prev, toDate: e.target.value })) - } - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`} + value={endTime} + onChange={(e) => setEndTime(e.target.value)} + className="input input-bordered input-sm w-full focus:input-primary transition-all" />
-
handlePtoChange('toDate', e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`} + type="text" + placeholder="Calculated automatically" + readOnly + className="input input-bordered input-sm w-full bg-base-200" /> - {ptoErrors.toDate && ( - - )}
@@ -385,10 +402,7 @@ const LeaveAndAdjustments: React.FC = () => { Description / Reason