Compare commits
2 Commits
e2b0561251
...
94a6ebdac7
| Author | SHA1 | Date |
|---|---|---|
|
|
94a6ebdac7 | |
|
|
ea74c00dde |
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.13.2",
|
||||
"chalk": "^5.6.2",
|
||||
"electron-store": "^11.0.2",
|
||||
"get-windows": "^9.2.3",
|
||||
"lucide-react": "^0.552.0",
|
||||
|
|
@ -288,6 +289,21 @@
|
|||
"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": {
|
||||
"version": "7.24.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
|
||||
|
|
@ -3832,17 +3848,15 @@
|
|||
}
|
||||
},
|
||||
"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,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
|
|
@ -5422,6 +5436,7 @@
|
|||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
|
|
@ -6319,6 +6334,7 @@
|
|||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
|
|
@ -8168,6 +8184,21 @@
|
|||
"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": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
|
|
@ -9863,6 +9894,7 @@
|
|||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.13.2",
|
||||
"chalk": "^5.6.2",
|
||||
"electron-store": "^11.0.2",
|
||||
"get-windows": "^9.2.3",
|
||||
"lucide-react": "^0.552.0",
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
// electron/ipc/apiHandler.ts
|
||||
import { ipcMain } from 'electron';
|
||||
import api from '../api/axiosClient.js';
|
||||
// // electron/ipc/apiHandler.ts
|
||||
// import { ipcMain } from 'electron';
|
||||
// import api from '../api/axiosClient.js';
|
||||
|
||||
interface ApiRequestArgs {
|
||||
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||
url: string;
|
||||
data?: any;
|
||||
config?: Record<string, any>;
|
||||
}
|
||||
// interface ApiRequestArgs {
|
||||
// method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||
// url: string;
|
||||
// data?: any;
|
||||
// config?: Record<string, any>;
|
||||
// }
|
||||
|
||||
ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
|
||||
try {
|
||||
const response = await api.request({
|
||||
method: args.method,
|
||||
url: args.url,
|
||||
data: args.data,
|
||||
...args.config,
|
||||
});
|
||||
// ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
|
||||
// try {
|
||||
// const response = await api.request({
|
||||
// method: args.method,
|
||||
// url: args.url,
|
||||
// data: args.data,
|
||||
// ...args.config,
|
||||
// });
|
||||
|
||||
return { success: true, data: response };
|
||||
} catch (error: any) {
|
||||
console.error('[API Handler Error]', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
status: error.response?.status,
|
||||
};
|
||||
}
|
||||
});
|
||||
// return { success: true, data: response };
|
||||
// } catch (error: any) {
|
||||
// console.error('[API Handler Error]', error.message);
|
||||
// return {
|
||||
// success: false,
|
||||
// error: error.message,
|
||||
// status: error.response?.status,
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// electron/api/axiosClient.ts
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { getToken } from '../utils/tokenHelper.js';
|
||||
import chalk from "chalk";
|
||||
|
||||
const TOKEN_KEY = 'ACCESS_TOKEN';
|
||||
|
||||
|
|
@ -9,6 +10,10 @@ const api: AxiosInstance = axios.create({
|
|||
// 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 ---
|
||||
api.interceptors.request.use(
|
||||
async (config: AxiosRequestConfig): Promise<any> => {
|
||||
|
|
@ -20,7 +25,11 @@ api.interceptors.request.use(
|
|||
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;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
import { app, BrowserWindow } from 'electron';
|
||||
import { app, BrowserWindow, session } from 'electron';
|
||||
import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
|
||||
import { getStaticData, pollResources } from './resourceManager.js';
|
||||
import { getPreloadPath, getUIPath } from './pathResolver.js';
|
||||
|
||||
import "./ipc/ipcHandler.js"
|
||||
import "./ipc/ipcHandler.js";
|
||||
|
||||
app.on('ready', async () => {
|
||||
console.log("ready")
|
||||
console.log("ready");
|
||||
|
||||
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: {
|
||||
preload: getPreloadPath(),
|
||||
},
|
||||
|
||||
resizable: false, // disables resizing (no maximize)
|
||||
minimizable: false, // disables minimize button
|
||||
maximizable: false, // disables maximize button
|
||||
closable: true,
|
||||
alwaysOnTop: true,
|
||||
// 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()) {
|
||||
mainWindow.loadURL('http://localhost:5123');
|
||||
|
|
@ -32,39 +35,37 @@ app.on('ready', async () => {
|
|||
|
||||
pollResources(mainWindow);
|
||||
|
||||
ipcMainHandle('getStaticData', () => {
|
||||
return getStaticData();
|
||||
// IPC handlers
|
||||
ipcMainHandle('getStaticData', () => getStaticData());
|
||||
|
||||
ipcMainOn('sendFrameAction', (payload) => {
|
||||
switch (payload) {
|
||||
case 'CLOSE':
|
||||
mainWindow.close();
|
||||
break;
|
||||
case 'MAXIMIZE':
|
||||
// prevent maximize
|
||||
break;
|
||||
case 'MINIMIZE':
|
||||
mainWindow.minimize();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// ipcMainOn('sendFrameAction', (payload) => {
|
||||
// switch (payload) {
|
||||
// case 'CLOSE':
|
||||
// mainWindow.close();
|
||||
// break;
|
||||
// case 'MAXIMIZE':
|
||||
// mainWindow.maximize();
|
||||
// break;
|
||||
// case 'MINIMIZE':
|
||||
// mainWindow.minimize();
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
|
||||
// createTray(mainWindow);
|
||||
handleCloseEvents(mainWindow);
|
||||
// createMenu(mainWindow);
|
||||
// await takeScreenshot();
|
||||
});
|
||||
|
||||
|
||||
|
||||
function handleCloseEvents(mainWindow: BrowserWindow) {
|
||||
let willClose = false;
|
||||
|
||||
mainWindow.on('close', (e) => {
|
||||
if (willClose) {
|
||||
return;
|
||||
}
|
||||
if (willClose) return;
|
||||
|
||||
e.preventDefault();
|
||||
mainWindow.hide();
|
||||
|
||||
if (app.dock) {
|
||||
app.dock.hide();
|
||||
}
|
||||
|
|
@ -78,3 +79,5 @@ function handleCloseEvents(mainWindow: BrowserWindow) {
|
|||
willClose = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -29,4 +29,4 @@ contextBridge.exposeInMainWorld('electron', {
|
|||
// payload: EventPayloadMapping[Key]
|
||||
// ) {
|
||||
// ipcRenderer.send(key, payload);
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ import {
|
|||
SettingsIcon,
|
||||
TimerIcon,
|
||||
HelpCircleIcon,
|
||||
Pause,
|
||||
ShieldBan,
|
||||
ChevronDown,
|
||||
Play,
|
||||
} from "lucide-react";
|
||||
|
||||
interface User {
|
||||
|
|
@ -37,6 +41,9 @@ interface ProjectTrackingData {
|
|||
endTime: Date | null;
|
||||
isTracking: boolean;
|
||||
isPaused: boolean;
|
||||
pauseStartTime: Date | null;
|
||||
totalPausedTime: number;
|
||||
accumulatedTime: number; // ✅ NEW: Stores time from previous sessions
|
||||
}
|
||||
|
||||
interface TrackingState {
|
||||
|
|
@ -54,12 +61,9 @@ interface TrackingState {
|
|||
projectsData: Project[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
user: User | null;
|
||||
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
||||
// trackingState: TrackingState;
|
||||
}
|
||||
|
||||
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||
|
|
@ -68,7 +72,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
// Tracking states
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const [isTracking, setIsTracking] = useState(false);
|
||||
|
|
@ -88,18 +91,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
Map<number, ProjectTrackingData>
|
||||
>(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 () => {
|
||||
try {
|
||||
const res = await window.electron.logout()
|
||||
|
|
@ -109,7 +100,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
navigate("/login");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.error("Logout error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -128,8 +119,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
{ 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(() => {
|
||||
if (selectedProject) {
|
||||
const projectData = projectTrackingMap.get(selectedProject.id);
|
||||
|
|
@ -143,14 +133,57 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
}
|
||||
}, [selectedProject, projectTrackingMap]);
|
||||
|
||||
// Start tracking
|
||||
const handleStartTracking = (project: Project) => {
|
||||
// Clear any existing timer
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
// ✅ Timer effect - calculates elapsed time based on timestamps + accumulated time
|
||||
useEffect(() => {
|
||||
if (isTracking && !isPaused && selectedProject) {
|
||||
timerRef.current = setInterval(() => {
|
||||
setProjectTrackingMap((prev) => {
|
||||
const updated = new Map(prev);
|
||||
const current = updated.get(selectedProject.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) => {
|
||||
const updated = new Map(prev);
|
||||
const current = updated.get(project.id) || {
|
||||
|
|
@ -159,46 +192,37 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
endTime: null,
|
||||
isTracking: false,
|
||||
isPaused: false,
|
||||
pauseStartTime: null,
|
||||
totalPausedTime: 0,
|
||||
accumulatedTime: 0,
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
// ✅ Keep accumulated time from previous sessions
|
||||
current.startTime = now;
|
||||
current.isTracking = true;
|
||||
current.isPaused = false;
|
||||
current.pauseStartTime = null;
|
||||
current.totalPausedTime = 0; // Reset pause time for new session
|
||||
updated.set(project.id, current);
|
||||
|
||||
// Update global tracking states
|
||||
setIsTracking(true);
|
||||
setIsPaused(false);
|
||||
setStartTime(now);
|
||||
setElapsedTime(current.accumulatedTime); // Start from accumulated time
|
||||
|
||||
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 = () => {
|
||||
if (!selectedProject) return;
|
||||
|
||||
// Get current state before toggling to avoid double-click issues
|
||||
const currentProjectData = projectTrackingMap.get(selectedProject.id);
|
||||
if (!currentProjectData || !currentProjectData.isTracking) return;
|
||||
|
||||
const now = new Date();
|
||||
const newPausedState = !currentProjectData.isPaused;
|
||||
|
||||
setProjectTrackingMap((prev) => {
|
||||
|
|
@ -206,48 +230,63 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
const current = updated.get(selectedProject.id);
|
||||
if (!current) return updated;
|
||||
|
||||
// Set the new pause state
|
||||
current.isPaused = newPausedState;
|
||||
if (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 });
|
||||
setIsPaused(newPausedState);
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Update global isPaused state immediately
|
||||
setIsPaused(newPausedState);
|
||||
};
|
||||
|
||||
// Stop tracking
|
||||
// ✅ Stop tracking - PRESERVES elapsed time
|
||||
const handleStopTracking = () => {
|
||||
if (!selectedProject) return;
|
||||
|
||||
// Clear the timer
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
// Update project tracking map
|
||||
const now = new Date();
|
||||
|
||||
setProjectTrackingMap((prev) => {
|
||||
const updated = new Map(prev);
|
||||
const current = updated.get(selectedProject.id);
|
||||
if (!current) return updated;
|
||||
|
||||
const now = new Date();
|
||||
// ✅ FIX: Store current elapsed time as accumulated time
|
||||
current.accumulatedTime = current.elapsedTime;
|
||||
current.isTracking = false;
|
||||
current.isPaused = false;
|
||||
current.endTime = now;
|
||||
current.startTime = null; // Clear startTime for stopped state
|
||||
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;
|
||||
});
|
||||
|
||||
// Reset global tracking states to allow selecting another project
|
||||
setIsTracking(false);
|
||||
setIsPaused(false);
|
||||
setEndTime(new Date());
|
||||
|
||||
console.log("projectTrackingMap", projectTrackingMap)
|
||||
console.log("Tracking stopped. Time preserved:", elapsedTime);
|
||||
};
|
||||
|
||||
// Cleanup timer on unmount
|
||||
|
|
@ -259,16 +298,14 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Handle project selection
|
||||
// ✅ Handle project selection
|
||||
const handleProjectSelect = (id: number) => {
|
||||
// Only allow project selection when not tracking
|
||||
if (isTracking) return;
|
||||
|
||||
const project = projectsData.find((p) => p.id === id);
|
||||
if (project) {
|
||||
setSelectedProject(project);
|
||||
|
||||
// Load existing tracking data if available
|
||||
const projectData = projectTrackingMap.get(id);
|
||||
if (projectData) {
|
||||
setElapsedTime(projectData.elapsedTime);
|
||||
|
|
@ -277,7 +314,6 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
setIsTracking(projectData.isTracking);
|
||||
setIsPaused(projectData.isPaused);
|
||||
} else {
|
||||
// Reset states for new project
|
||||
setElapsedTime(0);
|
||||
setStartTime(null);
|
||||
setEndTime(null);
|
||||
|
|
@ -302,132 +338,129 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
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 (
|
||||
<div className="flex h-screen bg-base-200 overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
fixed md:relative inset-y-0 left-0 z-40 transform
|
||||
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out
|
||||
${isMobile
|
||||
? isSidebarOpen
|
||||
? "translate-x-0 w-64"
|
||||
: "-translate-x-full w-64"
|
||||
: isSidebarOpen
|
||||
? "w-50"
|
||||
: "w-20"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Sidebar Header */}
|
||||
<div className="h-14 text-xl font-bold flex justify-between items-center border-b border-base-300">
|
||||
{isSidebarOpen ? (
|
||||
<span className="whitespace-nowrap mx-auto">VA Tracker</span>
|
||||
<>
|
||||
{/* Header */}
|
||||
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30">
|
||||
{/* Left Section */}
|
||||
<div className="flex items-center space-x-3">
|
||||
{selectedProject ? (
|
||||
isTracking ? (
|
||||
<button
|
||||
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
|
||||
onClick={handleStopTracking}
|
||||
title="Stop Tracking"
|
||||
>
|
||||
<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>
|
||||
)
|
||||
) : (
|
||||
<span className="text-primary font-extrabold mx-auto">VA</span>
|
||||
)}
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="btn btn-ghost btn-sm md:hidden"
|
||||
aria-label="Close sidebar"
|
||||
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"
|
||||
>
|
||||
<X size={20} />
|
||||
<Play size={16} />
|
||||
</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>
|
||||
|
||||
{/* Menu */}
|
||||
<ul className="menu px-2 py-4">
|
||||
{menuItems.map((item) => (
|
||||
<li key={item.label}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
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>
|
||||
{/* Right Section */}
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Timer */}
|
||||
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
|
||||
{formatTime(elapsedTime || 0)}
|
||||
</div>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{isMobile && isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden"
|
||||
onClick={closeSidebarOnMobile}
|
||||
></div>
|
||||
)}
|
||||
{/* Alert Icon */}
|
||||
<span className="text-error">
|
||||
<ShieldBan size={18} />
|
||||
</span>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-col flex-1 bg-base-200 transition-all duration-300 ease-in-out">
|
||||
{/* Navbar */}
|
||||
<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">
|
||||
{/* Left */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="btn btn-ghost btn-square"
|
||||
aria-label="Toggle sidebar"
|
||||
{/* Dropdown */}
|
||||
<div className="dropdown dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-sm btn-outline btn-primary p-1.5 rounded-lg"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</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>
|
||||
<ChevronDown />
|
||||
</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>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
||||
{/* Layout Body */}
|
||||
<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 cursor-pointer"
|
||||
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} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
||||
export default DashboardLayout;
|
||||
Loading…
Reference in New Issue