This commit is contained in:
BhagwanSingh 2025-11-12 12:21:46 +05:30
commit 11398c6869
14 changed files with 1101 additions and 343 deletions

View File

@ -6,13 +6,13 @@ import chalk from "chalk";
const TOKEN_KEY = 'ACCESS_TOKEN';
const api: AxiosInstance = axios.create({
baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va', // 🔁 your backend
baseURL: 'https://dreadingly-unfighting-pandora.ngrok-free.dev/api/v1/va', // 🔁 your backend
// timeout: 10000,
});
console.log("Creating Axios Instance", axios.VERSION);
console.log("Axios .get reference:", api.get);
console.log("Axios .request reference:", api.request);
// 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(
@ -22,14 +22,9 @@ api.interceptors.request.use(
if (token) {
config.headers['token'] = token;
config.headers['ngrok-skip-browser-warning'] = 98098;
config.headers['ngrok-skip-browser-warning'] = 'true';
}
// 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)

View File

@ -55,13 +55,150 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
}
});
ipcMain.handle('resize-window', (event, width: number, height: number) => {
const mainWindow = getMainWindow()
console.log("toggle-close-btn Called", mainWindow)
// if (mainWindow && !mainWindow.isDestroyed()) {
ipcMain.handle("resize-window", (event, width: number, height: number) => {
const mainWindow = getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
console.log(`📏 Resizing window to ${width}x${height}`);
mainWindow.setResizable(true); // temporarily enable resizing
mainWindow.setSize(width, height);
mainWindow.setResizable(false); // disable again if you want to lock it
}
});
// ✅ Start background polling when main window is ready
};
// startWindowPolling(mainWindow);
ipcMain.handle("get-project-list", async () => {
// console.log("📡 get-project-list called");
try {
const res = await api.get("/list/projects");
// console.log(" API /list/project response:", res);
// ✅ Return only serializable data
return JSON.parse(JSON.stringify(res));
} catch (error: any) {
console.error(" get-project-list error:", error);
return {
success: false,
message: error?.message || String(error) || "Unknown error"
};
}
});
// When project tracking start
ipcMain.handle("start-project-tracking", async (event, payload) => {
try {
console.log("🚀 start-project-tracking called with:", payload);
const response = await api.post("/log/project", payload);
console.log(" Tracking start synced:", response);
return response;
} catch (error) {
console.error(" Error syncing project start:", error);
throw error;
}
});
// When project tracking stop
ipcMain.handle("stop-project-tracking", async (event, payload) => {
try {
console.log("stop-project-tracking called with:", payload);
const response = await api.post("/log/project", payload);
console.log(" Tracking stop synced:", response);
return response;
} catch (error) {
console.error(" Error syncing project start:", error);
throw error;
}
});
// tabs opening and closing when and tracking start
ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
try {
console.log("🟦 IPC tracking-tabs-opening-and-closing:", payload);
let response;
if (!payload.id) {
// 🟢 POST new log (new tab opened)
response = await api.post("/log/project/track", {
logId: payload.logId,
projectId: payload.projectId,
title: payload.title,
appName: payload.appName,
});
console.log("✅ New log created:", response.data);
} else {
// 🟡 PATCH existing log (same tab, update)
response = await api.patch("/log/project/track", {
id: payload.id,
logId: payload.logId,
projectId: payload.projectId,
title: payload.title,
appName: payload.appName,
});
console.log("🟨 Log updated:", response.data);
}
return response.data;
} catch (error) {
console.error("❌ Error syncing project tab tracking:", error);
throw error;
}
});
// ipcMain.handle("get-project-logs", async (event, projectID) => {
// try {
// console.log("get-project-logs",projectID)
// const response = await api.get(`/report/project/${projectID}`);
// console.log("response-logs 164" , response)
// return response;
// } catch (error) {
// console.error("Error fetching project logs:", error);
// throw error;
// }
})
// });
ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => {
try {
console.log("📥 get-project-logs called with:", { projectID, fromDate, toDate });
let url = `/report/project/${projectID}`;
console.log("url" , url)
// 🧠 Append query params only if dates are provided
if (fromDate && toDate) {
const params = new URLSearchParams({
from: fromDate,
to: toDate,
});
url += `?${params.toString()}`;
}
const response = await api.get(url);
console.log("✅ response-logs:", response);
return response;
} catch (error) {
console.error("❌ Error fetching project logs:", error);
throw error;
}
});
};

View File

@ -22,7 +22,7 @@ app.on('ready', async () => {
width: 500, // fixed width
height: 600, // fixed height
resizable: true, // prevents resizing
resizable: false, // prevents resizing
fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar)
minimizable: false, // disables minimize button
maximizable: false, // prevents maximize

View File

@ -10,10 +10,23 @@ contextBridge.exposeInMainWorld('electron', {
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
toggleCloseBtn: (shouldEnable: boolean) => ipcRenderer.invoke('toggle-close-btn', shouldEnable),
resizeWindow: (width: number, height: number) => ipcRenderer.invoke('resize-window', width, height),
getProjectList: () => ipcRenderer.invoke("get-project-list"),
getRunningApps: () => ipcRenderer.invoke('get-running-apps'),
captureAppScreenShot: (appTitle: string) : Promise<string> => ipcRenderer.invoke('capture-specific-window', appTitle),
onWindowUpdate: (callback) =>
ipcRenderer.on("window:updated", (_, data) => callback(data)),
startProjectTracking: (data) => ipcRenderer.invoke("start-project-tracking", data),
stopProjectTracking: (data) => ipcRenderer.invoke("stop-project-tracking", data),
trackingTabsOpeningClosing: (data) =>
ipcRenderer.invoke("tracking-tabs-opening-and-closing", data),
getProjectLogs: (
projectID: string,
fromDate?: string,
toDate?: string
) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }),
} satisfies Window['electron']);
// function ipcInvoke<Key extends keyof EventPayloadMapping>(

View File

@ -57,7 +57,7 @@ export const handleLogin = async (credentials: { email: string, password: string
try {
console.log("➡️ Calling API...");
const response = await axios.post(
'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login',
'https://dreadingly-unfighting-pandora.ngrok-free.dev/api/v1/va/login',
{ userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 } }
);

View File

@ -1,51 +1,141 @@
// src/hooks/useActiveAppTracker.ts
import { useEffect, useRef } from "react";
import { useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice";
export const useActiveAppTracker = (
isTracking: boolean,
projectId: string,
activeProjectLogId: string | null,
projectId: string | null,
projectName: string
) => {
const dispatch = useDispatch();
const lastActiveRef = useRef<WindowInfo | null>(null);
const lastLogIdRef = useRef<string | null>(null);
const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
useEffect(() => {
if (!isTracking || !projectId) {
console.log(" Tracking not active or project not selected");
// ✅ Common guard to check if we can send
const canSend = !!(activeProjectLogId && projectId);
const sendActivityLog = async (
projectLogId: string,
projectId: string,
title: string,
appName: string,
isNew: boolean
) => {
if (!canSend) {
console.log("🚫 Skipping API — missing activeProjectLogId or projectId");
return null;
}
try {
const payload = {
id: !isNew ? lastLogIdRef.current : undefined,
logId: projectLogId,
projectId,
title,
appName,
};
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
const res = await window.electron.trackingTabsOpeningClosing(payload);
if (isNew && res?._id) lastLogIdRef.current = res._id;
return res;
} catch (error) {
console.error("❌ Activity log error:", error);
throw error;
}
};
// ✅ Final PATCH before stopping
const sendFinalPatch = useCallback(async () => {
if (!canSend) {
console.log("🚫 Skipping final PATCH — no active project log ID");
return;
}
console.log(` Tracking started for project: ${projectName}`);
console.log("🛑 Sending final PATCH before stopping...");
if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) {
try {
await sendActivityLog(
activeProjectLogId!,
projectId!,
lastTitleRef.current,
lastActiveRef.current.owner.name || "Unknown",
false
);
console.log("✅ Final PATCH sent for", lastTitleRef.current);
} catch (error) {
console.error("❌ Final PATCH failed:", error);
}
} else {
console.log("ℹ️ No active log to finalize");
}
lastActiveRef.current = null;
lastTitleRef.current = null;
lastLogIdRef.current = null;
}, [activeProjectLogId, projectId, canSend]);
useEffect(() => {
finalizeTrackingRef.current = sendFinalPatch;
}, [sendFinalPatch]);
useEffect(() => {
// 🚫 If not tracking OR missing IDs, skip everything
if (!isTracking || !canSend) {
console.log("🟡 Tracking inactive or missing IDs — skipping tracker setup");
return;
}
console.log(`🟢 Starting active tracker for: ${projectName}`);
const fetchActiveApp = async () => {
try {
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id) return;
const prev = lastActiveRef.current;
const hasChanged =
!prev ||
prev.title !== details.title ||
prev.owner.processId !== details.owner.processId;
const title = details.title;
const appName = details.owner.name;
const prevTitle = lastTitleRef.current;
const hasChanged = !prevTitle || prevTitle !== title;
if (hasChanged) {
console.log("🪟 Window changed:", details.title);
dispatch(setActiveWindow({ window: details, projectId, projectName }));
if (lastLogIdRef.current && prevTitle) {
console.log("🔄 Patching previous log:", prevTitle);
await sendActivityLog(activeProjectLogId!, projectId!, prevTitle, appName, false);
}
console.log("🆕 New title detected → creating POST:", title);
await sendActivityLog(activeProjectLogId!, projectId!, title, appName, true);
lastActiveRef.current = details;
lastTitleRef.current = title;
dispatch(setActiveWindow({ window: details, projectId, projectName }));
} else {
console.log("⏸ Same window, skipping log");
}
} catch (error) {
console.error("❌ Failed to fetch active app:", error);
console.error("Failed to fetch active app:", error);
}
};
fetchActiveApp();
const intervalId = setInterval(fetchActiveApp, 5000);
intervalIdRef.current = setInterval(fetchActiveApp, 5000);
return () => {
console.log("🛑 Stopping active window tracker");
clearInterval(intervalId);
console.log("🧹 Cleaning up tracker interval");
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};
}, [isTracking, activeProjectLogId]);
return {
finalizeTracking: finalizeTrackingRef.current,
};
}, [isTracking, dispatch, projectId, projectName]);
};

View File

@ -27,18 +27,23 @@ import {
ChevronUp,
} from "lucide-react";
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../store";
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice";
interface User {
email: string;
password: string;
}
// Updated Project interface to match the JSON structure
interface Project {
id: number;
projectId: string;
projectName: string;
clientName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
}
interface ProjectTrackingData {
@ -49,12 +54,12 @@ interface ProjectTrackingData {
isPaused: boolean;
pauseStartTime: Date | null;
totalPausedTime: number;
accumulatedTime: number; // NEW: Stores time from previous sessions
accumulatedTime: number;
}
interface TrackingState {
export interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<number, ProjectTrackingData>;
projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
@ -63,7 +68,7 @@ interface TrackingState {
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (id: number) => void;
handleProjectSelect: (projectId: string) => void;
projectsData: Project[];
}
@ -73,16 +78,24 @@ interface DashboardLayoutProps {
}
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const [isSidebarOpen, setSidebarOpen] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const { windowLogs } = useSelector((state: RootState) => state.window);
console.log("🧾 windowLogs in component:", windowLogs);
const menuItems = [
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
// { label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
];
// Tracking states
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
@ -92,32 +105,114 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
const [startTime, setStartTime] = useState<Date | null>(null);
const [endTime, setEndTime] = useState<Date | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [projectSummary, setProjectSummary] = useState({
totalProjects: 0,
activeProjects: 0,
awayProjects: 0,
totalWorkTime: "0h 0m 0s",
totalWorkMs: 0,
vaName: "",
vaId: "",
});
const [activeProjectLogId, setActiveProjectLogId] = useState("")
useActiveAppTracker(isTracking, selectedProject?.id, selectedProject?.projectName);
const { finalizeTracking } = useActiveAppTracker(
isTracking,
activeProjectLogId,
selectedProject?.projectId,
selectedProject?.projectName
);
const socketRef = useRef<Socket | null>(null);
const intervalRef = useRef<number | null>(null);
const [isConnected, setIsConnected] = useState(false);
// const [seclectedScreen, setSelectedScreen] = useState<string | 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 [projectsData, setProjectsData] = useState<Project[]>([]);
// fetch the project list of VA
useEffect(() => {
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...");
const res = await window.electron.getProjectList();
console.log(" Project list response:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
setProjectsData(res.data);
// Store project summary info
setProjectSummary({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
});
// Store data redux store
dispatch(setProjectsDataSlice(res.data));
dispatch(
setProjectSummarySlice({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
})
);
// Auto-select first project
if (res.data.length > 0 && !selectedProject) {
const firstProject = res.data[0];
setSelectedProject(firstProject);
dispatch(setSelectedProjectSlice(firstProject));
// Initialize tracking map for that project
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
if (!updated.has(firstProject.projectId)) {
updated.set(firstProject.projectId, {
elapsedTime: 0,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: 0,
});
}
return updated;
});
}
} else {
console.warn("⚠️ Unexpected response:", res);
}
} catch (err) {
console.error("❌ Error fetching projects:", err);
}
};
fetchProjects();
}, []);
const [projectTrackingMap, setProjectTrackingMap] = useState<
Map<number, ProjectTrackingData>
Map<string, ProjectTrackingData>
>(new Map());
const handleLogout = async () => {
try {
const res = await window.electron.logout()
if (res && res.success) {
const res = await window.electron.logout();
localStorage.removeItem("user");
setUser(null);
navigate("/login");
}
} catch (error) {
console.error("Logout error:", error);
}
@ -194,20 +289,10 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}
};
const menuItems = [
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
{ label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ 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);
const projectData = projectTrackingMap.get(selectedProject.projectId);
if (projectData) {
setIsTracking(projectData.isTracking);
setIsPaused(projectData.isPaused);
@ -224,7 +309,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
timerRef.current = setInterval(() => {
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
const current = updated.get(selectedProject.projectId);
if (current && current.isTracking && !current.isPaused && current.startTime) {
const now = Date.now();
@ -236,9 +321,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
current.elapsedTime = calculatedElapsed;
updated.set(selectedProject.id, { ...current });
updated.set(selectedProject.projectId, { ...current });
// Update global elapsedTime in sync
setElapsedTime(calculatedElapsed);
}
return updated;
@ -260,17 +344,18 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}, [isTracking, isPaused, selectedProject]);
// Start tracking
const handleStartTracking = (project: Project) => {
const handleStartTracking = async (project: Project) => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
const now = new Date();
window.electron.toggleCloseBtn(false)
window.electron.toggleCloseBtn(false);
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(project.id) || {
const current = updated.get(project.projectId) || {
elapsedTime: 0,
startTime: null,
endTime: null,
@ -281,29 +366,42 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
accumulatedTime: 0,
};
// 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);
current.totalPausedTime = 0;
updated.set(project.projectId, current);
// Update global tracking states
setIsTracking(true);
setIsPaused(false);
setStartTime(now);
setElapsedTime(current.accumulatedTime); // Start from accumulated time
setElapsedTime(current.accumulatedTime);
return updated;
});
try {
const payload = {
vaId: projectSummary.vaId,
projectId: project.projectId,
type: "LOGIN",
};
// console.log("📡 Sending start tracking payload:", payload);
const res = await window.electron.startProjectTracking(payload);
console.log(" Synced start tracking:", res);
setActiveProjectLogId(res?.logId)
} catch (error) {
console.error(" Failed to sync project start:", error);
}
};
// Pause/Resume tracking with pause duration tracking
const handlePauseResume = () => {
if (!selectedProject) return;
const currentProjectData = projectTrackingMap.get(selectedProject.id);
const currentProjectData = projectTrackingMap.get(selectedProject.projectId);
if (!currentProjectData || !currentProjectData.isTracking) return;
const now = new Date();
@ -311,15 +409,13 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
const current = updated.get(selectedProject.projectId);
if (!current) return updated;
if (newPausedState) {
// Starting pause
current.isPaused = true;
current.pauseStartTime = now;
} else {
// Resuming from pause
current.isPaused = false;
if (current.pauseStartTime) {
const pauseDuration = Math.floor(
@ -330,7 +426,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}
}
updated.set(selectedProject.id, { ...current });
updated.set(selectedProject.projectId, { ...current });
setIsPaused(newPausedState);
return updated;
@ -338,38 +434,69 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
};
// Stop tracking - PRESERVES elapsed time
const handleStopTracking = () => {
const handleStopTracking = async () => {
if (!selectedProject) return;
console.log("🛑 Stop tracking initiated...");
// STEP 1: Send final PATCH first
if (finalizeTracking) {
try {
await finalizeTracking();
console.log(" Final tracking data sent");
} catch (error) {
console.error("❌ Error sending final tracking data:", error);
}
}
// STEP 2: Clear timer
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// STEP 3: Send LOGOUT request
try {
const payload = {
vaId: projectSummary.vaId,
projectId: selectedProject.projectId,
type: "LOGOUT",
};
console.log("📡 Sending stop tracking payload:", payload);
const res = await window.electron.stopProjectTracking(payload);
console.log(" Synced stop tracking:", res);
} catch (error) {
console.error("❌ Failed to sync project stop:", error);
}
// STEP 4: Update UI and state
const now = new Date();
window.electron.toggleCloseBtn(true)
await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
setProjectTrackingMap((prev) => {
const updated = new Map(prev);
const current = updated.get(selectedProject.id);
const current = updated.get(selectedProject.projectId);
if (!current) return updated;
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 });
current.startTime = null;
updated.set(selectedProject.projectId, { ...current });
// Update global states - keep elapsed time
setIsTracking(false);
setIsPaused(false);
setEndTime(now);
setElapsedTime(current.elapsedTime); // Keep the elapsed time
setElapsedTime(current.elapsedTime);
return updated;
});
console.log("Tracking stopped. Time preserved:", elapsedTime);
console.log(" Tracking stopped successfully. Time preserved:", elapsedTime);
};
// Cleanup timer on unmount
@ -382,14 +509,14 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
}, []);
// Handle project selection
const handleProjectSelect = (id: number) => {
const handleProjectSelect = (projectId: string) => {
if (isTracking) return;
const project = projectsData.find((p) => p.id === id);
const project = projectsData.find((p) => p.projectId === projectId);
if (project) {
setSelectedProject(project);
const projectData = projectTrackingMap.get(id);
const projectData = projectTrackingMap.get(projectId);
if (projectData) {
setElapsedTime(projectData.elapsedTime);
setStartTime(projectData.startTime);
@ -428,11 +555,12 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
return `${hrs}h ${mins}m ${secs}s`;
};
const toggleWindowSize = useRef(true); // true -> Open, false -> Close
const toggleWindowSize = useRef(true);
return (
<>
{/* Header */}
{isTracking &&
<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 */}
{!isConnected ? (
@ -506,28 +634,27 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
<button
onClick={() => {
if (toggleWindowSize.current) {
window.electron.resizeWindow(500, 85)
window.electron.resizeWindow(500, 85);
toggleWindowSize.current = false;
} else {
window.electron.resizeWindow(500, 600)
window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
}
}}
className="btn btn-xs btn-outline btn-primary ">
className="btn btn-xs btn-outline btn-primary"
>
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
</button>
</div>
</header>
}
{/* 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"
>
<div className={`flex ${!isTracking ? "max-h-screen" : " h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
{/* Sidebar */}
<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) => (
<div className="tooltip z-100 tooltip-right" data-tip={item.label}>
<NavLink
key={item.label}
to={item.path}
@ -541,11 +668,12 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
>
{item.icon}
</NavLink>
</div>
))}
<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"
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 tooltip tooltip-right "
data-tip={"Logout"}
>
<LogOut size={18} />
</button>

View File

@ -2,11 +2,14 @@ import React, { useEffect, useRef, useState } from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import { useOutletContext } from "react-router-dom";
// ----- Interfaces -----
// Updated Project interface to match JSON structure
interface Project {
id: number;
projectId: string;
projectName: string;
clientName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
}
interface ProjectTrackingData {
@ -19,7 +22,7 @@ interface ProjectTrackingData {
interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<number, ProjectTrackingData>;
projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
@ -28,11 +31,11 @@ interface TrackingState {
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (id: number) => void;
handleProjectSelect: (projectId: string) => void;
projectsData: Project[];
}
// ----- Helper -----
// Helper function
const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
@ -40,7 +43,7 @@ const formatTime = (seconds: number) => {
return `${hrs}h ${mins}m ${secs}s`;
};
// ----- Widget -----
// Widget Component
const Widget: React.FC<{
title: string;
value: string | number;
@ -71,7 +74,7 @@ const Widget: React.FC<{
</div>
);
// ----- Dashboard -----
// Dashboard Component
const Dashboard: React.FC = () => {
const {
selectedProject,
@ -96,25 +99,9 @@ const Dashboard: React.FC = () => {
// const [runningApps, setRunningApps] = useState<AppsListInt[]>([]);
const selectedData = selectedProject
? projectTrackingMap.get(selectedProject.id)
? projectTrackingMap.get(selectedProject.projectId)
: null;
// const getRunningApps = async () => {
// const res = await window.electron.getRunningApps()
// const appsList: AppsListInt[] = Array.isArray(res) ? res : [];
// setRunningApps(appsList)
// }
// useEffect(() => {
// setInterval(() => {
// getRunningApps();
// }, 5000)
// }, [])
return (
<main className="flex-1 overflow-y-auto no-scrollbar">
{/* Header */}
@ -157,7 +144,7 @@ const Dashboard: React.FC = () => {
</div>
{/* Widgets */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
{/* <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Widget
title="Logged Hours Today"
value="6h 24m"
@ -176,13 +163,13 @@ const Dashboard: React.FC = () => {
icon={<ProjectorIcon size={24} />}
color="warning"
/>
</div>
</div> */}
{/* Tracking Section */}
<div className="w-full mb-4">
<div className="flex justify-between items-center mb-2">
{/* <div className="flex justify-between items-center mb-2">
<span className="text-md font-bold">Tracking</span>
</div>
</div> */}
<div className="card bg-base-100 border border-base-200 shadow-sm">
<div className="card-body">
@ -226,11 +213,18 @@ const Dashboard: React.FC = () => {
</span>
</div>
<div>
<span className="text-base-content/70">Client:</span>{" "}
<span>{selectedProject.clientName}</span>
<span className="text-base-content/70">Status:</span>{" "}
<span
className={`badge ${selectedProject.activityStatus === "ACTIVE"
? "badge-success"
: "badge-warning"
}`}
>
{selectedProject.activityStatus}
</span>
</div>
<div>
<span className="text-base-content/70">Status:</span>{" "}
<span className="text-base-content/70">Tracking:</span>{" "}
<span
className={`badge ${selectedData?.isPaused
? "badge-warning"
@ -268,35 +262,33 @@ const Dashboard: React.FC = () => {
{/* Projects Table */}
<div>
<p className="text-md mb-2 font-bold">Projects</p>
<div className="overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md">
<div className="relative rounded-box border border-base-content/10 bg-base-100 shadow-md min-h-[62vh] max-h-[62vh]">
{projectsData.length > 0 ? (
<div className="overflow-y-auto h-full">
<table className="table table-zebra w-full text-sm">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Project</th>
<th>Client</th>
<th>Name</th>
<th>Logs Hours</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{projectsData.map((project) => {
const isCurrentlySelected = selectedProject?.id === project.id;
// ✅ FIX: Disable ALL projects when ANY project is tracking
// Only enable selection when not tracking
{projectsData.map((project, index) => {
const isCurrentlySelected = selectedProject?.projectId === project.projectId;
const isDisabled = isTracking && !isCurrentlySelected;
return (
<tr key={project.id}>
<td>{project.id}</td>
<td>{project.projectName}</td>
<td>{project.clientName}</td>
<tr key={project.projectId}>
<td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium">{project.projectName}</td>
<td className="font-mono text-xs">{project.totalWorkTime}</td>
<td>
<button
className={`btn btn-xs ${isCurrentlySelected
? "btn-success"
: "btn-primary"
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
}`}
onClick={() => handleProjectSelect(project.id)}
onClick={() => handleProjectSelect(project.projectId)}
disabled={isDisabled}
>
{isCurrentlySelected ? "Selected" : "Select"}
@ -308,6 +300,13 @@ const Dashboard: React.FC = () => {
</tbody>
</table>
</div>
) : (
<div className="flex items-center justify-center h-full text-sm text-base-content/60">
<p className="mt-10 font-bold text-lg">No projects found.</p>
</div>
)}
</div>
</div>
</main>
);

View File

@ -1,10 +1,11 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import AdjustmentModal from './AdjustmentModal';
import { RootState } from '../../store';
import { useSelector } from 'react-redux';
interface LeaveFormData {
fromDate: string;
toDate: string;
description: string;
}
interface AdjustmentEntry {
@ -29,16 +30,18 @@ const data: LogEntry[] = [
{ id: 5, date: "2025-11-05", hours: 5.5, task: "Code Review", status: "Completed" },
];
type TabType = 'pto' | 'sick' | 'adjustment';
type TabType = 'create_and_adjust_timesheet' | 'sick' | 'adjustment';
const LeaveAndAdjustments: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('pto');
const [activeTab, setActiveTab] = useState<TabType>('create_and_adjust_timesheet');
const projects = useSelector((state: RootState) => state.projects.projectsData);
console.log("LeaveAndAdjustments", projects)
// PTO State
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
const [ptoLeave, setPtoLeave] = useState({
fromDate: '',
toDate: '',
description: ''
});
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
@ -46,9 +49,10 @@ const LeaveAndAdjustments: React.FC = () => {
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
fromDate: '',
toDate: '',
description: ''
});
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
const [selectedProjectId, setSelectedProjectId] = useState("")
const [selectedDate, setSelectedDate] = useState("")
// Adjustment State
const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
@ -83,13 +87,6 @@ const LeaveAndAdjustments: React.FC = () => {
return errors;
};
// PTO Handlers
const handlePtoChange = (field: keyof LeaveFormData, value: string) => {
setPtoLeave(prev => ({ ...prev, [field]: value }));
if (ptoErrors[field]) {
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const handlePtoSubmit = async () => {
const errors = validateLeaveForm(ptoLeave);
@ -162,12 +159,57 @@ const LeaveAndAdjustments: React.FC = () => {
handleCloseModal();
};
const handleProjectSelect = (projectId: string) => {
console.log("handleProjectSelect", projectId)
setSelectedProjectId(projectId)
}
const handleSelectedDate = (value: string) => {
// Ensure value stays as "YYYY-MM-DD"
setSelectedDate(value);
};
const fetchLogs = async () => {
const res = await window.electron.getProjectLogs(
selectedProjectId,
selectedDate || undefined,
selectedDate || undefined,
);
if (res.success) {
console.log("✅ Logs: fetchLogs", res);
// Safely extract time if from/to exist
const fromTime = res.from ? new Date(res.from).toISOString().slice(11, 16) : "";
const toTime = res.to ? new Date(res.to).toISOString().slice(11, 16) : "";
setPtoLeave({
fromDate: fromTime,
toDate: toTime,
});
} else {
console.warn("⚠️ Failed to fetch project logs");
}
};
useEffect(() => {
if (selectedProjectId && selectedDate) {
fetchLogs()
}
}, [selectedProjectId, selectedDate])
return (
<div className="min-h-screen">
<div className="max-h-screen">
<div className="w-full mx-auto">
{/* Header */}
<div className=" ">
<h1 className="text-mb font-bold mb-2">Leave & Adjustments</h1>
<h1 className="text-mb font-bold mb-2">Adjust Timesheet</h1>
</div>
{/* Main Container */}
@ -175,8 +217,8 @@ const LeaveAndAdjustments: React.FC = () => {
{/* Tabs Navigation */}
<div className="flex ">
<button
onClick={() => setActiveTab('pto')}
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'pto'
onClick={() => setActiveTab('create_and_adjust_timesheet')}
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'create_and_adjust_timesheet'
? ' text-primary border-b-2 border-primary'
: ''
}`}
@ -186,24 +228,11 @@ const LeaveAndAdjustments: React.FC = () => {
className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
PTO Request
Create or Adjust Timesheet
</span>
</button>
<button
onClick={() => setActiveTab('sick')}
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative cursor-pointer ${activeTab === 'sick'
? ' text-primary border-b-2 border-primary'
: ''
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Sick Leave
</span>
</button>
<button
{/* <button
onClick={() => setActiveTab('adjustment')}
className={`flex-1 px-6 py-3 text-xs font-semibold cursor-pointer transition-all duration-300 relative ${activeTab === 'adjustment'
? ' text-primary border-b-2 border-primary'
@ -216,30 +245,71 @@ const LeaveAndAdjustments: React.FC = () => {
</svg>
Adjustments
</span>
</button>
</button> */}
</div>
{/* Tab Content */}
<div className="px-8 py-4">
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[80vh]">
{/* PTO Tab */}
{activeTab === 'pto' && (
{activeTab === 'create_and_adjust_timesheet' && (
<div className="animate-fade-in">
<div className="max-w-4xl ">
<div className="mb-1">
{/* <div className="mb-1">
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
</div>
</div> */}
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="form-control">
<div className="form-control col-span-2">
<label className="label">
<span className="label-text font-semibold ">From Date</span>
<span className="label-text font-semibold text-sm">Select project</span>
</label>
<select
className="select select-sm"
defaultValue=""
onChange={(e) => handleProjectSelect(e.target.value)}
>
<option value="" disabled>
Pick a project
</option>
{projects.map((item, index) => (
<option key={item.projectId || index} value={item.projectId}>
{item.projectName}
</option>
))}
</select>
{ptoErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
</label>
)}
</div>
<div className="form-control col-span-2">
<label className="label">
<span className="label-text font-semibold text-sm">Select timesheet date</span>
</label>
<input
type="date"
value={ptoLeave.fromDate}
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
value={selectedDate || ""}
onChange={(e) => handleSelectedDate(e.target.value)}
className="input input-bordered input-sm w-full focus:input-primary transition-all"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-sm ">Current total time</span>
</label>
<input
type="time"
// onChange={(e) => handlePtoChange('fromDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
/>
{ptoErrors.fromDate && (
<label className="label">
@ -250,13 +320,58 @@ const LeaveAndAdjustments: React.FC = () => {
<div className="form-control">
<label className="label">
<span className="label-text font-semibold">To Date</span>
<span className="label-text font-semibold text-sm">Current pending time</span>
</label>
<input
type="date"
type="time"
// onChange={(e) => handlePtoChange('toDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
/>
{ptoErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-sm">Start time</span>
</label>
<input
type="time"
value={ptoLeave.fromDate}
onChange={(e) =>
setPtoLeave((prev) => ({ ...prev, fromDate: e.target.value }))
}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-sm">End time</span>
</label>
<input
type="time"
value={ptoLeave.toDate}
onChange={(e) => handlePtoChange('toDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
onChange={(e) =>
setPtoLeave((prev) => ({ ...prev, toDate: e.target.value }))
}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
/>
</div>
<div className="form-control col-span-2">
<label className="label">
<span className="label-text font-semibold text-sm">Total time adjustment</span>
</label>
<input
type="input"
// onChange={(e) => handlePtoChange('toDate', e.target.value)}
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
/>
{ptoErrors.toDate && (
<label className="label">
@ -268,13 +383,13 @@ const LeaveAndAdjustments: React.FC = () => {
<div className="form-control flex flex-col gap-1">
<label className="label">
<span className="label-text font-semibold ">Description / Reason</span>
<span className="label-text font-semibold text-sm ">Description / Reason</span>
</label>
<textarea
value={ptoLeave.description}
onChange={(e) => handlePtoChange('description', e.target.value)}
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
placeholder="Please provide a reason for your PTO request..."
// onChange={(e) => handlePtoChange('description', e.target.value)}
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
placeholder="Please enter the task name or leave notes to help us better understand your request."
/>
{ptoErrors.description && (
<label className="label">
@ -493,14 +608,14 @@ const LeaveAndAdjustments: React.FC = () => {
</div>
{/* View Status */}
<div className='mt-4'>
{/* <div className='mt-4'>
<h1 className="text-md font-bold mb-2">View Status</h1>
<div
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
style={{ minHeight: "150px", maxHeight: "300px" }}
>
<table className="table table-zebra w-full">
{/* Table Head */}
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
@ -511,7 +626,7 @@ const LeaveAndAdjustments: React.FC = () => {
</tr>
</thead>
{/* Table Body */}
<tbody>
{data.map((log, index) => (
<tr key={log.id}>
@ -535,7 +650,7 @@ const LeaveAndAdjustments: React.FC = () => {
))}
</tbody>
</table>
</div></div>
</div></div> */}
<style>{`
@keyframes fade-in {
from {

View File

@ -0,0 +1,97 @@
import React from "react";
interface TabActivity {
title: string;
appName: string;
from: string;
to: string;
durationMs: number;
durationFormatted: string;
}
interface ProjectSession {
from: string;
to: string;
totalSessionTime: string;
tabs: TabActivity[];
}
interface Props {
isOpen: boolean;
onClose: () => void;
projectName: string;
logs: ProjectSession[];
loading: boolean;
}
const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs, loading }) => {
if (!isOpen) return null;
return (
<dialog className="modal modal-open">
<div className="modal-box max-h-[97vh] max-w-4xl bg-base-100 flex flex-col">
<h3 className="font-bold text-xs mb-3">
🗂️ Project Logs — <span className="text-primary">{projectName}</span>
</h3>
{loading ? (
<div className="flex justify-center items-center flex-grow">
<span className="loading loading-spinner text-primary"></span>
</div>
) : logs.length === 0 ? (
<p className="text-center text-gray-500 flex-grow">No logs available.</p>
) : (
<div className="overflow-y-auto flex-grow space-y-4 no-scrollbar">
{logs.map((session, idx) => (
<div key={idx} className="border border-base-300 overflow-hidden">
<div className="bg-base-200 p-2 font-semibold flex justify-between items-center">
<span className="text-[12px]">
🕒 Session {idx + 1} — {session.totalSessionTime}
</span>
<span className="text-xs text-gray-500">
{new Date(session.from).toLocaleTimeString()} -{" "}
{new Date(session.to).toLocaleTimeString()}
</span>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra w-full font-semibold text-[12px]">
<thead className="sticky top-0 bg-base-300 z-10">
<tr>
<th>#</th>
<th>Title</th>
<th>Time</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{session.tabs.map((tab, tabIdx) => (
<tr key={tabIdx} className="text-[10px]">
<td>{tabIdx + 1}</td>
<td className="truncate max-w-[150px]" title={tab.title}>
{tab.title} <br/>
{tab.appName}
</td>
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
<td>{tab.durationFormatted}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
)}
<div className="modal-action mt-2">
<button className="btn btn-outline btn-sm" onClick={onClose}>
Close
</button>
</div>
</div>
</dialog>
);
};
export default ProjectLogsModal;

View File

@ -1,7 +1,8 @@
import React from "react";
import { CalendarDays, Clock, FileText, CheckCircle, XCircle } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CalendarDays, Clock, FileText, CheckCircle, XCircle, Eye } from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
import ProjectLogsModal from "./ProjectLogsModal";
interface TimesheetEntry {
id: number;
@ -20,6 +21,15 @@ const timesheetData: TimesheetEntry[] = [
{ id: 5, date: "2025-11-01", project: "AI Model Trainer", hours: 8, status: "Approved", remarks: "Normal day" },
];
interface Project {
projectId: string;
projectName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
}
const Timesheets: React.FC = () => {
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
@ -27,6 +37,62 @@ const Timesheets: React.FC = () => {
const { windowLogs } = useSelector((state: RootState) => state.window);
console.log("🧾 Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState([]);
const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...");
const res = await window.electron.getProjectList();
console.log("✅ Project list response:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
setProjectsData(res.data);
} else {
console.warn(" Unexpected response:", res);
}
} catch (err) {
console.error("Error fetching projects:", err);
}
};
fetchProjects();
}, []);
const handleViewLogs = async (projectID: string, projectName: string) => {
try {
setLoading(true);
setSelectedProject(projectID);
setSelectedProjectName(projectName);
console.log("projectID" ,projectID)
const res = await window.electron.getProjectLogs(projectID);
console.log("📦 Full Logs:", res);
if (res?.success && Array.isArray(res.data)) {
setLogs(res.data[0].sessions || []);
} else {
setLogs([]);
}
setModalOpen(true);
} catch (error) {
console.error("Error fetching logs:", error);
} finally {
setLoading(false);
}
};
return (
<div className="flex flex-col gap-2 max-h-screen">
@ -38,7 +104,7 @@ const Timesheets: React.FC = () => {
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{/* <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
@ -70,42 +136,33 @@ const Timesheets: React.FC = () => {
<FileText size={20} className="text-warning" />
</div>
</div>
</div>
</div> */}
{/* Timesheet Table */}
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[60vh]">
<table className="table table-zebra w-full text-sm">
<thead className="sticky top-0 bg-base-200 z-10 text-sm">
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh]">
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10 ">
<tr>
<th>#</th>
<th>Date</th>
<th>Project</th>
<th>Hours</th>
<th>Status</th>
<th>Remarks</th>
<th>Logs</th>
</tr>
</thead>
<tbody>
{timesheetData.map((entry) => (
<tr key={entry.id}>
<td>{entry.id}</td>
<td className="whitespace-nowrap">{entry.date}</td>
<td>{entry.project}</td>
<td>{entry.hours} hrs</td>
{projectsData.map((entry, index) => (
<tr key={entry.projectId}>
<td>{index+1}</td>
<td>{entry.projectName}</td>
<td>{entry.totalWorkTime}</td>
<td>
<div
className={`badge badge-sm ${
entry.status === "Approved"
? "badge-success"
: entry.status === "Pending"
? "badge-warning"
: "badge-error"
}`}
<button
onClick={() => handleViewLogs(entry.projectId, entry.projectName)}
className="btn btn-ghost btn-xs"
>
{entry.status}
</div>
<Eye size={16} />
</button>
</td>
<td>{entry.remarks || "-"}</td>
</tr>
))}
</tbody>
@ -121,6 +178,14 @@ const Timesheets: React.FC = () => {
Last updated: {new Date().toLocaleDateString()}
</span>
</div>
<ProjectLogsModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
projectName={selectedProjectName}
logs={logs}
loading={loading}
/>
</div>
);
};

View File

@ -1,10 +1,12 @@
// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import windowReducer from "./windowSlice";
import projectsReducer from "./projectSlice";
export const store = configureStore({
reducer: {
window: windowReducer,
projects: projectsReducer,
},
});

View File

@ -0,0 +1,50 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface Project {
projectId: string;
projectName: string;
[key: string]: any;
}
interface ProjectSummary {
totalProjects: number;
activeProjects: number;
awayProjects: number;
totalWorkTime: string;
totalWorkMs: number;
vaName: string;
vaId: string;
}
interface ProjectsState {
projectsData: Project[];
projectSummary: ProjectSummary | null;
selectedProject: Project | null;
}
const initialState: ProjectsState = {
projectsData: [],
projectSummary: null,
selectedProject: null,
};
const projectsSlice = createSlice({
name: "projects",
initialState,
reducers: {
setProjectsDataSlice(state, action: PayloadAction<Project[]>) {
state.projectsData = action.payload;
},
setProjectSummarySlice(state, action: PayloadAction<ProjectSummary>) {
state.projectSummary = action.payload;
},
setSelectedProjectSlice(state, action: PayloadAction<Project | null>) {
state.selectedProject = action.payload;
},
},
});
export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } =
projectsSlice.actions;
export default projectsSlice.reducer;

67
types.d.ts vendored
View File

@ -23,6 +23,46 @@ type EventPayloadMapping = {
type UnsubscribeFunction = () => void;
interface ProjectListResponse {
success: boolean;
vaId?: string;
vaName?: string;
totalProjects?: number;
activeProjects?: number;
awayProjects?: number;
totalWorkTime?: string;
totalWorkMs?: number;
data?: any[]; // you can replace `any[]` with a typed array later
}
interface TabActivity {
title: string;
appName: string;
from: string;
to: string;
durationMs: number;
durationFormatted: string;
}
// Each session includes tabs and time span
interface ProjectSession {
from: string;
to: string;
totalSessionTime: string;
tabs: TabActivity[];
}
// Complete response for project logs
interface ProjectLogResponse {
vaId: string;
vaName: string;
vaEmail: string;
totalWorkTime: string;
totalWorkMs: number;
sessions: ProjectSession[];
}
interface Window {
electron: {
login: (credentials: { email: string; password: string }) => Promise<any>;
@ -35,6 +75,33 @@ interface Window {
getRunningApps: () => {},
captureAppScreenShot: (appTitle: string) => Promise<string>,
onWindowUpdate: (callback: (data: any) => void) => void;
getProjectList: () => Promise<ProjectListResponse>;
startProjectTracking: (data: {
vaId: string;
projectId: string;
type: string;
}) => Promise<any>;
stopProjectTracking: (data: {
vaId: string;
projectId: string;
type: string;
}) => Promise<any>;
trackingTabsOpeningClosing: (data: {
id?: string; // optional, only present if updating
logId: string;
projectId: string;
title: string;
appName: string;
}) => Promise<any>;
// getProjectLogs: (projectId: string) => Promise<{ success: boolean; logs: ProjectLogResponse[] }>;
getProjectLogs: (
projectID: string,
fromDate?: string,
toDate?: string
) => Promise<{ success: boolean; logs: ProjectLogResponse[] }>;
};
}