login logout and api logs apis
This commit is contained in:
parent
fbc5de3a59
commit
b448e2a253
|
|
@ -6,13 +6,13 @@ import chalk from "chalk";
|
||||||
const TOKEN_KEY = 'ACCESS_TOKEN';
|
const TOKEN_KEY = 'ACCESS_TOKEN';
|
||||||
|
|
||||||
const api: AxiosInstance = axios.create({
|
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,
|
// timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Creating Axios Instance", axios.VERSION);
|
// console.log("Creating Axios Instance", axios.VERSION);
|
||||||
console.log("Axios .get reference:", api.get);
|
// console.log("Axios .get reference:", api.get);
|
||||||
console.log("Axios .request reference:", api.request);
|
// console.log("Axios .request reference:", api.request);
|
||||||
|
|
||||||
// --- Request Interceptor ---
|
// --- Request Interceptor ---
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
|
|
@ -22,14 +22,9 @@ api.interceptors.request.use(
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers['token'] = 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;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => Promise.reject(error)
|
||||||
|
|
|
||||||
|
|
@ -54,16 +54,150 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.setClosable(shouldEnable);
|
mainWindow.setClosable(shouldEnable);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('resize-window', (event, width: number, height: number) => {
|
|
||||||
const mainWindow = getMainWindow()
|
|
||||||
console.log("toggle-close-btn Called", mainWindow)
|
|
||||||
// if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.setSize(width, height);
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
|
|
||||||
// ✅ Start background polling when main window is ready
|
// ✅ Start background polling when main window is ready
|
||||||
};
|
};
|
||||||
// startWindowPolling(mainWindow);
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ app.on('ready', async () => {
|
||||||
width: 500, // fixed width
|
width: 500, // fixed width
|
||||||
height: 600, // fixed height
|
height: 600, // fixed height
|
||||||
|
|
||||||
resizable: true, // prevents resizing
|
resizable: false, // prevents resizing
|
||||||
fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar)
|
fullscreenable: false, // disables fullscreen toggle (F11 or double-click title bar)
|
||||||
minimizable: false, // disables minimize button
|
minimizable: false, // disables minimize button
|
||||||
maximizable: false, // prevents maximize
|
maximizable: false, // prevents maximize
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,21 @@ contextBridge.exposeInMainWorld('electron', {
|
||||||
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
|
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
|
||||||
toggleCloseBtn: (shouldEnable: boolean) => ipcRenderer.invoke('toggle-close-btn', shouldEnable),
|
toggleCloseBtn: (shouldEnable: boolean) => ipcRenderer.invoke('toggle-close-btn', shouldEnable),
|
||||||
resizeWindow: (width: number, height: number) => ipcRenderer.invoke('resize-window', width, height),
|
resizeWindow: (width: number, height: number) => ipcRenderer.invoke('resize-window', width, height),
|
||||||
|
getProjectList: () => ipcRenderer.invoke("get-project-list"),
|
||||||
onWindowUpdate: (callback) =>
|
onWindowUpdate: (callback) =>
|
||||||
ipcRenderer.on("window:updated", (_, data) => callback(data)),
|
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']);
|
} satisfies Window['electron']);
|
||||||
|
|
||||||
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export const handleLogin = async (credentials: { email: string, password: string
|
||||||
try {
|
try {
|
||||||
console.log("➡️ Calling API...");
|
console.log("➡️ Calling API...");
|
||||||
const response = await axios.post(
|
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 } },
|
{ userDetails: { ...credentials } },
|
||||||
{ headers: { "ngrok-skip-browser-warning": 69420 } }
|
{ headers: { "ngrok-skip-browser-warning": 69420 } }
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,141 @@
|
||||||
// src/hooks/useActiveAppTracker.ts
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { setActiveWindow, WindowInfo } from "../store/windowSlice";
|
import { setActiveWindow, WindowInfo } from "../store/windowSlice";
|
||||||
|
|
||||||
export const useActiveAppTracker = (
|
export const useActiveAppTracker = (
|
||||||
isTracking: boolean,
|
isTracking: boolean,
|
||||||
projectId: string,
|
activeProjectLogId: string | null,
|
||||||
|
projectId: string | null,
|
||||||
projectName: string
|
projectName: string
|
||||||
) => {
|
) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const lastActiveRef = useRef<WindowInfo | null>(null);
|
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(() => {
|
// ✅ Common guard to check if we can send
|
||||||
if (!isTracking || !projectId) {
|
const canSend = !!(activeProjectLogId && projectId);
|
||||||
console.log(" Tracking not active or project not selected");
|
|
||||||
|
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;
|
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 () => {
|
const fetchActiveApp = async () => {
|
||||||
try {
|
try {
|
||||||
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
|
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
|
||||||
if (!details || !details.id) return;
|
if (!details || !details.id) return;
|
||||||
|
|
||||||
const prev = lastActiveRef.current;
|
const title = details.title;
|
||||||
const hasChanged =
|
const appName = details.owner.name;
|
||||||
!prev ||
|
const prevTitle = lastTitleRef.current;
|
||||||
prev.title !== details.title ||
|
const hasChanged = !prevTitle || prevTitle !== title;
|
||||||
prev.owner.processId !== details.owner.processId;
|
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
console.log("🪟 Window changed:", details.title);
|
if (lastLogIdRef.current && prevTitle) {
|
||||||
dispatch(setActiveWindow({ window: details, projectId, projectName }));
|
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;
|
lastActiveRef.current = details;
|
||||||
|
lastTitleRef.current = title;
|
||||||
|
dispatch(setActiveWindow({ window: details, projectId, projectName }));
|
||||||
|
} else {
|
||||||
|
console.log("⏸ Same window, skipping log");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to fetch active app:", error);
|
console.error("Failed to fetch active app:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchActiveApp();
|
fetchActiveApp();
|
||||||
const intervalId = setInterval(fetchActiveApp, 5000);
|
intervalIdRef.current = setInterval(fetchActiveApp, 5000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log("🛑 Stopping active window tracker");
|
console.log("🧹 Cleaning up tracker interval");
|
||||||
clearInterval(intervalId);
|
if (intervalIdRef.current) {
|
||||||
|
clearInterval(intervalIdRef.current);
|
||||||
|
intervalIdRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isTracking, activeProjectLogId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
finalizeTracking: finalizeTrackingRef.current,
|
||||||
};
|
};
|
||||||
}, [isTracking, dispatch, projectId, projectName]);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,23 @@ import {
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
|
||||||
import { useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RootState } from "../store";
|
import { RootState } from "../store";
|
||||||
|
import { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice } from "../store/projectSlice";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated Project interface to match the JSON structure
|
||||||
interface Project {
|
interface Project {
|
||||||
id: number;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
clientName: string;
|
activityStatus: "ACTIVE" | "AWAY";
|
||||||
|
totalWorkTime: string;
|
||||||
|
totalWorkMs: number;
|
||||||
|
totalLogs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectTrackingData {
|
interface ProjectTrackingData {
|
||||||
|
|
@ -47,12 +52,12 @@ interface ProjectTrackingData {
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
pauseStartTime: Date | null;
|
pauseStartTime: Date | null;
|
||||||
totalPausedTime: number;
|
totalPausedTime: number;
|
||||||
accumulatedTime: number; // NEW: Stores time from previous sessions
|
accumulatedTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrackingState {
|
export interface TrackingState {
|
||||||
selectedProject: Project | null;
|
selectedProject: Project | null;
|
||||||
projectTrackingMap: Map<number, ProjectTrackingData>;
|
projectTrackingMap: Map<string, ProjectTrackingData>;
|
||||||
isTracking: boolean;
|
isTracking: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
|
|
@ -61,7 +66,7 @@ interface TrackingState {
|
||||||
handleStartTracking: (project: Project) => void;
|
handleStartTracking: (project: Project) => void;
|
||||||
handlePauseResume: () => void;
|
handlePauseResume: () => void;
|
||||||
handleStopTracking: () => void;
|
handleStopTracking: () => void;
|
||||||
handleProjectSelect: (id: number) => void;
|
handleProjectSelect: (projectId: string) => void;
|
||||||
projectsData: Project[];
|
projectsData: Project[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,16 +76,24 @@ interface DashboardLayoutProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
|
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { windowLogs } = useSelector((state: RootState) => state.window);
|
const { windowLogs } = useSelector((state: RootState) => state.window);
|
||||||
|
|
||||||
console.log("🧾 windowLogs in component:", windowLogs);
|
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
|
// Tracking states
|
||||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||||
|
|
@ -90,46 +103,118 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
const [startTime, setStartTime] = useState<Date | null>(null);
|
const [startTime, setStartTime] = useState<Date | null>(null);
|
||||||
const [endTime, setEndTime] = useState<Date | null>(null);
|
const [endTime, setEndTime] = useState<Date | null>(null);
|
||||||
const timerRef = useRef<ReturnType<typeof setInterval> | 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 [projectsData, setProjectsData] = useState<Project[]>([
|
const [projectsData, setProjectsData] = useState<Project[]>([]);
|
||||||
{ id: 1, projectName: "CRM Redesign", clientName: "Salesforce" },
|
|
||||||
{ id: 2, projectName: "AI Assistant", clientName: "OpenAI" },
|
// fetch the project list of VA
|
||||||
{ id: 3, projectName: "Marketing Dashboard", clientName: "Google" },
|
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<
|
const [projectTrackingMap, setProjectTrackingMap] = useState<
|
||||||
Map<number, ProjectTrackingData>
|
Map<string, ProjectTrackingData>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await window.electron.logout()
|
const res = await window.electron.logout();
|
||||||
if (res && res.success) {
|
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout error:", error);
|
console.error("Logout error:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
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
|
// Sync global states with selected project tracking data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
const projectData = projectTrackingMap.get(selectedProject.id);
|
const projectData = projectTrackingMap.get(selectedProject.projectId);
|
||||||
if (projectData) {
|
if (projectData) {
|
||||||
setIsTracking(projectData.isTracking);
|
setIsTracking(projectData.isTracking);
|
||||||
setIsPaused(projectData.isPaused);
|
setIsPaused(projectData.isPaused);
|
||||||
|
|
@ -146,7 +231,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(() => {
|
||||||
setProjectTrackingMap((prev) => {
|
setProjectTrackingMap((prev) => {
|
||||||
const updated = new Map(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) {
|
if (current && current.isTracking && !current.isPaused && current.startTime) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -154,14 +239,12 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
const currentSessionTime = Math.floor((now - startTimestamp) / 1000);
|
const currentSessionTime = Math.floor((now - startTimestamp) / 1000);
|
||||||
const totalPausedTime = current.totalPausedTime || 0;
|
const totalPausedTime = current.totalPausedTime || 0;
|
||||||
|
|
||||||
// FIX: Add accumulated time from previous sessions
|
|
||||||
const calculatedElapsed =
|
const calculatedElapsed =
|
||||||
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
|
(current.accumulatedTime || 0) + currentSessionTime - totalPausedTime;
|
||||||
|
|
||||||
current.elapsedTime = calculatedElapsed;
|
current.elapsedTime = calculatedElapsed;
|
||||||
updated.set(selectedProject.id, { ...current });
|
updated.set(selectedProject.projectId, { ...current });
|
||||||
|
|
||||||
// Update global elapsedTime in sync
|
|
||||||
setElapsedTime(calculatedElapsed);
|
setElapsedTime(calculatedElapsed);
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -183,17 +266,18 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
}, [isTracking, isPaused, selectedProject]);
|
}, [isTracking, isPaused, selectedProject]);
|
||||||
|
|
||||||
// Start tracking
|
// Start tracking
|
||||||
const handleStartTracking = (project: Project) => {
|
const handleStartTracking = async (project: Project) => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
window.electron.toggleCloseBtn(false)
|
window.electron.toggleCloseBtn(false);
|
||||||
|
|
||||||
setProjectTrackingMap((prev) => {
|
setProjectTrackingMap((prev) => {
|
||||||
const updated = new Map(prev);
|
const updated = new Map(prev);
|
||||||
const current = updated.get(project.id) || {
|
const current = updated.get(project.projectId) || {
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
startTime: null,
|
startTime: null,
|
||||||
endTime: null,
|
endTime: null,
|
||||||
|
|
@ -204,29 +288,42 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
accumulatedTime: 0,
|
accumulatedTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep accumulated time from previous sessions
|
|
||||||
current.startTime = now;
|
current.startTime = now;
|
||||||
current.isTracking = true;
|
current.isTracking = true;
|
||||||
current.isPaused = false;
|
current.isPaused = false;
|
||||||
current.pauseStartTime = null;
|
current.pauseStartTime = null;
|
||||||
current.totalPausedTime = 0; // Reset pause time for new session
|
current.totalPausedTime = 0;
|
||||||
updated.set(project.id, current);
|
updated.set(project.projectId, current);
|
||||||
|
|
||||||
// Update global tracking states
|
|
||||||
setIsTracking(true);
|
setIsTracking(true);
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
setStartTime(now);
|
setStartTime(now);
|
||||||
setElapsedTime(current.accumulatedTime); // Start from accumulated time
|
setElapsedTime(current.accumulatedTime);
|
||||||
|
|
||||||
return updated;
|
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
|
// Pause/Resume tracking with pause duration tracking
|
||||||
const handlePauseResume = () => {
|
const handlePauseResume = () => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
|
|
||||||
const currentProjectData = projectTrackingMap.get(selectedProject.id);
|
const currentProjectData = projectTrackingMap.get(selectedProject.projectId);
|
||||||
if (!currentProjectData || !currentProjectData.isTracking) return;
|
if (!currentProjectData || !currentProjectData.isTracking) return;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
@ -234,15 +331,13 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
|
|
||||||
setProjectTrackingMap((prev) => {
|
setProjectTrackingMap((prev) => {
|
||||||
const updated = new Map(prev);
|
const updated = new Map(prev);
|
||||||
const current = updated.get(selectedProject.id);
|
const current = updated.get(selectedProject.projectId);
|
||||||
if (!current) return updated;
|
if (!current) return updated;
|
||||||
|
|
||||||
if (newPausedState) {
|
if (newPausedState) {
|
||||||
// Starting pause
|
|
||||||
current.isPaused = true;
|
current.isPaused = true;
|
||||||
current.pauseStartTime = now;
|
current.pauseStartTime = now;
|
||||||
} else {
|
} else {
|
||||||
// Resuming from pause
|
|
||||||
current.isPaused = false;
|
current.isPaused = false;
|
||||||
if (current.pauseStartTime) {
|
if (current.pauseStartTime) {
|
||||||
const pauseDuration = Math.floor(
|
const pauseDuration = Math.floor(
|
||||||
|
|
@ -253,7 +348,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated.set(selectedProject.id, { ...current });
|
updated.set(selectedProject.projectId, { ...current });
|
||||||
setIsPaused(newPausedState);
|
setIsPaused(newPausedState);
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -261,39 +356,69 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stop tracking - PRESERVES elapsed time
|
// Stop tracking - PRESERVES elapsed time
|
||||||
const handleStopTracking = () => {
|
const handleStopTracking = async () => {
|
||||||
if (!selectedProject) return;
|
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) {
|
if (timerRef.current) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
timerRef.current = null;
|
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();
|
const now = new Date();
|
||||||
window.electron.toggleCloseBtn(true)
|
await window.electron.toggleCloseBtn(true);
|
||||||
|
await window.electron.resizeWindow(500, 600);
|
||||||
|
toggleWindowSize.current = true;
|
||||||
|
|
||||||
setProjectTrackingMap((prev) => {
|
setProjectTrackingMap((prev) => {
|
||||||
const updated = new Map(prev);
|
const updated = new Map(prev);
|
||||||
const current = updated.get(selectedProject.id);
|
const current = updated.get(selectedProject.projectId);
|
||||||
if (!current) return updated;
|
if (!current) return updated;
|
||||||
|
|
||||||
// FIX: Store current elapsed time as accumulated time
|
|
||||||
current.accumulatedTime = current.elapsedTime;
|
current.accumulatedTime = current.elapsedTime;
|
||||||
current.isTracking = false;
|
current.isTracking = false;
|
||||||
current.isPaused = false;
|
current.isPaused = false;
|
||||||
current.endTime = now;
|
current.endTime = now;
|
||||||
current.startTime = null; // Clear startTime for stopped state
|
current.startTime = null;
|
||||||
updated.set(selectedProject.id, { ...current });
|
updated.set(selectedProject.projectId, { ...current });
|
||||||
|
|
||||||
// Update global states - keep elapsed time
|
|
||||||
setIsTracking(false);
|
setIsTracking(false);
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
setEndTime(now);
|
setEndTime(now);
|
||||||
setElapsedTime(current.elapsedTime); // Keep the elapsed time
|
setElapsedTime(current.elapsedTime);
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Tracking stopped. Time preserved:", elapsedTime);
|
console.log(" Tracking stopped successfully. Time preserved:", elapsedTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cleanup timer on unmount
|
// Cleanup timer on unmount
|
||||||
|
|
@ -306,14 +431,14 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle project selection
|
// Handle project selection
|
||||||
const handleProjectSelect = (id: number) => {
|
const handleProjectSelect = (projectId: string) => {
|
||||||
if (isTracking) return;
|
if (isTracking) return;
|
||||||
|
|
||||||
const project = projectsData.find((p) => p.id === id);
|
const project = projectsData.find((p) => p.projectId === projectId);
|
||||||
if (project) {
|
if (project) {
|
||||||
setSelectedProject(project);
|
setSelectedProject(project);
|
||||||
|
|
||||||
const projectData = projectTrackingMap.get(id);
|
const projectData = projectTrackingMap.get(projectId);
|
||||||
if (projectData) {
|
if (projectData) {
|
||||||
setElapsedTime(projectData.elapsedTime);
|
setElapsedTime(projectData.elapsedTime);
|
||||||
setStartTime(projectData.startTime);
|
setStartTime(projectData.startTime);
|
||||||
|
|
@ -352,11 +477,12 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
return `${hrs}h ${mins}m ${secs}s`;
|
return `${hrs}h ${mins}m ${secs}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleWindowSize = useRef(true); // true -> Open, false -> Close
|
const toggleWindowSize = useRef(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header */}
|
{/* 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">
|
<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 */}
|
{/* Left Section */}
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
|
|
@ -415,28 +541,27 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (toggleWindowSize.current) {
|
if (toggleWindowSize.current) {
|
||||||
|
window.electron.resizeWindow(500, 85);
|
||||||
window.electron.resizeWindow(500, 85)
|
|
||||||
toggleWindowSize.current = false;
|
toggleWindowSize.current = false;
|
||||||
} else {
|
} else {
|
||||||
window.electron.resizeWindow(500, 600)
|
window.electron.resizeWindow(500, 600);
|
||||||
toggleWindowSize.current = true;
|
toggleWindowSize.current = true;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn btn-xs btn-outline btn-primary ">
|
className="btn btn-xs btn-outline btn-primary"
|
||||||
|
>
|
||||||
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
|
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
}
|
||||||
|
|
||||||
{/* Layout Body */}
|
{/* Layout Body */}
|
||||||
<div className="flex h-[calc(100vh-3rem)] bg-base-200 overflow-hidden">
|
<div className={`flex ${!isTracking ? "max-h-screen" : " h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
||||||
{/* Sidebar - Always visible */}
|
{/* Sidebar */}
|
||||||
<aside
|
<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">
|
||||||
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) => (
|
{menuItems.map((item) => (
|
||||||
|
<div className="tooltip z-100 tooltip-right" data-tip={item.label}>
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.label}
|
key={item.label}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
|
|
@ -450,11 +575,12 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
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"
|
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 "
|
||||||
title="Logout"
|
data-tip={"Logout"}
|
||||||
>
|
>
|
||||||
<LogOut size={18} />
|
<LogOut size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import React, { useRef } from "react";
|
import React from "react";
|
||||||
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
|
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
|
||||||
// ----- Interfaces -----
|
// Updated Project interface to match JSON structure
|
||||||
interface Project {
|
interface Project {
|
||||||
id: number;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
clientName: string;
|
activityStatus: "ACTIVE" | "AWAY";
|
||||||
|
totalWorkTime: string;
|
||||||
|
totalWorkMs: number;
|
||||||
|
totalLogs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectTrackingData {
|
interface ProjectTrackingData {
|
||||||
|
|
@ -19,7 +22,7 @@ interface ProjectTrackingData {
|
||||||
|
|
||||||
interface TrackingState {
|
interface TrackingState {
|
||||||
selectedProject: Project | null;
|
selectedProject: Project | null;
|
||||||
projectTrackingMap: Map<number, ProjectTrackingData>;
|
projectTrackingMap: Map<string, ProjectTrackingData>;
|
||||||
isTracking: boolean;
|
isTracking: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
|
|
@ -28,11 +31,11 @@ interface TrackingState {
|
||||||
handleStartTracking: (project: Project) => void;
|
handleStartTracking: (project: Project) => void;
|
||||||
handlePauseResume: () => void;
|
handlePauseResume: () => void;
|
||||||
handleStopTracking: () => void;
|
handleStopTracking: () => void;
|
||||||
handleProjectSelect: (id: number) => void;
|
handleProjectSelect: (projectId: string) => void;
|
||||||
projectsData: Project[];
|
projectsData: Project[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Helper -----
|
// Helper function
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
const hrs = Math.floor(seconds / 3600);
|
const hrs = Math.floor(seconds / 3600);
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
@ -40,7 +43,7 @@ const formatTime = (seconds: number) => {
|
||||||
return `${hrs}h ${mins}m ${secs}s`;
|
return `${hrs}h ${mins}m ${secs}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----- Widget -----
|
// Widget Component
|
||||||
const Widget: React.FC<{
|
const Widget: React.FC<{
|
||||||
title: string;
|
title: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
|
|
@ -71,7 +74,7 @@ const Widget: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----- Dashboard -----
|
// Dashboard Component
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
selectedProject,
|
selectedProject,
|
||||||
|
|
@ -88,12 +91,9 @@ const Dashboard: React.FC = () => {
|
||||||
} = useOutletContext<TrackingState>();
|
} = useOutletContext<TrackingState>();
|
||||||
|
|
||||||
const selectedData = selectedProject
|
const selectedData = selectedProject
|
||||||
? projectTrackingMap.get(selectedProject.id)
|
? projectTrackingMap.get(selectedProject.projectId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 overflow-y-auto no-scrollbar">
|
<main className="flex-1 overflow-y-auto no-scrollbar">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -110,7 +110,7 @@ const Dashboard: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Widgets */}
|
{/* 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
|
<Widget
|
||||||
title="Logged Hours Today"
|
title="Logged Hours Today"
|
||||||
value="6h 24m"
|
value="6h 24m"
|
||||||
|
|
@ -129,13 +129,13 @@ const Dashboard: React.FC = () => {
|
||||||
icon={<ProjectorIcon size={24} />}
|
icon={<ProjectorIcon size={24} />}
|
||||||
color="warning"
|
color="warning"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Tracking Section */}
|
{/* Tracking Section */}
|
||||||
<div className="w-full mb-4">
|
<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>
|
<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 bg-base-100 border border-base-200 shadow-sm">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
|
@ -179,11 +179,18 @@ const Dashboard: React.FC = () => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-base-content/70">Client:</span>{" "}
|
<span className="text-base-content/70">Status:</span>{" "}
|
||||||
<span>{selectedProject.clientName}</span>
|
<span
|
||||||
|
className={`badge ${selectedProject.activityStatus === "ACTIVE"
|
||||||
|
? "badge-success"
|
||||||
|
: "badge-warning"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedProject.activityStatus}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-base-content/70">Status:</span>{" "}
|
<span className="text-base-content/70">Tracking:</span>{" "}
|
||||||
<span
|
<span
|
||||||
className={`badge ${selectedData?.isPaused
|
className={`badge ${selectedData?.isPaused
|
||||||
? "badge-warning"
|
? "badge-warning"
|
||||||
|
|
@ -221,35 +228,33 @@ const Dashboard: React.FC = () => {
|
||||||
{/* Projects Table */}
|
{/* Projects Table */}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md mb-2 font-bold">Projects</p>
|
<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">
|
<table className="table table-zebra w-full text-sm">
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
<thead className="sticky top-0 bg-base-200 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Project</th>
|
<th>Name</th>
|
||||||
<th>Client</th>
|
<th>Logs Hours</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{projectsData.map((project) => {
|
{projectsData.map((project, index) => {
|
||||||
const isCurrentlySelected = selectedProject?.id === project.id;
|
const isCurrentlySelected = selectedProject?.projectId === project.projectId;
|
||||||
// ✅ FIX: Disable ALL projects when ANY project is tracking
|
|
||||||
// Only enable selection when not tracking
|
|
||||||
const isDisabled = isTracking && !isCurrentlySelected;
|
const isDisabled = isTracking && !isCurrentlySelected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={project.id}>
|
<tr key={project.projectId}>
|
||||||
<td>{project.id}</td>
|
<td className="text-xs opacity-70">{index + 1}</td>
|
||||||
<td>{project.projectName}</td>
|
<td className="font-medium">{project.projectName}</td>
|
||||||
<td>{project.clientName}</td>
|
<td className="font-mono text-xs">{project.totalWorkTime}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-xs ${isCurrentlySelected
|
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
|
||||||
? "btn-success"
|
|
||||||
: "btn-primary"
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleProjectSelect(project.id)}
|
onClick={() => handleProjectSelect(project.projectId)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
{isCurrentlySelected ? "Selected" : "Select"}
|
{isCurrentlySelected ? "Selected" : "Select"}
|
||||||
|
|
@ -261,6 +266,13 @@ const Dashboard: React.FC = () => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import AdjustmentModal from './AdjustmentModal';
|
import AdjustmentModal from './AdjustmentModal';
|
||||||
|
import { RootState } from '../../store';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
interface LeaveFormData {
|
interface LeaveFormData {
|
||||||
fromDate: string;
|
fromDate: string;
|
||||||
toDate: string;
|
toDate: string;
|
||||||
description: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdjustmentEntry {
|
interface AdjustmentEntry {
|
||||||
|
|
@ -29,16 +30,18 @@ const data: LogEntry[] = [
|
||||||
{ id: 5, date: "2025-11-05", hours: 5.5, task: "Code Review", status: "Completed" },
|
{ 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 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
|
// PTO State
|
||||||
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
|
const [ptoLeave, setPtoLeave] = useState({
|
||||||
fromDate: '',
|
fromDate: '',
|
||||||
toDate: '',
|
toDate: '',
|
||||||
description: ''
|
|
||||||
});
|
});
|
||||||
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
|
|
@ -46,9 +49,10 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
||||||
fromDate: '',
|
fromDate: '',
|
||||||
toDate: '',
|
toDate: '',
|
||||||
description: ''
|
|
||||||
});
|
});
|
||||||
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState("")
|
||||||
|
const [selectedDate, setSelectedDate] = useState("")
|
||||||
|
|
||||||
// Adjustment State
|
// Adjustment State
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
|
const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
|
||||||
|
|
@ -83,13 +87,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
return errors;
|
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 handlePtoSubmit = async () => {
|
||||||
const errors = validateLeaveForm(ptoLeave);
|
const errors = validateLeaveForm(ptoLeave);
|
||||||
|
|
@ -162,12 +159,57 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
handleCloseModal();
|
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 (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="max-h-screen">
|
||||||
<div className="w-full mx-auto">
|
<div className="w-full mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className=" ">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
|
|
@ -175,8 +217,8 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
<div className="flex ">
|
<div className="flex ">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('pto')}
|
onClick={() => setActiveTab('create_and_adjust_timesheet')}
|
||||||
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'pto'
|
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'
|
? ' 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">
|
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" />
|
<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>
|
</svg>
|
||||||
PTO Request
|
Create or Adjust Timesheet
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('sick')}
|
{/* <button
|
||||||
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
|
|
||||||
onClick={() => setActiveTab('adjustment')}
|
onClick={() => setActiveTab('adjustment')}
|
||||||
className={`flex-1 px-6 py-3 text-xs font-semibold cursor-pointer transition-all duration-300 relative ${activeTab === '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'
|
? ' text-primary border-b-2 border-primary'
|
||||||
|
|
@ -216,30 +245,71 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</svg>
|
</svg>
|
||||||
Adjustments
|
Adjustments
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="px-8 py-4">
|
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[80vh]">
|
||||||
{/* PTO Tab */}
|
{/* PTO Tab */}
|
||||||
{activeTab === 'pto' && (
|
{activeTab === 'create_and_adjust_timesheet' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="max-w-4xl ">
|
<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>
|
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="form-control">
|
<div className="form-control col-span-2">
|
||||||
<label className="label">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={ptoLeave.fromDate}
|
value={selectedDate || ""}
|
||||||
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
|
onChange={(e) => handleSelectedDate(e.target.value)}
|
||||||
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
|
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 && (
|
{ptoErrors.fromDate && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
@ -250,13 +320,58 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
|
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label">
|
<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>
|
</label>
|
||||||
<input
|
<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}
|
value={ptoLeave.toDate}
|
||||||
onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
onChange={(e) =>
|
||||||
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
|
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 && (
|
{ptoErrors.toDate && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
@ -268,13 +383,13 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
|
|
||||||
<div className="form-control flex flex-col gap-1">
|
<div className="form-control flex flex-col gap-1">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text font-semibold ">Description / Reason</span>
|
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={ptoLeave.description}
|
value={ptoLeave.description}
|
||||||
onChange={(e) => handlePtoChange('description', e.target.value)}
|
// onChange={(e) => handlePtoChange('description', e.target.value)}
|
||||||
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
|
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
|
||||||
placeholder="Please provide a reason for your PTO request..."
|
placeholder="Please enter the task name or leave notes to help us better understand your request."
|
||||||
/>
|
/>
|
||||||
{ptoErrors.description && (
|
{ptoErrors.description && (
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
@ -493,14 +608,14 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Status */}
|
{/* View Status */}
|
||||||
<div className='mt-4'>
|
{/* <div className='mt-4'>
|
||||||
<h1 className="text-md font-bold mb-2">View Status</h1>
|
<h1 className="text-md font-bold mb-2">View Status</h1>
|
||||||
<div
|
<div
|
||||||
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
|
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
|
||||||
style={{ minHeight: "150px", maxHeight: "300px" }}
|
style={{ minHeight: "150px", maxHeight: "300px" }}
|
||||||
>
|
>
|
||||||
<table className="table table-zebra w-full">
|
<table className="table table-zebra w-full">
|
||||||
{/* Table Head */}
|
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
<thead className="sticky top-0 bg-base-200 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
|
|
@ -511,7 +626,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
{/* Table Body */}
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((log, index) => (
|
{data.map((log, index) => (
|
||||||
<tr key={log.id}>
|
<tr key={log.id}>
|
||||||
|
|
@ -535,7 +650,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div></div>
|
</div></div> */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from {
|
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 React, { useEffect, useState } from "react";
|
||||||
import { CalendarDays, Clock, FileText, CheckCircle, XCircle } from "lucide-react";
|
import { CalendarDays, Clock, FileText, CheckCircle, XCircle, Eye } from "lucide-react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "../../store";
|
import { RootState } from "../../store";
|
||||||
|
import ProjectLogsModal from "./ProjectLogsModal";
|
||||||
|
|
||||||
interface TimesheetEntry {
|
interface TimesheetEntry {
|
||||||
id: number;
|
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" },
|
{ 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 Timesheets: React.FC = () => {
|
||||||
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
|
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
|
||||||
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
|
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
|
||||||
|
|
@ -27,6 +37,62 @@ const Timesheets: React.FC = () => {
|
||||||
const { windowLogs } = useSelector((state: RootState) => state.window);
|
const { windowLogs } = useSelector((state: RootState) => state.window);
|
||||||
|
|
||||||
console.log("🧾 Timesheet in component:", windowLogs);
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-2 max-h-screen">
|
<div className="flex flex-col gap-2 max-h-screen">
|
||||||
|
|
@ -38,7 +104,7 @@ const Timesheets: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* 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 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 className="card-body py-3 px-4 flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -70,42 +136,33 @@ const Timesheets: React.FC = () => {
|
||||||
<FileText size={20} className="text-warning" />
|
<FileText size={20} className="text-warning" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Timesheet Table */}
|
{/* Timesheet Table */}
|
||||||
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[60vh]">
|
<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-sm">
|
<table className="table table-zebra w-full text-[12px]">
|
||||||
<thead className="sticky top-0 bg-base-200 z-10 text-sm">
|
<thead className="sticky top-0 bg-base-200 z-10 ">
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Date</th>
|
|
||||||
<th>Project</th>
|
<th>Project</th>
|
||||||
<th>Hours</th>
|
<th>Hours</th>
|
||||||
<th>Status</th>
|
<th>Logs</th>
|
||||||
<th>Remarks</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{timesheetData.map((entry) => (
|
{projectsData.map((entry, index) => (
|
||||||
<tr key={entry.id}>
|
<tr key={entry.projectId}>
|
||||||
<td>{entry.id}</td>
|
<td>{index+1}</td>
|
||||||
<td className="whitespace-nowrap">{entry.date}</td>
|
<td>{entry.projectName}</td>
|
||||||
<td>{entry.project}</td>
|
<td>{entry.totalWorkTime}</td>
|
||||||
<td>{entry.hours} hrs</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div
|
<button
|
||||||
className={`badge badge-sm ${
|
onClick={() => handleViewLogs(entry.projectId, entry.projectName)}
|
||||||
entry.status === "Approved"
|
className="btn btn-ghost btn-xs"
|
||||||
? "badge-success"
|
|
||||||
: entry.status === "Pending"
|
|
||||||
? "badge-warning"
|
|
||||||
: "badge-error"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{entry.status}
|
<Eye size={16} />
|
||||||
</div>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>{entry.remarks || "-"}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -121,6 +178,14 @@ const Timesheets: React.FC = () => {
|
||||||
Last updated: {new Date().toLocaleDateString()}
|
Last updated: {new Date().toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ProjectLogsModal
|
||||||
|
isOpen={modalOpen}
|
||||||
|
onClose={() => setModalOpen(false)}
|
||||||
|
projectName={selectedProjectName}
|
||||||
|
logs={logs}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
// src/store/index.ts
|
// src/store/index.ts
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import windowReducer from "./windowSlice";
|
import windowReducer from "./windowSlice";
|
||||||
|
import projectsReducer from "./projectSlice";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
window: windowReducer,
|
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;
|
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 {
|
interface Window {
|
||||||
electron: {
|
electron: {
|
||||||
login: (credentials: { email: string; password: string }) => Promise<any>;
|
login: (credentials: { email: string; password: string }) => Promise<any>;
|
||||||
|
|
@ -33,6 +73,33 @@ interface Window {
|
||||||
toggleCloseBtn: (shouldEnable: boolean) => {},
|
toggleCloseBtn: (shouldEnable: boolean) => {},
|
||||||
resizeWindow: (width: number, height: number) => {},
|
resizeWindow: (width: number, height: number) => {},
|
||||||
onWindowUpdate: (callback: (data: any) => void) => void;
|
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