This commit is contained in:
JaiPandey425 2025-11-07 13:09:33 +05:30
parent ec383c109f
commit ea74c00dde
9 changed files with 345 additions and 251 deletions

52
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2", "axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
"os-utils": "^0.0.14", "os-utils": "^0.0.14",
@ -288,6 +289,21 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.24.5", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
@ -3651,17 +3667,15 @@
} }
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "2.4.2", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true, "license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": { "engines": {
"node": ">=4" "node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/check-error": { "node_modules/check-error": {
@ -5236,6 +5250,7 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
@ -5808,6 +5823,7 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@ -7609,6 +7625,21 @@
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"node_modules/npm-run-all/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/npm-run-all/node_modules/cross-spawn": { "node_modules/npm-run-all/node_modules/cross-spawn": {
"version": "6.0.5", "version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -9293,6 +9324,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
}, },

View File

@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2", "axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2", "electron-store": "^11.0.2",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
"os-utils": "^0.0.14", "os-utils": "^0.0.14",

View File

@ -1,30 +1,30 @@
// electron/ipc/apiHandler.ts // // electron/ipc/apiHandler.ts
import { ipcMain } from 'electron'; // import { ipcMain } from 'electron';
import api from '../api/axiosClient.js'; // import api from '../api/axiosClient.js';
interface ApiRequestArgs { // interface ApiRequestArgs {
method: 'get' | 'post' | 'put' | 'patch' | 'delete'; // method: 'get' | 'post' | 'put' | 'patch' | 'delete';
url: string; // url: string;
data?: any; // data?: any;
config?: Record<string, any>; // config?: Record<string, any>;
} // }
ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => { // ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
try { // try {
const response = await api.request({ // const response = await api.request({
method: args.method, // method: args.method,
url: args.url, // url: args.url,
data: args.data, // data: args.data,
...args.config, // ...args.config,
}); // });
return { success: true, data: response }; // return { success: true, data: response };
} catch (error: any) { // } catch (error: any) {
console.error('[API Handler Error]', error.message); // console.error('[API Handler Error]', error.message);
return { // return {
success: false, // success: false,
error: error.message, // error: error.message,
status: error.response?.status, // status: error.response?.status,
}; // };
} // }
}); // });

View File

@ -1,14 +1,34 @@
// electron/api/axiosClient.ts // electron/api/axiosClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken } from '../utils/tokenHelper.js'; import { getToken } from '../utils/tokenHelper.js';
import chalk from "chalk";
const TOKEN_KEY = 'auth_token'; const TOKEN_KEY = 'auth_token';
const Reset = "\x1b[0m";
const Bright = "\x1b[1m";
const Dim = "\x1b[2m";
const Underscore = "\x1b[4m";
const Blink = "\x1b[5m";
const Reverse = "\x1b[7m";
const Hidden = "\x1b[8m";
const FgRed = "\x1b[31m";
const FgGreen = "\x1b[32m";
const FgYellow = "\x1b[33m";
const FgBlue = "\x1b[34m";
const FgMagenta = "\x1b[35m";
const FgCyan = "\x1b[36m";
const FgWhite = "\x1b[37m";
const api: AxiosInstance = axios.create({ const api: AxiosInstance = axios.create({
baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1', // 🔁 your backend baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1', // 🔁 your backend
timeout: 10000, timeout: 10000,
}); });
console.log("Creating Axios Instance", axios.VERSION);
console.log("Axios .get reference:", api.get);
console.log("Axios .request reference:", api.request);
// --- Request Interceptor --- // --- Request Interceptor ---
api.interceptors.request.use( api.interceptors.request.use(
async (config: AxiosRequestConfig): Promise<any> => { async (config: AxiosRequestConfig): Promise<any> => {
@ -18,7 +38,11 @@ api.interceptors.request.use(
config.headers['ngrok-skip-browser-warning'] = 98098; config.headers['ngrok-skip-browser-warning'] = 98098;
} }
console.log(`[Axios] ${config.method?.toUpperCase()} ${config.url}`); // Log method, URL, and headers
console.log(
chalk.red(`[Axios] ${config.method?.toUpperCase()} ${config.url}`)
);
console.log(chalk.blue("Headers: "), config.headers); // <-- log headers
return config; return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)

View File

@ -28,9 +28,14 @@ ipcMain.handle('login-request', async (event, credentials: { email: string, pass
return handleLogin(credentials); return handleLogin(credentials);
}); });
ipcMain.handle('logout-request', async () => { ipcMain.handle("logout-request", async () => {
(async () => { console.log("Logout handler triggered");
console.log("Handle Logout listened") try {
await api.get("/logout"); const res = await api.get("/logout");
})() console.log("logout response",res)
return { success: true, data: res.data };
} catch (err: any) {
console.error("Logout failed:", err);
return { success: false, error: err.message };
}
}); });

View File

@ -1,21 +1,30 @@
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow, session } from 'electron';
import { ipcMainHandle, ipcMainOn, isDev } from './util.js'; import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
import { getStaticData, pollResources } from './resourceManager.js'; import { getStaticData, pollResources } from './resourceManager.js';
import { getPreloadPath, getUIPath } from './pathResolver.js'; import { getPreloadPath, getUIPath } from './pathResolver.js';
import "./ipc/ipcHandler.js" import "./ipc/ipcHandler.js";
app.on('ready', async () => { app.on('ready', async () => {
console.log("ready") console.log("ready");
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 550, // fixed width
height: 750, // fixed height
resizable: false, // prevents resizing
fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar)
maximizable: false, // prevents maximize
autoHideMenuBar: true,
frame: process.platform === 'darwin' ? false : true,
webPreferences: { webPreferences: {
preload: getPreloadPath(), preload: getPreloadPath(),
}, },
// disables default system frame (dont do this if you want a proper working menu bar)
frame: process.platform === 'darwin' ? false : true,
autoHideMenuBar: true
}); });
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
console.log('🌐 Request:', details.method, details.url);
callback({ cancel: false });
});
if (isDev()) { if (isDev()) {
mainWindow.loadURL('http://localhost:5123'); mainWindow.loadURL('http://localhost:5123');
@ -26,9 +35,8 @@ app.on('ready', async () => {
pollResources(mainWindow); pollResources(mainWindow);
ipcMainHandle('getStaticData', () => { // IPC handlers
return getStaticData(); ipcMainHandle('getStaticData', () => getStaticData());
});
ipcMainOn('sendFrameAction', (payload) => { ipcMainOn('sendFrameAction', (payload) => {
switch (payload) { switch (payload) {
@ -36,7 +44,7 @@ app.on('ready', async () => {
mainWindow.close(); mainWindow.close();
break; break;
case 'MAXIMIZE': case 'MAXIMIZE':
mainWindow.maximize(); // prevent maximize
break; break;
case 'MINIMIZE': case 'MINIMIZE':
mainWindow.minimize(); mainWindow.minimize();
@ -44,21 +52,20 @@ app.on('ready', async () => {
} }
}); });
// createTray(mainWindow);
handleCloseEvents(mainWindow); handleCloseEvents(mainWindow);
// createMenu(mainWindow);
// await takeScreenshot();
}); });
function handleCloseEvents(mainWindow: BrowserWindow) { function handleCloseEvents(mainWindow: BrowserWindow) {
let willClose = false; let willClose = false;
mainWindow.on('close', (e) => { mainWindow.on('close', (e) => {
if (willClose) { if (willClose) return;
return;
}
e.preventDefault(); e.preventDefault();
mainWindow.hide(); mainWindow.hide();
if (app.dock) { if (app.dock) {
app.dock.hide(); app.dock.hide();
} }
@ -72,3 +79,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
willClose = false; willClose = false;
}); });
} }

View File

@ -23,6 +23,7 @@ contextBridge.exposeInMainWorld('electron', {
getToken: (TOKEN_KEY: string) => ipcRenderer.invoke('get-token', TOKEN_KEY), getToken: (TOKEN_KEY: string) => ipcRenderer.invoke('get-token', TOKEN_KEY),
uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'), uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'),
} satisfies Window['electron']); } satisfies Window['electron']);
function ipcInvoke<Key extends keyof EventPayloadMapping>( function ipcInvoke<Key extends keyof EventPayloadMapping>(
@ -45,11 +46,4 @@ function ipcSend<Key extends keyof EventPayloadMapping>(
payload: EventPayloadMapping[Key] payload: EventPayloadMapping[Key]
) { ) {
electron.ipcRenderer.send(key, payload); electron.ipcRenderer.send(key, payload);
} }
contextBridge.exposeInMainWorld('electronAPI', {
getToken: (key: string) => ipcRenderer.invoke('get-token', key),
saveToken: (key: string, token: string) => ipcRenderer.invoke('save-token', key, token),
clearToken: (key: string) => ipcRenderer.invoke('clear-token', key),
});

View File

@ -18,6 +18,10 @@ import {
SettingsIcon, SettingsIcon,
TimerIcon, TimerIcon,
HelpCircleIcon, HelpCircleIcon,
Pause,
ShieldBan,
ChevronDown,
Play,
} from "lucide-react"; } from "lucide-react";
interface User { interface User {
@ -37,6 +41,9 @@ interface ProjectTrackingData {
endTime: Date | null; endTime: Date | null;
isTracking: boolean; isTracking: boolean;
isPaused: boolean; isPaused: boolean;
pauseStartTime: Date | null;
totalPausedTime: number;
accumulatedTime: number; // ✅ NEW: Stores time from previous sessions
} }
interface TrackingState { interface TrackingState {
@ -54,12 +61,9 @@ interface TrackingState {
projectsData: Project[]; projectsData: Project[];
} }
interface DashboardLayoutProps { interface DashboardLayoutProps {
user: User | null; user: User | null;
setUser: React.Dispatch<React.SetStateAction<User | null>>; setUser: React.Dispatch<React.SetStateAction<User | null>>;
// trackingState: TrackingState;
} }
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => { const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
@ -68,7 +72,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
// Tracking states // Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null); const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isTracking, setIsTracking] = useState(false); const [isTracking, setIsTracking] = useState(false);
@ -88,33 +91,17 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
Map<number, ProjectTrackingData> Map<number, ProjectTrackingData>
>(new Map()); >(new Map());
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
setIsMobile(mobile);
setSidebarOpen(!mobile);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const handleLogout = async () => { const handleLogout = async () => {
localStorage.removeItem("user"); localStorage.removeItem("user");
setUser(null); setUser(null);
console.log("calling Logout UI");
console.log("calling Logout UI")
try { try {
const res = await window.electron.logout() const res = await window.electron.logout();
console.log(res) console.log(res, "Logout done");
} catch (error) { } catch (error) {
console.log(error) console.error("Logout error:", error);
} }
navigate("/login");
}; };
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen); const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
@ -132,8 +119,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> }, { label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
]; ];
// ✅ Sync global states with selected project tracking data
// Sync global states with selected project tracking data
useEffect(() => { useEffect(() => {
if (selectedProject) { if (selectedProject) {
const projectData = projectTrackingMap.get(selectedProject.id); const projectData = projectTrackingMap.get(selectedProject.id);
@ -147,14 +133,57 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
} }
}, [selectedProject, projectTrackingMap]); }, [selectedProject, projectTrackingMap]);
// Start tracking // ✅ Timer effect - calculates elapsed time based on timestamps + accumulated time
const handleStartTracking = (project: Project) => { useEffect(() => {
// Clear any existing timer if (isTracking && !isPaused && selectedProject) {
if (timerRef.current) { timerRef.current = setInterval(() => {
clearInterval(timerRef.current); setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
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;
// ✅ FIX: Add accumulated time from previous sessions
const calculatedElapsed =
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
current.elapsedTime = calculatedElapsed;
updated.set(selectedProject.id, { ...current });
// Update global elapsedTime in sync
setElapsedTime(calculatedElapsed);
}
return updated;
});
}, 1000);
} else {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
} }
// Update project tracking map return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [isTracking, isPaused, selectedProject]);
// ✅ Start tracking
const handleStartTracking = (project: Project) => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
const now = new Date();
setProjectTrackingMap((prev) => { setProjectTrackingMap((prev) => {
const updated = new Map(prev); const updated = new Map(prev);
const current = updated.get(project.id) || { const current = updated.get(project.id) || {
@ -163,46 +192,37 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
endTime: null, endTime: null,
isTracking: false, isTracking: false,
isPaused: false, isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: 0,
}; };
const now = new Date(); // ✅ Keep accumulated time from previous sessions
current.startTime = now; current.startTime = now;
current.isTracking = true; current.isTracking = true;
current.isPaused = false; current.isPaused = false;
current.pauseStartTime = null;
current.totalPausedTime = 0; // Reset pause time for new session
updated.set(project.id, current); updated.set(project.id, current);
// Update global tracking states
setIsTracking(true);
setIsPaused(false);
setStartTime(now);
setElapsedTime(current.accumulatedTime); // Start from accumulated time
return updated; return updated;
}); });
// Update global tracking states
setIsTracking(true);
setIsPaused(false);
setStartTime(new Date());
// Start the interval timer
timerRef.current = setInterval(() => {
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(project.id);
// Only increment when tracking AND not paused
if (current && current.isTracking && !current.isPaused) {
current.elapsedTime += 1;
updated.set(project.id, { ...current });
}
return updated;
});
}, 1000);
}; };
// Pause/Resume tracking // ✅ Pause/Resume tracking with pause duration tracking
const handlePauseResume = () => { const handlePauseResume = () => {
if (!selectedProject) return; if (!selectedProject) return;
// Get current state before toggling to avoid double-click issues
const currentProjectData = projectTrackingMap.get(selectedProject.id); const currentProjectData = projectTrackingMap.get(selectedProject.id);
if (!currentProjectData || !currentProjectData.isTracking) return; if (!currentProjectData || !currentProjectData.isTracking) return;
const now = new Date();
const newPausedState = !currentProjectData.isPaused; const newPausedState = !currentProjectData.isPaused;
setProjectTrackingMap((prev) => { setProjectTrackingMap((prev) => {
@ -210,48 +230,63 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const current = updated.get(selectedProject.id); const current = updated.get(selectedProject.id);
if (!current) return updated; if (!current) return updated;
// Set the new pause state if (newPausedState) {
current.isPaused = newPausedState; // Starting pause
current.isPaused = true;
current.pauseStartTime = now;
} else {
// Resuming from pause
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.id, { ...current }); updated.set(selectedProject.id, { ...current });
setIsPaused(newPausedState);
return updated; return updated;
}); });
// Update global isPaused state immediately
setIsPaused(newPausedState);
}; };
// Stop tracking // ✅ Stop tracking - PRESERVES elapsed time
const handleStopTracking = () => { const handleStopTracking = () => {
if (!selectedProject) return; if (!selectedProject) return;
// Clear the timer
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
timerRef.current = null; timerRef.current = null;
} }
// Update project tracking map const now = new Date();
setProjectTrackingMap((prev) => { setProjectTrackingMap((prev) => {
const updated = new Map(prev); const updated = new Map(prev);
const current = updated.get(selectedProject.id); const current = updated.get(selectedProject.id);
if (!current) return updated; if (!current) return updated;
const now = new Date(); // ✅ FIX: Store current elapsed time as accumulated time
current.accumulatedTime = current.elapsedTime;
current.isTracking = false; current.isTracking = false;
current.isPaused = false; current.isPaused = false;
current.endTime = now; current.endTime = now;
current.startTime = null; // Clear startTime for stopped state
updated.set(selectedProject.id, { ...current }); updated.set(selectedProject.id, { ...current });
// Update global states - keep elapsed time
setIsTracking(false);
setIsPaused(false);
setEndTime(now);
setElapsedTime(current.elapsedTime); // ✅ Keep the elapsed time
return updated; return updated;
}); });
// Reset global tracking states to allow selecting another project console.log("Tracking stopped. Time preserved:", elapsedTime);
setIsTracking(false);
setIsPaused(false);
setEndTime(new Date());
console.log("projectTrackingMap", projectTrackingMap)
}; };
// Cleanup timer on unmount // Cleanup timer on unmount
@ -263,16 +298,14 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}; };
}, []); }, []);
// Handle project selection // ✅ Handle project selection
const handleProjectSelect = (id: number) => { const handleProjectSelect = (id: number) => {
// Only allow project selection when not tracking
if (isTracking) return; if (isTracking) return;
const project = projectsData.find((p) => p.id === id); const project = projectsData.find((p) => p.id === id);
if (project) { if (project) {
setSelectedProject(project); setSelectedProject(project);
// Load existing tracking data if available
const projectData = projectTrackingMap.get(id); const projectData = projectTrackingMap.get(id);
if (projectData) { if (projectData) {
setElapsedTime(projectData.elapsedTime); setElapsedTime(projectData.elapsedTime);
@ -281,7 +314,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
setIsTracking(projectData.isTracking); setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused); setIsPaused(projectData.isPaused);
} else { } else {
// Reset states for new project
setElapsedTime(0); setElapsedTime(0);
setStartTime(null); setStartTime(null);
setEndTime(null); setEndTime(null);
@ -306,132 +338,129 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
handleProjectSelect, handleProjectSelect,
}; };
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 ( return (
<div className="flex h-screen bg-base-200 overflow-hidden"> <>
{/* Sidebar */} {/* Header */}
<aside <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">
className={` {/* Left Section */}
fixed md:relative inset-y-0 left-0 z-40 transform <div className="flex items-center space-x-3">
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out {selectedProject ? (
${isMobile isTracking ? (
? isSidebarOpen <button
? "translate-x-0 w-64" className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
: "-translate-x-full w-64" onClick={handleStopTracking}
: isSidebarOpen title="Stop Tracking"
? "w-50" >
: "w-20" <Pause size={16} />
} </button>
`} ) : (
> <button
{/* Sidebar Header */} className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
<div className="h-14 text-xl font-bold flex justify-between items-center border-b border-base-300"> onClick={() => handleStartTracking(selectedProject)}
{isSidebarOpen ? ( title="Start Tracking"
<span className="whitespace-nowrap mx-auto">VA Tracker</span> >
<Play size={16} />
</button>
)
) : ( ) : (
<span className="text-primary font-extrabold mx-auto">VA</span>
)}
{isMobile && (
<button <button
onClick={toggleSidebar} className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed"
className="btn btn-ghost btn-sm md:hidden" disabled
aria-label="Close sidebar" title="Select a project to start"
> >
<X size={20} /> <Play size={16} />
</button> </button>
)} )}
{selectedProject ? (
<span className="ml-2 text-sm text-base-content/70">
{selectedProject.projectName}
</span>
) : (
<span className="ml-2 text-sm text-base-content/40 italic">
No Project Selected
</span>
)}
</div> </div>
{/* Menu */} {/* Right Section */}
<ul className="menu px-2 py-4"> <div className="flex items-center space-x-3">
{menuItems.map((item) => ( {/* Timer */}
<li key={item.label}> <div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
<NavLink {formatTime(elapsedTime || 0)}
to={item.path} </div>
end={item.exact}
className={({ isActive }) =>
`flex items-center gap-3 my-1 font-medium transition-all duration-150 ${isActive
? "bg-primary text-primary-content rounded-lg"
: "hover:bg-base-300 rounded-lg"
}`
}
onClick={closeSidebarOnMobile}
>
<div className="min-w-[24px] flex justify-center">{item.icon}</div>
{isSidebarOpen && <span>{item.label}</span>}
</NavLink>
</li>
))}
</ul>
</aside>
{/* Mobile overlay */} {/* Alert Icon */}
{isMobile && isSidebarOpen && ( <span className="text-error">
<div <ShieldBan size={18} />
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden" </span>
onClick={closeSidebarOnMobile}
></div>
)}
{/* Main Content */} {/* Dropdown */}
<div className="flex flex-col flex-1 bg-base-200 transition-all duration-300 ease-in-out"> <div className="dropdown dropdown-end">
{/* Navbar */} <div
<nav className="h-14 bg-base-100 border-b border-t border-base-300 px-4 flex justify-between items-center sticky top-0 z-30"> tabIndex={0}
{/* Left */} role="button"
<div className="flex items-center gap-2"> className="btn btn-sm btn-outline btn-primary p-1.5 rounded-lg"
<button
onClick={toggleSidebar}
className="btn btn-ghost btn-square"
aria-label="Toggle sidebar"
> >
<Menu size={20} /> <ChevronDown />
</button>
<span className="text-lg font-semibold md:hidden">
{location.pathname.includes("projects")
? "Projects"
: location.pathname.includes("leave-adjustments")
? "Leave & Adjustments"
: "Dashboard"}
</span>
</div>
{/* Right */}
<div className="flex items-center gap-1">
<button className="btn btn-ghost btn-circle">
<Bell size={20} />
</button>
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div className="w-8 h-8 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
<User size={18} className="text-base-content" />
</div>
</label>
<ul
tabIndex={0}
className="mt-3 p-2 shadow menu menu-sm dropdown-content bg-base-200 rounded-box w-52"
>
<li>
<a>
<Settings size={16} /> Settings
</a>
</li>
<li>
<a onClick={handleLogout} className="text-error">
<LogOut size={16} /> Logout
</a>
</li>
</ul>
</div> </div>
<ul
tabIndex={0}
className="dropdown-content menu p-2 shadow-lg bg-base-100 rounded-box w-40 mt-2"
>
<li><a>View Logs</a></li>
<li><a>Settings</a></li>
</ul>
</div> </div>
</nav> </div>
</header>
{/* Page Content */} {/* Layout Body */}
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar"> <div className="flex h-[calc(100vh-3rem)] bg-base-200 overflow-hidden">
{/* Sidebar - Always visible */}
<aside
className="w-20 bg-base-100 border-r border-base-300 flex-shrink-0
flex flex-col items-center py-4 space-y-4"
>
{menuItems.map((item) => (
<NavLink
key={item.label}
to={item.path}
end={item.exact}
className={({ isActive }) =>
`flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${
isActive
? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
}`
}
>
{item.icon}
</NavLink>
))}
<button
onClick={handleLogout}
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"
title="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} /> <Outlet context={trackingState} />
</main> </main>
</div> </div>
</div> </>
); );
}; };
export default DashboardLayout; export default DashboardLayout;

2
types.d.ts vendored
View File

@ -34,7 +34,7 @@ interface Window {
) => UnsubscribeFunction; ) => UnsubscribeFunction;
sendFrameAction: (payload: FrameWindowAction) => void; sendFrameAction: (payload: FrameWindowAction) => void;
login: (credentials: { email: string; password: string }) => Promise<any>; login: (credentials: { email: string; password: string }) => Promise<any>;
logout: () => {}; logout: () => Promise<{ success: boolean; error?: string }>;
getToken: (tokenKey: string) => Promise<string | null>; getToken: (tokenKey: string) => Promise<string | null>;
uploadScreenshot: () => {}; uploadScreenshot: () => {};
}; };