diff --git a/src/electron/api/axiosClient.ts b/src/electron/api/axiosClient.ts index c4662a1..c42b094 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..8d1b67f 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,63 @@ 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 + }; + } +}); + +ipcMain.handle("get-adjustments-list", async (event, vaId: string) => { + + try { + const res = await api.get(`/timesheet/adjustments/list?vaId=${vaId}`); + + 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 + }; + } +}); diff --git a/src/electron/preload.cts b/src/electron/preload.cts index 34c9b41..668496d 100644 --- a/src/electron/preload.cts +++ b/src/electron/preload.cts @@ -51,11 +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), + getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId), + getLeaveList: () => ipcRenderer.invoke("get-leave-list"), + getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId), } 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 afd12cd..547fe9f 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( @@ -385,6 +398,8 @@ const DashboardLayout = () => { console.log(`โฐ Starting tracking. Remaining time: ${limitCheck.remaining}`); + limitAlertShownRef.current = false; + setLoadingStart(true); const now = Date.now(); @@ -469,15 +484,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(); @@ -538,7 +553,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, @@ -676,170 +691,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..5506739 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"; @@ -16,7 +16,7 @@ interface Project { totalWorkTime: string; totalWorkMs: number; totalLogs: number; - status: string + status: string; } interface ProjectTrackingData { @@ -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,69 @@ 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 () => { + setIsRefreshing(true) + 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); + } finally { + setIsRefreshing(false) + } + }; + + 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 +244,7 @@ const Dashboard: React.FC = () => { }; return ( -
+
{/* Header */}

Dashboard

@@ -243,7 +253,7 @@ const Dashboard: React.FC = () => {
- {isStopping ? ( + {(isStopping || isRefreshing) ? ( // Skeleton loader for tracking section
@@ -318,10 +328,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/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/ApplyLeaveAndAdjustmentsTime.tsx b/src/ui/pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime.tsx new file mode 100644 index 0000000..4b5b4a7 --- /dev/null +++ b/src/ui/pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime.tsx @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; +import PtoLeaveTab from './tabs/PtoLeaveTab'; +import SickLeaveTab from './tabs/SickLeaveTab'; +import AdjustmentTab from './tabs/AdjustmentTab'; +import { useNavigate } from "react-router-dom"; + +type TabType = 'pto' | 'sick' | 'adjustment'; + +const ApplyLeaveAndAdjustmentsTime = () => { + const [activeTab, setActiveTab] = useState('pto'); + const [isSubmitting, setIsSubmitting] = useState(false); // NEW + const projects = useSelector((state: RootState) => state.projects.projectsData); + + const navigate = useNavigate() + + // ------- COMMON API CALL FOR BOTH TABS ------- + const submitLeave = async (payload: { + dateFrom: string; + dateTo: string; + leaveType: "PTO" | "SICK"; + comment: string; + }) => { + try { + setIsSubmitting(true); + + console.log("Leave /sick Payload:", payload); + + const response = await window.electron.createPTOAndSickLeaveRequest(payload) + console.log("Leave Payload:", response) + if (response.success) { + return { success: true }; + } + + return { + success: false, + message: response.message + }; + } catch (err) { + console.error(err); + return { success: false, message: "Request failed" }; + } finally { + setIsSubmitting(false); + } + }; + + + const handleBack = () => { + navigate('/dashboard/leave-adjustments') + } + return ( + <> +
+
+
+

Apply Leave & Adjust Timesheet

+ +
+ {/* Tabs โ€” Disabled while submitting */} + + + {/* Main Container */} +
+
+ + {activeTab === "pto" && ( + + )} + + {activeTab === "sick" && ( + + )} + + {activeTab === "adjustment" && ( + + )} + +
+
+
+
+ + ) +} + +export default ApplyLeaveAndAdjustmentsTime \ No newline at end of file diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index 8c71d47..e6e728f 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -1,104 +1,172 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate, useOutletContext } from "react-router-dom"; +import { TrackingState } from "../../layout/DashboardLayout"; +import LeaveTable from "./LeaveTable"; +import AdjustmentsTable from "./AdjustmentsTable"; +import { useSelector } from "react-redux"; -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../store'; -import PtoLeaveTab from './tabs/PtoLeaveTab'; -import SickLeaveTab from './tabs/SickLeaveTab'; -import AdjustmentTab from './tabs/AdjustmentTab'; +interface LeaveItem { + leaveId: string; + dateFrom: string; + dateTo: string; + totalDays: number; + leaveType: string; + comment: string; + status: string; + appliedOn: string; + reviewedAt: string | null; + rejectionReason: string | null; +} -type TabType = 'pto' | 'sick' | 'adjustment'; +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 [activeTab, setActiveTab] = useState('pto'); - const [isSubmitting, setIsSubmitting] = useState(false); // ๐Ÿš€ NEW - const projects = useSelector((state: RootState) => state.projects.projectsData); + const { isTracking } = useOutletContext(); + const navigate = useNavigate(); - // ------- COMMON API CALL FOR BOTH TABS ------- - const submitLeave = async (payload: { - dateFrom: string; - dateTo: string; - leaveType: "PTO" | "SICK"; - comment: string; - }) => { + 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 [adjustments, setAdjustments] = useState([]); + + // Loading states + const [loadingLeaves, setLoadingLeaves] = useState(true); + const [loadingAdjustments, setLoadingAdjustments] = useState(true); + + // Refresh state + const [lastRefreshed, setLastRefreshed] = useState(null); + + const handleApplyLeaveAndAdjust = () => { + navigate("/dashboard/leave-adjustments/apply"); + }; + + // Fetch leaves data + const fetchLeaves = async () => { + setLoadingLeaves(true); try { - setIsSubmitting(true); - - console.log("Leave /sick Payload:", payload); - - const response = await window.electron.createPTOAndSickLeaveRequest(payload) - console.log("Leave Payload:", response) - if (response.success) { - return { success: true }; + const res: ApiResponse<{ leaves: LeaveItem[] }> = await window.electron.getLeaveList(); + if (res.success) { + setLeaves(res?.data?.leaves || []); } - - return { - success: false, - message: response.message - }; - } catch (err) { - console.error(err); - return { success: false, message: "Request failed" }; + } catch (e) { + console.error("Error fetching leaves:", e); } finally { - setIsSubmitting(false); + setLoadingLeaves(false); } }; + // Fetch adjustments data + const fetchAdjustments = async () => { + setLoadingAdjustments(true); + try { + const res: ApiResponse = await window.electron.getAdjustmentsList(projectSummary?.vaId); + 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 ( -
-
-

Leave & Adjust Timesheet

- - {/* Tabs โ€” Disabled while submitting */} - - - {/* Main Container */} -
-
- - {activeTab === "pto" && ( - - )} - - {activeTab === "sick" && ( - - )} - - {activeTab === "adjustment" && ( - - )} - -
-
+
+ {/* Header */} +
+

Leave & Adjust Timesheet

+
-
+ + {/* Tabs */} +
+ + + +
+ + {/* 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 f923f9a..516127f 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, @@ -653,13 +662,8 @@ const AdjustmentTab: React.FC = ({ projects }) => { diff --git a/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx b/src/ui/pages/LeaveAndAdjustments/tabs/PtoLeaveTab.tsx index 3b9a30a..1bd14a1 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 Request 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 c64a501..8ac6d6f 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 leave applied!"); + 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..759ebd8 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,60 @@ 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 () => { + setRefreshing(true); + setLoading(true); + 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 () => { + + 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 +79,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 +155,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/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 7a0d6b1..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(); @@ -82,32 +84,33 @@ const Settings: React.FC = () => {

-
-

- ๐Ÿ’ฌ Skype -

-

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

-
{/* 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)}

)} @@ -137,8 +140,9 @@ const Settings: React.FC = () => {
- {/* Change Password Modal */} + {/* Modal */} setModalOpen(false)} /> + setShowPaymentModal(false)} /> ); }; 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 51bc4b0..b6cc269 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,48 +42,83 @@ 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 () => { + setInitialLoading(true); + 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 { + setModalOpen(true); setLoading(true); setSelectedProject(projectID); setSelectedProjectName(projectName); @@ -85,11 +134,13 @@ const Timesheets: React.FC = () => { setLogs([]); } - setModalOpen(true); + } catch (error) { console.error("Error fetching logs:", error); + setModalOpen(false) } finally { setLoading(false); + } }; @@ -105,54 +156,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 +207,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/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 bb7af91..51df381 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; } @@ -211,6 +212,16 @@ interface Window { data?: PayslipResponse[]; message?: string; }>; + + getLeaveList: () => Promise<{ + success: boolean; + data?: any; + }>; + + getAdjustmentsList: (vaId: string) => Promise<{ + success: boolean; + data?: any; + }>; }; }