axios setup
This commit is contained in:
parent
e11d4e52f2
commit
6783fba264
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VA Tracker</title>
|
||||
<meta
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
// 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>;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -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<any> => {
|
||||
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;
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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<Buffer> {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Key extends keyof EventPayloadMapping>(
|
|||
) {
|
||||
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),
|
||||
});
|
||||
|
|
@ -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<string | null> {
|
||||
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<void> {
|
||||
const encryptedBuffer = safeStorage.encryptString(token);
|
||||
store.set(TOKEN_KEY, encryptedBuffer.toString('base64'));
|
||||
}
|
||||
|
||||
export async function clearToken(TOKEN_KEY: string): Promise<void> {
|
||||
store.delete(TOKEN_KEY);
|
||||
}
|
||||
|
|
@ -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<Buffer> {
|
||||
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" };
|
||||
}
|
||||
}
|
||||
|
|
@ -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<User | null>(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 (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
|
@ -65,8 +68,8 @@ const App: React.FC = () => {
|
|||
<Route path="projects" element={<Projects />} />
|
||||
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
|
||||
<Route path="timesheet" element={<Timesheets />} />
|
||||
<Route path="settings" element={<Settings/>} />
|
||||
<Route path="help-support" element={<HelpSupport/>} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="help-support" element={<HelpSupport />} />
|
||||
</Route>
|
||||
|
||||
{/* Default Redirect */}
|
||||
|
|
|
|||
|
|
@ -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<number, ProjectTrackingData>;
|
||||
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<React.SetStateAction<User | null>>;
|
||||
// trackingState: TrackingState;
|
||||
}
|
||||
|
||||
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||
|
|
@ -36,6 +68,26 @@ 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);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [elapsedTime, setElapsedTime] = useState(0);
|
||||
const [startTime, setStartTime] = useState<Date | null>(null);
|
||||
const [endTime, setEndTime] = useState<Date | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const [projectsData, setProjectsData] = useState<Project[]>([
|
||||
{ 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<number, ProjectTrackingData>
|
||||
>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const mobile = window.innerWidth < 768;
|
||||
|
|
@ -69,6 +121,180 @@ 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
|
||||
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 (
|
||||
<div className="flex h-screen bg-base-200 overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
|
|
@ -192,7 +418,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
|||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
||||
<Outlet />
|
||||
<Outlet context={trackingState}/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,347 +1,161 @@
|
|||
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<WidgetProps> = ({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
icon,
|
||||
trend,
|
||||
color = "primary",
|
||||
}) => {
|
||||
return (
|
||||
interface ProjectTrackingData {
|
||||
elapsedTime: number;
|
||||
startTime: Date | null;
|
||||
endTime: Date | null;
|
||||
isTracking: boolean;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
interface TrackingState {
|
||||
selectedProject: Project | null;
|
||||
projectTrackingMap: Map<number, ProjectTrackingData>;
|
||||
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" }) => (
|
||||
<div className="card bg-base-100 shadow-sm hover:shadow-lg transition-shadow">
|
||||
<div className="card-body">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className=" font-medium text-base-content/60">{title}</h3>
|
||||
<div>
|
||||
<h3 className="font-medium text-base-content/60">{title}</h3>
|
||||
<p className="text-sm font-bold mt-2">{value}</p>
|
||||
</div>
|
||||
<div className={`p-3 bg-${color}/10 rounded-lg`}>
|
||||
<div className={`text-${color}`}>{icon}</div>
|
||||
</div>
|
||||
</div>
|
||||
{subtitle && <p className="text-xs text-base-content/60 mt-2">{subtitle}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
|
||||
// Main Dashboard Component
|
||||
// ----- Dashboard -----
|
||||
const Dashboard: React.FC = () => {
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const [isTracking, setIsTracking] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [elapsedTime, setElapsedTime] = useState(0); // in seconds
|
||||
const [startTime, setStartTime] = useState<Date | null>(null);
|
||||
const [endTime, setEndTime] = useState<Date | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const {
|
||||
selectedProject,
|
||||
projectTrackingMap,
|
||||
isTracking,
|
||||
isPaused,
|
||||
elapsedTime,
|
||||
startTime,
|
||||
handleStartTracking,
|
||||
handlePauseResume,
|
||||
handleStopTracking,
|
||||
handleProjectSelect,
|
||||
projectsData,
|
||||
} = useOutletContext<TrackingState>();
|
||||
|
||||
// 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 (
|
||||
<main className="flex-1 overflow-y-auto md:px-0.5 md:py-0 no-scrollbar">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-4">
|
||||
<h1 className="text-lg font-bold">Dashboard</h1>
|
||||
|
||||
<main className="flex-1 overflow-y-auto no-scrollbar">
|
||||
{/* Header */}
|
||||
<div className="mb-2">
|
||||
<h1 className="text-md font-bold">Dashboard</h1>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await window.electron.uploadScreenshot();
|
||||
}}
|
||||
className="btn btn-xs btn-outline mt-2"
|
||||
>
|
||||
ScreenShot Test
|
||||
Screenshot Test
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Widgets Grid */}
|
||||
<div className="mb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4 ">
|
||||
{/* Widgets */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<Widget
|
||||
title="Logged Hours Today"
|
||||
value="6h 24m"
|
||||
subtitle="Today's total time"
|
||||
icon={<Clock size={24} />}
|
||||
trend={12}
|
||||
color="primary"
|
||||
/>
|
||||
<Widget
|
||||
title="Logged Hours This Week"
|
||||
value="4h 48m"
|
||||
subtitle="75% productivity"
|
||||
icon={<TrendingUp size={24} />}
|
||||
trend={8}
|
||||
color="success"
|
||||
/>
|
||||
<Widget
|
||||
title="Active Project"
|
||||
value={selectedProject ? selectedProject.projectName : "—"}
|
||||
subtitle="Until day ends"
|
||||
icon={<ProjectorIcon size={24} />}
|
||||
color="warning"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tracking Section */}
|
||||
<div className="w-full mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-md font-bold">Tracking</span>
|
||||
</div>
|
||||
|
||||
{/* Tracking Card */}
|
||||
<div className="w-full mb-4">
|
||||
<div className="text-md mb-2 font-bold flex justify-between items-center">
|
||||
<span>Tracking</span>
|
||||
{isTracking && (
|
||||
<div className="card bg-base-100 border border-base-200 shadow-sm">
|
||||
<div className="card-body">
|
||||
{selectedProject ? (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="text-center sm:text-left">
|
||||
<p className="text-lg font-extrabold text-success">
|
||||
{formatTime(selectedData?.elapsedTime || 0)}
|
||||
</p>
|
||||
<p className="text-xs text-base-content/70 mt-1">
|
||||
{selectedData?.isTracking ? "Current Session" : "Ready"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!selectedData?.isTracking ? (
|
||||
<button
|
||||
onClick={() => handleStartTracking(selectedProject)}
|
||||
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
|
||||
>
|
||||
<Play className="w-3 h-3" /> Start Tracking
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-xs btn-error rounded-full"
|
||||
onClick={handleStopTracking}
|
||||
|
|
@ -352,92 +166,49 @@ const Dashboard: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="card bg-base-100 shadow-sm border border-base-200 transition-all duration-300 w-full">
|
||||
<div className="card-body">
|
||||
{selectedProject ? (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="text-center sm:text-left">
|
||||
<p className="text-sm font-extrabold text-success tracking-tight">
|
||||
{formatTime(elapsedTime)}
|
||||
</p>
|
||||
<p className="text-xs text-base-content/70 mt-1">
|
||||
{isTracking ? "Current Session" : "Ready to Start"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isTracking ? (
|
||||
<button
|
||||
onClick={handleStartTracking}
|
||||
className="btn btn-sm btn-success rounded-xl hover:scale-105 transition-transform duration-200 w-full sm:w-auto"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
Start Tracking
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handlePauseResume}
|
||||
className={`btn btn-sm rounded-xl transition-transform duration-200 w-full sm:w-auto ${isPaused ? "btn-primary" : "btn-warning"
|
||||
}`}
|
||||
>
|
||||
{isPaused ? (
|
||||
<>
|
||||
<Play className="w-3 h-3" />
|
||||
Resume
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pause className="w-3 h-3" />
|
||||
Pause
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="divider my-2"></div>
|
||||
<div className="divider my-3" />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
|
||||
{/* Project Name */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className="text-base-content/70">Project:</span>
|
||||
<span className="font-semibold text-base-content truncate">
|
||||
<div>
|
||||
<span className="text-base-content/70">Project:</span>{" "}
|
||||
<span className="font-semibold">
|
||||
{selectedProject.projectName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Client Name */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className="text-base-content/70">Client:</span>
|
||||
<span className="truncate">{selectedProject.clientName}</span>
|
||||
<div>
|
||||
<span className="text-base-content/70">Client:</span>{" "}
|
||||
<span>{selectedProject.clientName}</span>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className="text-base-content/70">Status:</span>
|
||||
<div>
|
||||
<span className="text-base-content/70">Status:</span>{" "}
|
||||
<span
|
||||
className={`badge ${isPaused
|
||||
className={`badge ${selectedData?.isPaused
|
||||
? "badge-warning"
|
||||
: isTracking
|
||||
: selectedData?.isTracking
|
||||
? "badge-success"
|
||||
: "badge-neutral"
|
||||
}`}
|
||||
>
|
||||
{isPaused ? "Paused" : isTracking ? "Active" : "Ready"}
|
||||
{selectedData?.isPaused
|
||||
? "Paused"
|
||||
: selectedData?.isTracking
|
||||
? "Active"
|
||||
: "Ready"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Start Time */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className="text-base-content/70">Start Time:</span>
|
||||
<span>{startTime?.toLocaleTimeString() || "—"}</span>
|
||||
<div>
|
||||
<span className="text-base-content/70">Start:</span>{" "}
|
||||
<span>
|
||||
{selectedData?.startTime
|
||||
? selectedData.startTime.toLocaleTimeString()
|
||||
: "—"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-base-content/60 text-center">
|
||||
Select a project to start tracking.
|
||||
Select a project below to start tracking.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -447,55 +218,44 @@ const Dashboard: React.FC = () => {
|
|||
{/* Projects Table */}
|
||||
<div>
|
||||
<p className="text-md mb-2 font-bold">Projects</p>
|
||||
<div
|
||||
className="overflow-y-auto no-scrollbar overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
|
||||
style={{ minHeight: "100px", maxHeight: "30vh" }}
|
||||
>
|
||||
<div className="overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md">
|
||||
<table className="table table-zebra w-full text-sm">
|
||||
<thead className="sticky top-0 bg-base-200 z-10">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Project Name</th>
|
||||
<th>Client Name</th>
|
||||
<th>Project</th>
|
||||
<th>Client</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projectsData.map((project) => (
|
||||
<tr key={project.id} className="align-middle">
|
||||
<th>{project.id}</th>
|
||||
{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 (
|
||||
<tr key={project.id}>
|
||||
<td>{project.id}</td>
|
||||
<td>{project.projectName}</td>
|
||||
<td>{project.clientName}</td>
|
||||
<td>
|
||||
<div
|
||||
className={`badge badge-sm whitespace-nowrap ${project.status === "Active"
|
||||
? "badge-success"
|
||||
: project.status === "Pending"
|
||||
? "badge-warning"
|
||||
: project.status === "Completed"
|
||||
? "badge-info"
|
||||
: "badge-neutral"
|
||||
<button
|
||||
className={`btn btn-xs ${isCurrentlySelected
|
||||
? "btn-success"
|
||||
: "btn-primary"
|
||||
}`}
|
||||
onClick={() => handleProjectSelect(project.id)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{project.status}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className={`badge badge-sm whitespace-nowrap ${project.action === "View Report"
|
||||
? "badge-info"
|
||||
: project.action === "Resume"
|
||||
? "badge-secondary"
|
||||
: "badge-primary"
|
||||
}`}
|
||||
>
|
||||
Select
|
||||
{isCurrentlySelected ? "Selected" : "Select"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface LoginProps {
|
|||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||
const [formData, setFormData] = useState<User>({ email: 'jai@gmail.com', password: '12345' })
|
||||
const [formData, setFormData] = useState<User>({ email: '', password: '' })
|
||||
const inputRef = useRef<HTMLFormElement | null>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue