From 1d71f8f36971eddf13dc25c72c899978484e2cff Mon Sep 17 00:00:00 2001 From: BhagwanSingh Date: Tue, 18 Nov 2025 17:15:03 +0530 Subject: [PATCH 1/6] Live View Done --- package-lock.json | 7 ++ package.json | 3 +- src/electron/api/axiosClient.ts | 8 +- src/electron/main.ts | 34 +++---- src/electron/utils/utilityFunctions.ts | 10 +- src/ui/layout/DashboardLayout.tsx | 126 +++++++++++++++++++++---- 6 files changed, 149 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2babc3e..270cb8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "chalk": "^5.6.2", "electron-store": "^11.0.2", "get-windows": "^9.2.3", + "https": "^1.0.0", "lucide-react": "^0.552.0", "luxon": "^3.7.2", "os-utils": "^0.0.14", @@ -7040,6 +7041,12 @@ "node": ">=10.19.0" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", diff --git a/package.json b/package.json index d54bd5f..3e8cd39 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "npm-run-all --parallel dev:react dev:electron", "dev:react": "vite", - "dev:electron": "npm run build && cross-env NODE_ENV=development electron .", + "dev:electron": "npm run build && cross-env NODE_ENV=development electron --inspect=5858 .", "prod:electron": "npm run build && cross-env NODE_ENV=production electron .", "build": "tsc && vite build", "preview": "vite preview", @@ -26,6 +26,7 @@ "chalk": "^5.6.2", "electron-store": "^11.0.2", "get-windows": "^9.2.3", + "https": "^1.0.0", "lucide-react": "^0.552.0", "luxon": "^3.7.2", "os-utils": "^0.0.14", diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index 5fedd1c..df8c255 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -3,11 +3,17 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { getToken } from '../utils/tokenHelper.js'; import chalk from "chalk"; import { sendTokenExpired } from '../ipc/ipcHandler.js'; +import https from 'https'; const TOKEN_KEY = 'ACCESS_TOKEN'; +const httpsAgent = new https.Agent({ + rejectUnauthorized: false +}); + const api: AxiosInstance = axios.create({ - baseURL: 'https://dreadingly-unfighting-pandora.ngrok-free.dev/api/v1/va', // πŸ” your backend + baseURL: 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va', // πŸ” your backend + httpsAgent // timeout: 10000, }); diff --git a/src/electron/main.ts b/src/electron/main.ts index 3d9d9e5..e7660bb 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -53,7 +53,7 @@ app.on("ready", async () => { show: true, skipTaskbar: true, webPreferences: { - devTools: false, + devTools: true, } }); @@ -77,7 +77,7 @@ app.on("ready", async () => { frame: process.platform === "darwin" ? false : true, show: false, // speed improvement webPreferences: { - devTools: false, + devTools: true, preload: getPreloadPath(), contextIsolation: true, nodeIntegration: false, @@ -101,7 +101,7 @@ app.on("ready", async () => { // ------------------------------------ if (isDev()) { win.loadURL("http://localhost:5123"); - // win.webContents.openDevTools(); // Optional (slows startup) + win.webContents.openDevTools(); // Optional (slows startup) } else { win.loadFile(getUIPath()); } @@ -128,21 +128,21 @@ app.on("ready", async () => { }); -app.on('browser-window-focus', () => { - globalShortcut.register('CommandOrControl+R', () => { - // Do nothing; disables reload - }); - globalShortcut.register('F5', () => { - // Do nothing; disables reload - }); - globalShortcut.register('CommandOrControl+Shift+I', () => { - // Do nothing; disables DevTools - }); -}); +// app.on('browser-window-focus', () => { +// globalShortcut.register('CommandOrControl+R', () => { +// // Do nothing; disables reload +// }); +// globalShortcut.register('F5', () => { +// // Do nothing; disables reload +// }); +// globalShortcut.register('CommandOrControl+Shift+I', () => { +// // Do nothing; disables DevTools +// }); +// }); -app.on('browser-window-blur', () => { - globalShortcut.unregisterAll(); -}); +// app.on('browser-window-blur', () => { +// globalShortcut.unregisterAll(); +// }); // --------------------------- diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts index 91324c2..ce15b8f 100644 --- a/src/electron/utils/utilityFunctions.ts +++ b/src/electron/utils/utilityFunctions.ts @@ -2,12 +2,18 @@ import { app, BrowserWindow, desktopCapturer } from "electron"; import * as fs from "fs"; import path from "path"; import axios from 'axios'; +import https from 'https'; import sharp from 'sharp'; import { saveToken } from "./tokenHelper.js"; import { activeWindow } from "get-windows"; import { exec } from "child_process"; import { getUIPath } from "../pathResolver.js"; +const httpsAgent = new https.Agent({ + rejectUnauthorized: false +}); + + // Blur using an offscreen canvas async function blurAndCompressBase64Image(base64: string): Promise { const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64'); @@ -64,9 +70,9 @@ export const handleLogin = async (credentials: { email: string, password: string try { console.log("➑️ Calling API..."); const response = await axios.post( - 'https://dreadingly-unfighting-pandora.ngrok-free.dev/api/v1/va/login', + 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login', { userDetails: { ...credentials } }, - { headers: { "ngrok-skip-browser-warning": 69420 } } + { headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent } ); console.log("API CAlled", response) diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 75f2b71..e6e4aa8 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -146,7 +146,7 @@ const DashboardLayout = () => { return () => { console.log("🧹 Cleaning token-expired listener"); }; - }, []); + }, []); @@ -262,7 +262,7 @@ const DashboardLayout = () => { dispatch(clearUser()); // βœ… Removes from Redux + localStorage navigate("/login"); } - } catch (error) { + } catch (error) { console.error("Logout error:", error); } @@ -371,6 +371,9 @@ const DashboardLayout = () => { const res = await window.electron.startProjectTracking(payload); console.log(" Synced start tracking:", res); setActiveProjectLogId(res?.logId) + + handleConnect(); + } catch (error) { console.error(" Failed to sync project start:", error); } @@ -445,6 +448,7 @@ const DashboardLayout = () => { console.log("πŸ“‘ Sending stop tracking payload:", payload); const res = await window.electron.stopProjectTracking(payload); console.log(" Synced stop tracking:", res); + handleDisconnect(); } catch (error) { console.error("❌ Failed to sync project stop:", error); } @@ -536,6 +540,92 @@ const DashboardLayout = () => { }; + + + const handleConnect = async () => { + + const token = await window.electron.getToken("ACCESS_TOKEN"); + + if (socketRef.current) { + console.warn("Already connected"); + return; + } + if (!selectedProject?.projectId) return + + console.log(selectedProject?.projectId, "SelectedProject ID") + socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", { + transports: ["websocket"], + secure: true, + rejectUnauthorized: false, + reconnection: true, + withCredentials: true, + forceNew: true, + extraHeaders: { + "Access-Control-Allow-Origin": "*" + }, + auth: { + token: token, + projectId: selectedProject?.projectId, + userType: "va" + }, + }); + + socketRef.current.on("connect", () => { + console.log("🟒 Connected:", socketRef.current?.id); + setIsConnected(true); + + // Start sending every 5 seconds + intervalRef.current = setInterval(async () => { + const apps = await window.electron.getRunningApps() + socketRef.current?.emit("getVisibleApps", { token: await token, apps }); + + // const base64String = await window.electron.captureAppScreenShot("Screen 1"); + // if (base64String) { + // socketRef.current?.emit("sendImage", {token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5MGIzMjhkMDkxMDFmNzRjZjUyNGMwNyIsImVtYWlsIjoiYmhhZ3dhbi5yQGhpY2dsb2JhbHNvbHV0aW9ucy5jb20iLCJuYW1lIjoiUm9oaXQiLCJpYXQiOjE3NjI4NTgxOTMsImV4cCI6MTc2Mjk0NDU5M30.UlHy3RU9W5LOid7smG7WzyNT7dnqhQT4qqzRrN1axmU", image: base64String, title: "Screen 1"}); + // } + }, 5000); + }); + + socketRef.current.on("appToCapture", async ({ appTitle }) => { + console.log("πŸ“Έ Capture requested for:", appTitle); + + const base64String = await window.electron.captureAppScreenShot(appTitle); + if (base64String && socketRef.current) { + socketRef.current.emit("sendImage", { + token: token, + image: base64String, + title: appTitle + }); + } + }); + + socketRef.current.on("disconnect", () => { + console.log("πŸ”΄ Disconnected"); + setIsConnected(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }); + }; + + const handleDisconnect = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + setIsConnected(false); + console.log("πŸ”Œ Disconnected manually"); + } + }; + + + + const trackingState: TrackingState = { selectedProject, projectTrackingMap, @@ -567,21 +657,21 @@ const DashboardLayout = () => {
{/* Left Section */} {/* {!isConnected ? ( - - ) : ( - - )} */} -
+ + ) : ( + + )} */} +
{selectedProject ? ( isTracking ? ( - ) : ( - - )} */} -
+
+
{selectedProject ? ( isTracking ? ( + ) : (
- {/* Right Section */}
- {/* Timer */}
{formatTime(elapsedTime || 0)}
- {/* Alert Icon */} - {/* Dropdown */} } + {!isTracking && ( + + )} {/* Main Content */} diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index d90aacd..08d0a60 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -2,6 +2,10 @@ import React, { useEffect, useRef, useState } from "react"; import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react"; import { useOutletContext } from "react-router-dom"; import NoData from "./NoData"; +import { useDispatch, useSelector } from "react-redux"; +import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice"; +import { setError, setLoading, setProfile } from "../store/profileSlice"; +import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits'; // Updated Project interface to match JSON structure interface Project { @@ -11,6 +15,7 @@ interface Project { totalWorkTime: string; totalWorkMs: number; totalLogs: number; + status: string } interface ProjectTrackingData { @@ -23,7 +28,7 @@ interface ProjectTrackingData { interface TrackingState { selectedProject: Project | null; - projectTrackingMap: Map; + // projectTrackingMap: Map; isTracking: boolean; isPaused: boolean; elapsedTime: number; @@ -34,6 +39,8 @@ interface TrackingState { handleStopTracking: () => void; handleProjectSelect: (projectId: string) => void; projectsData: Project[]; + loadingStart: boolean, + loadingStop: boolean } // Helper function @@ -79,8 +86,8 @@ const Widget: React.FC<{ // Dashboard Component const Dashboard: React.FC = () => { const { - selectedProject, - projectTrackingMap, + // selectedProject, + // projectTrackingMap, isTracking, isPaused, elapsedTime, @@ -89,7 +96,9 @@ const Dashboard: React.FC = () => { handlePauseResume, handleStopTracking, handleProjectSelect, - projectsData, + // projectsData, + loadingStart, + loadingStop } = useOutletContext(); // interface AppsListInt { @@ -100,10 +109,112 @@ const Dashboard: React.FC = () => { // const [runningApps, setRunningApps] = useState([]); + const projectsData = useSelector((state: any) => state.projects.projectsData); + const selectedProject = useSelector((state: any) => state.projects.selectedProject); + const projectSummary = useSelector((state: any) => state.projects.projectSummary); + const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); + + + console.log("Line 111", projectsData, selectedProject, projectTrackingMap) + + + const dispatch = useDispatch(); + + + const fetchProfile = async () => { + try { + dispatch(setLoading(true)); + const res = await window.electron.getProfile(); + console.log("Profile", res) + if (res.success && res.data) { + dispatch(setProfile(res.data)); + } else { + dispatch(setError("Failed to fetch profile")); + } + } catch (err: any) { + dispatch(setError(err.message)); + } finally { + dispatch(setLoading(false)); + } + }; + + useEffect(() => { + const fetchProjects = async () => { + try { + console.log("πŸ“‘ Fetching projects...Dashboard"); + const res = await window.electron.getProjectList(); + console.log(" Project list response in dashboard:", 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]; + + // Select the project + dispatch(setSelectedProjectSlice(firstProject)); + + // Convert total work ms β†’ seconds + const initialElapsed = Math.floor((firstProject.totalWorkMs || 0) / 1000); + + // Initialize tracking data for first project (your slice matches this) + dispatch( + initializeProjectTracking({ + projectId: firstProject.projectId, + initialTime: initialElapsed, + }) + ); + } + + fetchProfile() + + } else { + console.warn("⚠️ Unexpected response:", res); + // if (res?.status) { + // // handleLogout() + // } + } + } catch (err) { + console.error("❌ Error fetching projects:", err); + } + }; + + fetchProjects(); + }, []); + const selectedData = selectedProject - ? projectTrackingMap.get(selectedProject.projectId) + ? projectTrackingMap[selectedProject.projectId] : null; + const formatWorkTime = (time: string) => { if (!time) return "0h 0m"; @@ -115,49 +226,40 @@ const Dashboard: React.FC = () => { }; + const handleProjectSelectLocal = (projectId: string) => { + + const project = projectsData.find((p: any) => p.projectId === projectId); + if (!project) return; + + dispatch(setSelectedProjectSlice(project)); + + const existingData = projectTrackingMap[projectId]; + + if (!existingData) { + const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000); + + dispatch(initializeProjectTracking({ + projectId, + initialTime: initialElapsed + })); + } else { + // Restore saved tracking state + dispatch(updateProjectTracking({ + projectId, + data: existingData + })); + } + }; + + return (
{/* Header */}

Dashboard

- {/* */}
- - {/* Widgets */} - {/*
- } - color="primary" - /> - } - color="success" - /> - } - color="warning" - /> -
*/} - {/* Tracking Section */}
- {/*
- Tracking -
*/} -
{selectedProject ? ( @@ -183,60 +285,33 @@ const Dashboard: React.FC = () => { ) : ( )} +
- - {/*
-
- Status:{" "} - - {selectedProject.activityStatus} - -
-
- Tracking:{" "} - - {selectedData?.isPaused - ? "Paused" - : selectedData?.isTracking - ? "Active" - : "Ready"} - -
-
- Start:{" "} - - {selectedData?.startTime - ? selectedData.startTime.toLocaleTimeString() - : "β€”"} - -
-
*/} ) : (

@@ -257,7 +332,7 @@ const Dashboard: React.FC = () => { {/* πŸ”Ή Table or Empty State */} {projectsData.length > 0 ? (

{ # Name - {/* Logs Hours */} + Logs Hours Action - {projectsData.map((project, index) => { + {projectsData.map((project: any, index: any) => { const isCurrentlySelected = selectedProject?.projectId === project.projectId; const isDisabled = isTracking && !isCurrentlySelected; + const limitReached = isProjectLimitReached(project); + const remainingTime = getFormattedRemainingTime(project); return ( - + {index + 1} - {project.projectName} - {/* - {formatWorkTime(project.totalWorkTime)} - */} +
+ {project.projectName} + {limitReached && ( + Daily limit reached + )} +
+ +
+ + {formatWorkTime(project.totalWorkTime)} + + + of {project.dedicatedHours} + +
+ - +
+ + {!limitReached && ( + + {remainingTime} left + + )} +
); diff --git a/src/ui/pages/Login.tsx b/src/ui/pages/Login.tsx index 3f0f16f..138c08f 100644 --- a/src/ui/pages/Login.tsx +++ b/src/ui/pages/Login.tsx @@ -5,6 +5,7 @@ import { setUserSlice } from '../store/userSlice'; import DocVaLogoLight from '../../assets/logo-colored-vertical.svg' import DocVaLogoDark from '../../assets/logo-white-vertical.svg' import { ThemeToggle } from '../helper/ThemeToggle'; +import { KeyRound, Mail } from 'lucide-react'; const Login = () => { const theme = ThemeToggle(); @@ -41,7 +42,8 @@ const Login = () => { email: response.user.email, })); navigate("/") - } else if (response.success == false) { + } + else if (response.success == false) { console.log(response.error.message) setErrorMessage(response.error.message) } @@ -112,15 +114,8 @@ const Login = () => { -
@@ -384,11 +384,11 @@ const Dashboard: React.FC = () => { > {isCurrentlySelected ? "Selected" : "Select"} - {!limitReached && ( + {/* {!limitReached && ( {remainingTime} left - )} + )} */}
diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index a342136..6bf360f 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -535,7 +535,7 @@ const LeaveAndAdjustments: React.FC = () => { {/* Header */}
-

Adjust Timesheet

+

Leave & Adjust Timesheet

{/* General Error Alert */} @@ -545,6 +545,9 @@ const LeaveAndAdjustments: React.FC = () => {
)} + + + {/* Main Container */}
{/* Tabs Navigation */} From 4ab4164f65116ea4916fb56e6a02d9e0a2edc7cd Mon Sep 17 00:00:00 2001 From: BhagwanSingh Date: Thu, 20 Nov 2025 12:57:36 +0530 Subject: [PATCH 4/6] bm 2011 --- src/ui/layout/DashboardLayout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index e6e4aa8..c009f77 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -594,7 +594,8 @@ const DashboardLayout = () => { socketRef.current.emit("sendImage", { token: token, image: base64String, - title: appTitle + title: appTitle, + projectId: selectedProject?.projectId, }); } }); From e7696925e70c3cceee224924bc9a4c0a4cf15bb1 Mon Sep 17 00:00:00 2001 From: BhagwanSingh Date: Fri, 21 Nov 2025 12:18:33 +0530 Subject: [PATCH 5/6] s --- src/electron/utils/utilityFunctions.ts | 1 - src/ui/layout/DashboardLayout.tsx | 82 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts index ce15b8f..c56e5a2 100644 --- a/src/electron/utils/utilityFunctions.ts +++ b/src/electron/utils/utilityFunctions.ts @@ -61,7 +61,6 @@ export const handleLogin = async (credentials: { email: string, password: string // Call your login API console.log("🟑 login Request received", credentials); - console.log("Doom"); console.log("app.getAppPath()", app.getAppPath()); console.log("UIPATH", getUIPath()); console.log("Doom 1"); diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 3978ebb..26b6c27 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -134,6 +134,7 @@ const DashboardLayout = () => { console.log(" Current elapsed time:", elapsedTime); setLoadingStop(true); + handleSocketDisconnect(); // STEP 1: Send final PATCH with last active window data // This ensures the last window activity is recorded before logout @@ -345,6 +346,8 @@ const DashboardLayout = () => { }) ); + handleSocketConnect() + setActiveProjectLogId(res?.data?.logId); console.log(" Tracking started. Log ID:", res?.data?.logId); @@ -396,6 +399,7 @@ const DashboardLayout = () => { // Trigger auto-stop handleLimitExceeded(); + handleSocketDisconnect(); } }, 1000); @@ -438,6 +442,84 @@ const DashboardLayout = () => { } }; + const handleSocketConnect = async () => { + + const token = await window.electron.getToken("ACCESS_TOKEN"); + + if (socketRef.current) { + console.warn("Already connected"); + return; + } + if (!selectedProject?.projectId) return + + console.log(selectedProject?.projectId, "SelectedProject ID") + socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", { + transports: ["websocket"], + secure: true, + rejectUnauthorized: false, + reconnection: true, + withCredentials: true, + forceNew: true, + extraHeaders: { + "Access-Control-Allow-Origin": "*" + }, + auth: { + token: token, + projectId: selectedProject?.projectId, + userType: "va" + }, + }); + + socketRef.current.on("connect", () => { + console.log("🟒 Connected:", socketRef.current?.id); + setIsConnected(true); + + // Start sending every 5 seconds + intervalRef.current = setInterval(async () => { + const apps = await window.electron.getRunningApps() + socketRef.current?.emit("getVisibleApps", { token: await token, apps }); + }, 5000); + }); + + socketRef.current.on("appToCapture", async ({ appTitle }) => { + console.log("πŸ“Έ Capture requested for:", appTitle); + + const base64String = await window.electron.captureAppScreenShot(appTitle); + if (base64String && socketRef.current) { + socketRef.current.emit("sendImage", { + token: token, + image: base64String, + title: appTitle, + projectId: selectedProject?.projectId, + }); + } + }); + + socketRef.current.on("disconnect", () => { + console.log("πŸ”΄ Disconnected"); + setIsConnected(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }); + }; + + const handleSocketDisconnect = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + setIsConnected(false); + console.log("πŸ”Œ Disconnected manually"); + } + }; + + const trackingState: TrackingState = { selectedProject, isTracking, From 3a345201524575af98ae7e95ad0212ecbe2376b3 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Fri, 21 Nov 2025 12:22:02 +0530 Subject: [PATCH 6/6] pto-and-leave-dark-light-mode-toggle --- src/ui/App.tsx | 7 +- src/ui/helper/ThemeToggle.tsx | 22 +- src/ui/layout/DashboardLayout.tsx | 56 +- src/ui/modal/LogoutModal.tsx | 47 ++ .../LeaveAndAdjustments.tsx | 784 +----------------- .../tabs/AdjustmentTab.tsx | 537 ++++++++++++ .../LeaveAndAdjustments/tabs/PtoLeaveTab.tsx | 149 ++++ .../LeaveAndAdjustments/tabs/SickLeaveTab.tsx | 148 ++++ src/ui/pages/Login.tsx | 7 +- src/ui/pages/Settings/Settings.tsx | 11 +- src/ui/pages/Timesheets/Timesheets.tsx | 65 +- 11 files changed, 1034 insertions(+), 799 deletions(-) create mode 100644 src/ui/modal/LogoutModal.tsx create mode 100644 src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx create mode 100644 src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx create mode 100644 src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx diff --git a/src/ui/App.tsx b/src/ui/App.tsx index a792f6c..fbbf871 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -89,7 +89,6 @@ import Projects from "./pages/Projects/Projects"; import DashboardLayout from "./layout/DashboardLayout"; import ProtectedRoute from "./routes/ProtectedRoute"; import PublicRoute from "./routes/PublicRoute"; -import { ThemeToggle } from "./helper/ThemeToggle"; import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments"; import Timesheets from "./pages/Timesheets/Timesheets"; import Settings from "./pages/Settings/Settings"; @@ -98,15 +97,13 @@ import Vpn from "./pages/Vpn/Vpn"; const App: React.FC = () => { - const theme = ThemeToggle(); + const dispatch = useDispatch(); // βœ… Get user directly from Redux const user = useSelector((state: RootState) => state.user.user); - useEffect(() => { - document.documentElement.setAttribute("data-theme", theme); - }, [theme]); + useEffect(() => { dispatch(loadUserFromStorage()); diff --git a/src/ui/helper/ThemeToggle.tsx b/src/ui/helper/ThemeToggle.tsx index 050d963..3ecd3b6 100644 --- a/src/ui/helper/ThemeToggle.tsx +++ b/src/ui/helper/ThemeToggle.tsx @@ -1,24 +1,30 @@ -// src/hooks/ThemeToggle.js import { useState, useEffect } from "react"; -export const ThemeToggle = () => { +export const useTheme = () => { const [theme, setTheme] = useState("light"); useEffect(() => { const darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); - // Initial check setTheme(darkQuery.matches ? "dark" : "light"); - // Listen for changes - const handleChange = (e: MediaQueryListEvent) => { + const handleChange = (e:any) => { setTheme(e.matches ? "dark" : "light"); }; - darkQuery.addEventListener("change", handleChange); + darkQuery.addEventListener?.("change", handleChange); + darkQuery.addListener?.(handleChange); // For older Electron - return () => darkQuery.removeEventListener("change", handleChange); + return () => { + darkQuery.removeEventListener?.("change", handleChange); + darkQuery.removeListener?.(handleChange); + }; }, []); - return theme; + // Apply theme to + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); + + return { theme, setTheme }; }; diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 26b6c27..5953436 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -34,6 +34,8 @@ import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice import { clearUser } from "../store/userSlice"; import { setError, setLoading, setProfile } from "../store/profileSlice"; import { canStartTracking } from "../utils/projectLimits"; +import { useTheme } from "../helper/ThemeToggle"; +import LogoutModal from "../modal/LogoutModal"; interface User { id: string; @@ -79,6 +81,11 @@ export interface TrackingState { } const DashboardLayout = () => { + const { theme, setTheme } = useTheme(); + + const toggleTheme = () => { + setTheme(theme === "dark" ? "light" : "dark"); + }; const [isSidebarOpen, setSidebarOpen] = useState(true); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const navigate = useNavigate(); @@ -114,6 +121,11 @@ const DashboardLayout = () => { const toggleWindowSize = useRef(true); const [loadingStart, setLoadingStart] = useState(false); const [loadingStop, setLoadingStop] = useState(false); + const [showLogoutModal, setShowLogoutModal] = useState(false); + + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); // Create a ref to store the stop function so it can be called from the hook @@ -155,9 +167,9 @@ const DashboardLayout = () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; - console.log("⏹️ Timer cleared"); + console.log("Timer cleared"); } else { - console.log("ℹ️ No active timer to clear"); + console.log("No active timer to clear"); } // STEP 3: Send LOGOUT to backend @@ -222,6 +234,7 @@ const DashboardLayout = () => { return; } + console.log("Alert Message 1") // Show alert to user alert( `⏰ Daily Limit Reached!\n\n` + @@ -229,7 +242,7 @@ const DashboardLayout = () => { `Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` + `Your tracking has been stopped automatically because you've reached the daily time limit for this project.` ); - + console.log("Alert Message 2") // Stop tracking await handleStopTracking(); }, [selectedProject, handleStopTracking]); @@ -625,7 +638,7 @@ const DashboardLayout = () => { {/* Layout Body */}
{/* Sidebar */} -
+ ); }; diff --git a/src/ui/modal/LogoutModal.tsx b/src/ui/modal/LogoutModal.tsx new file mode 100644 index 0000000..5beac1e --- /dev/null +++ b/src/ui/modal/LogoutModal.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +interface LogoutModalProps { + open: boolean; + onClose: () => void; + onConfirm: () => void | Promise; +} + +const LogoutModal: React.FC = ({ open, onClose, onConfirm }) => { + if (!open) return null; + + return ( + +
+

Confirm Logout

+ +

+ Are you sure you want to logout? You will need to login again to access your dashboard. +

+ +
+ + {/* Cancel */} + + + {/* Logout */} + + +
+
+
+ ); +}; + +export default LogoutModal; diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index 6bf360f..9d8471c 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -1,769 +1,71 @@ -import React, { useEffect, useState } from 'react'; -import AdjustmentModal from './AdjustmentModal'; -import { RootState } from '../../store'; +import React, { useState } from 'react'; import { useSelector } from 'react-redux'; -import { DateTime } from 'luxon'; +import { RootState } from '../../store'; +import PtoLeaveTab from './tabs/PtoLeaveTab'; +import SickLeaveTab from './tabs/SickLeaveTab'; +import AdjustmentTab from './tabs/AdjustmentTab'; -interface LeaveFormData { - fromDate: string; - toDate: string; -} - -interface AdjustmentEntry { - id: number; - date: string; - loggedHours: number; -} - -interface LogEntry { - id: number; - date: string; - hours: number; - task: string; - status: "Completed" | "In Progress" | "Pending"; -} - -interface ProjectSummaryResponse { - totalWorkedHours: string; - totalWorkedHoursDetailed: string; - totalWorkedHoursDecimal: string; - totalWorkedMinutes: string; - dedicatedHours: number; - pendingHours: string; - firstLoginTime: string; - lastLogoutTime: string; -} - -const data: LogEntry[] = [ - { id: 1, date: "2025-11-01", hours: 8, task: "Frontend Development", status: "Completed" }, - { id: 2, date: "2025-11-02", hours: 6, task: "API Integration", status: "In Progress" }, - { id: 3, date: "2025-11-03", hours: 7.5, task: "UI Testing", status: "Pending" }, - { id: 4, date: "2025-11-04", hours: 9, task: "Bug Fixing", status: "Completed" }, - { id: 5, date: "2025-11-05", hours: 5.5, task: "Code Review", status: "Completed" }, -]; - -type TabType = 'create_and_adjust_timesheet' | 'sick' | 'adjustment'; - -// Validation interface -interface FormErrors { - selectedProjectId?: string; - selectedDate?: string; - startTime?: string; - endTime?: string; - general?: string; -} +type TabType = 'pto' | 'sick' | 'adjustment'; const LeaveAndAdjustments: React.FC = () => { - const [activeTab, setActiveTab] = useState('create_and_adjust_timesheet'); + const [activeTab, setActiveTab] = useState('pto'); const projects = useSelector((state: RootState) => state.projects.projectsData); - console.log("LeaveAndAdjustments", projects) - - // PTO State - const [ptoLeave, setPtoLeave] = useState({ - fromDate: '', - toDate: '', - }); - const [ptoErrors, setPtoErrors] = useState>({}); - - // Sick Leave State - const [sickLeave, setSickLeave] = useState({ - fromDate: '', - toDate: '', - }); - const [sickErrors, setSickErrors] = useState>({}); - const [selectedProjectId, setSelectedProjectId] = useState("") - const [selectedDate, setSelectedDate] = useState("") - const [totalAdjustment, setTotalAdjustment] = useState("0h 0m"); - const [pendingOverride, setPendingOverride] = useState(null); - const [loadingSummary, setLoadingSummary] = useState(false); - - // Form Validation State - const [formErrors, setFormErrors] = useState({}); - - // Project Summary State - const [projectSummary, setProjectSummary] = useState(null); - const [startTime, setStartTime] = useState(""); - const [endTime, setEndTime] = useState(""); - - // Adjustment State - const [selectedPeriod, setSelectedPeriod] = useState('week'); - const [adjustmentData, setAdjustmentData] = useState([ - { id: 1, date: '2025-11-01', loggedHours: 8 }, - { id: 2, date: '2025-11-02', loggedHours: 7.5 }, - { id: 3, date: '2025-11-03', loggedHours: 8 }, - ]); - const [editingId, setEditingId] = useState(null); - const [editHours, setEditHours] = useState(''); - - // Validation function - const validateLeaveForm = (data: LeaveFormData): Partial => { - const errors: Partial = {}; - - if (!data.fromDate) { - errors.fromDate = 'From date is required'; - } - - if (!data.toDate) { - errors.toDate = 'To date is required'; - } - - if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) { - errors.toDate = 'To date must be after from date'; - } - - return errors; - }; - - // Function to convert UTC time to local time in HH:mm format - const convertToLocalTime = (utcTimeString: string): string => { - if (!utcTimeString) return ''; - - try { - // Parse the UTC time and convert to local timezone - const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal(); - - // Format to HH:mm (24-hour format) - return localTime.toFormat('HH:mm'); - } catch (error) { - console.error('Error converting time:', error); - return ''; - } - }; - - // Field validation functions - const validateProjectId = (projectId: string): string | undefined => { - if (!projectId) return 'Project selection is required'; - return undefined; - }; - - const validateDate = (date: string): string | undefined => { - if (!date) return 'Date is required'; - - const selectedDateObj = new Date(date); - const today = new Date(); - today.setHours(0, 0, 0, 0); - - if (selectedDateObj > today) return 'Cannot select future dates'; - return undefined; - }; - - const validateTime = (time: string, fieldName: string): string | undefined => { - if (!time) return `${fieldName} is required`; - - const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; - if (!timeRegex.test(time)) return 'Invalid time format'; - return undefined; - }; - - const validateTimeRange = (start: string, end: string): string | undefined => { - if (!start || !end) return undefined; - - const startTime = DateTime.fromFormat(start, "HH:mm"); - const endTime = DateTime.fromFormat(end, "HH:mm"); - - if (!startTime.isValid || !endTime.isValid) return 'Invalid time values'; - if (endTime <= startTime) return 'End time must be after start time'; - return undefined; - }; - - const validateDedicatedHours = (start: string, end: string): string | undefined => { - if (!start || !end || !projectSummary) return undefined; - - const startTime = DateTime.fromFormat(start, "HH:mm"); - const endTime = DateTime.fromFormat(end, "HH:mm"); - - if (!startTime.isValid || !endTime.isValid) return undefined; - - const diff = endTime.diff(startTime, ["hours", "minutes"]).toObject(); - const workedHours = (diff.hours || 0) + (diff.minutes || 0) / 60; - - if (workedHours > projectSummary.dedicatedHours) { - return `Cannot exceed project's dedicated hours (${projectSummary.dedicatedHours} hrs)`; - } - - return undefined; - }; - - // Real-time field validation - const validateField = (field: keyof FormErrors, value: string) => { - let error: string | undefined; - - switch (field) { - case 'selectedProjectId': - error = validateProjectId(value); - break; - case 'selectedDate': - error = validateDate(value); - break; - case 'startTime': - error = validateTime(value, 'Start time'); - if (!error && endTime) { - error = validateTimeRange(value, endTime) || validateDedicatedHours(value, endTime); - } - break; - case 'endTime': - error = validateTime(value, 'End time'); - if (!error && startTime) { - error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value); - } - break; - } - - setFormErrors(prev => ({ - ...prev, - [field]: error - })); - - return !error; // Return true if valid - }; - - // Comprehensive form validation - const validateForm = (): boolean => { - const errors: FormErrors = {}; - - // Validate individual fields - errors.selectedProjectId = validateProjectId(selectedProjectId); - errors.selectedDate = validateDate(selectedDate); - errors.startTime = validateTime(startTime, 'Start time'); - errors.endTime = validateTime(endTime, 'End time'); - - // Validate time range and dedicated hours - if (!errors.startTime && !errors.endTime) { - errors.startTime = validateTimeRange(startTime, endTime); - if (!errors.startTime) { - errors.startTime = validateDedicatedHours(startTime, endTime); - } - } - - // Filter out undefined errors - const filteredErrors = Object.fromEntries( - Object.entries(errors).filter(([_, value]) => value !== undefined) - ) as FormErrors; - - setFormErrors(filteredErrors); - return Object.keys(filteredErrors).length === 0; - }; - - const handlePtoSubmit = async () => { - const errors = validateLeaveForm(ptoLeave); - - if (Object.keys(errors).length > 0) { - setPtoErrors(errors); - return; - } - - try { - console.log('Submitting PTO:', ptoLeave); - alert('PTO request submitted successfully!'); - - setPtoLeave({ fromDate: '', toDate: '' }); - setPtoErrors({}); - } catch (error) { - alert('Failed to submit request. Please try again.'); - console.error('Error:', error); - } - }; - - // Sick Leave Handlers - const handleSickChange = (field: keyof LeaveFormData, value: string) => { - setSickLeave(prev => ({ ...prev, [field]: value })); - if (sickErrors[field]) { - setSickErrors(prev => ({ ...prev, [field]: undefined })); - } - }; - - const handleSickSubmit = async () => { - const errors = validateLeaveForm(sickLeave); - - if (Object.keys(errors).length > 0) { - setSickErrors(errors); - return; - } - - try { - console.log('Submitting Sick Leave:', sickLeave); - alert('Sick Leave request submitted successfully!'); - - setSickLeave({ fromDate: '', toDate: '' }); - setSickErrors({}); - } catch (error) { - alert('Failed to submit request. Please try again.'); - console.error('Error:', error); - } - }; - - const [editingEntry, setEditingEntry] = useState(null); - const [newHours, setNewHours] = useState(0); - const [comment, setComment] = useState("") - const [isSubmitting, setIsSubmitting] = useState(false); - - const handlePeriodChange = (value: string) => setSelectedPeriod(value); - - const handleEditClick = (entry: AdjustmentEntry) => { - setEditingEntry(entry); - const modal = document.getElementById("editModal") as HTMLDialogElement; - modal?.showModal(); - }; - - const handleCloseModal = () => { - setEditingEntry(null); - setNewHours(0) - const modal = document.getElementById("editModal") as HTMLDialogElement; - modal?.close(); - }; - - const handleSave = () => { - console.log("Saving:", { ...editingEntry, updatedHours: newHours }); - handleCloseModal(); - }; - - const handleProjectSelect = (projectId: string) => { - console.log("handleProjectSelect", projectId) - setSelectedProjectId(projectId); - validateField('selectedProjectId', projectId); - } - - const handleSelectedDate = (value: string) => { - setSelectedDate(value); - validateField('selectedDate', value); - }; - - const debugTimezoneInfo = (utcTimeString: string, check: string) => { - const utcTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }); - const localTime = utcTime.toLocal(); - - console.log(`-------------------------Timezone Debug Info: ${check}----------------------`); - console.log('UTC Time:', utcTime.toString()); - console.log('Local Time:', localTime.toString()); - console.log('System Timezone:', localTime.zoneName); - console.log('Formatted Local Time:', localTime.toFormat('HH:mm')); - }; - - const fetchLogs = async () => { - try { - console.log("Sending =>", selectedProjectId, selectedDate); - setLoadingSummary(true); - - const res = await window.electron.getProjectSummary( - selectedProjectId, - selectedDate - ); - - console.log("πŸ“₯ Summary Response:", res); - - if (res.success) { - setProjectSummary(res?.data?.summary); - - // Convert UTC times to local timezone and set them - if (res?.data?.summary?.firstLoginTime) { - const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime); - debugTimezoneInfo(localStartTime, "start"); - setStartTime(localStartTime); - } - - if (res?.data?.summary?.lastLogoutTime) { - const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime); - debugTimezoneInfo(localEndTime, "end"); - setEndTime(localEndTime); - } - } else { - console.warn("⚠️ Summary API failed:", res.message); - setProjectSummary(null); - setStartTime(""); - setEndTime(""); - } - } catch (err) { - console.error("❌ Error fetching project summary:", err); - setProjectSummary(null); - setStartTime(""); - setEndTime(""); - } finally { - setLoadingSummary(false); - } - }; - - useEffect(() => { - if (selectedProjectId && selectedDate) { - fetchLogs(); - } else { - setProjectSummary(null); - setStartTime(""); - setEndTime(""); - } - }, [selectedProjectId, selectedDate]); - - // Auto calculations --- derived state - useEffect(() => { - if (!projectSummary) return; // No API summary yet - if (!startTime || !endTime) return; - - // Clear previous time range errors when times change - if (formErrors.startTime?.includes('time') || formErrors.startTime?.includes('dedicated')) { - setFormErrors(prev => ({ - ...prev, - startTime: undefined - })); - } - - // 1. Validation: prevent end < start - const start = DateTime.fromFormat(startTime, "HH:mm"); - const end = DateTime.fromFormat(endTime, "HH:mm"); - - if (!start.isValid || !end.isValid) return; - - if (end < start) { - setTotalAdjustment("0h 0m"); - return; - } - - // 2. Calculate worked minutes from UI input - const diff = end.diff(start, ["hours", "minutes"]).toObject(); - const workedHours = diff.hours || 0; - const workedMinutes = diff.minutes || 0; - - const totalWorkedDecimal = workedHours + workedMinutes / 60; - - // 3. Compare with server original total - const originalWorkedDecimal = Number(projectSummary.totalWorkedHoursDecimal); - const adjustment = totalWorkedDecimal - originalWorkedDecimal; - - // 4. Build readable format (always positive for display) - const adjH = Math.floor(Math.abs(adjustment)); - const adjM = Math.round((Math.abs(adjustment) % 1) * 60); - - const formatted = `${adjustment >= 0 ? "+" : "-"}${adjH}h ${adjM}m`; - setTotalAdjustment(formatted); - - // 5. Update Pending Hours -> dedicated - new worked - const newPending = projectSummary.dedicatedHours - totalWorkedDecimal; - const newPendingRounded = newPending.toFixed(2); - - setPendingOverride(newPendingRounded); - - }, [startTime, endTime, projectSummary]); - - const handleStartTimeChange = (value: string) => { - console.log("Start changed:", value); - setStartTime(value); - validateField('startTime', value); - }; - - const handleEndTimeChange = (value: string) => { - console.log("End changed:", value); - setEndTime(value); - validateField('endTime', value); - }; - - const handleSubmit = async () => { - - if (isSubmitting) return; - - // Clear previous errors - setFormErrors({}); - - // Validate all fields - if (!validateForm()) { - return; - } - - // Final dedicated hours check - const dedicatedHoursError = validateDedicatedHours(startTime, endTime); - if (dedicatedHoursError) { - setFormErrors(prev => ({ - ...prev, - startTime: dedicatedHoursError - })); - return; - } - - const payload = { - projectId: selectedProjectId, - date: selectedDate, - loginTime: `${startTime}:00`, - logoutTime: `${endTime}:00`, - comment - }; - - console.log("Submitting payload:", payload); - setIsSubmitting(true); - try { - - // Add await here to properly wait for the response - const res = await window.electron.createTimesheet(payload); - console.log("API Response:", res); - - if (res?.success) { - console.log("Timesheet Created Successfully"); - - // Show success message - alert(res.message); - - // Reset form or navigate away - setSelectedProjectId(""); - setSelectedDate(""); - setStartTime(""); - setEndTime(""); - setComment(""); - setFormErrors({}); - setTotalAdjustment("0h 0m"); - - } else { - setFormErrors(prev => ({ - ...prev, - general: res?.message || "Failed to create timesheet" - })); - } - } catch (err: any) { - console.error("Submission error:", err); - setFormErrors(prev => ({ - ...prev, - general: err.message || "Something went wrong while saving timesheet" - })); - } finally { - setIsSubmitting(false); - } - }; - - const handleDescription = (value: string) => { - setComment(value); - }; + console.log("LeaveAndAdjustments", projects); return (
- {/* Header */} -
+

Leave & Adjust Timesheet

- {/* General Error Alert */} - {formErrors.general && ( -
- {formErrors.general} -
- )} + {/* Tabs Navigation */} + {/* Main Container */}
- {/* Tabs Navigation */} -
- -
- - {/* Tab Content */}
- {/* PTO Tab */} - {activeTab === 'create_and_adjust_timesheet' && ( + {activeTab === 'pto' && (
-
-
-
- {/* Project Selection */} -
- - - {formErrors.selectedProjectId && ( - - )} -
- - {/* Date Selection */} -
- - handleSelectedDate(e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`} - /> - {formErrors.selectedDate && ( - - )} -
- - {/* Current Total Time */} -
- - {loadingSummary ? ( -
- ) : - - } -
- - {/* Current Pending Time */} -
- - {loadingSummary ? ( -
- ) : - - } -
- - {/* Start Time */} -
- - {loadingSummary ? ( -
- ) : - handleStartTimeChange(e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`} - /> - } - {formErrors.startTime && ( - - )} -
- - {/* End Time */} -
- - {loadingSummary ? ( -
- ) : - handleEndTimeChange(e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`} - /> - } - {formErrors.endTime && ( - - )} -
- - {/* Total Time Adjustment */} -
- - -
-
- - {/* Description */} -
- -