diff --git a/index.html b/index.html index 2fa73c5..1ea5a8d 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + VA Tracker ; +} + +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, + }; + } +}); diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts new file mode 100644 index 0000000..502cf47 --- /dev/null +++ b/src/electron/api/axiosClient.ts @@ -0,0 +1,38 @@ +// electron/api/axiosClient.ts +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { getToken } from '../utils/tokenHelper.js'; + +const TOKEN_KEY = 'auth_token'; + +const api: AxiosInstance = axios.create({ + baseURL: 'https://api.example.com', // ๐Ÿ” your backend + timeout: 10000, +}); + +// --- Request Interceptor --- +api.interceptors.request.use( + async (config: AxiosRequestConfig) : Promise => { + const token = await getToken(TOKEN_KEY); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + + console.log(`[Axios] ${config.method?.toUpperCase()} ${config.url}`); + return config; + }, + (error) => Promise.reject(error) +); + +// --- Response Interceptor --- +api.interceptors.response.use( + (response: AxiosResponse) => response.data, + async (error) => { + console.error('[Axios Error]', error.message); + if (error.response?.status === 401) { + console.warn('Token expired โ€” trigger refresh logic if needed'); + } + return Promise.reject(error); + } +); + +export default api; diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts new file mode 100644 index 0000000..c73db15 --- /dev/null +++ b/src/electron/ipc/ipcHandler.ts @@ -0,0 +1,28 @@ +// electron/ipc/tokenHandler.ts +import { ipcMain } from 'electron'; +import { getToken, saveToken, clearToken } from '../utils/tokenHelper.js'; +import { handleLogin, takeScreenshot } from '../utils/utilityFunctions.js'; + +ipcMain.handle('get-token', async (_, TOKEN_KEY: string) => { + return await getToken(TOKEN_KEY); +}); + +ipcMain.handle('save-token', async (_, TOKEN_KEY: string, token: string) => { + await saveToken(TOKEN_KEY, token); + return true; +}); + +ipcMain.handle('clear-token', async (_, TOKEN_KEY: string) => { + await clearToken(TOKEN_KEY); + return true; +}); + + +ipcMain.handle('upload-screenshot', async () => { + takeScreenshot() +}) + +// Handle login request from renderer +ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => { + return handleLogin(credentials); +}); \ No newline at end of file diff --git a/src/electron/main.ts b/src/electron/main.ts index 44672f1..c30d46d 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -1,15 +1,9 @@ -import { app, BrowserWindow, Menu, desktopCapturer, ipcMain, safeStorage } from 'electron'; +import { app, BrowserWindow } from 'electron'; import { ipcMainHandle, ipcMainOn, isDev } from './util.js'; import { getStaticData, pollResources } from './resourceManager.js'; import { getPreloadPath, getUIPath } from './pathResolver.js'; -import { createTray } from './tray.js'; -import { createMenu } from './menu.js'; -import Store from 'electron-store'; -import * as fs from "fs"; -import path from "path"; -import axios from 'axios'; -import sharp from 'sharp'; +import "./ipc/ipcHandler.js" app.on('ready', async () => { console.log("ready") @@ -56,132 +50,6 @@ app.on('ready', async () => { // await takeScreenshot(); }); -// Blur using an offscreen canvas -async function blurAndCompressBase64Image(base64: string) : Promise { - const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64'); - const outputBuffer = await sharp(buffer) - .blur(8) // adjust blur radius here - .jpeg({ quality: 70 }) // compress output - .toBuffer(); - return outputBuffer; -} - -async function takeScreenshot() { - // const sources = await desktopCapturer.getSources({ types: ["screen"] }); - const sources = await desktopCapturer.getSources({ - types: ['window'], thumbnailSize: { - height: 768, - width: 1366 - } - }); - - - // Capture the first screen (you can choose others by filtering sources) - const screen = sources[0]; - let image = screen.thumbnail.toDataURL(); - console.log("blurAndCompressBase64Image Calling") - - const blurredBuffer = await blurAndCompressBase64Image(image); - - const folderPath = "C:\\Users\\Public\\vatracker\\screenshots"; - - // Ensure directory exists - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath, { recursive: true }); - } - - const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`); - fs.writeFileSync(filePath, blurredBuffer); - - - - console.log(`Screenshot saved at: ${filePath}`); -} - -ipcMain.handle('upload-screenshot', async () => { - takeScreenshot() -}) - -async function saveToken(accessToken: string, refreshToken: string | null = null) { - const encryptedAccessToken = safeStorage.encryptString(accessToken); - let encryptedRefreshToken = null; - - if (refreshToken != null) { - encryptedRefreshToken = safeStorage.encryptString(refreshToken); - } - // Store encrypted token (example: using a file or electron-store) - // For simplicity, using Electron's built-in localStorage is not available in main, so use electron-store or filesystem. - // Here assume electron-store is already installed and set up: - const store = new Store(); - store.set("access_token", encryptedAccessToken.toString('base64')); - if (encryptedRefreshToken != null) { - store.set("refresh_token", encryptedRefreshToken.toString('base64')); - } -} - -async function getToken(TOKEN_KEY: string) { - const store = new Store(); - const encryptedBase64 = store.get(TOKEN_KEY) as string | undefined; - if (!encryptedBase64) return null; - const encryptedBuffer = Buffer.from(encryptedBase64, 'base64'); - return safeStorage.decryptString(encryptedBuffer); -} - -ipcMain.handle('get-token', async (event, TOKEN_KEY) => { - // return decrypted token from secure storage - return await getToken(TOKEN_KEY); -}); - -ipcMain.handle('save-token', async (event, token) => { - await saveToken(token); - return true; -}); - -// Handle login request from renderer -ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => { - try { - // Call your login API - console.log("๐ŸŸก login Request received", credentials); - - try { - console.log("โžก๏ธ Calling API..."); - const response = await axios.post( - 'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login', - { userDetails: { ...credentials } }, - { headers: { "ngrok-skip-browser-warning": 69420 } } - ); - - const { accessToken, refreshToken } = response.data; - await saveToken(accessToken, refreshToken); - return { success: true, user: response.data.user }; - - } catch (error) { - if (error instanceof Error) { - if ("response" in error && typeof (error.response) == "object" && error.response !== null && ("data" in error.response)) { - console.log("โŒ login API call failed:", error?.response.data); - console.log("Server responded:", error?.response.data); - return { success: false, error: error?.response.data || "Request failed" }; - } else { - return { success: false, error: error?.message || "Check you Internet connection and try again" }; - } - } - } - - } catch (err) { - if (err instanceof Error) { - return { success: false, error: err.message }; - } - - if (typeof err === "object" && err !== null && "response" in err) { - const axiosErr = err as any; - return { success: false, error: axiosErr.response?.data?.message || "Unknown error" }; - } - - return { success: false, error: "Unexpected error occurred" }; - } -}); - - function handleCloseEvents(mainWindow: BrowserWindow) { let willClose = false; diff --git a/src/electron/preload.cts b/src/electron/preload.ts similarity index 83% rename from src/electron/preload.cts rename to src/electron/preload.ts index 4e1ab97..0d1361c 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.ts @@ -3,6 +3,8 @@ import { contextBridge, ipcRenderer } from "electron"; const electron = require('electron'); console.log("setting PreLoad") + + contextBridge.exposeInMainWorld('electron', { subscribeStatistics: (callback) => ipcOn('statistics', (stats) => { @@ -40,3 +42,10 @@ function ipcSend( ) { 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), +}); \ No newline at end of file diff --git a/src/electron/utils/tokenHelper.ts b/src/electron/utils/tokenHelper.ts new file mode 100644 index 0000000..805cc4f --- /dev/null +++ b/src/electron/utils/tokenHelper.ts @@ -0,0 +1,21 @@ +// electron/utils/tokenHelper.ts +import { safeStorage } from 'electron'; +import Store from 'electron-store'; + +const store = new Store(); + +export async function getToken(TOKEN_KEY: string): Promise { + const encryptedBase64 = store.get(TOKEN_KEY) as string | undefined; + if (!encryptedBase64) return null; + const encryptedBuffer = Buffer.from(encryptedBase64, 'base64'); + return safeStorage.decryptString(encryptedBuffer); +} + +export async function saveToken(TOKEN_KEY: string, token: string): Promise { + const encryptedBuffer = safeStorage.encryptString(token); + store.set(TOKEN_KEY, encryptedBuffer.toString('base64')); +} + +export async function clearToken(TOKEN_KEY: string): Promise { + store.delete(TOKEN_KEY); +} diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts new file mode 100644 index 0000000..46bb829 --- /dev/null +++ b/src/electron/utils/utilityFunctions.ts @@ -0,0 +1,92 @@ +import { desktopCapturer } from "electron"; +import * as fs from "fs"; +import path from "path"; +import axios from 'axios'; +import sharp from 'sharp'; +import { saveToken } from "./tokenHelper.js"; + +// Blur using an offscreen canvas +async function blurAndCompressBase64Image(base64: string): Promise { + const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64'); + const outputBuffer = await sharp(buffer) + .blur(8) // adjust blur radius here + .jpeg({ quality: 70 }) // compress output + .toBuffer(); + return outputBuffer; +} + +export const takeScreenshot = async () => { + // const sources = await desktopCapturer.getSources({ types: ["screen"] }); + const sources = await desktopCapturer.getSources({ + types: ['window'], thumbnailSize: { + height: 768, + width: 1366 + } + }); + + + // Capture the first screen (you can choose others by filtering sources) + const screen = sources[0]; + let image = screen.thumbnail.toDataURL(); + console.log("blurAndCompressBase64Image Calling") + + const blurredBuffer = await blurAndCompressBase64Image(image); + + const folderPath = "C:\\Users\\Public\\vatracker\\screenshots"; + + // Ensure directory exists + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + + const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`); + fs.writeFileSync(filePath, blurredBuffer); + + + + console.log(`Screenshot saved at: ${filePath}`); +} + +export const handleLogin = async (credentials: { email: string, password: string }) => { + try { + // Call your login API + console.log("๐ŸŸก login Request received", credentials); + + try { + console.log("โžก๏ธ Calling API..."); + const response = await axios.post( + 'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login', + { userDetails: { ...credentials } }, + { headers: { "ngrok-skip-browser-warning": 69420 } } + ); + + console.log("API CAlled", response) + const { accessToken, refreshToken } = response.data; + await saveToken(accessToken, refreshToken); + return { success: true, user: response.data.user }; + + } catch (error) { + if (error instanceof Error) { + if ("response" in error && typeof (error.response) == "object" && error.response !== null && ("data" in error.response)) { + console.log("โŒ login API call failed:", error?.response.data); + console.log("Server responded:", error?.response.data); + return { success: false, error: error?.response.data || "Request failed" }; + } else { + return { success: false, error: error?.message || "Check you Internet connection and try again" }; + } + } + } + + } catch (err) { + if (err instanceof Error) { + return { success: false, error: err.message }; + } + + if (typeof err === "object" && err !== null && "response" in err) { + const axiosErr = err as any; + return { success: false, error: axiosErr.response?.data?.message || "Unknown error" }; + } + + return { success: false, error: "Unexpected error occurred" }; + } +} \ No newline at end of file diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 22f3cd7..c5f0a6e 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; @@ -12,22 +12,23 @@ import Timesheets from "./pages/Timesheets/Timesheets"; import Settings from "./pages/Settings/Settings"; import HelpSupport from "./pages/HelpSupport/HelpSupport"; -// โœ… Define User type (customize based on your structure) interface User { email: string; password: string; } + const App: React.FC = () => { const theme = ThemeToggle(); const [user, setUser] = useState(null); - // โœ… Apply theme + + // Apply theme useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); - // โœ… Load user from localStorage + // Load user from localStorage useEffect(() => { const storedUser = localStorage.getItem("user"); if (storedUser) { @@ -39,6 +40,8 @@ const App: React.FC = () => { } }, []); + + return ( @@ -65,8 +68,8 @@ const App: React.FC = () => { } /> } /> } /> - } /> - } /> + } /> + } /> {/* Default Redirect */} @@ -79,4 +82,4 @@ const App: React.FC = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 8331fd3..cb3ecac 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Outlet, NavLink, @@ -25,9 +25,41 @@ interface User { password: string; } +interface Project { + id: number; + projectName: string; + clientName: string; +} + +interface ProjectTrackingData { + elapsedTime: number; + startTime: Date | null; + endTime: Date | null; + isTracking: boolean; + isPaused: boolean; +} + +interface TrackingState { + selectedProject: Project | null; + projectTrackingMap: Map; + isTracking: boolean; + isPaused: boolean; + elapsedTime: number; + startTime: Date | null; + endTime: Date | null; + handleStartTracking: (project: Project) => void; + handlePauseResume: () => void; + handleStopTracking: () => void; + handleProjectSelect: (id: number) => void; + projectsData: Project[]; +} + + + interface DashboardLayoutProps { user: User | null; setUser: React.Dispatch>; + // trackingState: TrackingState; } const DashboardLayout: React.FC = ({ setUser }) => { @@ -36,6 +68,26 @@ const DashboardLayout: React.FC = ({ setUser }) => { const navigate = useNavigate(); const location = useLocation(); + + // Tracking states + const [selectedProject, setSelectedProject] = useState(null); + const [isTracking, setIsTracking] = useState(false); + const [isPaused, setIsPaused] = useState(false); + const [elapsedTime, setElapsedTime] = useState(0); + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + const timerRef = useRef | null>(null); + + const [projectsData, setProjectsData] = useState([ + { id: 1, projectName: "CRM Redesign", clientName: "Salesforce" }, + { id: 2, projectName: "AI Assistant", clientName: "OpenAI" }, + { id: 3, projectName: "Marketing Dashboard", clientName: "Google" }, + ]); + + const [projectTrackingMap, setProjectTrackingMap] = useState< + Map + >(new Map()); + useEffect(() => { const handleResize = () => { const mobile = window.innerWidth < 768; @@ -69,6 +121,180 @@ const DashboardLayout: React.FC = ({ setUser }) => { { label: "Help & Support", path: "/dashboard/help-support", icon: }, ]; + + // Sync global states with selected project tracking data + useEffect(() => { + if (selectedProject) { + const projectData = projectTrackingMap.get(selectedProject.id); + if (projectData) { + setIsTracking(projectData.isTracking); + setIsPaused(projectData.isPaused); + setElapsedTime(projectData.elapsedTime); + setStartTime(projectData.startTime); + setEndTime(projectData.endTime); + } + } + }, [selectedProject, projectTrackingMap]); + + // Start tracking + const handleStartTracking = (project: Project) => { + // Clear any existing timer + if (timerRef.current) { + clearInterval(timerRef.current); + } + + // Update project tracking map + setProjectTrackingMap((prev) => { + const updated = new Map(prev); + const current = updated.get(project.id) || { + elapsedTime: 0, + startTime: null, + endTime: null, + isTracking: false, + isPaused: false, + }; + + const now = new Date(); + current.startTime = now; + current.isTracking = true; + current.isPaused = false; + updated.set(project.id, current); + + 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 + 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 newPausedState = !currentProjectData.isPaused; + + setProjectTrackingMap((prev) => { + const updated = new Map(prev); + const current = updated.get(selectedProject.id); + if (!current) return updated; + + // Set the new pause state + current.isPaused = newPausedState; + updated.set(selectedProject.id, { ...current }); + + return updated; + }); + + // Update global isPaused state immediately + setIsPaused(newPausedState); + }; + + // Stop tracking + const handleStopTracking = () => { + if (!selectedProject) return; + + // Clear the timer + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + + // Update project tracking map + setProjectTrackingMap((prev) => { + const updated = new Map(prev); + const current = updated.get(selectedProject.id); + if (!current) return updated; + + const now = new Date(); + current.isTracking = false; + current.isPaused = false; + current.endTime = now; + updated.set(selectedProject.id, { ...current }); + + return updated; + }); + + // Reset global tracking states to allow selecting another project + setIsTracking(false); + setIsPaused(false); + setEndTime(new Date()); + + console.log("projectTrackingMap",projectTrackingMap) + }; + + // Cleanup timer on unmount + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + + // 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); + setStartTime(projectData.startTime); + setEndTime(projectData.endTime); + setIsTracking(projectData.isTracking); + setIsPaused(projectData.isPaused); + } else { + // Reset states for new project + setElapsedTime(0); + setStartTime(null); + setEndTime(null); + setIsTracking(false); + setIsPaused(false); + } + } + }; + + const trackingState: TrackingState = { + selectedProject, + projectTrackingMap, + isTracking, + isPaused, + elapsedTime, + startTime, + endTime, + handleStartTracking, + handlePauseResume, + handleStopTracking, + projectsData, + handleProjectSelect, + }; + return (
{/* Sidebar */} @@ -192,7 +418,7 @@ const DashboardLayout: React.FC = ({ setUser }) => { {/* Page Content */}
- +
diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index a1ac0c4..a28f51f 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -1,443 +1,214 @@ -import React, { useState, useEffect, useRef } from "react"; +import React from "react"; import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react"; +import { useOutletContext } from "react-router-dom"; -const projectsData = [ - { - id: 1, - projectName: "Employee Tracker", - clientName: "TechCorp Pvt Ltd", - status: "Active", - action: "Start Tracking", - }, - { - id: 2, - projectName: "Sales Dashboard", - clientName: "BlueEdge Analytics", - status: "Pending", - action: "Start Tracking", - }, - { - id: 3, - projectName: "Inventory System", - clientName: "LogiFlow Inc", - status: "Completed", - action: "View Report", - }, - { - id: 4, - projectName: "CRM Upgrade", - clientName: "NextWave Solutions", - status: "On Hold", - action: "Resume", - }, - { - id: 5, - projectName: "Mobile App Redesign", - clientName: "PixelFusion", - status: "Active", - action: "Start Tracking", - }, - { - id: 6, - projectName: "E-Commerce Backend", - clientName: "ShopEasy", - status: "Pending", - action: "Start Tracking", - }, - { - id: 7, - projectName: "Finance Tracker", - clientName: "MoneyWise", - status: "Active", - action: "Start Tracking", - }, - { - id: 8, - projectName: "Helpdesk Portal", - clientName: "ServePro", - status: "Completed", - action: "View Report", - }, - { - id: 9, - projectName: "Marketing Automation", - clientName: "AdBoost", - status: "Active", - action: "Start Tracking", - }, - { - id: 10, - projectName: "Data Pipeline", - clientName: "CloudWave", - status: "On Hold", - action: "Resume", - }, - { - id: 11, - projectName: "Support Chatbot", - clientName: "SmartServe", - status: "Active", - action: "Start Tracking", - }, - { - id: 12, - projectName: "Payroll System", - clientName: "PeopleFirst HR", - status: "Pending", - action: "Start Tracking", - }, - { - id: 13, - projectName: "IoT Dashboard", - clientName: "SensorGrid", - status: "Completed", - action: "View Report", - }, - { - id: 14, - projectName: "Security Portal", - clientName: "SafeNet", - status: "Active", - action: "Start Tracking", - }, - { - id: 15, - projectName: "Social Media Tool", - clientName: "Buzzify", - status: "Pending", - action: "Start Tracking", - }, - { - id: 16, - projectName: "AI Model Trainer", - clientName: "NeuroTech", - status: "On Hold", - action: "Resume", - }, - { - id: 17, - projectName: "Project Tracker", - clientName: "TaskFlow", - status: "Active", - action: "Start Tracking", - }, - { - id: 18, - projectName: "Booking Portal", - clientName: "GoTravel", - status: "Completed", - action: "View Report", - }, - { - id: 19, - projectName: "Time Logger", - clientName: "Clockify Pro", - status: "Active", - action: "Start Tracking", - }, - { - id: 20, - projectName: "Analytics Engine", - clientName: "DataStream", - status: "Pending", - action: "Start Tracking", - }, -]; - -interface WidgetProps { - title: string; - value: string | number; - subtitle?: string; - icon?: React.ReactNode; - trend?: number; - color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info"; -} - +// ----- Interfaces ----- interface Project { id: number; projectName: string; clientName: string; } -// Widget Component -const Widget: React.FC = ({ - title, - value, - subtitle, - icon, - trend, - color = "primary", -}) => { - return ( -
-
-
-
-

{title}

-

{value}

-
-
-
{icon}
-
-
-
-
- ); +interface ProjectTrackingData { + elapsedTime: number; + startTime: Date | null; + endTime: Date | null; + isTracking: boolean; + isPaused: boolean; +} + +interface TrackingState { + selectedProject: Project | null; + projectTrackingMap: Map; + isTracking: boolean; + isPaused: boolean; + elapsedTime: number; + startTime: Date | null; + endTime: Date | null; + handleStartTracking: (project: Project) => void; + handlePauseResume: () => void; + handleStopTracking: () => void; + handleProjectSelect: (id: number) => void; + projectsData: Project[]; +} + +// ----- Helper ----- +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`; }; +// ----- Widget ----- +const Widget: React.FC<{ + title: string; + value: string | number; + subtitle?: string; + icon?: React.ReactNode; + color?: + | "primary" + | "secondary" + | "accent" + | "success" + | "warning" + | "error" + | "info"; +}> = ({ title, value, subtitle, icon, color = "primary" }) => ( +
+
+
+
+

{title}

+

{value}

+
+
+
{icon}
+
+
+ {subtitle &&

{subtitle}

} +
+
+); -// Main Dashboard Component +// ----- Dashboard ----- const Dashboard: React.FC = () => { - const [selectedProject, setSelectedProject] = useState(null); - const [isTracking, setIsTracking] = useState(false); - const [isPaused, setIsPaused] = useState(false); - const [elapsedTime, setElapsedTime] = useState(0); // in seconds - const [startTime, setStartTime] = useState(null); - const [endTime, setEndTime] = useState(null); - const timerRef = useRef | null>(null); + const { + selectedProject, + projectTrackingMap, + isTracking, + isPaused, + elapsedTime, + startTime, + handleStartTracking, + handlePauseResume, + handleStopTracking, + handleProjectSelect, + projectsData, + } = useOutletContext(); - // Load cached project or auto-select single project - useEffect(() => { - const cachedProject = localStorage.getItem("activeProject"); - if (cachedProject) { - const parsed = JSON.parse(cachedProject) as Project; - setSelectedProject(parsed); - } else if (projectsData.length === 1) { - setSelectedProject(projectsData[0]); - } - }, []); - - // Format elapsed time (hh:mm:ss) - const formatTime = (seconds: number) => { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = seconds % 60; - return `${h}h ${m}m ${s}s`; - }; - - // Start tracking manually - const handleStartTracking = () => { - if (!selectedProject) return; - - setIsTracking(true); - setIsPaused(false); - setElapsedTime(0); - setStartTime(new Date()); - setEndTime(null); - - // Cache selected project - localStorage.setItem("activeProject", JSON.stringify(selectedProject)); - }; - - // โฑ Timer logic - useEffect(() => { - if (isTracking && !isPaused) { - timerRef.current = setInterval(() => { - setElapsedTime((prev) => prev + 1); - }, 1000); - } else if (timerRef.current) { - clearInterval(timerRef.current); - } - - return () => { - if (timerRef.current) clearInterval(timerRef.current); - }; - }, [isTracking, isPaused]); - - // โธ Pause/Resume button handler - const handlePauseResume = () => { - setIsPaused((prev) => !prev); - }; - - // Stop tracking - const handleStopTracking = () => { - if (!selectedProject) return; - - setIsTracking(false); - setIsPaused(false); - setEndTime(new Date()); - - if (timerRef.current) clearInterval(timerRef.current); - - // Keep project in view, just stop tracking. - // Remove tracking state but keep the selected project cached. - const summary = { - project: selectedProject.projectName, - totalTime: formatTime(elapsedTime), - start: startTime?.toLocaleTimeString(), - end: new Date().toLocaleTimeString(), - }; - - console.log("โœ… Final Tracking Summary:", summary); - - // Save the stopped state for reference - localStorage.setItem( - "activeProject", - JSON.stringify({ - ...selectedProject, - lastSession: summary, - }) - ); - - // Reset timer values but keep selected project - setElapsedTime(0); - }; - - // ๐Ÿงพ When user picks a project manually - const handleProjectSelect = (id: number) => { - if (isTracking) return; - const project = projectsData.find((item) => item.id === id); - if (project) { - setSelectedProject(project); - localStorage.setItem("activeProject", JSON.stringify(project)); - } - }; + const selectedData = selectedProject + ? projectTrackingMap.get(selectedProject.id) + : null; return ( -
- {/* Welcome Section */} -
-

Dashboard

- +
+ {/* Header */} +
+

Dashboard

- {/* Widgets Grid */} -
-
- } - trend={12} - color="primary" - /> - } - trend={8} - color="success" - /> - } - color="warning" - /> -
+ {/* Widgets */} +
+ } + color="primary" + /> + } + color="success" + /> + } + color="warning" + />
- {/* Tracking Card */} + {/* Tracking Section */}
-
- Tracking - {isTracking && ( - - )} +
+ Tracking
-
+
{selectedProject ? ( <>
-

- {formatTime(elapsedTime)} +

+ {formatTime(selectedData?.elapsedTime || 0)}

- {isTracking ? "Current Session" : "Ready to Start"} + {selectedData?.isTracking ? "Current Session" : "Ready"}

- {!isTracking ? ( + {!selectedData?.isTracking ? ( ) : ( )}
-
+
- {/* Project Name */} -
- Project: - +
+ Project:{" "} + {selectedProject.projectName}
- - {/* Client Name */} -
- Client: - {selectedProject.clientName} +
+ Client:{" "} + {selectedProject.clientName}
- - {/* Status */} -
- Status: +
+ Status:{" "} - {isPaused ? "Paused" : isTracking ? "Active" : "Ready"} + {selectedData?.isPaused + ? "Paused" + : selectedData?.isTracking + ? "Active" + : "Ready"}
- - {/* Start Time */} -
- Start Time: - {startTime?.toLocaleTimeString() || "โ€”"} +
+ Start:{" "} + + {selectedData?.startTime + ? selectedData.startTime.toLocaleTimeString() + : "โ€”"} +
- ) : (

- Select a project to start tracking. + Select a project below to start tracking.

)}
@@ -447,55 +218,44 @@ const Dashboard: React.FC = () => { {/* Projects Table */}

Projects

-
+
- - + + - {projectsData.map((project) => ( - - - - - - - - ))} - + {projectsData.map((project) => { + const isCurrentlySelected = selectedProject?.id === project.id; + // โœ… FIX: Disable ALL projects when ANY project is tracking + // Only enable selection when not tracking + const isDisabled = isTracking && !isCurrentlySelected; + return ( + + + + + + + ); + })} +
#Project NameClient NameProjectClient Action
{project.id}{project.projectName}{project.clientName} -
- {project.status} -
-
-
- Select - -
{project.id}{project.projectName}{project.clientName} + +
@@ -503,4 +263,4 @@ const Dashboard: React.FC = () => { ); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/src/ui/pages/Login.tsx b/src/ui/pages/Login.tsx index e5f5af0..d1fea2f 100644 --- a/src/ui/pages/Login.tsx +++ b/src/ui/pages/Login.tsx @@ -11,7 +11,7 @@ interface LoginProps { } const Login: React.FC = ({ setUser }) => { - const [formData, setFormData] = useState({ email: 'jai@gmail.com', password: '12345' }) + const [formData, setFormData] = useState({ email: '', password: '' }) const inputRef = useRef(null) const navigate = useNavigate()