diff --git a/package.json b/package.json index b104664..90580f5 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,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", @@ -124,4 +126,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/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index bc14fc3..e496bff 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -342,26 +342,44 @@ ipcMain.handle("create-timesheet", async (event, data) => { ipcMain.handle("create-leave-request", async (event, payload) => { try { - console.log(" Leave Request Received:", payload); + console.log("Leave Request Received:", payload); const response = await api.post("/leave/request", payload); - console.log(" Leave Saved:", response.data); + console.log("Leave Saved:", response); - return { success: true, data: response.data }; + return { + success: true, + data: response.data + }; - } catch (error) { - console.error("โŒ Leave Save Failed:", error); + } catch (error: any) { + console.error("โŒ Leave Save Failed:"); + + let message = "Unknown error"; + + if (error.response) { + console.log("Status:", error.response.status); + console.log("Message:", error.response.statusText); + console.log("Server Response:", error.response.data); + + // Extract the backend message properly + message = error.response.data?.message || error.response.statusText; + } else { + message = error.message; + } return { success: false, - error: error || "Unknown error" + message }; } }); + + export const sendTokenExpired = () => { console.log("โš ๏ธ Sending token-expired event to renderer"); 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/App.tsx b/src/ui/App.tsx index fbbf871..72f3ab8 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -94,6 +94,7 @@ import Timesheets from "./pages/Timesheets/Timesheets"; import Settings from "./pages/Settings/Settings"; import HelpSupport from "./pages/HelpSupport/HelpSupport"; import Vpn from "./pages/Vpn/Vpn"; +import Schedule from "./pages/Schedule/Schedule"; const App: React.FC = () => { @@ -140,6 +141,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> {/* Default Redirect */} 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..4eef5d3 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -26,16 +26,18 @@ import { Play, ChevronUp, GlobeLock, + ClipboardClock, } from "lucide-react"; 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 +79,8 @@ export interface TrackingState { handleProjectSelect: (projectId: string) => void; projectsData: Project[]; loadingStart: boolean, - loadingStop: boolean + loadingStop: boolean, + isStopping: boolean } const DashboardLayout = () => { @@ -91,6 +94,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); @@ -108,6 +112,7 @@ const DashboardLayout = () => { { label: "Dashboard", path: "/dashboard", icon: , exact: true }, { label: "Timesheet", path: "/dashboard/timesheet", icon: }, { label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: }, + { label: "Schedule", path: "/dashboard/schedule", icon: }, { label: "Settings", path: "/dashboard/settings", icon: }, { label: "VPN", path: "/dashboard/vpn", icon: }, { label: "Help & Support", path: "/dashboard/help-support", icon: }, @@ -123,6 +128,7 @@ const DashboardLayout = () => { const [loadingStop, setLoadingStop] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false); + useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); @@ -140,16 +146,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 +171,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 +180,7 @@ const DashboardLayout = () => { vaId: projectSummary.vaId, projectId: projectId, type: "LOGOUT", + time: currentStopTime }; console.log("๐Ÿ“ก Sending LOGOUT request:", payload); @@ -187,17 +190,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 +200,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 +366,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 +385,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 +404,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 +421,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 +442,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 +463,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 +514,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 +614,8 @@ const DashboardLayout = () => { projectsData, handleProjectSelect, loadingStart, - loadingStop + loadingStop, + isStopping }; const formatTime = (seconds: number) => { @@ -611,7 +632,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..8c71d47 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -23,16 +23,21 @@ const LeaveAndAdjustments: React.FC = () => { try { setIsSubmitting(true); - console.log("Leave Payload:", payload); + console.log("Leave /sick Payload:", payload); const response = await window.electron.createPTOAndSickLeaveRequest(payload) - console.log("Leave Payload:" ,response) + console.log("Leave Payload:", response) if (response.success) { - return true; + return { success: true }; } + + return { + success: false, + message: response.message + }; } catch (err) { console.error(err); - return false; + return { success: false, message: "Request failed" }; } finally { setIsSubmitting(false); } @@ -47,9 +52,8 @@ const LeaveAndAdjustments: React.FC = () => {
setActiveTab("pto")} > PTO Leave @@ -57,9 +61,8 @@ const LeaveAndAdjustments: React.FC = () => { setActiveTab("sick")} > Sick Leave @@ -67,9 +70,8 @@ const LeaveAndAdjustments: React.FC = () => { setActiveTab("adjustment")} > Adjustment Submission diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx index 436d51e..f923f9a 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,107 @@ const AdjustmentTab: React.FC = ({ projects }) => { } }; + useEffect(() => { + const editor = quillRef.current?.getEditor(); + if (!editor) return; + + // Override image handler in toolbar + editor.getModule("toolbar").handlers.image = () => { + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("accept", "image/*"); + input.click(); + + input.onchange = async () => { + const file = input.files?.[0]; + if (file) { + const base64 = await convertToBase64(file); + const range = editor.getSelection(); + if (range) { + editor.insertEmbed(range.index, "image", base64); + } + } + }; + }; + + // COMPLETELY disable Quill's default paste behavior + const disableQuillPaste = () => { + // Remove all existing paste event listeners from Quill + const pasteListeners = (editor.root as any).__pasteHandlers || []; + pasteListeners.forEach((handler: any) => { + editor.root.removeEventListener('paste', handler); + }); + + // Override the clipboard module to do nothing + const clipboard = editor.getModule('clipboard'); + const originalPaste = clipboard.paste; + clipboard.paste = (e: any) => { + // Completely prevent Quill's default paste behavior + e.preventDefault(); + e.stopPropagation(); + return false; + }; + }; + + disableQuillPaste(); + + // Our custom paste handler - this is the ONLY handler that will run + const handlePaste = async (e: ClipboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const items = e.clipboardData?.items; + if (!items || items.length === 0) return; + + // Check for images first + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.type.startsWith("image/")) { + const file = item.getAsFile(); + if (file) { + try { + const base64 = await convertToBase64(file); + const range = editor.getSelection(true); + if (range !== null) { + // Insert the image + editor.insertEmbed(range.index, "image", base64); + // Move cursor after the image + editor.setSelection(range.index + 1, 0); + } + } catch (error) { + console.error("Error processing pasted image:", error); + } + } + return; // Exit after handling the first image + } + } + + // If no images found, handle text + const text = e.clipboardData?.getData('text/plain'); + if (text) { + const range = editor.getSelection(true); + if (range !== null) { + editor.insertText(range.index, text); + editor.setSelection(range.index + text.length, 0); + } + } + }; + + // Add our custom paste handler with high priority + editor.root.addEventListener("paste", handlePaste, true); + + return () => { + editor.root.removeEventListener("paste", handlePaste, true); + }; + }, []); + + 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 +398,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 +445,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 +453,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 +503,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { {formErrors.selectedProjectId && ( )}
@@ -396,7 +521,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { /> {formErrors.selectedDate && ( )} @@ -460,7 +585,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { } {formErrors.startTime && ( )} @@ -482,7 +607,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { } {formErrors.endTime && ( )} @@ -502,18 +627,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 - - )} */}