Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker
This commit is contained in:
commit
11398c6869
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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[] }>;
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue