From 4fb0720e7de406237369fdfb2394289e3e28ae77 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Mon, 1 Dec 2025 12:05:13 +0530 Subject: [PATCH 1/7] change the web-socket ngrok url --- src/ui/layout/DashboardLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 99f7bbf..8ff0f2b 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -536,7 +536,7 @@ const DashboardLayout = () => { if (!selectedProject?.projectId) return console.log(selectedProject?.projectId, "SelectedProject ID") - socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", { + socketRef.current = io("https://daniela-nonprofessed-boastfully.ngrok-free.app", { transports: ["websocket"], secure: true, rejectUnauthorized: false, From 17dcdec1c6c014ed2b5f2429db6a3a75b2cd9e87 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Mon, 1 Dec 2025 16:40:24 +0530 Subject: [PATCH 2/7] refresh dashboard projects --- src/ui/layout/DashboardLayout.tsx | 307 +++++++++++++------------ src/ui/pages/Dashboard.tsx | 112 +++++---- src/ui/pages/Timesheets/Timesheets.tsx | 4 +- 3 files changed, 220 insertions(+), 203 deletions(-) diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 8ff0f2b..8ac3cf2 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -113,8 +113,8 @@ 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: "Pays Slips", path: "/dashboard/pays-slips", icon: }, + { label: "Schedule", path: "/dashboard/schedule", icon: }, + { label: "Pays Slips", path: "/dashboard/pays-slips", icon: }, { label: "Settings", path: "/dashboard/settings", icon: }, { label: "VPN", path: "/dashboard/vpn", icon: }, { label: "Help & Support", path: "/dashboard/help-support", icon: }, @@ -630,170 +630,171 @@ const DashboardLayout = () => { return ( <> - {/* Header */} - {isTracking && -
-
- {isStopping ? ( - // Skeleton for left side during stop -
-
-
-
- ) : selectedProject ? ( - isTracking ? ( - +
+ {/* Header */} + {isTracking && +
+
+ {isStopping ? ( + // Skeleton for left side during stop +
+
+
+
+ ) : selectedProject ? ( + isTracking ? ( + + ) : ( + + ) ) : ( - ) - ) : ( + )} + + {!isStopping && selectedProject ? ( + + {selectedProject.projectName} + + ) : !isStopping ? ( + + No Project Selected + + ) : null} +
+ +
+ {isStopping ? ( + // Skeleton for right side during stop +
+
+
+
+
+ ) : ( + <> +
+ {formatTime( + isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s") + )} +
+ + {/* + + */} + + + + )} +
+
+ } + + {/* Layout Body */} +
+ {/* Sidebar */} + - {!isStopping && selectedProject ? ( - - {selectedProject.projectName} - - ) : !isStopping ? ( - - No Project Selected - - ) : null} -
+ {/* Main Content */} +
+ +
-
- {isStopping ? ( - // Skeleton for right side during stop -
-
-
-
-
- ) : ( - <> -
- {formatTime( - isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s") - )} -
- - - - - - - - )} -
-
- } - - {/* Layout Body */} -
- {/* Sidebar */} - - - {/* Main Content */} -
- -
- - setShowLogoutModal(false)} - onConfirm={handleLogout} - /> + setShowLogoutModal(false)} + onConfirm={handleLogout} + /> +
- ); }; diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index 26d8220..fd6f7e9 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; -import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react"; +import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause, RefreshCw } from "lucide-react"; import { useOutletContext } from "react-router-dom"; import NoData from "./NoData"; import { useDispatch, useSelector } from "react-redux"; @@ -105,6 +105,7 @@ const Dashboard: React.FC = () => { const selectedProject = useSelector((state: any) => state.projects.selectedProject); const projectSummary = useSelector((state: any) => state.projects.projectSummary); const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); + const [isRefreshing, setIsRefreshing] = useState(false); console.log("Line 111", projectsData, selectedProject, projectTrackingMap) @@ -127,60 +128,66 @@ const Dashboard: React.FC = () => { } }; - useEffect(() => { - const fetchProjects = async () => { - try { - console.log("๐Ÿ“ก Fetching projects...Dashboard"); - const res = await window.electron.getProjectList(); - console.log(" Project list response in dashboard:", res); + 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 data redux store - dispatch(setProjectsDataSlice(res.data)); + if (res?.success && Array.isArray(res.data)) { + // 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)); + + const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime); + + // Initialize tracking data for first project 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 || "", + initializeProjectTracking({ + projectId: firstProject.projectId, + initialTime: initialElapsed, }) ); - - // Auto-select first project - if (res.data.length > 0 && !selectedProject) { - const firstProject = res.data[0]; - - // Select the project - dispatch(setSelectedProjectSlice(firstProject)); - - const initialElapsed = parseTimeStringToSeconds(firstProject.totalWorkTime); - - // Initialize tracking data for first project - dispatch( - initializeProjectTracking({ - projectId: firstProject.projectId, - initialTime: initialElapsed, - }) - ); - } - - fetchProfile() - - } else { - console.warn("โš ๏ธ Unexpected response:", res); } - } catch (err) { - console.error("โŒ Error fetching projects:", err); - } - }; + fetchProfile() + + } else { + console.warn("โš ๏ธ Unexpected response:", res); + } + } catch (err) { + console.error("โŒ Error fetching projects:", err); + } + }; + + useEffect(() => { fetchProjects(); }, []); + const handleRefresh = async () => { + setIsRefreshing(true); + await fetchProjects(); + setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback + }; + const selectedData = selectedProject ? projectTrackingMap[selectedProject.projectId] : null; @@ -234,7 +241,7 @@ const Dashboard: React.FC = () => { }; return ( -
+
{/* Header */}

Dashboard

@@ -243,7 +250,7 @@ const Dashboard: React.FC = () => {
- {isStopping ? ( + {(isStopping || isRefreshing) ? ( // Skeleton loader for tracking section
@@ -318,10 +325,19 @@ const Dashboard: React.FC = () => { {/* ๐Ÿ”น Sticky Header */}

Projects

+
{/* ๐Ÿ”น Table or Empty State or Skeleton */} - {isStopping ? ( + {(isStopping || isRefreshing) ? ( // Skeleton loader for projects table
{[...Array(3)].map((_, index) => ( diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index 51bc4b0..943104d 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -106,7 +106,7 @@ const Timesheets: React.FC = () => { return ( -
+
{/* Header */}
@@ -150,7 +150,7 @@ const Timesheets: React.FC = () => {
*/} {/* Timesheet Table */} -
+
From 0eea767e5e923b5ba4cc5d738ff2751df70c5729 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 2 Dec 2025 17:16:50 +0530 Subject: [PATCH 3/7] listing all the menus components and add the refresh button with the last time refresh list --- src/electron/api/axiosClient.ts | 2 +- src/electron/ipc/ipcHandler.ts | 62 +++-- src/electron/preload.cts | 1 + src/ui/App.tsx | 2 + src/ui/index.css | 15 + src/ui/layout/DashboardLayout.tsx | 2 +- src/ui/pages/Dashboard.tsx | 7 +- .../ApplyLeaveAndAdjustmentsTime.tsx | 114 ++++++++ .../LeaveAndAdjustments.tsx | 258 +++++++++++------ .../tabs/AdjustmentTab.tsx | 14 +- .../LeaveAndAdjustments/tabs/PtoLeaveTab.tsx | 3 + .../LeaveAndAdjustments/tabs/SickLeaveTab.tsx | 3 + src/ui/pages/Schedule/Schedule.tsx | 207 +++++++++----- src/ui/pages/Settings/Settings.tsx | 8 - src/ui/pages/Timesheets/Timesheets.tsx | 259 ++++++++++++------ types.d.ts | 5 + 16 files changed, 695 insertions(+), 267 deletions(-) create mode 100644 src/ui/pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime.tsx diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index a017ae4..a4960b4 100644 --- a/src/electron/api/axiosClient.ts +++ b/src/electron/api/axiosClient.ts @@ -42,7 +42,7 @@ api.interceptors.response.use( (response: AxiosResponse) => response.data, async (error) => { // console.error('[Axios Error]', error.message); - if (error.response?.status === 401) { + if (error.response?.status === 401 || 403) { console.warn('Token expired โ€” trigger refresh logic if needed'); sendTokenExpired(); } diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index cd9b1f9..759d285 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -29,7 +29,7 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => { return { success: true }; }); - ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => { + ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => { return handleLogin(credentials); }); @@ -46,8 +46,8 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => { return { success: false, message: "Logout failed" }; } }); - - + + @@ -96,7 +96,7 @@ ipcMain.handle("get-project-list", async () => { return { success: false, message: error?.message || String(error) || "Unknown error", - status:error?.status + status: error?.status }; } }); @@ -110,8 +110,8 @@ ipcMain.handle("start-project-tracking", async (event, payload) => { const response = await api.post("/log/project", payload); console.log(" Tracking start synced:", response); - return { success: true, data: response }; - } catch (error:any) { + return { success: true, data: response }; + } catch (error: any) { console.error("โŒ Error syncing project start:", error); // If API sent a response (like 400) @@ -261,14 +261,14 @@ ipcMain.handle("change-password", async (event, data) => { // Build final payload const payload = { - userDetails: { - oldPassword: data.oldPassword, - password: data.password, - cPassword: data.cPassword, - }, - }; + userDetails: { + oldPassword: data.oldPassword, + password: data.password, + cPassword: data.cPassword, + }, + }; - console.log("Change Password payload" , payload) + console.log("Change Password payload", payload) try { const response = await api.patch("/profile", payload); console.log("Chnage-passowrd-response", response) @@ -303,20 +303,20 @@ ipcMain.handle("get-project-summary", async (_, { projectId, date }) => { ipcMain.handle("create-timesheet", async (event, data) => { - console.log("data" , data) + console.log("data", data) try { console.log("๐Ÿ“ฉ Timesheet Payload:", data); const res = await api.post("/timesheet/adjustment/request", data); - console.log(res , " Line 323") + console.log(res, " Line 323") return { success: true, message: "Timesheet created successfully", data: res, }; - } catch (error:any) { + } catch (error: any) { console.error("โŒ Timesheet Error:", error); return { @@ -402,7 +402,7 @@ ipcMain.handle("get-schedules", async () => { ipcMain.handle("get-payslips", async (event, vaId: string) => { - console.log("get-payslips",vaId) + console.log("get-payslips", vaId) try { const response = await api.get(`/paySlip/${vaId}`); @@ -433,6 +433,34 @@ ipcMain.handle("get-payslips", async (event, vaId: string) => { +ipcMain.handle("get-leave-list", async () => { + + try { + const res = await api.get("/leaves/all"); + + console.log("/leaves/all", res.data); + return { + success: true, + data: res.data, + }; + } catch (error: any) { + let message = "Unknown error occurred"; + + if (error.response) { + console.log("Status:", error.response.status); + console.log("Message:", error.response.statusText); + console.log("Server Response:", error.response.data); + + message = error.response.data?.message || error.response.statusText; + } else { + message = error.message; + } + return { + success: false, + message + }; + } +}); diff --git a/src/electron/preload.cts b/src/electron/preload.cts index 34c9b41..3ebc272 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('electron', { getSchedules: () => ipcRenderer.invoke("get-schedules"), getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId), + getLeaveList: () => ipcRenderer.invoke("get-leave-list") } satisfies Window['electron']); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 7b59fca..7de1c12 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -96,6 +96,7 @@ import HelpSupport from "./pages/HelpSupport/HelpSupport"; import Vpn from "./pages/Vpn/Vpn"; import Schedule from "./pages/Schedule/Schedule"; import PaysSlips from "./pages/PaySlips/PaysSlips"; +import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime"; const App: React.FC = () => { @@ -138,6 +139,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/ui/index.css b/src/ui/index.css index 4f0c033..72448ce 100644 --- a/src/ui/index.css +++ b/src/ui/index.css @@ -22,6 +22,21 @@ } } +@layer utilities { + .thin-scrollbar::-webkit-scrollbar { + width: 6px; + height: 6px; + } + .thin-scrollbar::-webkit-scrollbar-thumb { + background-color: hsl(var(--bc) / 0.3); /* base-content/30 */ + border-radius: 999px; + } + .thin-scrollbar::-webkit-scrollbar-track { + background: transparent; + } +} + + input[type=number] { -moz-appearance: textfield; } diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 8ac3cf2..3f0f0f2 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -746,7 +746,7 @@ const DashboardLayout = () => { ))} -
+
+ + + + + + + + )); + }; + + const formatLastRefreshed = () => { + if (!lastRefreshed) return "Never"; + + return DateTime.fromJSDate(lastRefreshed) + .toLocal() + .toFormat("HH:mm:ss"); // Only time + }; return ( -
-
-

Leave & Adjust Timesheet

+
+ {/* Header */} +
+

Leave & Adjust Timesheet

+ +
- {/* Tabs โ€” Disabled while submitting */} -
- setActiveTab("pto")} - > - PTO Leave - + {/* Table Container */} +
+
+ + + + + + + + + + - setActiveTab("sick")} - > - Sick Leave - + + {loading ? ( + renderSkeletonRows() + ) : refreshing && leaves.length === 0 ? ( + renderSkeletonRows() + ) : leaves.length > 0 ? ( + leaves.map((leave, index) => ( + + - setActiveTab("adjustment")} - > - Adjustment Submission - + + + + + + + + + + + )) + ) : ( + + + + )} + + {refreshing && leaves.length > 0 && ( + + + + )} + +
#Leave TypeDate From โ†’ ToStatusTotal DaysApplied On
{index + 1} + {leave.leaveType} + + {leave.dateFrom} +
+ {leave.dateTo} +
+ + {leave.status} + + {leave.totalDays}{new Date(leave.appliedOn).toLocaleString()}
+ No leaves found +
+
+ + Refreshing... +
+
+
+ + {/* Footer */} +
+
+ {leaves.length} leave{leaves.length !== 1 ? "s" : ""} found
- {/* Main Container */} -
-
+
+ {lastRefreshed && ( +
+ Last updated: {formatLastRefreshed()} +
+ )} - {activeTab === "pto" && ( - +
+
-
+
); }; diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx index f923f9a..fb85198 100644 --- a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx +++ b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx @@ -4,6 +4,7 @@ import ReactQuill from 'react-quill-new'; import { quillModules } from '../../../utils/quillConfig'; import Quill from 'quill'; import 'quill/dist/quill.snow.css'; +import { useNavigate } from 'react-router-dom'; interface Project { projectId: string; @@ -48,6 +49,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { const [comment, setComment] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const quillRef = useRef(null); + const navigate = useNavigate() // Field validation functions const validateProjectId = (projectId: string): string | undefined => { @@ -323,6 +325,12 @@ const AdjustmentTab: React.FC = ({ projects }) => { useEffect(() => { if (selectedProjectId && selectedDate) { + setProjectSummary(null) + setStartTime(""); + setEndTime(""); + setComment(""); + setFormErrors({}); + setTotalAdjustment("0h 0m"); fetchLogs(); } else { setProjectSummary(null); @@ -454,6 +462,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { console.log("Timesheet Created Successfully"); alert(res.message); resetForm(); + navigate('/dashboard/leave-adjustments') } else { setFormErrors(prev => ({ ...prev, @@ -655,11 +664,6 @@ const AdjustmentTab: React.FC = ({ projects }) => { disabled={isSubmitting} className={`btn btn-primary flex-1 hover:shadow-lg transition-all duration-300 ${isSubmitting ? 'loading' : ''}`} > - {!isSubmitting && ( - - - - )} {isSubmitting ? 'Submitting...' : 'Submit Request'} diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx index e41ffbd..a9a8956 100644 --- a/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx +++ b/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx @@ -4,6 +4,7 @@ import { quillModules } from "../../../utils/quillConfig"; import ReactQuill from "react-quill-new"; import Quill from "quill"; import "quill/dist/quill.snow.css"; +import { useNavigate } from "react-router-dom"; interface LeaveFormData { fromDate: string; @@ -23,6 +24,7 @@ const PtoLeaveTab: React.FC<{ }); const [serverError, setServerError] = useState(""); const quillRef = useRef(null); + const navigate = useNavigate() const [errors, setErrors] = useState>({}); @@ -199,6 +201,7 @@ const PtoLeaveTab: React.FC<{ if (result.success) { resetForm(); alert("PTO submitted!"); + navigate('/dashboard/leave-adjustments') } else { setServerError(result.message || "Something went wrong"); } diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx index 180e6ae..fd817ad 100644 --- a/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx +++ b/src/ui/pages/LeaveAndAdjustments/tabs/SickLeaveTab.tsx @@ -3,6 +3,7 @@ import { quillModules } from "../../../utils/quillConfig"; import ReactQuill from "react-quill-new"; import Quill from "quill"; import "quill/dist/quill.snow.css"; // correct CSS +import { useNavigate } from 'react-router-dom'; interface LeaveFormData { fromDate: string; @@ -22,6 +23,7 @@ const SickLeaveTab: React.FC<{ const [serverError, setServerError] = useState(""); const quillRef = useRef(null); const [sickErrors, setSickErrors] = useState>({}); + const navigate = useNavigate() useEffect(() => { const editor = quillRef.current?.getEditor(); @@ -189,6 +191,7 @@ const SickLeaveTab: React.FC<{ if (result.success) { resetForm(); alert("SICK submitted!"); + navigate('/dashboard/leave-adjustments') } else { setServerError(result.message || "Something went wrong"); } diff --git a/src/ui/pages/Schedule/Schedule.tsx b/src/ui/pages/Schedule/Schedule.tsx index ca8ae10..63d0461 100644 --- a/src/ui/pages/Schedule/Schedule.tsx +++ b/src/ui/pages/Schedule/Schedule.tsx @@ -1,4 +1,7 @@ import React, { useEffect, useState } from "react"; +import { useOutletContext } from "react-router-dom"; +import { TrackingState } from "../../layout/DashboardLayout"; +import { RefreshCw, Loader2 } from "lucide-react"; import { DateTime } from "luxon"; interface ScheduleItem { @@ -12,50 +15,58 @@ interface ScheduleItem { } const Schedule = () => { + const { isTracking } = useOutletContext(); + const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); + // const [lastUpdated, setLastUpdated] = useState(null); + const [lastRefreshed, setLastRefreshed] = useState(null); - // Format helpers using Luxon - const formatDateTime = (input: string | null) => { - if (!input) return "-"; - return DateTime.fromISO(input) - .setZone("local") - .toLocaleString(DateTime.DATETIME_MED); - }; - + /** Format: Only Date */ const formatOnlyDate = (input: string | null) => { if (!input) return "-"; - return DateTime.fromISO(input) - .setZone("local") - .toLocaleString(DateTime.DATE_MED); + return DateTime.fromISO(input).toLocaleString(DateTime.DATE_MED); }; - // Fetch schedules - useEffect(() => { - const loadSchedules = async () => { - try { - const res = await window.electron.getSchedules(); - console.log(res.result); + /** Format: Date + Time */ + const formatDateTime = (input: string | null) => { + if (!input) return "-"; + return DateTime.fromISO(input).toLocaleString(DateTime.DATETIME_MED); + }; - if (!res.success) { - setError(res.message || "Failed to load schedules"); - return; - } + /** Fetch Data */ + const fetchSchedules = async () => { + try { + const res = await window.electron.getSchedules(); - const list = res.result || []; // Use data array - setRows(list); - } catch (err: any) { - setError(err.message || "Unexpected error"); - } finally { - setLoading(false); + if (!res.success) { + setError(res.message || "Failed to load schedules"); + return; } - }; - loadSchedules(); + setRows(res.result || []); + setLastRefreshed(new Date()); + } catch (err: any) { + setError(err.message || "Unexpected error"); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + /** Refresh Button */ + const handleRefresh = async () => { + setRefreshing(true); + await fetchSchedules(); + }; + + useEffect(() => { + fetchSchedules(); }, []); - // Determine if current date is within the schedule period + /** Determine Active Period */ const isActiveRow = (row: ScheduleItem) => { if (!row.periodStart || !row.periodEnd) return false; @@ -66,50 +77,69 @@ const Schedule = () => { return now >= start && now <= end; }; - // Get Active Schedule (only one) const activeSchedule = rows.find((r) => isActiveRow(r)); + /** Skeleton Rows */ + const renderSkeletonRows = () => + Array.from({ length: 5 }).map((_, idx) => ( + +
+
+
+
+ + )); + + const formatLastRefreshed = () => { + if (!lastRefreshed) return "Never"; + + return DateTime.fromJSDate(lastRefreshed) + .toLocal() + .toFormat("HH:mm:ss"); // Only time + }; + return ( -
- {/* Header */} -
+
+ {/* HEADER */} +

Schedules

- {activeSchedule && ( -
-

- Pay Date: - {formatOnlyDate(activeSchedule.ScheduleDate)} - -

-
- )} + +
+ {activeSchedule && ( +
+ Pay Date: {formatOnlyDate(activeSchedule.ScheduleDate)} +
+ )} + +
-
- {/* Loader */} - {loading && ( -
- + {/* TABLE WRAPPER */} +
+ {/* Loading */} + {loading ? ( + + {renderSkeletonRows()} +
+ ) : error ? ( +
{error}
+ ) : rows.length === 0 ? ( +
+ No schedules found
- )} - - {/* Error */} - {!loading && error && ( -
โŒ {error}
- )} - - {/* No Data */} - {!loading && !error && rows.length === 0 && ( -
No schedules found.
- )} - - - - - {/* Table */} - {!loading && !error && rows.length > 0 && ( -
- + ) : ( +
+
@@ -123,22 +153,59 @@ const Schedule = () => { {rows.map((row, index) => ( ))} + + {refreshing && ( + + + + )}
#
{index + 1} - {formatOnlyDate(row.periodStart)} โ†’ {formatOnlyDate(row.periodEnd)} + {formatOnlyDate(row.periodStart)} โ†’{" "} + {formatOnlyDate(row.periodEnd)} {formatDateTime(row.cutOffTime)} {formatOnlyDate(row.ScheduleDate)}
+ + Refreshing... +
)}
-
+ + {/* FOOTER */} +
+
{rows.length} schedules found
+ + +
+ {lastRefreshed && + `Last updated: ${formatLastRefreshed()}`} + +
+
+
); }; diff --git a/src/ui/pages/Settings/Settings.tsx b/src/ui/pages/Settings/Settings.tsx index 7a0d6b1..180d786 100644 --- a/src/ui/pages/Settings/Settings.tsx +++ b/src/ui/pages/Settings/Settings.tsx @@ -82,14 +82,6 @@ const Settings: React.FC = () => {

-
-

- ๐Ÿ’ฌ Skype -

-

- {profile?.skype || "Not Linked"} -

-
{/* Payment Info */}
diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index 943104d..33d9d00 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -1,8 +1,22 @@ import React, { useEffect, useState } from "react"; -import { CalendarDays, Clock, FileText, CheckCircle, XCircle, Eye, EyeOff } from "lucide-react"; +import { + CalendarDays, + Clock, + FileText, + CheckCircle, + XCircle, + Eye, + EyeOff, + RefreshCw, + Loader2 +} from "lucide-react"; import { useSelector } from "react-redux"; import { RootState } from "../../store"; import ProjectLogsModal from "./ProjectLogsModal"; +import { TrackingState } from "../../layout/DashboardLayout"; +import { useOutletContext } from "react-router-dom"; +import { DateTime } from "luxon"; + interface TimesheetEntry { id: number; @@ -28,45 +42,78 @@ interface Project { totalWorkTime: string; totalWorkMs: number; totalLogs: number; - status:string + status: string } const Timesheets: React.FC = () => { + const { isTracking } = useOutletContext(); const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0); const approvedCount = timesheetData.filter((t) => t.status === "Approved").length; const { windowLogs } = useSelector((state: RootState) => state.window); - console.log("๐Ÿงพ Timesheet in component:", windowLogs); + console.log(" Timesheet in component:", windowLogs); const [projectsData, setProjectsData] = useState([]); const [logs, setLogs] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [selectedProjectName, setSelectedProjectName] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [lastRefreshed, setLastRefreshed] = useState(null); + const [initialLoading, setInitialLoading] = useState(true); + const [tick, setTick] = useState(0); + + const activeProjects = projectsData.filter(p => p.status === "ACTIVE"); + useEffect(() => { - const fetchProjects = async () => { - try { - console.log("๐Ÿ“ก Fetching projects..."); - const res = await window.electron.getProjectList(); - console.log("โœ… Project list response:", res); + const interval = setInterval(() => { + setTick((t) => t + 1); // force re-render every second + }, 1000); - if (res?.success && Array.isArray(res.data)) { - // Store project list - setProjectsData(res.data); - } else { - console.warn(" Unexpected response:", res); - } - } catch (err) { - console.error("Error fetching projects:", err); + return () => clearInterval(interval); + }, []); + + const formatLastRefreshed = () => { + if (!lastRefreshed) return "Never"; + + return DateTime.fromJSDate(lastRefreshed) + .toLocal() + .toFormat("HH:mm:ss"); // Only time + }; + + + + const fetchProjects = async () => { + try { + console.log("๐Ÿ“ก Fetching projects..."); + const res = await window.electron.getProjectList(); + console.log("โœ… Project list response:", res); + + if (res?.success && Array.isArray(res.data)) { + // Store project list + setProjectsData(res.data); + setLastRefreshed(new Date()); + } else { + console.warn(" Unexpected response:", res); } - }; + } catch (err) { + console.error("Error fetching projects:", err); + } finally { + setInitialLoading(false); + setRefreshing(false); + } + }; + useEffect(() => { fetchProjects(); }, []); - + const handleRefresh = async () => { + setRefreshing(true); + await fetchProjects(); + }; const handleViewLogs = async (projectID: string, projectName: string) => { try { @@ -105,54 +152,49 @@ const Timesheets: React.FC = () => { + + + // Skeleton loader rows + const renderSkeletonRows = () => { + return Array.from({ length: 5 }).map((_, index) => ( + + +
+ + +
+ + +
+ + +
+ + + )); + }; + return ( -
+
{/* Header */} -
-
-

Timesheets

-
+
+

Timesheets

- {/* Summary Cards */} - {/*
-
-
-
-

Total Logged Hours

-

{totalHours} hrs

-
- -
-
- -
-
-
-

Approved Days

-

{approvedCount}

-
- -
-
- -
-
-
-

Pending Days

-

- {timesheetData.length - approvedCount} -

-
- -
-
-
*/} - {/* Timesheet Table */} -
- - +
+
+ @@ -161,34 +203,83 @@ const Timesheets: React.FC = () => { - {projectsData.map((entry, index) => ( - - - - - + + + + + + )) + )} + + {refreshing && projectsData.length > 0 && ( + // Show a single skeleton row at the top while refreshing with existing data + + - ))} + )}
# Project
{index + 1}{entry.projectName}{formatWorkTime(entry.totalWorkTime)} - + {initialLoading ? ( + // Show skeleton while initially loading + renderSkeletonRows() + ) : refreshing && projectsData.length === 0 ? ( + // Show skeleton while refreshing with empty data + renderSkeletonRows() + ) : ( + // Show actual data + projectsData.map((entry, index) => ( +
{index + 1}{entry.projectName}{formatWorkTime(entry.totalWorkTime)} + +
+
+ + Refreshing... +
{/* Footer Summary */} -
- {/* - Showing {timesheetData.length} entries - */} - - Last updated: {new Date().toLocaleDateString()} - +
+
+ {activeProjects.length} project{activeProjects.length !== 1 ? 's' : ''} shown +
+ +
+ {lastRefreshed && ( +
+ Last updated: {formatLastRefreshed()} +
+ )} + + + +
{ logs={logs} loading={loading} /> -
+
); }; -export default Timesheets; +export default Timesheets; \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index bb7af91..062dd79 100644 --- a/types.d.ts +++ b/types.d.ts @@ -211,6 +211,11 @@ interface Window { data?: PayslipResponse[]; message?: string; }>; + + getLeaveList: () => Promise<{ + success: boolean; + data?: any; + }>; }; } From 475776b2147315eb55cc2dea95fcfda50dee7ce0 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 2 Dec 2025 19:22:48 +0530 Subject: [PATCH 4/7] payment method modal added in settings sections --- src/ui/pages/Dashboard.tsx | 2 +- src/ui/pages/Schedule/Schedule.tsx | 4 +- .../SettingModal/ChangePaymentModal.tsx | 157 ++++++++++++++++++ src/ui/pages/Settings/Settings.tsx | 36 ++-- src/ui/store/profileSlice.tsx | 1 + types.d.ts | 1 + 6 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 src/ui/pages/Settings/SettingModal/ChangePaymentModal.tsx diff --git a/src/ui/pages/Dashboard.tsx b/src/ui/pages/Dashboard.tsx index 03d742e..5506739 100644 --- a/src/ui/pages/Dashboard.tsx +++ b/src/ui/pages/Dashboard.tsx @@ -16,7 +16,7 @@ interface Project { totalWorkTime: string; totalWorkMs: number; totalLogs: number; - status: string + status: string; } interface ProjectTrackingData { diff --git a/src/ui/pages/Schedule/Schedule.tsx b/src/ui/pages/Schedule/Schedule.tsx index 63d0461..759ebd8 100644 --- a/src/ui/pages/Schedule/Schedule.tsx +++ b/src/ui/pages/Schedule/Schedule.tsx @@ -38,6 +38,8 @@ const Schedule = () => { /** Fetch Data */ const fetchSchedules = async () => { + setRefreshing(true); + setLoading(true); try { const res = await window.electron.getSchedules(); @@ -58,7 +60,7 @@ const Schedule = () => { /** Refresh Button */ const handleRefresh = async () => { - setRefreshing(true); + await fetchSchedules(); }; diff --git a/src/ui/pages/Settings/SettingModal/ChangePaymentModal.tsx b/src/ui/pages/Settings/SettingModal/ChangePaymentModal.tsx new file mode 100644 index 0000000..6c08447 --- /dev/null +++ b/src/ui/pages/Settings/SettingModal/ChangePaymentModal.tsx @@ -0,0 +1,157 @@ +import React, { useState, useEffect } from "react"; +import { CreditCard } from "lucide-react"; +import { useSelector, useDispatch } from "react-redux"; +import { RootState } from "../../../store"; + +interface ChangePaymentModalProps { + isOpen: boolean; + onClose: () => void; +} + +const ChangePaymentModal: React.FC = ({ + isOpen, + onClose, +}) => { + const dispatch = useDispatch(); + const { profile } = useSelector((state: RootState) => state.profile); + + const [method, setMethod] = useState(profile?.paymentMethod || ""); + const [wiseEmail, setWiseEmail] = useState(profile?.wiseEmail || ""); + const [veemAccount, setVeemAccount] = useState(profile?.veemAccount || ""); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + if (isOpen) { + setMethod(profile?.paymentMethod || ""); + setWiseEmail(profile?.wiseEmail || ""); + setVeemAccount(profile?.veemAccount || ""); + setError(""); + } + }, [isOpen]); + + if (!isOpen) return null; + + const handleSubmit = async () => { + setError(""); + + // Validation + if (!method) { + return setError("Please select a payment method."); + } + + if (method === "wise" && !wiseEmail.trim()) { + return setError("Wise email is required."); + } + + if (method === "wise" && !/^\S+@\S+\.\S+$/.test(wiseEmail)) { + return setError("Enter a valid Wise email."); + } + + if (method === "veem" && !veemAccount.trim()) { + return setError("Veem account number is required."); + } + + setLoading(true); + + // try { + // // Replace with your electron API + // await window.electron.updatePayment({ + // method, + // wiseEmail: method === "wise" ? wiseEmail.trim() : "", + // veemAccount: method === "veem" ? veemAccount.trim() : "", + // }); + + // // Refresh or update Redux + // // dispatch(updateProfile({...})); + + // onClose(); + // } catch (err) { + // setError("Failed to update payment info. Try again."); + // } + + setLoading(false); + }; + + return ( + +
+ +

+ Change Payment Method +

+ + {/* Error */} + {error && ( +
{error}
+ )} + + {/* Dropdown */} +
+ + + +
+ + {/* Wise Email */} + {method === "wise" && ( +
+ + setWiseEmail(e.target.value)} + /> +
+ )} + + {/* Veem Account */} + {method === "veem" && ( +
+ + setVeemAccount(e.target.value)} + /> +
+ )} + +
+ + + +
+
+
+ ); +}; + +export default ChangePaymentModal; diff --git a/src/ui/pages/Settings/Settings.tsx b/src/ui/pages/Settings/Settings.tsx index 180d786..c8a8f4a 100644 --- a/src/ui/pages/Settings/Settings.tsx +++ b/src/ui/pages/Settings/Settings.tsx @@ -1,13 +1,15 @@ import React, { useState } from "react"; -import { User, Mail, Phone, Calendar, MapPin, CreditCard, Globe, Lock } from "lucide-react"; +import { User, Mail, Phone, Calendar, MapPin, CreditCard, Globe, Lock, SquarePen } from "lucide-react"; import { useSelector } from "react-redux"; import { RootState } from "../../store"; import ChangePasswordModal from "./ChangePasswordModal"; +import ChangePaymentModal from "./SettingModal/ChangePaymentModal"; const Settings: React.FC = () => { const { profile } = useSelector((state: RootState) => state.profile); const [modalOpen, setModalOpen] = useState(false); + const [showPaymentModal,setShowPaymentModal] = useState(false) const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim(); @@ -84,22 +86,31 @@ const Settings: React.FC = () => { {/* Payment Info */} -
-

- Payment -

-

- {profile?.paymentMethod || "N/A"} -

+
setShowPaymentModal(true)}> +
+
+

+ Payment +

+

+ {profile?.paymentMethod || "N/A"} +

+
+ + + +
- {profile?.paymentMethod === "payoneer" && ( + + {profile?.paymentMethod === "wise" && (

- ๐Ÿ’ณ Payoneer + ๐Ÿ’ณ Wise

- {maskLast5(profile?.payoneerAccount)} + {maskLast5(profile?.wiseEmail)}

)} @@ -129,8 +140,9 @@ const Settings: React.FC = () => {
- {/* Change Password Modal */} + {/* Modal */} setModalOpen(false)} /> + setShowPaymentModal(false)} />
); }; diff --git a/src/ui/store/profileSlice.tsx b/src/ui/store/profileSlice.tsx index eca87d4..35f250d 100644 --- a/src/ui/store/profileSlice.tsx +++ b/src/ui/store/profileSlice.tsx @@ -19,6 +19,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; paymentMethod:string; payoneerAccount:string; veemAccount:string; + wiseEmail:string _id: string; } diff --git a/types.d.ts b/types.d.ts index 062dd79..6eb395d 100644 --- a/types.d.ts +++ b/types.d.ts @@ -83,6 +83,7 @@ interface ProfileData { paymentMethod: string; payoneerAccount: string; veemAccount: string; + wiseEmail:string _id: string; } From 70193501d3fb0c1728ef89e59f6473a10cf79788 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 2 Dec 2025 19:42:19 +0530 Subject: [PATCH 5/7] Timesheets open the modal with loader show the logs with loading --- src/ui/pages/Timesheets/ProjectLogsModal.tsx | 1 + src/ui/pages/Timesheets/Timesheets.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ui/pages/Timesheets/ProjectLogsModal.tsx b/src/ui/pages/Timesheets/ProjectLogsModal.tsx index 9724311..7144932 100644 --- a/src/ui/pages/Timesheets/ProjectLogsModal.tsx +++ b/src/ui/pages/Timesheets/ProjectLogsModal.tsx @@ -95,3 +95,4 @@ const ProjectLogsModal: React.FC = ({ isOpen, onClose, projectName, logs, }; export default ProjectLogsModal; + diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index 33d9d00..98e893f 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -117,6 +117,7 @@ const Timesheets: React.FC = () => { const handleViewLogs = async (projectID: string, projectName: string) => { try { + setModalOpen(true); setLoading(true); setSelectedProject(projectID); setSelectedProjectName(projectName); @@ -132,11 +133,13 @@ const Timesheets: React.FC = () => { setLogs([]); } - setModalOpen(true); + } catch (error) { console.error("Error fetching logs:", error); + setModalOpen(false) } finally { setLoading(false); + } }; From df2b35a4e4832d02753dadcabf37a3fd8c6b4572 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Wed, 3 Dec 2025 16:17:10 +0530 Subject: [PATCH 6/7] leave and adjut. show both the table and tabs based table --- src/electron/ipc/ipcHandler.ts | 29 ++ src/electron/preload.cts | 9 +- .../LeaveAndAdjustments/AdjustmentsTable.tsx | 183 ++++++++++++ .../LeaveAndAdjustments.tsx | 278 ++++++++---------- .../pages/LeaveAndAdjustments/LeaveTable.tsx | 184 ++++++++++++ .../tabs/AdjustmentTab.tsx | 2 +- src/ui/pages/Timesheets/Timesheets.tsx | 4 +- types.d.ts | 7 +- 8 files changed, 533 insertions(+), 163 deletions(-) create mode 100644 src/ui/pages/LeaveAndAdjustments/AdjustmentsTable.tsx create mode 100644 src/ui/pages/LeaveAndAdjustments/LeaveTable.tsx diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index 759d285..8c50481 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -462,6 +462,35 @@ ipcMain.handle("get-leave-list", async () => { } }); +ipcMain.handle("get-adjustments-list", async () => { + + try { + const res = await api.get("/timesheet/adjustments/list"); + + console.log("/timesheet/adjustments/list", res.data); + return { + success: true, + data: res.data, + }; + } catch (error: any) { + let message = "Unknown error occurred"; + + if (error.response) { + console.log("Status:", error.response.status); + console.log("Message:", error.response.statusText); + console.log("Server Response:", error.response.data); + + message = error.response.data?.message || error.response.statusText; + } else { + message = error.message; + } + return { + success: false, + message + }; + } +}); + export const sendTokenExpired = () => { diff --git a/src/electron/preload.cts b/src/electron/preload.cts index 3ebc272..142079a 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -51,12 +51,13 @@ contextBridge.exposeInMainWorld('electron', { createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data), createPTOAndSickLeaveRequest: (data) => - ipcRenderer.invoke("create-leave-request", data), + ipcRenderer.invoke("create-leave-request", data), -getSchedules: () => ipcRenderer.invoke("get-schedules"), + getSchedules: () => ipcRenderer.invoke("get-schedules"), -getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId), - getLeaveList: () => ipcRenderer.invoke("get-leave-list") + getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId), + getLeaveList: () => ipcRenderer.invoke("get-leave-list"), + getAdjustmentsList:() => ipcRenderer.invoke("get-adjustments-list"), } satisfies Window['electron']); diff --git a/src/ui/pages/LeaveAndAdjustments/AdjustmentsTable.tsx b/src/ui/pages/LeaveAndAdjustments/AdjustmentsTable.tsx new file mode 100644 index 0000000..0e0e65d --- /dev/null +++ b/src/ui/pages/LeaveAndAdjustments/AdjustmentsTable.tsx @@ -0,0 +1,183 @@ +import React from "react"; +import { Loader2, RefreshCw } from "lucide-react"; +import { DateTime } from "luxon"; + +interface AdjustmentItem { + id: string; + date: string; + loginTime: string; + logoutTime: string; + comment: string; + status: string; + requestedAt: string; + adjustedHours: string; + project: { + id: string; + name: string; + }; +} + +interface AdjustmentTableProps { + isTracking: boolean; + adjustments: AdjustmentItem[]; + loading: boolean; + lastRefreshed: Date | null; + onRefresh: () => Promise; +} + +const AdjustmentsTable: React.FC = ({ + isTracking, + adjustments, + loading, + lastRefreshed, + onRefresh +}) => { + const [refreshing, setRefreshing] = React.useState(false); + + // ๐ŸŒ Convert UTC โ†’ Local System Time + const toLocal = (utcString: string, format: string) => { + return DateTime.fromISO(utcString, { zone: "utc" }) + .setZone(DateTime.local().zoneName) + .toFormat(format); + }; + + const formatTime = (t: string) => toLocal(t, "HH:mm:ss"); + const formatDate = (d: string) => toLocal(d, "yyyy-MM-dd"); + + const handleRefresh = async () => { + setRefreshing(true); + await onRefresh(); + setRefreshing(false); + }; + + const renderSkeleton = () => + Array.from({ length: 7 }).map((_, i) => ( + +
+
+
+
+
+
+
+ + )); + + return ( + <> +
+ + + + + + + + + + + + + + + {loading + ? renderSkeleton() + : adjustments.length === 0 + ? ( + + + + ) + : adjustments.map((item, idx) => ( + + + + + + + + + + + + + + + + ))} + + {/* {refreshing && adjustments.length > 0 && ( + + + + )} */} + +
#ProjectDateIn โ†’ OutAdjusted HoursStatusRequested At
+ No adjustments found +
{idx + 1} + + {item.project?.name || "N/A"} + + {formatDate(item.date)} + {formatTime(item.loginTime)} + โ†’ {" "} + {formatTime(item.logoutTime)} + + + {item.adjustedHours} + + + + {item.status} + + {toLocal(item.requestedAt, "yyyy-MM-dd HH:mm:ss")}
+
+ {renderSkeleton()} +
+
+
+ + {/* FOOTER */} +
+
+ {adjustments.length} adjustment{adjustments.length !== 1 ? "s" : ""} found +
+ +
+ {lastRefreshed && ( +
+ Last updated: {toLocal(lastRefreshed.toISOString(), "HH:mm:ss")} +
+ )} + + +
+
+ + ); +}; + +export default AdjustmentsTable; \ No newline at end of file diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index 1ab0085..c87ca3c 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -1,8 +1,9 @@ -import React, { useEffect, useState } from "react"; +import React, { useState, useEffect } from "react"; import { useNavigate, useOutletContext } from "react-router-dom"; import { TrackingState } from "../../layout/DashboardLayout"; -import { Loader2, RefreshCw } from "lucide-react"; -import { DateTime } from "luxon"; +import LeaveTable from "./LeaveTable"; +import AdjustmentsTable from "./AdjustmentsTable"; +import { useSelector } from "react-redux"; interface LeaveItem { leaveId: string; @@ -17,70 +18,104 @@ interface LeaveItem { rejectionReason: string | null; } +interface AdjustmentItem { + id: string; + date: string; + loginTime: string; + logoutTime: string; + comment: string; + status: string; + requestedAt: string; + adjustedHours: string; + project: { + id: string; + name: string; + }; +} + +interface ApiResponse { + success: boolean; + data?: T; +} + const LeaveAndAdjustments: React.FC = () => { const { isTracking } = useOutletContext(); const navigate = useNavigate(); + const { projectSummary } = useSelector((state: any) => state.projects); +console.log("projectSummary46" , projectSummary) + + // Tabs state + const [activeTab, setActiveTab] = useState<"leave" | "adjustments">("leave"); + + // Data states const [leaves, setLeaves] = useState([]); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); + const [adjustments, setAdjustments] = useState([]); + + // Loading states + const [loadingLeaves, setLoadingLeaves] = useState(true); + const [loadingAdjustments, setLoadingAdjustments] = useState(true); + + // Refresh state const [lastRefreshed, setLastRefreshed] = useState(null); - const fetchLeaves = async () => { - try { - const res = await window.electron.getLeaveList(); - - if (res.success) { - setLeaves(res?.data?.leaves || []); - } - setLastRefreshed(new Date()); - } catch (e) { - console.error("Error fetching leaves:", e); - } finally { - setLoading(false); - setRefreshing(false); - } - }; - - const handleRefresh = async () => { - setRefreshing(true); - await fetchLeaves(); - }; - - useEffect(() => { - fetchLeaves(); - }, []); - const handleApplyLeaveAndAdjust = () => { navigate("/dashboard/leave-adjustments/apply"); }; - const renderSkeletonRows = () => { - return Array.from({ length: 5 }).map((_, index) => ( - -
-
-
-
-
-
- - )); + // Fetch leaves data + const fetchLeaves = async () => { + setLoadingLeaves(true); + try { + const res: ApiResponse<{ leaves: LeaveItem[] }> = await window.electron.getLeaveList(); + if (res.success) { + setLeaves(res?.data?.leaves || []); + } + } catch (e) { + console.error("Error fetching leaves:", e); + } finally { + setLoadingLeaves(false); + } }; - const formatLastRefreshed = () => { - if (!lastRefreshed) return "Never"; - - return DateTime.fromJSDate(lastRefreshed) - .toLocal() - .toFormat("HH:mm:ss"); // Only time - }; + // Fetch adjustments data + const fetchAdjustments = async () => { + setLoadingAdjustments(true); + try { + const res: ApiResponse = await window.electron.getAdjustmentsList(); + if (res.success) { + setAdjustments(res.data || []); + } + } catch (err) { + console.error("Error fetching adjustments:", err); + } finally { + setLoadingAdjustments(false); + } + }; + + // Combined refresh function + const handleRefresh = async () => { + if (activeTab === "leave") { + await fetchLeaves(); + } else { + await fetchAdjustments(); + } + setLastRefreshed(new Date()); + }; + + // Initial data fetch + useEffect(() => { + fetchLeaves(); + fetchAdjustments(); + setLastRefreshed(new Date()); + }, []); + return (
{/* Header */}
@@ -90,115 +125,48 @@ const LeaveAndAdjustments: React.FC = () => {
- {/* Table Container */} -
- - - - - - - - - - - + {/* Tabs */} +
+ -
- {loading ? ( - renderSkeletonRows() - ) : refreshing && leaves.length === 0 ? ( - renderSkeletonRows() - ) : leaves.length > 0 ? ( - leaves.map((leave, index) => ( - - - - - - - - - - - - - - )) - ) : ( - - - - )} - - {refreshing && leaves.length > 0 && ( - - - - )} - -
#Leave TypeDate From โ†’ ToStatusTotal DaysApplied On
{index + 1} - {leave.leaveType} - - {leave.dateFrom} -
- {leave.dateTo} -
- - {leave.status} - - {leave.totalDays}{new Date(leave.appliedOn).toLocaleString()}
- No leaves found -
-
- - Refreshing... -
-
+
- {/* Footer */} -
-
- {leaves.length} leave{leaves.length !== 1 ? "s" : ""} found -
- -
- {lastRefreshed && ( -
- Last updated: {formatLastRefreshed()} -
- )} - - -
+ {/* TABLE AREA - Scrollable */} +
+ {activeTab === "leave" && ( + + )} + {activeTab === "adjustments" && ( + + )}
); }; -export default LeaveAndAdjustments; +export default LeaveAndAdjustments; \ No newline at end of file diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveTable.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveTable.tsx new file mode 100644 index 0000000..8ffd576 --- /dev/null +++ b/src/ui/pages/LeaveAndAdjustments/LeaveTable.tsx @@ -0,0 +1,184 @@ +import React from "react"; +import { Loader2, RefreshCw } from "lucide-react"; +import { DateTime } from "luxon"; + +interface LeaveItem { + leaveId: string; + dateFrom: string; + dateTo: string; + totalDays: number; + leaveType: string; + comment: string; + status: string; + appliedOn: string; + reviewedAt: string | null; + rejectionReason: string | null; +} + +interface LeaveTableProps { + isTracking: boolean; + leaves: LeaveItem[]; + loading: boolean; + lastRefreshed: Date | null; + onRefresh: () => Promise; +} + +const LeaveTable: React.FC = ({ + isTracking, + leaves, + loading, + lastRefreshed, + onRefresh +}) => { + const [refreshing, setRefreshing] = React.useState(false); + + // ๐ŸŒ Convert UTC โ†’ Local System Time + const toLocal = (utcString: string, format: string) => { + return DateTime.fromISO(utcString, { zone: "utc" }) + .setZone(DateTime.local().zoneName) + .toFormat(format); + }; + + const formatDate = (d: string) => toLocal(d, "yyyy-MM-dd"); + const formatDateTime = (d: string) => toLocal(d, "yyyy-MM-dd HH:mm:ss"); + + const handleRefresh = async () => { + setRefreshing(true); + await onRefresh(); + setRefreshing(false); + }; + + const renderSkeletonRows = () => + Array.from({ length: 5 }).map((_, index) => ( + +
+
+
+
+
+
+ + )); + + const formatLastRefreshed = () => + lastRefreshed + ? DateTime.fromJSDate(lastRefreshed) + .setZone(DateTime.local().zoneName) + .toFormat("HH:mm:ss") + : "Never"; + + return ( + <> + {/* Table Container */} +
+ + + + + + + + + + + + + + {loading ? ( + renderSkeletonRows() + ) : refreshing && leaves.length === 0 ? ( + renderSkeletonRows() + ) : leaves.length > 0 ? ( + leaves.map((leave, index) => ( + + + + + + + + + + + + + + )) + ) : ( + + + + )} + + {/* {!refreshing && leaves.length > 0 && ( + + + + )} */} + +
#Leave TypeDate From โ†’ ToStatusTotal DaysApplied On
{index + 1} + + {leave.leaveType} + + + {formatDate(leave.dateFrom)} + โ†’ {" "} + {formatDate(leave.dateTo)} + + + {leave.status} + + {leave.totalDays}{formatDateTime(leave.appliedOn)}
+ No leaves found +
+
+ {renderSkeletonRows()} +
+
+
+ + {/* Footer */} +
+
+ {leaves.length} leave{leaves.length !== 1 ? "s" : ""} found +
+ +
+ {lastRefreshed && ( +
+ Last updated: {formatLastRefreshed()} +
+ )} + + +
+
+ + ); +}; + +export default LeaveTable; \ No newline at end of file diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx index fb85198..516127f 100644 --- a/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx +++ b/src/ui/pages/LeaveAndAdjustments/tabs/AdjustmentTab.tsx @@ -662,7 +662,7 @@ const AdjustmentTab: React.FC = ({ projects }) => { diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index 98e893f..a73d773 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -133,13 +133,13 @@ const Timesheets: React.FC = () => { setLogs([]); } - + } catch (error) { console.error("Error fetching logs:", error); setModalOpen(false) } finally { setLoading(false); - + } }; diff --git a/types.d.ts b/types.d.ts index 6eb395d..495aa44 100644 --- a/types.d.ts +++ b/types.d.ts @@ -83,7 +83,7 @@ interface ProfileData { paymentMethod: string; payoneerAccount: string; veemAccount: string; - wiseEmail:string + wiseEmail: string _id: string; } @@ -217,6 +217,11 @@ interface Window { success: boolean; data?: any; }>; + + getAdjustmentsList: () => Promise<{ + success: boolean; + data?: any; + }>; }; } From 9eeb14ffcd64a10371fd077e85d449912e31f903 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Thu, 4 Dec 2025 11:43:00 +0530 Subject: [PATCH 7/7] Change the Dashboard Layout when times is up show the alert window but time still run when user handleStop then only stop --- src/electron/ipc/ipcHandler.ts | 4 +- src/electron/preload.cts | 2 +- src/ui/layout/DashboardLayout.tsx | 41 +++++++++++++------ .../LeaveAndAdjustments.tsx | 2 +- src/ui/pages/Timesheets/Timesheets.tsx | 5 ++- types.d.ts | 2 +- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/electron/ipc/ipcHandler.ts b/src/electron/ipc/ipcHandler.ts index 8c50481..8d1b67f 100644 --- a/src/electron/ipc/ipcHandler.ts +++ b/src/electron/ipc/ipcHandler.ts @@ -462,10 +462,10 @@ ipcMain.handle("get-leave-list", async () => { } }); -ipcMain.handle("get-adjustments-list", async () => { +ipcMain.handle("get-adjustments-list", async (event, vaId: string) => { try { - const res = await api.get("/timesheet/adjustments/list"); + const res = await api.get(`/timesheet/adjustments/list?vaId=${vaId}`); console.log("/timesheet/adjustments/list", res.data); return { diff --git a/src/electron/preload.cts b/src/electron/preload.cts index 142079a..668496d 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -57,7 +57,7 @@ contextBridge.exposeInMainWorld('electron', { getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId), getLeaveList: () => ipcRenderer.invoke("get-leave-list"), - getAdjustmentsList:() => ipcRenderer.invoke("get-adjustments-list"), + getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId), } satisfies Window['electron']); diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 3f0f0f2..259550e 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -138,6 +138,7 @@ const DashboardLayout = () => { // Create a ref to store the stop function so it can be called from the hook const stopTrackingRef = useRef<(() => Promise) | null>(null); + const limitAlertShownRef = useRef(false); // Define handleStopTracking FIRST const handleStopTracking = useCallback(async () => { @@ -148,6 +149,9 @@ const DashboardLayout = () => { return; } + // Reset alert flag when stopping + limitAlertShownRef.current = false; + let currentStopTime = new Date().toISOString(); const projectId = selectedProject.projectId; @@ -282,25 +286,34 @@ const DashboardLayout = () => { // NOW define handleLimitExceeded using the defined handleStopTracking const handleLimitExceeded = useCallback(async () => { - console.log("๐Ÿšจ LIMIT EXCEEDED - Auto-stopping tracking"); + console.log("๐Ÿšจ LIMIT EXCEEDED - Showing alert (not stopping)"); if (!selectedProject) { - console.error(" No selected project for limit exceeded"); + console.error("โŒ No selected project for limit exceeded"); return; } - console.log("Alert Message 1") + // Only show alert once per tracking session + if (limitAlertShownRef.current) { + console.log("โš ๏ธ Alert already shown, skipping"); + return; + } + + limitAlertShownRef.current = true; + + console.log("โฐ Showing limit exceeded alert"); + // Show alert to user alert( `โฐ Daily Limit Reached!\n\n` + `Project: ${selectedProject.projectName}\n` + `Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` + - `Your tracking has been stopped automatically because you've reached the daily time limit for this project.` + `You've reached the daily time limit for this project.\n` + + `Timer will continue running. Please stop manually when ready.` ); - console.log("Alert Message 2") - // Stop tracking - // await handleStopTracking(); - }, [selectedProject, handleStopTracking]); + + console.log("โœ… Alert shown, timer continues running"); + }, [selectedProject]); // Initialize useActiveAppTracker and store finalizeTracking in ref const { finalizeTracking } = useActiveAppTracker( @@ -384,6 +397,8 @@ const DashboardLayout = () => { console.log(`โฐ Starting tracking. Remaining time: ${limitCheck.remaining}`); + limitAlertShownRef.current = false; + setLoadingStart(true); const now = Date.now(); @@ -468,15 +483,15 @@ const DashboardLayout = () => { // Check if time limit reached const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart; - if (totalTimeWorked >= dedicatedSeconds) { + if (totalTimeWorked >= dedicatedSeconds && !limitAlertShownRef.current) { console.warn("๐Ÿšจ CLIENT-SIDE: Time limit reached!"); console.log(`Total worked: ${Math.floor(totalTimeWorked / 60)}m vs Limit: ${Math.floor(dedicatedSeconds / 60)}m`); // Clear timer first to prevent multiple triggers - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } + // if (timerRef.current) { + // clearInterval(timerRef.current); + // timerRef.current = null; + // } // Trigger auto-stop handleLimitExceeded(); diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index c87ca3c..e6e728f 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -82,7 +82,7 @@ console.log("projectSummary46" , projectSummary) const fetchAdjustments = async () => { setLoadingAdjustments(true); try { - const res: ApiResponse = await window.electron.getAdjustmentsList(); + const res: ApiResponse = await window.electron.getAdjustmentsList(projectSummary?.vaId); if (res.success) { setAdjustments(res.data || []); } diff --git a/src/ui/pages/Timesheets/Timesheets.tsx b/src/ui/pages/Timesheets/Timesheets.tsx index a73d773..b6cc269 100644 --- a/src/ui/pages/Timesheets/Timesheets.tsx +++ b/src/ui/pages/Timesheets/Timesheets.tsx @@ -86,6 +86,7 @@ const Timesheets: React.FC = () => { const fetchProjects = async () => { + setInitialLoading(true); try { console.log("๐Ÿ“ก Fetching projects..."); const res = await window.electron.getProjectList(); @@ -241,7 +242,7 @@ const Timesheets: React.FC = () => { )) )} - {refreshing && projectsData.length > 0 && ( + {/* {refreshing && projectsData.length > 0 && ( // Show a single skeleton row at the top while refreshing with existing data @@ -251,7 +252,7 @@ const Timesheets: React.FC = () => { - )} + )} */} diff --git a/types.d.ts b/types.d.ts index 495aa44..51df381 100644 --- a/types.d.ts +++ b/types.d.ts @@ -218,7 +218,7 @@ interface Window { data?: any; }>; - getAdjustmentsList: () => Promise<{ + getAdjustmentsList: (vaId: string) => Promise<{ success: boolean; data?: any; }>;