From d94db40a80655400b6d570cbf3e96116d115bd36 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Thu, 27 Nov 2025 16:32:17 +0530 Subject: [PATCH 1/3] timmer fixed and server time set when stopping and quill setup --- package-lock.json | 84 ++++++ package.json | 8 +- src/electron/api/axiosClient.ts | 2 +- src/electron/utils/utilityFunctions.ts | 2 +- src/ui/hooks/useActiveAppTracker.tsx | 2 + src/ui/layout/DashboardLayout.tsx | 247 ++++++++++-------- src/ui/pages/Dashboard.tsx | 139 +++++----- .../LeaveAndAdjustments.tsx | 4 +- .../tabs/AdjustmentTab.tsx | 109 ++++++-- .../LeaveAndAdjustments/tabs/PtoLeaveTab.tsx | 94 +++++-- .../LeaveAndAdjustments/tabs/SickLeaveTab.tsx | 81 ++++-- src/ui/pages/Timesheets/ProjectLogsModal.tsx | 2 +- src/ui/store/projectSlice.tsx | 23 +- 13 files changed, 550 insertions(+), 247 deletions(-) diff --git a/package-lock.json b/package-lock.json index c122ced..87da375 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "lucide-react": "^0.552.0", "luxon": "^3.7.2", "os-utils": "^0.0.14", + "quill": "^2.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-quill-new": "^3.6.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.5", "recharts": "^2.12.7", @@ -6034,6 +6036,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, "node_modules/fast-equals": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", @@ -8220,6 +8228,18 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -8241,6 +8261,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -9481,6 +9508,12 @@ "devOptional": true, "license": "BlueOak-1.0.0" }, + "node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -9827,6 +9860,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/quill/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/rcedit": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz", @@ -9873,6 +9942,21 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-quill-new": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.6.0.tgz", + "integrity": "sha512-weU6YfB2+7Cujw5Hjgmi0aN/qJd3B6ADWrxgUJMp2MO3tEvKX5kfB0sg3P0UdOVfU0z8icsKFzlnEIpeW1mLhw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21", + "quill": "~2.0.2" + }, + "peerDependencies": { + "quill-delta": "^5.1.0", + "react": "^16 || ^17 || ^18 || ^19", + "react-dom": "^16 || ^17 || ^18 || ^19" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", diff --git a/package.json b/package.json index 03d74a5..3a8865e 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,10 @@ "lucide-react": "^0.552.0", "luxon": "^3.7.2", "os-utils": "^0.0.14", + "quill": "^2.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-quill-new": "^3.6.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.5", "recharts": "^2.12.7", @@ -69,7 +71,9 @@ } ], "mac": { - "target": ["dmg"], + "target": [ + "dmg" + ], "icon": "build/icon.icns", "extendInfo": { "LSUIElement": false, @@ -120,4 +124,4 @@ "vite": "^5.4.7", "vitest": "^2.1.1" } -} \ No newline at end of file +} diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index c4662a1..a017ae4 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -12,7 +12,7 @@ const httpsAgent = new https.Agent({ }); const api: AxiosInstance = axios.create({ - baseURL: 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va', // ๐Ÿ” your backend + baseURL: 'http://localhost:3000/api/v1/va', // ๐Ÿ” your backend httpsAgent // timeout: 10000, }); diff --git a/src/electron/utils/utilityFunctions.ts b/src/electron/utils/utilityFunctions.ts index c09a7ab..d9f508a 100644 --- a/src/electron/utils/utilityFunctions.ts +++ b/src/electron/utils/utilityFunctions.ts @@ -68,7 +68,7 @@ export const handleLogin = async (credentials: { email: string, password: string try { console.log("โžก๏ธ Calling API..."); const response = await axios.post( - 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login', + 'http://localhost:3000/api/v1/va/login', { userDetails: { ...credentials } }, { headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent } ); diff --git a/src/ui/hooks/useActiveAppTracker.tsx b/src/ui/hooks/useActiveAppTracker.tsx index 5dc48b8..a7620d1 100644 --- a/src/ui/hooks/useActiveAppTracker.tsx +++ b/src/ui/hooks/useActiveAppTracker.tsx @@ -207,6 +207,8 @@ export const useActiveAppTracker = ( const details = (await window.electron.getActiveAppDetails()) as WindowInfo; if (!details || !details.id) return; + console.log("details" , details) + const title = details.title; const appName = details.owner.name; const prevTitle = lastTitleRef.current; diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index f0f9768..0ef023d 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -30,12 +30,13 @@ import { import { useActiveAppTracker } from "../hooks/useActiveAppTracker"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../store"; -import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime } from "../store/projectSlice"; +import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, startTracking, stopTrackingReducer, updateElapsedTime, updateProjectTracking } from "../store/projectSlice"; 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"; +import { parseTimeStringToSeconds } from "../utils/timeParser"; interface User { id: string; @@ -77,7 +78,8 @@ export interface TrackingState { handleProjectSelect: (projectId: string) => void; projectsData: Project[]; loadingStart: boolean, - loadingStop: boolean + loadingStop: boolean, + isStopping: boolean } const DashboardLayout = () => { @@ -91,6 +93,7 @@ const DashboardLayout = () => { const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); + const [isStopping, setIsStopping] = useState(false); const { projectsData, projectSummary, selectedProject, projectTrackingMap } = useSelector((state: any) => state.projects); @@ -123,6 +126,7 @@ const DashboardLayout = () => { const [loadingStop, setLoadingStop] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false); + useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); @@ -140,16 +144,24 @@ const DashboardLayout = () => { return; } + let currentStopTime = new Date().toISOString(); const projectId = selectedProject.projectId; console.log(" Stop tracking initiated for:", selectedProject.projectName); console.log(" Current elapsed time:", elapsedTime); + setIsStopping(true); setLoadingStop(true); handleSocketDisconnect(); - // STEP 1: Send final PATCH with last active window data - // This ensures the last window activity is recorded before logout + // STEP 1: Clear timer FIRST to prevent further client-side updates + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + console.log("Timer cleared"); + } + + // STEP 2: Send final PATCH with last active window data if (stopTrackingRef.current) { try { console.log(" Sending final PATCH to record last activity..."); @@ -157,19 +169,7 @@ const DashboardLayout = () => { console.log(" Final tracking data sent successfully"); } catch (error) { console.error(" Error sending final tracking data:", error); - // Continue with stop even if PATCH fails } - } else { - console.warn("โš ๏ธ No finalizeTracking function available"); - } - - // STEP 2: Stop timer everywhere - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - console.log("Timer cleared"); - } else { - console.log("No active timer to clear"); } // STEP 3: Send LOGOUT to backend @@ -178,6 +178,7 @@ const DashboardLayout = () => { vaId: projectSummary.vaId, projectId: projectId, type: "LOGOUT", + time: currentStopTime }; console.log("๐Ÿ“ก Sending LOGOUT request:", payload); @@ -187,17 +188,7 @@ const DashboardLayout = () => { console.error(" Failed to send LOGOUT:", error); } - // STEP 4: Update UI (window restore) - try { - await window.electron.toggleCloseBtn(true); - await window.electron.resizeWindow(500, 600); - toggleWindowSize.current = true; - console.log(" Window UI restored"); - } catch (error) { - console.error(" Error restoring window:", error); - } - - // STEP 5: Update Redux tracking state โ†’ THIS TRIGGERS UI UPDATE + // STEP 4: Update Redux tracking state to stop tracking console.log("๐Ÿ”„ Updating Redux state to stop tracking..."); dispatch( stopTrackingReducer({ @@ -207,70 +198,82 @@ const DashboardLayout = () => { // Allow React to process the state update await new Promise(resolve => setTimeout(resolve, 50)); - console.log(" Redux state updated - isTracking is now false"); - // STEP 6: Clear active project log ID - setActiveProjectLogId(""); - - // โœจ NEW STEP 7: Refresh project list to get updated work hours + // STEP 5: Refresh project list to get updated work hours from server try { console.log("๐Ÿ”„ Refreshing project list..."); - const res = await window.electron.getProjectList(); - console.log("๐Ÿ“Š Updated project list:", res); + const projectListRes = await window.electron.getProjectList(); + console.log("๐Ÿ“Š Updated project list:", projectListRes); - if (res?.success && Array.isArray(res.data)) { + if (projectListRes?.success && Array.isArray(projectListRes.data)) { // Update projects data - dispatch(setProjectsDataSlice(res.data)); + dispatch(setProjectsDataSlice(projectListRes.data)); // Update project summary 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 || "", + totalProjects: projectListRes.totalProjects || 0, + activeProjects: projectListRes.activeProjects || 0, + awayProjects: projectListRes.awayProjects || 0, + totalWorkTime: projectListRes.totalWorkTime || "0h 0m 0s", + totalWorkMs: projectListRes.totalWorkMs || 0, + vaName: projectListRes.vaName || "", + vaId: projectListRes.vaId || "", }) ); // Update selected project with fresh data - const updatedProject = res.data.find( + const updatedProject = projectListRes.data.find( (p: Project) => p.projectId === projectId ); if (updatedProject) { dispatch(setSelectedProjectSlice(updatedProject)); - // Update tracking map with new total work time - const initialElapsed = Math.floor((updatedProject.totalWorkMs || 0) / 1000); + // CRITICAL FIX: Use server time instead of client elapsedTime + const serverElapsedTime = parseTimeStringToSeconds(updatedProject.totalWorkTime); + + console.log("๐Ÿ”„ Syncing client time with server time:", { + serverTime: updatedProject.totalWorkTime, + serverElapsedSeconds: serverElapsedTime, + previousClientTime: elapsedTime, + difference: serverElapsedTime - elapsedTime + }); + + // Force update the tracking map with server time dispatch( - initializeProjectTracking({ + updateProjectTracking({ projectId: updatedProject.projectId, - initialTime: initialElapsed, + data: { + elapsedTime: serverElapsedTime, + accumulatedTime: serverElapsedTime, + isTracking: false, + isPaused: false, + startTime: null, + endTime: Date.now(), + pauseStartTime: null, + totalPausedTime: 0 + } }) ); - console.log("โœ… Selected project updated with fresh data"); + console.log("โœ… Selected project updated with server time"); } } + + // STEP 6: Update UI (window restore) + await window.electron.toggleCloseBtn(true); + await window.electron.resizeWindow(500, 600); + toggleWindowSize.current = true; + } catch (error) { console.error("โŒ Error refreshing project list:", error); + } finally { + // Clear loading states + setLoadingStop(false); + setIsStopping(false); + console.log("๐ŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); } - - setLoadingStop(false); - console.log("๐Ÿงน Active project log ID cleared"); - - console.log("=".repeat(50)); - console.log("๐ŸŽ‰ TRACKING STOPPED SUCCESSFULLY"); - console.log(`Project: ${selectedProject.projectName}`); - console.log(`Final Time: ${formatTime(elapsedTime)}`); - console.log("UI Updates:"); - console.log(" โœ“ Header should be hidden"); - console.log(" โœ“ Start button should be visible"); - console.log(" โœ“ Layout should expand to full height"); - console.log("=".repeat(50)); }, [selectedProject, projectSummary, elapsedTime, dispatch]); // NOW define handleLimitExceeded using the defined handleStopTracking @@ -361,10 +364,9 @@ const DashboardLayout = () => { : null; const handleStartTracking = async (project: Project) => { - if (loadingStart || loadingStop) return; - // Check limit before starting + // Check limit before starting const limitCheck = canStartTracking(project); if (!limitCheck.canStart) { alert( @@ -381,13 +383,14 @@ const DashboardLayout = () => { setLoadingStart(true); const now = Date.now(); - + let currentStartTime = new Date().toISOString(); window.electron.toggleCloseBtn(false); const payload = { vaId: projectSummary.vaId, projectId: project.projectId, type: "LOGIN", + time: currentStartTime }; try { @@ -399,6 +402,15 @@ const DashboardLayout = () => { return; } + // CRITICAL FIX: Get the current accumulated time BEFORE starting timer + const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime); + + console.log("๐Ÿ“Š Starting with accumulated time:", { + accumulatedTime: currentAccumulatedTime, + projectTotalWorkTime: project.totalWorkTime, + parsedTime: parseTimeStringToSeconds(project.totalWorkTime) + }); + // โฌ‡๏ธ REDUX UPDATE: Start Tracking dispatch( startTracking({ @@ -407,22 +419,17 @@ const DashboardLayout = () => { }) ); - handleSocketConnect() - + handleSocketConnect(); setActiveProjectLogId(res?.data?.logId); console.log(" Tracking started. Log ID:", res?.data?.logId); // โฌ‡๏ธ Calculate time limit in seconds const parseTimeToSeconds = (timeStr: string): number => { - const hourMatch = timeStr.match(/(\d+)h/); - const minMatch = timeStr.match(/(\d+)m/); - const hours = hourMatch ? parseInt(hourMatch[1]) : 0; - const minutes = minMatch ? parseInt(minMatch[1]) : 0; - return (hours * 3600) + (minutes * 60); + return parseTimeStringToSeconds(timeStr); }; const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours); - const alreadyWorkedSeconds = Math.floor((project.totalWorkMs || 0) / 1000); + const alreadyWorkedSeconds = parseTimeStringToSeconds(project.totalWorkTime); const remainingSeconds = dedicatedSeconds - alreadyWorkedSeconds; console.log("โฑ๏ธ Time Limits:", { @@ -433,10 +440,19 @@ const DashboardLayout = () => { remainingMinutes: Math.floor(remainingSeconds / 60) }); - // โฌ‡๏ธ Start global timer loop with limit check + // โฌ‡๏ธ FIXED: Start timer with proper initial time + // Set initial elapsed time immediately to avoid jump + dispatch( + updateElapsedTime({ + projectId: project.projectId, + elapsedTime: currentAccumulatedTime, + }) + ); + + // Start global timer loop with limit check timerRef.current = setInterval(() => { - const currentElapsed = Math.floor((Date.now() - now) / 1000) + - (selectedData?.accumulatedTime || 0); + const timeSinceStart = Math.floor((Date.now() - now) / 1000); + const currentElapsed = currentAccumulatedTime + timeSinceStart; dispatch( updateElapsedTime({ @@ -445,8 +461,8 @@ const DashboardLayout = () => { }) ); - // Check if time limit reached - const totalTimeWorked = alreadyWorkedSeconds + Math.floor((Date.now() - now) / 1000); + // Check if time limit reached + const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart; if (totalTimeWorked >= dedicatedSeconds) { console.warn("๐Ÿšจ CLIENT-SIDE: Time limit reached!"); @@ -496,9 +512,11 @@ const DashboardLayout = () => { const existing = projectTrackingMap[projectId]; if (!existing) { + // Use parsed totalWorkTime instead of totalWorkMs + const initialTime = parseTimeStringToSeconds(project.totalWorkTime); dispatch(initializeProjectTracking({ projectId, - initialTime: Math.floor((project.totalWorkMs || 0) / 1000) + initialTime: initialTime })); } }; @@ -594,7 +612,8 @@ const DashboardLayout = () => { projectsData, handleProjectSelect, loadingStart, - loadingStop + loadingStop, + isStopping }; const formatTime = (seconds: number) => { @@ -611,7 +630,13 @@ const DashboardLayout = () => { {isTracking &&
- {selectedProject ? ( + {isStopping ? ( + // Skeleton for left side during stop +
+
+
+
+ ) : selectedProject ? ( isTracking ? ( - ) : ( )} - {selectedProject ? ( + {!isStopping && selectedProject ? ( {selectedProject.projectName} - ) : ( + ) : !isStopping ? ( No Project Selected - )} + ) : null}
-
- {formatTime(elapsedTime || 0)} -
+ {isStopping ? ( + // Skeleton for right side during stop +
+
+
+
+
+ ) : ( + <> +
+ {formatTime( + isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s") + )} +
- - - + + + - + + + )}
} diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index 0c9d455..26d8220 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -6,6 +6,7 @@ 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'; +import { parseTimeStringToSeconds } from "../utils/timeParser"; // Updated Project interface to match JSON structure interface Project { @@ -40,7 +41,8 @@ interface TrackingState { handleProjectSelect: (projectId: string) => void; projectsData: Project[]; loadingStart: boolean, - loadingStop: boolean + loadingStop: boolean, + isStopping: boolean } // Helper function @@ -86,8 +88,6 @@ const Widget: React.FC<{ // Dashboard Component const Dashboard: React.FC = () => { const { - // selectedProject, - // projectTrackingMap, isTracking, isPaused, elapsedTime, @@ -96,31 +96,20 @@ const Dashboard: React.FC = () => { handlePauseResume, handleStopTracking, handleProjectSelect, - // projectsData, loadingStart, - loadingStop + loadingStop, + isStopping } = useOutletContext(); - // interface AppsListInt { - // name: string; - // pid: string; - // title: string; - // } - - // 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)); @@ -146,20 +135,6 @@ const Dashboard: React.FC = () => { 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)); @@ -175,17 +150,16 @@ const Dashboard: React.FC = () => { }) ); - // Auto-select first project + // 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); + const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime); - // Initialize tracking data for first project (your slice matches this) + // Initialize tracking data for first project dispatch( initializeProjectTracking({ projectId: firstProject.projectId, @@ -198,9 +172,6 @@ const Dashboard: React.FC = () => { } else { console.warn("โš ๏ธ Unexpected response:", res); - // if (res?.status) { - // // handleLogout() - // } } } catch (err) { console.error("โŒ Error fetching projects:", err); @@ -214,6 +185,20 @@ const Dashboard: React.FC = () => { ? projectTrackingMap[selectedProject.projectId] : null; + // FIX: Get the correct time to display + const getDisplayTime = () => { + if (!selectedProject) return 0; + + // If tracking is active, use real-time elapsedTime + if (selectedData?.isTracking) { + return selectedData.elapsedTime || 0; + } + + // If not tracking, use server time from selectedProject + return parseTimeStringToSeconds(selectedProject.totalWorkTime); + }; + + const displayTime = getDisplayTime(); const formatWorkTime = (time: string) => { if (!time) return "0h 0m"; @@ -225,9 +210,7 @@ const Dashboard: React.FC = () => { return parts.slice(0, 2).join(" "); }; - const handleProjectSelectLocal = (projectId: string) => { - const project = projectsData.find((p: any) => p.projectId === projectId); if (!project) return; @@ -236,8 +219,7 @@ const Dashboard: React.FC = () => { const existingData = projectTrackingMap[projectId]; if (!existingData) { - const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000); - + const initialElapsed = parseTimeStringToSeconds(project.totalWorkTime); dispatch(initializeProjectTracking({ projectId, initialTime: initialElapsed @@ -251,7 +233,6 @@ const Dashboard: React.FC = () => { } }; - return (
{/* Header */} @@ -262,23 +243,35 @@ const Dashboard: React.FC = () => {
- {selectedProject ? ( + {isStopping ? ( + // Skeleton loader for tracking section +
+
+
+
+
+
+
+ ) : selectedProject ? ( <>
+ {/* FIX: Use displayTime instead of always using server time */}

- {formatTime(selectedData?.elapsedTime || 0)} + {formatTime(displayTime)}

- {/*

- {selectedData?.isTracking ? "Current Session" : "Ready"} -

*/} -
Project:{" "} {selectedProject.projectName}
+ {/* Show tracking status */} + {selectedData?.isTracking && ( +
+ โ— Live Tracking +
+ )}
{!selectedData?.isTracking ? ( @@ -308,9 +301,7 @@ const Dashboard: React.FC = () => { {loadingStop ? "Stopping..." : "Stop Tracking"} )} -
-
) : ( @@ -323,19 +314,31 @@ const Dashboard: React.FC = () => {
{/* Projects Table */} -
+
{/* ๐Ÿ”น Sticky Header */} -
+

Projects

- {/* ๐Ÿ”น Table or Empty State */} - {projectsData.length > 0 ? ( + {/* ๐Ÿ”น Table or Empty State or Skeleton */} + {isStopping ? ( + // Skeleton loader for projects table +
+ {[...Array(3)].map((_, index) => ( +
+
+
+
+
+
+ ))} +
+ ) : projectsData.length > 0 ? (
@@ -358,20 +361,19 @@ const Dashboard: React.FC = () => { return ( - + @@ -403,8 +400,6 @@ const Dashboard: React.FC = () => { )} - - ); }; diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index 030d943..c3c9716 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -23,7 +23,9 @@ const LeaveAndAdjustments: React.FC = () => { try { setIsSubmitting(true); - console.log("Leave Payload:", payload); + console.log("Leave /sick Payload:", payload); + + return const response = await window.electron.createPTOAndSickLeaveRequest(payload) console.log("Leave Payload:" ,response) diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx index 436d51e..4dcf8a3 100644 --- a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx +++ b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx @@ -1,10 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { DateTime } from 'luxon'; +import ReactQuill from 'react-quill-new'; +import { quillModules } from '../../../utils/quillConfig'; +import Quill from 'quill'; +import 'quill/dist/quill.snow.css'; interface Project { projectId: string; projectName: string; - status?: string | boolean; + status?: string | boolean; } interface ProjectSummaryResponse { @@ -23,6 +27,7 @@ interface FormErrors { selectedDate?: string; startTime?: string; endTime?: string; + comment?: string; general?: string; } @@ -42,6 +47,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { const [endTime, setEndTime] = useState(""); const [comment, setComment] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const quillRef = useRef(null); // Field validation functions const validateProjectId = (projectId: string): string | undefined => { @@ -120,6 +126,11 @@ const AdjustmentTab: React.FC = ({ projects }) => { error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value); } break; + case 'comment': + if (!value.trim()) { + error = 'Description is required'; + } + break; } setFormErrors(prev => ({ @@ -138,6 +149,9 @@ const AdjustmentTab: React.FC = ({ projects }) => { errors.selectedDate = validateDate(selectedDate); errors.startTime = validateTime(startTime, 'Start time'); errors.endTime = validateTime(endTime, 'End time'); + if (!comment.trim()) { + errors.comment = 'Description is required'; + } if (!errors.startTime && !errors.endTime) { errors.startTime = validateTimeRange(startTime, endTime); @@ -206,6 +220,39 @@ const AdjustmentTab: React.FC = ({ projects }) => { } }; + useEffect(() => { + const editor = quillRef.current?.getEditor(); + + if (!editor) return; + + editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => { + return delta; + }); + + editor.root.addEventListener("paste", async (e: any) => { + const items = e.clipboardData?.items; + + if (!items) return; + + for (const item of items) { + if (item.type.indexOf("image") === 0) { + const file = item.getAsFile(); + const base64 = await convertToBase64(file); + + const range = editor.getSelection(); + editor.insertEmbed(range.index, "image", base64); + } + } + }); + }, []); + + const convertToBase64 = (file: File): Promise => + new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(file); + }); + useEffect(() => { if (selectedProjectId && selectedDate) { fetchLogs(); @@ -283,6 +330,23 @@ const AdjustmentTab: React.FC = ({ projects }) => { const handleDescription = (value: string) => { setComment(value); + validateField('comment', value); + }; + + const resetForm = () => { + setSelectedProjectId(""); + setSelectedDate(""); + setStartTime(""); + setEndTime(""); + setComment(""); + setFormErrors({}); + setTotalAdjustment("0h 0m"); + + // Properly reset the Quill editor + if (quillRef.current) { + const editor = quillRef.current.getEditor(); + editor.setContents([]); // Clear editor contents completely + } }; const handleSubmit = async () => { @@ -313,7 +377,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { console.log("Submitting payload:", payload); setIsSubmitting(true); - + try { const res = await window.electron.createTimesheet(payload); console.log("API Response:", res); @@ -321,14 +385,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { if (res?.success) { console.log("Timesheet Created Successfully"); alert(res.message); - - setSelectedProjectId(""); - setSelectedDate(""); - setStartTime(""); - setEndTime(""); - setComment(""); - setFormErrors({}); - setTotalAdjustment("0h 0m"); + resetForm(); } else { setFormErrors(prev => ({ ...prev, @@ -378,7 +435,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { {formErrors.selectedProjectId && ( )} @@ -396,7 +453,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { /> {formErrors.selectedDate && ( )} @@ -460,7 +517,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { } {formErrors.startTime && ( )} @@ -482,7 +539,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { } {formErrors.endTime && ( )} @@ -502,18 +559,26 @@ const AdjustmentTab: React.FC = ({ projects }) => { - {/* Description */} -
+ {/* Description with ReactQuill */} +
-
{index + 1}
- {project.projectName} - {limitReached && ( - Daily limit reached - )} -
+
+ {project.projectName} + {limitReached && ( + Daily limit reached + )} +
+
{formatWorkTime(project.totalWorkTime)} - {/* - of {project.dedicatedHours} - */}
@@ -384,11 +386,6 @@ const Dashboard: React.FC = () => { > {isCurrentlySelected ? "Selected" : "Select"} - {/* {!limitReached && ( - - {remainingTime} left - - )} */}