This commit is contained in:
Bhagwan Singh 2025-12-04 12:55:49 +05:30
commit ec566bb14a
23 changed files with 1848 additions and 2282 deletions

1763
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@ api.interceptors.response.use(
(response: AxiosResponse) => response.data, (response: AxiosResponse) => response.data,
async (error) => { async (error) => {
// console.error('[Axios Error]', error.message); // 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'); console.warn('Token expired — trigger refresh logic if needed');
sendTokenExpired(); sendTokenExpired();
} }

View File

@ -29,7 +29,7 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
return { success: true }; 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); return handleLogin(credentials);
}); });
@ -46,8 +46,8 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
return { success: false, message: "Logout failed" }; return { success: false, message: "Logout failed" };
} }
}); });
@ -96,7 +96,7 @@ ipcMain.handle("get-project-list", async () => {
return { return {
success: false, success: false,
message: error?.message || String(error) || "Unknown error", 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); const response = await api.post("/log/project", payload);
console.log(" Tracking start synced:", response); console.log(" Tracking start synced:", response);
return { success: true, data: response }; return { success: true, data: response };
} catch (error:any) { } catch (error: any) {
console.error("❌ Error syncing project start:", error); console.error("❌ Error syncing project start:", error);
// If API sent a response (like 400) // If API sent a response (like 400)
@ -183,19 +183,6 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
}); });
// ipcMain.handle("get-project-logs", async (event, projectID) => {
// try {
// console.log("get-project-logs",projectID)
// const response = await api.get(`/report/project/${projectID}`);
// console.log("response-logs 164" , response)
// return response;
// } catch (error) {
// console.error("Error fetching project logs:", error);
// throw error;
// }
// });
ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => { ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => {
try { try {
@ -274,14 +261,14 @@ ipcMain.handle("change-password", async (event, data) => {
// Build final payload // Build final payload
const payload = { const payload = {
userDetails: { userDetails: {
oldPassword: data.oldPassword, oldPassword: data.oldPassword,
password: data.password, password: data.password,
cPassword: data.cPassword, cPassword: data.cPassword,
}, },
}; };
console.log("Change Password payload" , payload) console.log("Change Password payload", payload)
try { try {
const response = await api.patch("/profile", payload); const response = await api.patch("/profile", payload);
console.log("Chnage-passowrd-response", response) console.log("Chnage-passowrd-response", response)
@ -316,20 +303,20 @@ ipcMain.handle("get-project-summary", async (_, { projectId, date }) => {
ipcMain.handle("create-timesheet", async (event, data) => { ipcMain.handle("create-timesheet", async (event, data) => {
console.log("data" , data) console.log("data", data)
try { try {
console.log("📩 Timesheet Payload:", data); console.log("📩 Timesheet Payload:", data);
const res = await api.post("/timesheet/adjustment/request", data); const res = await api.post("/timesheet/adjustment/request", data);
console.log(res , " Line 323") console.log(res, " Line 323")
return { return {
success: true, success: true,
message: "Timesheet created successfully", message: "Timesheet created successfully",
data: res, data: res,
}; };
} catch (error:any) { } catch (error: any) {
console.error("❌ Timesheet Error:", error); console.error("❌ Timesheet Error:", error);
return { return {
@ -378,6 +365,132 @@ ipcMain.handle("create-leave-request", async (event, payload) => {
ipcMain.handle("get-schedules", async () => {
try {
console.log(" Fetching schedules...");
const response = await api.get("/payrollSchedule"); // Backend API call
console.log("✅ Schedule Data:", response.data);
return {
success: true,
result: response.data
};
} catch (error: any) {
console.error("❌ Fetch Schedules Failed:");
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-payslips", async (event, vaId: string) => {
console.log("get-payslips", vaId)
try {
const response = await api.get(`/paySlip/${vaId}`);
return {
success: true,
data: response.data?.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-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
};
}
});
export const sendTokenExpired = () => { export const sendTokenExpired = () => {

View File

@ -51,8 +51,13 @@ contextBridge.exposeInMainWorld('electron', {
createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data), createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data),
createPTOAndSickLeaveRequest: (data) => createPTOAndSickLeaveRequest: (data) =>
ipcRenderer.invoke("create-leave-request", data), ipcRenderer.invoke("create-leave-request", data),
getSchedules: () => ipcRenderer.invoke("get-schedules"),
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']); } satisfies Window['electron']);

View File

@ -95,6 +95,8 @@ import Settings from "./pages/Settings/Settings";
import HelpSupport from "./pages/HelpSupport/HelpSupport"; import HelpSupport from "./pages/HelpSupport/HelpSupport";
import Vpn from "./pages/Vpn/Vpn"; import Vpn from "./pages/Vpn/Vpn";
import Schedule from "./pages/Schedule/Schedule"; import Schedule from "./pages/Schedule/Schedule";
import PaysSlips from "./pages/PaySlips/PaysSlips";
import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime";
const App: React.FC = () => { const App: React.FC = () => {
@ -137,11 +139,13 @@ const App: React.FC = () => {
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="projects" element={<Projects />} /> <Route path="projects" element={<Projects />} />
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} /> <Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="leave-adjustments/apply" element={<ApplyLeaveAndAdjustmentsTime/>} />
<Route path="timesheet" element={<Timesheets />} /> <Route path="timesheet" element={<Timesheets />} />
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
<Route path="vpn" element={<Vpn />} /> <Route path="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} /> <Route path="help-support" element={<HelpSupport />} />
<Route path="schedule" element={<Schedule/>} /> <Route path="schedule" element={<Schedule/>} />
<Route path="pays-slips" element={<PaysSlips/>} />
</Route> </Route>
{/* Default Redirect */} {/* Default Redirect */}

View File

@ -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] { input[type=number] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }

View File

@ -27,6 +27,7 @@ import {
ChevronUp, ChevronUp,
GlobeLock, GlobeLock,
ClipboardClock, ClipboardClock,
CircleDollarSign,
} from "lucide-react"; } from "lucide-react";
import { useActiveAppTracker } from "../hooks/useActiveAppTracker"; import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@ -112,7 +113,8 @@ const DashboardLayout = () => {
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true }, { label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> }, { label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> }, { label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
{ label: "Schedule", path: "/dashboard/schedule", icon: <ClipboardClock size={18} />}, { label: "Schedule", path: "/dashboard/schedule", icon: <ClipboardClock size={18} /> },
{ label: "Pays Slips", path: "/dashboard/pays-slips", icon: <CircleDollarSign size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> }, { label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> }, { label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> }, { label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
@ -136,6 +138,7 @@ const DashboardLayout = () => {
// Create a ref to store the stop function so it can be called from the hook // Create a ref to store the stop function so it can be called from the hook
const stopTrackingRef = useRef<(() => Promise<void>) | null>(null); const stopTrackingRef = useRef<(() => Promise<void>) | null>(null);
const limitAlertShownRef = useRef(false);
// Define handleStopTracking FIRST // Define handleStopTracking FIRST
const handleStopTracking = useCallback(async () => { const handleStopTracking = useCallback(async () => {
@ -146,6 +149,9 @@ const DashboardLayout = () => {
return; return;
} }
// Reset alert flag when stopping
limitAlertShownRef.current = false;
let currentStopTime = new Date().toISOString(); let currentStopTime = new Date().toISOString();
const projectId = selectedProject.projectId; const projectId = selectedProject.projectId;
@ -280,25 +286,34 @@ const DashboardLayout = () => {
// NOW define handleLimitExceeded using the defined handleStopTracking // NOW define handleLimitExceeded using the defined handleStopTracking
const handleLimitExceeded = useCallback(async () => { const handleLimitExceeded = useCallback(async () => {
console.log("🚨 LIMIT EXCEEDED - Auto-stopping tracking"); console.log("🚨 LIMIT EXCEEDED - Showing alert (not stopping)");
if (!selectedProject) { if (!selectedProject) {
console.error(" No selected project for limit exceeded"); console.error("❌ No selected project for limit exceeded");
return; 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 // Show alert to user
alert( alert(
`⏰ Daily Limit Reached!\n\n` + `⏰ Daily Limit Reached!\n\n` +
`Project: ${selectedProject.projectName}\n` + `Project: ${selectedProject.projectName}\n` +
`Dedicated Hours: ${selectedProject.dedicatedHours}\n\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 console.log("✅ Alert shown, timer continues running");
await handleStopTracking(); }, [selectedProject]);
}, [selectedProject, handleStopTracking]);
// Initialize useActiveAppTracker and store finalizeTracking in ref // Initialize useActiveAppTracker and store finalizeTracking in ref
const { finalizeTracking } = useActiveAppTracker( const { finalizeTracking } = useActiveAppTracker(
@ -316,6 +331,7 @@ const DashboardLayout = () => {
const socketRef = useRef<Socket | null>(null); const socketRef = useRef<Socket | null>(null);
const intervalRef = useRef<number | null>(null); const intervalRef = useRef<number | null>(null);
const lastAppsRef = useRef<Object | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
useEffect(() => { useEffect(() => {
@ -382,6 +398,8 @@ const DashboardLayout = () => {
console.log(`⏰ Starting tracking. Remaining time: ${limitCheck.remaining}`); console.log(`⏰ Starting tracking. Remaining time: ${limitCheck.remaining}`);
limitAlertShownRef.current = false;
setLoadingStart(true); setLoadingStart(true);
const now = Date.now(); const now = Date.now();
@ -466,15 +484,15 @@ const DashboardLayout = () => {
// Check if time limit reached // Check if time limit reached
const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart; const totalTimeWorked = alreadyWorkedSeconds + timeSinceStart;
if (totalTimeWorked >= dedicatedSeconds) { if (totalTimeWorked >= dedicatedSeconds && !limitAlertShownRef.current) {
console.warn("🚨 CLIENT-SIDE: Time limit reached!"); console.warn("🚨 CLIENT-SIDE: Time limit reached!");
console.log(`Total worked: ${Math.floor(totalTimeWorked / 60)}m vs Limit: ${Math.floor(dedicatedSeconds / 60)}m`); console.log(`Total worked: ${Math.floor(totalTimeWorked / 60)}m vs Limit: ${Math.floor(dedicatedSeconds / 60)}m`);
// Clear timer first to prevent multiple triggers // Clear timer first to prevent multiple triggers
if (timerRef.current) { // if (timerRef.current) {
clearInterval(timerRef.current); // clearInterval(timerRef.current);
timerRef.current = null; // timerRef.current = null;
} // }
// Trigger auto-stop // Trigger auto-stop
handleLimitExceeded(); handleLimitExceeded();
@ -523,6 +541,7 @@ const DashboardLayout = () => {
} }
}; };
const handleSocketConnect = async () => { const handleSocketConnect = async () => {
const token = await window.electron.getToken("ACCESS_TOKEN"); const token = await window.electron.getToken("ACCESS_TOKEN");
@ -556,12 +575,56 @@ const DashboardLayout = () => {
setIsConnected(true); setIsConnected(true);
// Start sending every 5 seconds // Start sending every 5 seconds
// intervalRef.current = setInterval(async () => {
// const apps = await window.electron.getRunningApps()
// socketRef.current?.emit("getVisibleApps", { token: await token, apps });
// }, 5000);
});
// socketRef.current.on("startSendAppsDetails", async ({ apps }) => {
// socketRef.current?.emit("getVisibleApps", { token: await token, apps });
// });
socketRef.current.on("startSendAppsDetails", async ({projId}) => {
if (projId !== selectedProject?.projectId) return;
console.log("▶️ startSendAppsDetails received");
// avoid multiple intervals
if (intervalRef.current) return;
intervalRef.current = setInterval(async () => { intervalRef.current = setInterval(async () => {
const apps = await window.electron.getRunningApps() const currentApps = await window.electron.getRunningApps();
socketRef.current?.emit("getVisibleApps", { token: await token, apps });
// Compare with last sent data
const hasChanged = JSON.stringify(currentApps) !== JSON.stringify(lastAppsRef.current);
if (hasChanged) {
lastAppsRef.current = currentApps;
socketRef.current?.emit("getVisibleApps", {
token: await token,
apps: currentApps,
});
}
}, 5000); }, 5000);
}); });
socketRef.current.on("stopSendAppsDetails", async ({projId}) => {
if (projId !== selectedProject?.projectId) return;
console.log("⏹ stopSendAppsDetails received");
// clear the interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Send final stop event to server
// socketRef.current?.emit("stopSendAppsDetails", {
// token: await token,
// });
});
socketRef.current.on("appToCapture", async ({ appTitle }) => { socketRef.current.on("appToCapture", async ({ appTitle }) => {
console.log("📸 Capture requested for:", appTitle); console.log("📸 Capture requested for:", appTitle);
@ -628,170 +691,171 @@ const DashboardLayout = () => {
return ( return (
<> <>
{/* Header */} <div className="flex flex-col h-screen overflow-hidden">
{isTracking && {/* Header */}
<header className="w-full flex justify-between items-center h-12 px-6 bg-base-100 border-b border-base-300 sticky top-0 z-30"> {isTracking &&
<div className="flex items-center space-x-3"> <header className="w-full flex justify-between items-center px-4 py-2 bg-base-100 border-b border-base-300 sticky top-0 z-30">
{isStopping ? ( <div className="flex items-center space-x-3">
// Skeleton for left side during stop {isStopping ? (
<div className="flex items-center space-x-3"> // Skeleton for left side during stop
<div className="skeleton w-8 h-8 rounded-full"></div> <div className="flex items-center space-x-3">
<div className="skeleton w-32 h-4"></div> <div className="skeleton w-8 h-8 rounded-full"></div>
</div> <div className="skeleton w-32 h-4"></div>
) : selectedProject ? ( </div>
isTracking ? ( ) : selectedProject ? (
<button isTracking ? (
className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer" <button
onClick={handleStopTracking} className="bg-error text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
disabled={loadingStop} onClick={handleStopTracking}
title="Stop Tracking" disabled={loadingStop}
> title="Stop Tracking"
{loadingStop ? ( >
<span className="loading loading-spinner loading-xs text-white"></span> {loadingStop ? (
) : ( <span className="loading loading-spinner loading-xs text-white"></span>
<Pause size={16} /> ) : (
)} <Pause size={16} />
</button> )}
</button>
) : (
<button
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer"
onClick={() => handleStartTracking(selectedProject)}
title="Start Tracking"
>
<Play size={16} />
</button>
)
) : ( ) : (
<button <button
className="bg-success text-white p-1.5 rounded-full flex items-center justify-center cursor-pointer" className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed"
onClick={() => handleStartTracking(selectedProject)} disabled
title="Start Tracking" title="Select a project to start"
> >
<Play size={16} /> <Play size={16} />
</button> </button>
) )}
) : (
{!isStopping && selectedProject ? (
<span className="ml-2 text-sm text-base-content/70">
{selectedProject.projectName}
</span>
) : !isStopping ? (
<span className="ml-2 text-sm text-base-content/40 italic">
No Project Selected
</span>
) : null}
</div>
<div className="flex items-center space-x-3">
{isStopping ? (
// Skeleton for right side during stop
<div className="flex items-center space-x-3">
<div className="skeleton w-20 h-6 rounded"></div>
<div className="skeleton w-6 h-6 rounded"></div>
<div className="skeleton w-8 h-8 rounded"></div>
</div>
) : (
<>
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
{formatTime(
isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s")
)}
</div>
{/* <span className="text-error">
<ShieldBan size={18} />
</span> */}
<button
onClick={() => {
if (toggleWindowSize.current) {
window.electron.resizeWindow(500, 85);
toggleWindowSize.current = false;
} else {
window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
}
}}
className="btn btn-xs btn-outline btn-primary"
>
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
</button>
</>
)}
</div>
</header>
}
{/* Layout Body */}
<div className={`flex flex-1 bg-base-200 overflow-hidden`}>
{/* Sidebar */}
<aside className="flex-none w-16 border-r border-base-300 bg-base-100 flex flex-col items-center py-4 gap-2">
{menuItems.map((item) => (
<div className="tooltip z-100 tooltip-right " key={item.label} data-tip={item.label}>
<NavLink
to={item.path}
end={item.exact}
className={({ isActive }) =>
`flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-150 cursor-pointer ${isActive
? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
}`
}
>
{item.icon}
</NavLink>
</div>
))}
<div className={`absolute ${!isTracking ? "bottom-14" : "bottom-5"} `}>
<label className="swap swap-rotate p-2.5">
{/* this hidden checkbox controls the state */}
<input type="checkbox" className="theme-controller" onChange={toggleTheme}
checked={theme === "dark"} />
{/* sun icon */}
<svg
className="swap-off h-5 w-5 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
{/* moon icon */}
<svg
className="swap-on h-5 w-5 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>
{!isTracking && (
<button <button
className="bg-gray-400 text-white p-1.5 rounded-full flex items-center justify-center cursor-not-allowed" onClick={() => setShowLogoutModal(true)}
disabled className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right"
title="Select a project to start" data-tip={"Logout"}
> >
<Play size={16} /> <LogOut size={18} />
</button> </button>
)} )}
</aside>
{!isStopping && selectedProject ? ( {/* Main Content */}
<span className="ml-2 text-sm text-base-content/70"> <main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
{selectedProject.projectName} <Outlet context={trackingState} />
</span> </main>
) : !isStopping ? (
<span className="ml-2 text-sm text-base-content/40 italic">
No Project Selected
</span>
) : null}
</div>
<div className="flex items-center space-x-3"> <LogoutModal
{isStopping ? ( open={showLogoutModal}
// Skeleton for right side during stop onClose={() => setShowLogoutModal(false)}
<div className="flex items-center space-x-3"> onConfirm={handleLogout}
<div className="skeleton w-20 h-6 rounded"></div> />
<div className="skeleton w-6 h-6 rounded"></div>
<div className="skeleton w-8 h-8 rounded"></div>
</div>
) : (
<>
<div className="badge badge-success text-white font-semibold px-3 py-2 shadow-sm">
{formatTime(
isTracking ? elapsedTime : parseTimeStringToSeconds(selectedProject?.totalWorkTime || "0h 0m 0s")
)}
</div>
<span className="text-error">
<ShieldBan size={18} />
</span>
<button
onClick={() => {
if (toggleWindowSize.current) {
window.electron.resizeWindow(500, 85);
toggleWindowSize.current = false;
} else {
window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true;
}
}}
className="btn btn-xs btn-outline btn-primary"
>
{toggleWindowSize.current ? <ChevronDown /> : <ChevronUp />}
</button>
</>
)}
</div>
</header>
}
{/* Layout Body */}
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : "h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
{/* Sidebar */}
<aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4 relative">
{menuItems.map((item) => (
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}>
<NavLink
to={item.path}
end={item.exact}
className={({ isActive }) =>
`flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 cursor-pointer ${isActive
? "bg-primary text-primary-content"
: "hover:bg-base-300 text-base-content"
}`
}
>
{item.icon}
</NavLink>
</div>
))}
<div className="absolute bottom-13 ">
<label className="swap swap-rotate p-2.5">
{/* this hidden checkbox controls the state */}
<input type="checkbox" className="theme-controller" onChange={toggleTheme}
checked={theme === "dark"} />
{/* sun icon */}
<svg
className="swap-off h-5 w-5 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
{/* moon icon */}
<svg
className="swap-on h-5 w-5 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>
{!isTracking && (
<button
onClick={() => setShowLogoutModal(true)}
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right"
data-tip={"Logout"}
>
<LogOut size={18} />
</button>
)}
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
<Outlet context={trackingState} />
</main>
<LogoutModal
open={showLogoutModal}
onClose={() => setShowLogoutModal(false)}
onConfirm={handleLogout}
/>
</div>
</div> </div>
</> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from "react"; 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 { useOutletContext } from "react-router-dom";
import NoData from "./NoData"; import NoData from "./NoData";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
@ -16,7 +16,7 @@ interface Project {
totalWorkTime: string; totalWorkTime: string;
totalWorkMs: number; totalWorkMs: number;
totalLogs: number; totalLogs: number;
status: string status: string;
} }
interface ProjectTrackingData { interface ProjectTrackingData {
@ -105,6 +105,7 @@ const Dashboard: React.FC = () => {
const selectedProject = useSelector((state: any) => state.projects.selectedProject); const selectedProject = useSelector((state: any) => state.projects.selectedProject);
const projectSummary = useSelector((state: any) => state.projects.projectSummary); const projectSummary = useSelector((state: any) => state.projects.projectSummary);
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap); const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
const [isRefreshing, setIsRefreshing] = useState(false);
console.log("Line 111", projectsData, selectedProject, projectTrackingMap) console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
@ -127,60 +128,69 @@ const Dashboard: React.FC = () => {
} }
}; };
useEffect(() => { const fetchProjects = async () => {
const fetchProjects = async () => { setIsRefreshing(true)
try { try {
console.log("📡 Fetching projects...Dashboard"); console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList(); const res = await window.electron.getProjectList();
console.log(" Project list response in dashboard:", res); console.log(" Project list response in dashboard:", res);
if (res?.success && Array.isArray(res.data)) { if (res?.success && Array.isArray(res.data)) {
// Store data redux store // Store data redux store
dispatch(setProjectsDataSlice(res.data)); 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( dispatch(
setProjectSummarySlice({ initializeProjectTracking({
totalProjects: res.totalProjects || 0, projectId: firstProject.projectId,
activeProjects: res.activeProjects || 0, initialTime: initialElapsed,
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(
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(); fetchProjects();
}, []); }, []);
const handleRefresh = async () => {
// setIsRefreshing(true);
await fetchProjects();
// setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback
};
const selectedData = selectedProject const selectedData = selectedProject
? projectTrackingMap[selectedProject.projectId] ? projectTrackingMap[selectedProject.projectId]
: null; : null;
@ -234,7 +244,7 @@ const Dashboard: React.FC = () => {
}; };
return ( return (
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen"> <main className="flex-1 overflow-hidden no-scrollbar max-h-[85vh]">
{/* Header */} {/* Header */}
<div className="mb-2"> <div className="mb-2">
<h1 className="text-md font-bold">Dashboard</h1> <h1 className="text-md font-bold">Dashboard</h1>
@ -243,7 +253,7 @@ const Dashboard: React.FC = () => {
<div className="w-full mb-4"> <div className="w-full mb-4">
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm"> <div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body"> <div className="card-body">
{isStopping ? ( {(isStopping || isRefreshing) ? (
// Skeleton loader for tracking section // Skeleton loader for tracking section
<div className="flex flex-col sm:flex-row items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-center sm:text-left w-full"> <div className="text-center sm:text-left w-full">
@ -318,10 +328,19 @@ const Dashboard: React.FC = () => {
{/* 🔹 Sticky Header */} {/* 🔹 Sticky Header */}
<div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center"> <div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
<p className="text-md font-bold">Projects</p> <p className="text-md font-bold">Projects</p>
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="btn btn-xs btn-ghost gap-1 text-warning"
title="Refresh projects"
>
<RefreshCw className={`w-3 h-3 text-warning ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh
</button>
</div> </div>
{/* 🔹 Table or Empty State or Skeleton */} {/* 🔹 Table or Empty State or Skeleton */}
{isStopping ? ( {(isStopping || isRefreshing) ? (
// Skeleton loader for projects table // Skeleton loader for projects table
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{[...Array(3)].map((_, index) => ( {[...Array(3)].map((_, index) => (

View File

@ -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<void>;
}
const AdjustmentsTable: React.FC<AdjustmentTableProps> = ({
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) => (
<tr key={i}>
<td><div className="skeleton h-4 w-6"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
<td><div className="skeleton h-4 w-24"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
<td><div className="skeleton h-4 w-10"></div></td>
<td><div className="skeleton h-4 w-24"></div></td>
<td><div className="skeleton h-4 w-14"></div></td>
</tr>
));
return (
<>
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
style={{
height: isTracking ? "calc(100vh - 200px)" : "calc(100vh - 160px)",
}}
>
<table className="table table-zebra w-full text-[11px]">
<thead className="sticky top-0 bg-base-200 text-[12px] z-10">
<tr className="text-center">
<th>#</th>
<th>Project</th>
<th>Date</th>
<th>In → Out</th>
<th>Adjusted Hours</th>
<th>Status</th>
<th>Requested At</th>
</tr>
</thead>
<tbody className="text-center">
{loading
? renderSkeleton()
: adjustments.length === 0
? (
<tr>
<td colSpan={7} className="text-center py-4 text-base-content/50">
No adjustments found
</td>
</tr>
)
: adjustments.map((item, idx) => (
<tr key={item.id}>
<td>{idx + 1}</td>
<td>
<span className="badge badge-xs badge-info whitespace-nowrap">
{item.project?.name || "N/A"}
</span>
</td>
<td className="whitespace-nowrap">{formatDate(item.date)}</td>
<td className="whitespace-nowrap flex ">
<span className="me-1">{formatTime(item.loginTime)} </span>
→ {" "}
<span className="ms-1">{formatTime(item.logoutTime)}</span>
</td>
<td className="whitespace-nowrap">
<span className="badge badge-xs badge-primary">
{item.adjustedHours}
</span>
</td>
<td>
<span
className={`badge badge-xs ${
item.status === "PENDING"
? "badge-warning"
: item.status === "APPROVED"
? "badge-success"
: "badge-error"
}`}
>
{item.status}
</span>
</td>
<td className="whitespace-nowrap">{toLocal(item.requestedAt, "yyyy-MM-dd HH:mm:ss")}</td>
</tr>
))}
{/* {refreshing && adjustments.length > 0 && (
<tr>
<td colSpan={7} className="bg-base-200/50">
<div className="flex justify-center py-2">
{renderSkeleton()}
</div>
</td>
</tr>
)} */}
</tbody>
</table>
</div>
{/* FOOTER */}
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{adjustments.length} adjustment{adjustments.length !== 1 ? "s" : ""} found
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-base-content/60">
Last updated: {toLocal(lastRefreshed.toISOString(), "HH:mm:ss")}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
>
{refreshing ? (
<Loader2 size={14} className="animate-spin" />
) : (
<RefreshCw size={14} />
)}
</button>
</div>
</div>
</>
);
};
export default AdjustmentsTable;

View File

@ -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<TabType>('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 (
<>
<div className="max-h-screen">
<div className="w-full mx-auto">
<div className='mb-3 flex justify-between items-center'>
<h1 className="text-mb font-bold ">Apply Leave & Adjust Timesheet</h1>
<button className="btn btn-xs btn-error text-base-content " onClick={handleBack}>Back</button>
</div>
{/* Tabs — Disabled while submitting */}
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100">
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("pto")}
>
PTO Leave
</a>
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("sick")}
>
Sick Leave
</a>
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")}
>
Adjustment Submission
</a>
</div>
{/* Main Container */}
<div className="rounded-lg bg-base-100 overflow-hidden">
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
{activeTab === "pto" && (
<PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "sick" && (
<SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "adjustment" && (
<AdjustmentTab projects={projects} />
)}
</div>
</div>
</div>
</div>
</>
)
}
export default ApplyLeaveAndAdjustmentsTime

View File

@ -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'; interface LeaveItem {
import { useSelector } from 'react-redux'; leaveId: string;
import { RootState } from '../../store'; dateFrom: string;
import PtoLeaveTab from './tabs/PtoLeaveTab'; dateTo: string;
import SickLeaveTab from './tabs/SickLeaveTab'; totalDays: number;
import AdjustmentTab from './tabs/AdjustmentTab'; 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<T> {
success: boolean;
data?: T;
}
const LeaveAndAdjustments: React.FC = () => { const LeaveAndAdjustments: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('pto'); const { isTracking } = useOutletContext<TrackingState>();
const [isSubmitting, setIsSubmitting] = useState(false); // 🚀 NEW const navigate = useNavigate();
const projects = useSelector((state: RootState) => state.projects.projectsData);
// ------- COMMON API CALL FOR BOTH TABS ------- const { projectSummary } = useSelector((state: any) => state.projects);
const submitLeave = async (payload: { console.log("projectSummary46" , projectSummary)
dateFrom: string;
dateTo: string; // Tabs state
leaveType: "PTO" | "SICK"; const [activeTab, setActiveTab] = useState<"leave" | "adjustments">("leave");
comment: string;
}) => { // Data states
const [leaves, setLeaves] = useState<LeaveItem[]>([]);
const [adjustments, setAdjustments] = useState<AdjustmentItem[]>([]);
// Loading states
const [loadingLeaves, setLoadingLeaves] = useState(true);
const [loadingAdjustments, setLoadingAdjustments] = useState(true);
// Refresh state
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const handleApplyLeaveAndAdjust = () => {
navigate("/dashboard/leave-adjustments/apply");
};
// Fetch leaves data
const fetchLeaves = async () => {
setLoadingLeaves(true);
try { try {
setIsSubmitting(true); const res: ApiResponse<{ leaves: LeaveItem[] }> = await window.electron.getLeaveList();
if (res.success) {
console.log("Leave /sick Payload:", payload); setLeaves(res?.data?.leaves || []);
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
console.log("Leave Payload:", response)
if (response.success) {
return { success: true };
} }
} catch (e) {
return { console.error("Error fetching leaves:", e);
success: false,
message: response.message
};
} catch (err) {
console.error(err);
return { success: false, message: "Request failed" };
} finally { } finally {
setIsSubmitting(false); setLoadingLeaves(false);
} }
}; };
// Fetch adjustments data
const fetchAdjustments = async () => {
setLoadingAdjustments(true);
try {
const res: ApiResponse<AdjustmentItem[]> = 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 ( return (
<div className="max-h-screen"> <main
<div className="w-full mx-auto"> className="flex flex-col gap-2 overflow-hidden"
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1> // style={{
// height: isTracking ? "calc(100vh - 40px)" : "",
{/* Tabs — Disabled while submitting */} // }}
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100"> >
<a {/* Header */}
role="tab" <div className="flex-none mb-2 flex justify-between items-center">
className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : "" <h2 className="text-md font-bold">Leave & Adjust Timesheet</h2>
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} <button className="btn btn-xs btn-primary" onClick={handleApplyLeaveAndAdjust}>
onClick={() => setActiveTab("pto")} Apply
> </button>
PTO Leave
</a>
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("sick")}
>
Sick Leave
</a>
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")}
>
Adjustment Submission
</a>
</div>
{/* Main Container */}
<div className="rounded-lg bg-base-100 overflow-hidden">
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
{activeTab === "pto" && (
<PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "sick" && (
<SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "adjustment" && (
<AdjustmentTab projects={projects} />
)}
</div>
</div>
</div> </div>
</div>
{/* Tabs */}
<div role="tablist" className="tabs tabs-bordered w-full">
<button
role="tab"
className={`tab ${activeTab === "leave" ? "tab-active border-b-2 border-primary" : ""}`}
onClick={() => setActiveTab("leave")}
>
Leave
</button>
<button
role="tab"
className={`tab ${activeTab === "adjustments" ? "tab-active border-primary border-b-2" : ""}`}
onClick={() => setActiveTab("adjustments")}
>
Adjustments
</button>
</div>
{/* TABLE AREA - Scrollable */}
<div className="flex-1 overflow-y-auto no-scrollbar">
{activeTab === "leave" && (
<LeaveTable
isTracking={isTracking}
leaves={leaves}
loading={loadingLeaves}
lastRefreshed={lastRefreshed}
onRefresh={handleRefresh}
/>
)}
{activeTab === "adjustments" && (
<AdjustmentsTable
isTracking={isTracking}
adjustments={adjustments}
loading={loadingAdjustments}
lastRefreshed={lastRefreshed}
onRefresh={handleRefresh}
/>
)}
</div>
</main>
); );
}; };
export default LeaveAndAdjustments; export default LeaveAndAdjustments;

View File

@ -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<void>;
}
const LeaveTable: React.FC<LeaveTableProps> = ({
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) => (
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
<td><div className="skeleton h-4 w-6 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-20 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-28 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-16 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-10 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-24 bg-base-300"></div></td>
</tr>
));
const formatLastRefreshed = () =>
lastRefreshed
? DateTime.fromJSDate(lastRefreshed)
.setZone(DateTime.local().zoneName)
.toFormat("HH:mm:ss")
: "Never";
return (
<>
{/* Table Container */}
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
style={{
height: isTracking ? "calc(100vh - 200px)" : "calc(100vh - 160px)",
}}
>
<table className="table table-zebra w-full text-[11px]">
<thead className="sticky top-0 bg-base-200 z-10 text-[12px]">
<tr className="text-center">
<th>#</th>
<th>Leave Type</th>
<th>Date From → To</th>
<th>Status</th>
<th>Total Days</th>
<th>Applied On</th>
</tr>
</thead>
<tbody className="text-center">
{loading ? (
renderSkeletonRows()
) : refreshing && leaves.length === 0 ? (
renderSkeletonRows()
) : leaves.length > 0 ? (
leaves.map((leave, index) => (
<tr key={leave.leaveId}>
<td>{index + 1}</td>
<td>
<span className="badge badge-xs badge-primary">
{leave.leaveType}
</span>
</td>
<td className="whitespace-nowrap">
<span className="">{formatDate(leave.dateFrom)} </span>
→ {" "}
<span className="">{formatDate(leave.dateTo)}</span>
</td>
<td>
<span
className={`badge badge-xs ${
leave.status === "PENDING"
? "badge-warning"
: leave.status === "APPROVED"
? "badge-success"
: "badge-error"
}`}
>
{leave.status}
</span>
</td>
<td>{leave.totalDays}</td>
<td className="whitespace-nowrap">{formatDateTime(leave.appliedOn)}</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="text-center py-4 text-base-content/50">
No leaves found
</td>
</tr>
)}
{/* {!refreshing && leaves.length > 0 && (
<tr>
<td colSpan={6} className="bg-base-200/50">
<div className="flex justify-center py-2">
{renderSkeletonRows()}
</div>
</td>
</tr>
)} */}
</tbody>
</table>
</div>
{/* Footer */}
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{leaves.length} leave{leaves.length !== 1 ? "s" : ""} found
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh Leaves"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div>
</>
);
};
export default LeaveTable;

View File

@ -4,6 +4,7 @@ import ReactQuill from 'react-quill-new';
import { quillModules } from '../../../utils/quillConfig'; import { quillModules } from '../../../utils/quillConfig';
import Quill from 'quill'; import Quill from 'quill';
import 'quill/dist/quill.snow.css'; import 'quill/dist/quill.snow.css';
import { useNavigate } from 'react-router-dom';
interface Project { interface Project {
projectId: string; projectId: string;
@ -48,6 +49,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const navigate = useNavigate()
// Field validation functions // Field validation functions
const validateProjectId = (projectId: string): string | undefined => { const validateProjectId = (projectId: string): string | undefined => {
@ -323,6 +325,12 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
useEffect(() => { useEffect(() => {
if (selectedProjectId && selectedDate) { if (selectedProjectId && selectedDate) {
setProjectSummary(null)
setStartTime("");
setEndTime("");
setComment("");
setFormErrors({});
setTotalAdjustment("0h 0m");
fetchLogs(); fetchLogs();
} else { } else {
setProjectSummary(null); setProjectSummary(null);
@ -454,6 +462,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
console.log("Timesheet Created Successfully"); console.log("Timesheet Created Successfully");
alert(res.message); alert(res.message);
resetForm(); resetForm();
navigate('/dashboard/leave-adjustments')
} else { } else {
setFormErrors(prev => ({ setFormErrors(prev => ({
...prev, ...prev,
@ -653,13 +662,8 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} disabled={isSubmitting}
className={`btn btn-primary flex-1 hover:shadow-lg transition-all duration-300 ${isSubmitting ? 'loading' : ''}`} className={`btn btn-primary flex-1 hover:shadow-lg transition-all duration-300 `}
> >
{!isSubmitting && (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
{isSubmitting ? 'Submitting...' : 'Submit Request'} {isSubmitting ? 'Submitting...' : 'Submit Request'}
</button> </button>
</div> </div>

View File

@ -4,6 +4,7 @@ import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new"; import ReactQuill from "react-quill-new";
import Quill from "quill"; import Quill from "quill";
import "quill/dist/quill.snow.css"; import "quill/dist/quill.snow.css";
import { useNavigate } from "react-router-dom";
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -23,6 +24,7 @@ const PtoLeaveTab: React.FC<{
}); });
const [serverError, setServerError] = useState(""); const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const navigate = useNavigate()
const [errors, setErrors] = useState<Partial<LeaveFormData>>({}); const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
@ -198,7 +200,8 @@ const PtoLeaveTab: React.FC<{
if (result.success) { if (result.success) {
resetForm(); resetForm();
alert("PTO submitted!"); alert("PTO Request submitted!");
navigate('/dashboard/leave-adjustments')
} else { } else {
setServerError(result.message || "Something went wrong"); setServerError(result.message || "Something went wrong");
} }
@ -258,8 +261,8 @@ const PtoLeaveTab: React.FC<{
onChange={(val) => handleChange('comment', val)} onChange={(val) => handleChange('comment', val)}
className="h-60" className="h-60"
/> />
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div> </div>
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div> </div>
{serverError && ( {serverError && (

View File

@ -3,6 +3,7 @@ import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new"; import ReactQuill from "react-quill-new";
import Quill from "quill"; import Quill from "quill";
import "quill/dist/quill.snow.css"; // correct CSS import "quill/dist/quill.snow.css"; // correct CSS
import { useNavigate } from 'react-router-dom';
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -22,6 +23,7 @@ const SickLeaveTab: React.FC<{
const [serverError, setServerError] = useState(""); const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({}); const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
const editor = quillRef.current?.getEditor(); const editor = quillRef.current?.getEditor();
@ -188,7 +190,8 @@ const SickLeaveTab: React.FC<{
if (result.success) { if (result.success) {
resetForm(); resetForm();
alert("SICK submitted!"); alert("SICK leave applied!");
navigate('/dashboard/leave-adjustments')
} else { } else {
setServerError(result.message || "Something went wrong"); setServerError(result.message || "Something went wrong");
} }

View File

@ -0,0 +1,131 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
const PaysSlips = () => {
const { projectSummary } =
useSelector((state: any) => state.projects);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [rows, setRows] = useState<any[]>([]);
useEffect(() => {
if (!projectSummary?.vaId) return;
fetchPayslips(projectSummary?.vaId);
}, [projectSummary?.vaId]);
const fetchPayslips = async (vaId: string) => {
setLoading(true);
setError(null);
try {
console.log("getPayslips" , vaId)
const result = await window.electron.getPayslips(vaId);
if (!result.success) {
setError(result.message || "Failed to load payslips");
return;
}
const response = result.data;
if (!response || response.length === 0) {
setRows([]);
return;
}
// Extract payrolls array from first index
const payrolls = response[0]?.payrolls || [];
setRows(payrolls);
} catch (err: any) {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
return (
<>
<div className="flex flex-col gap-2 max-h-screen">
<div className="flex items-center justify-between">
<h2 className="text-md font-bold">Payslips</h2>
</div>
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh] no-scrollbar">
{loading && (
<div className="flex justify-center py-10">
<span className="loading loading-spinner loading-md"></span>
</div>
)}
{error && (
<div className="text-center text-red-500 py-5">{error}</div>
)}
{!loading && !error && rows.length === 0 && (
<div className="text-center py-10 text-gray-500">
No payslip data available.
</div>
)}
{!loading && !error && rows.length > 0 && (
<div className="overflow-x-auto w-full no-scrollbar">
<table className="table table-zebra w-full text-[12px] min-w-[650px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Status</th>
<th>Date</th>
<th>Total Hours</th>
<th>Total Pay</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={row.id}>
<td>{index + 1}</td>
<td>
<span
className={`badge ${
row.status === "Paid"
? "badge-success"
: "badge-warning"
}`}
>
{row.status}
</span>
</td>
<td>{row.payrollSchedule}</td>
<td>{row.totalHoursFormatted}</td>
<td>${row.totalPayAmount.toFixed(2)}</td>
<td>
<button
className="btn btn-xs btn-primary"
>
View
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</>
);
};
export default PaysSlips;

View File

@ -1,70 +1,147 @@
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 {
id: string;
isActive: boolean;
status: string | null;
ScheduleDate: string;
cutOffTime: string | null;
periodEnd: string | null;
periodStart: string | null;
}
const Schedule = () => { const Schedule = () => {
const rows = [ const { isTracking } = useOutletContext<TrackingState>();
{
period: "Oct 25, 2025 to Nov 07, 2025", const [rows, setRows] = useState<ScheduleItem[]>([]);
cutoff: "Nov 11, 2025 05:29:59 AM", const [loading, setLoading] = useState(true);
payDate: "Nov 14, 2025" const [refreshing, setRefreshing] = useState(false);
}, const [error, setError] = useState<string | null>(null);
{ // const [lastUpdated, setLastUpdated] = useState<DateTime | null>(null);
period: "Nov 08, 2025 to Nov 21, 2025", const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
cutoff: "Nov 25, 2025 05:29:59 AM",
payDate: "Nov 28, 2025" /** Format: Only Date */
}, const formatOnlyDate = (input: string | null) => {
{ if (!input) return "-";
period: "Nov 22, 2025 to Dec 05, 2025", return DateTime.fromISO(input).toLocaleString(DateTime.DATE_MED);
cutoff: "Dec 09, 2025 05:29:59 AM", };
payDate: "Dec 12, 2025"
}, /** Format: Date + Time */
{ const formatDateTime = (input: string | null) => {
period: "Dec 06, 2025 to Dec 19, 2025", if (!input) return "-";
cutoff: "Dec 23, 2025 05:29:59 AM", return DateTime.fromISO(input).toLocaleString(DateTime.DATETIME_MED);
payDate: "Dec 26, 2025" };
},
{ /** Fetch Data */
period: "Dec 20, 2025 to Jan 02, 2026", const fetchSchedules = async () => {
cutoff: "Jan 06, 2026 05:29:59 AM", setRefreshing(true);
payDate: "Jan 09, 2026" setLoading(true);
}, try {
{ const res = await window.electron.getSchedules();
period: "Jan 03, 2026 to Jan 16, 2026",
cutoff: "Jan 20, 2026 05:29:59 AM", if (!res.success) {
payDate: "Jan 23, 2026" setError(res.message || "Failed to load schedules");
}, return;
{ }
period: "Jan 17, 2026 to Jan 30, 2026",
cutoff: "Feb 03, 2026 05:29:59 AM", setRows(res.result || []);
payDate: "Feb 06, 2026" setLastRefreshed(new Date());
}, } catch (err: any) {
{ setError(err.message || "Unexpected error");
period: "Jan 31, 2026 to Feb 13, 2026", } finally {
cutoff: "Feb 17, 2026 05:29:59 AM", setLoading(false);
payDate: "Feb 20, 2026" setRefreshing(false);
},
{
period: "Feb 14, 2026 to Feb 27, 2026",
cutoff: "Mar 03, 2026 05:29:59 AM",
payDate: "Mar 06, 2026"
},
{
period: "Feb 28, 2026 to Mar 13, 2026",
cutoff: "Mar 17, 2026 05:29:59 AM",
payDate: "Mar 20, 2026"
} }
]; };
/** Refresh Button */
const handleRefresh = async () => {
await fetchSchedules();
};
useEffect(() => {
fetchSchedules();
}, []);
/** Determine Active Period */
const isActiveRow = (row: ScheduleItem) => {
if (!row.periodStart || !row.periodEnd) return false;
const now = DateTime.local();
const start = DateTime.fromISO(row.periodStart).startOf("day");
const end = DateTime.fromISO(row.periodEnd).endOf("day");
return now >= start && now <= end;
};
const activeSchedule = rows.find((r) => isActiveRow(r));
/** Skeleton Rows */
const renderSkeletonRows = () =>
Array.from({ length: 5 }).map((_, idx) => (
<tr key={`sk-${idx}`}>
<td><div className="skeleton h-4 w-6"></div></td>
<td><div className="skeleton h-4 w-32"></div></td>
<td><div className="skeleton h-4 w-28"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
</tr>
));
const formatLastRefreshed = () => {
if (!lastRefreshed) return "Never";
return DateTime.fromJSDate(lastRefreshed)
.toLocal()
.toFormat("HH:mm:ss"); // Only time
};
return ( return (
<> <main
<div className="flex flex-col gap-2 max-h-screen"> className="flex flex-col gap-2 overflow-hidden"
{/* Header */} style={{
<div className="flex items-center justify-between"> height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
<h2 className="text-md font-bold">Schedules</h2> }}
>
{/* HEADER */}
<div className="flex justify-between items-center">
<h2 className="text-md font-bold">Schedules</h2>
<div className="flex items-center gap-3">
{activeSchedule && (
<div className="px-2 py-1 bg-primary text-primary-content rounded-lg text-xs">
Pay Date: <b>{formatOnlyDate(activeSchedule.ScheduleDate)}</b>
</div>
)}
</div> </div>
</div>
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh] no-scrollbar"> {/* TABLE WRAPPER */}
<div
{/* ⭐ FIXED: Horizontal scroll wrapper */} className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
<div className="overflow-x-auto w-full no-scrollbar"> style={{
<table className="table table-zebra w-full text-[12px] min-w-[600px]"> height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
{/* Loading */}
{loading ? (
<table className="table w-full text-[12px]">
<tbody>{renderSkeletonRows()}</tbody>
</table>
) : error ? (
<div className="text-center text-red-500 py-8">{error}</div>
) : rows.length === 0 ? (
<div className="text-center py-8 text-base-content/50">
No schedules found
</div>
) : (
<div className="overflow-x-auto no-scrollbar">
<table className="table w-full text-[12px] min-w-[600px]">
<thead className="sticky top-0 bg-base-200 z-10"> <thead className="sticky top-0 bg-base-200 z-10">
<tr> <tr>
<th>#</th> <th>#</th>
@ -76,24 +153,61 @@ const Schedule = () => {
<tbody> <tbody>
{rows.map((row, index) => ( {rows.map((row, index) => (
<tr key={index}> <tr
key={row.id}
className={
isActiveRow(row)
? "bg-primary text-primary-content font-semibold"
: ""
}
>
<td>{index + 1}</td> <td>{index + 1}</td>
<td>{row.period}</td>
<td> <td>
{row.cutoff} {formatOnlyDate(row.periodStart)} →{" "}
<br /> {formatOnlyDate(row.periodEnd)}
<span className="text-gray-500 text-[11px]">(Asia/Calcutta)</span>
</td> </td>
<td>{row.payDate}</td> <td>{formatDateTime(row.cutOffTime)}</td>
<td>{formatOnlyDate(row.ScheduleDate)}</td>
</tr> </tr>
))} ))}
{refreshing && (
<tr>
<td colSpan={4} className="bg-base-200/40 text-center py-2">
<Loader2 className="animate-spin inline mr-2" size={16} />
Refreshing...
</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>
)}
</div>
{/* FOOTER */}
<div className="flex justify-between items-center text-xs text-base-content/60 mt-2">
<div>{rows.length} schedules found</div>
<div className="">
{lastRefreshed &&
`Last updated: ${formatLastRefreshed()}`}
<button
onClick={handleRefresh}
disabled={refreshing}
className="ms-2 btn btn-xs btn-circle bg-warning"
title="Refresh schedule"
>
{refreshing ? (
<Loader2 size={14} className="animate-spin text-base-content" />
) : (
<RefreshCw size={14} className="text-base-content" />
)}
</button>
</div> </div>
</div> </div>
</> </main>
); );
}; };

View File

@ -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<ChangePaymentModalProps> = ({
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 (
<dialog open className="modal modal-open">
<div className="modal-box w-96">
<h3 className="font-bold text-lg mb-4 flex items-center gap-2">
<CreditCard size={18} /> Change Payment Method
</h3>
{/* Error */}
{error && (
<div className="alert alert-error py-2 text-xs mb-3">{error}</div>
)}
{/* Dropdown */}
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Payment Method</span>
</label>
<select
className="select select-bordered select-sm"
value={method}
onChange={(e) => setMethod(e.target.value)}
>
<option value="">Select method</option>
<option value="wise">Wise</option>
<option value="veem">Veem</option>
</select>
</div>
{/* Wise Email */}
{method === "wise" && (
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Wise Email</span>
</label>
<input
type="email"
className="input input-bordered input-sm"
placeholder="Enter Wise email"
value={wiseEmail}
onChange={(e) => setWiseEmail(e.target.value)}
/>
</div>
)}
{/* Veem Account */}
{method === "veem" && (
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Veem Account Number</span>
</label>
<input
type="text"
className="input input-bordered input-sm"
placeholder="Enter Veem account"
value={veemAccount}
onChange={(e) => setVeemAccount(e.target.value)}
/>
</div>
)}
<div className="modal-action">
<button className="btn btn-sm btn-ghost" onClick={onClose}>
Cancel
</button>
<button
className={`btn btn-sm btn-primary ${loading ? "loading" : ""}`}
onClick={handleSubmit}
disabled={loading}
>
{loading ? "Updating..." : "Update"}
</button>
</div>
</div>
</dialog>
);
};
export default ChangePaymentModal;

View File

@ -1,13 +1,15 @@
import React, { useState } from "react"; 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 { useSelector } from "react-redux";
import { RootState } from "../../store"; import { RootState } from "../../store";
import ChangePasswordModal from "./ChangePasswordModal"; import ChangePasswordModal from "./ChangePasswordModal";
import ChangePaymentModal from "./SettingModal/ChangePaymentModal";
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { profile } = useSelector((state: RootState) => state.profile); const { profile } = useSelector((state: RootState) => state.profile);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [showPaymentModal,setShowPaymentModal] = useState(false)
const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim(); const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim();
@ -82,32 +84,33 @@ const Settings: React.FC = () => {
</p> </p>
</div> </div>
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
💬 Skype
</p>
<p className="text-xs font-medium truncate" title={profile?.skype || "Not Linked"}>
{profile?.skype || "Not Linked"}
</p>
</div>
{/* Payment Info */} {/* Payment Info */}
<div className="p-2 rounded bg-base-200/40"> <div
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5"> className=" p-2 rounded bg-base-200/40 cursor-pointer hover:text-primary transition-all duration-200 ease-in-out hover:scale-[1.02] active:scale-[0.98] " onClick={() => setShowPaymentModal(true)}>
<CreditCard size={11} /> Payment <div className="flex justify-between items-center">
</p> <div>
<p className="text-xs font-medium capitalize truncate"> <p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
{profile?.paymentMethod || "N/A"} <CreditCard size={11} /> Payment
</p> </p>
<p className="text-xs font-medium capitalize truncate">
{profile?.paymentMethod || "N/A"}
</p>
</div>
<span>
<SquarePen size={15} />
</span>
</div>
</div> </div>
{profile?.paymentMethod === "payoneer" && (
{profile?.paymentMethod === "wise" && (
<div className="p-2 rounded bg-base-200/40"> <div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5"> <p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
💳 Payoneer 💳 Wise
</p> </p>
<p className="text-xs font-medium truncate" title={profile?.payoneerAccount || "N/A"}> <p className="text-xs font-medium truncate" title={profile?.payoneerAccount || "N/A"}>
{maskLast5(profile?.payoneerAccount)} {maskLast5(profile?.wiseEmail)}
</p> </p>
</div> </div>
)} )}
@ -137,8 +140,9 @@ const Settings: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Change Password Modal */} {/* Modal */}
<ChangePasswordModal isOpen={modalOpen} onClose={() => setModalOpen(false)} /> <ChangePasswordModal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
<ChangePaymentModal isOpen={showPaymentModal} onClose={() => setShowPaymentModal(false)} />
</div> </div>
); );
}; };

View File

@ -95,3 +95,4 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
}; };
export default ProjectLogsModal; export default ProjectLogsModal;

View File

@ -1,8 +1,22 @@
import React, { useEffect, useState } from "react"; 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 { useSelector } from "react-redux";
import { RootState } from "../../store"; import { RootState } from "../../store";
import ProjectLogsModal from "./ProjectLogsModal"; import ProjectLogsModal from "./ProjectLogsModal";
import { TrackingState } from "../../layout/DashboardLayout";
import { useOutletContext } from "react-router-dom";
import { DateTime } from "luxon";
interface TimesheetEntry { interface TimesheetEntry {
id: number; id: number;
@ -28,48 +42,83 @@ interface Project {
totalWorkTime: string; totalWorkTime: string;
totalWorkMs: number; totalWorkMs: number;
totalLogs: number; totalLogs: number;
status:string status: string
} }
const Timesheets: React.FC = () => { const Timesheets: React.FC = () => {
const { isTracking } = useOutletContext<TrackingState>();
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0); const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length; const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
const { windowLogs } = useSelector((state: RootState) => state.window); const { windowLogs } = useSelector((state: RootState) => state.window);
console.log("🧾 Timesheet in component:", windowLogs); console.log(" Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]); const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [selectedProject, setSelectedProject] = useState<string | null>(null); const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>(""); const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [initialLoading, setInitialLoading] = useState(true);
const [tick, setTick] = useState(0);
const activeProjects = projectsData.filter(p => p.status === "ACTIVE");
useEffect(() => { useEffect(() => {
const fetchProjects = async () => { const interval = setInterval(() => {
try { setTick((t) => t + 1); // force re-render every second
console.log("📡 Fetching projects..."); }, 1000);
const res = await window.electron.getProjectList();
console.log("✅ Project list response:", res);
if (res?.success && Array.isArray(res.data)) { return () => clearInterval(interval);
// Store project list }, []);
setProjectsData(res.data);
} else { const formatLastRefreshed = () => {
console.warn(" Unexpected response:", res); if (!lastRefreshed) return "Never";
}
} catch (err) { return DateTime.fromJSDate(lastRefreshed)
console.error("Error fetching projects:", err); .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(); fetchProjects();
}, []); }, []);
const handleRefresh = async () => {
setRefreshing(true);
await fetchProjects();
};
const handleViewLogs = async (projectID: string, projectName: string) => { const handleViewLogs = async (projectID: string, projectName: string) => {
try { try {
setModalOpen(true);
setLoading(true); setLoading(true);
setSelectedProject(projectID); setSelectedProject(projectID);
setSelectedProjectName(projectName); setSelectedProjectName(projectName);
@ -85,11 +134,13 @@ const Timesheets: React.FC = () => {
setLogs([]); setLogs([]);
} }
setModalOpen(true);
} catch (error) { } catch (error) {
console.error("Error fetching logs:", error); console.error("Error fetching logs:", error);
setModalOpen(false)
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
@ -105,54 +156,49 @@ const Timesheets: React.FC = () => {
// Skeleton loader rows
const renderSkeletonRows = () => {
return Array.from({ length: 5 }).map((_, index) => (
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
<td>
<div className="skeleton h-4 w-6 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-4 w-32 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-4 w-16 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-8 w-8 rounded bg-base-300"></div>
</td>
</tr>
));
};
return ( return (
<div className="flex flex-col gap-2 max-h-screen"> <main
className="flex flex-col gap-2 overflow-hidden"
style={{
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
}}
>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex-none mb-2 flex justify-between items-center">
<div className="flex items-center gap-2"> <h2 className="text-md font-bold">Timesheets</h2>
<h2 className="text-md font-bold">Timesheets</h2>
</div>
</div> </div>
{/* Summary Cards */}
{/* <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Total Logged Hours</p>
<h3 className="text-lg font-semibold">{totalHours} hrs</h3>
</div>
<Clock size={20} className="text-primary" />
</div>
</div>
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Approved Days</p>
<h3 className="text-lg font-semibold">{approvedCount}</h3>
</div>
<CheckCircle size={20} className="text-success" />
</div>
</div>
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Pending Days</p>
<h3 className="text-lg font-semibold">
{timesheetData.length - approvedCount}
</h3>
</div>
<FileText size={20} className="text-warning" />
</div>
</div>
</div> */}
{/* Timesheet Table */} {/* Timesheet Table */}
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh]"> <div
<table className="table table-zebra w-full text-[12px]"> className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
<thead className="sticky top-0 bg-base-200 z-10 "> style={{
height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr> <tr>
<th>#</th> <th>#</th>
<th>Project</th> <th>Project</th>
@ -161,34 +207,83 @@ const Timesheets: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{projectsData.map((entry, index) => ( {initialLoading ? (
<tr key={entry.projectId} className={`${entry.status ==='INACTIVE' && 'hidden'}`}> // Show skeleton while initially loading
<td>{index + 1}</td> renderSkeletonRows()
<td>{entry.projectName}</td> ) : refreshing && projectsData.length === 0 ? (
<td>{formatWorkTime(entry.totalWorkTime)}</td> // Show skeleton while refreshing with empty data
<td> renderSkeletonRows()
<button ) : (
disabled={formatWorkTime(entry.totalWorkTime) === "0h 0m"} // Show actual data
onClick={() => handleViewLogs(entry.projectId, entry.projectName)} projectsData.map((entry, index) => (
className="btn btn-ghost btn-xs" <tr
> key={entry.projectId}
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? <EyeOff size={16} /> : <Eye size={16} />} className={`${entry.status === "INACTIVE" && "hidden"}`}
</button> >
<td>{index + 1}</td>
<td>{entry.projectName}</td>
<td>{formatWorkTime(entry.totalWorkTime)}</td>
<td>
<button
disabled={formatWorkTime(entry.totalWorkTime) === "0h 0m"}
onClick={() =>
handleViewLogs(entry.projectId, entry.projectName)
}
className="btn btn-ghost btn-xs"
>
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
</td>
</tr>
))
)}
{/* {refreshing && projectsData.length > 0 && (
// Show a single skeleton row at the top while refreshing with existing data
<tr>
<td colSpan={4} className="bg-base-200/50">
<div className="flex justify-center py-2">
<Loader2 size={20} className="animate-spin" />
<span className="ml-2 text-sm">Refreshing...</span>
</div>
</td> </td>
</tr> </tr>
))} )} */}
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Footer Summary */} {/* Footer Summary */}
<div className="flex justify-end items-center text-xs mt-2"> <div className="flex-none flex justify-between items-center text-xs mt-2">
{/* <span className="text-base-content/60"> <div className="text-base-content/60">
Showing {timesheetData.length} entries {activeProjects.length} project{activeProjects.length !== 1 ? 's' : ''} shown
</span> */} </div>
<span className="text-base-content/60">
Last updated: {new Date().toLocaleDateString()} <div className="flex items-center gap-4">
</span> {lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh projects"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div> </div>
<ProjectLogsModal <ProjectLogsModal
@ -198,8 +293,8 @@ const Timesheets: React.FC = () => {
logs={logs} logs={logs}
loading={loading} loading={loading}
/> />
</div> </main>
); );
}; };
export default Timesheets; export default Timesheets;

View File

@ -19,6 +19,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
paymentMethod:string; paymentMethod:string;
payoneerAccount:string; payoneerAccount:string;
veemAccount:string; veemAccount:string;
wiseEmail:string
_id: string; _id: string;
} }

51
types.d.ts vendored
View File

@ -83,6 +83,7 @@ interface ProfileData {
paymentMethod: string; paymentMethod: string;
payoneerAccount: string; payoneerAccount: string;
veemAccount: string; veemAccount: string;
wiseEmail: string
_id: string; _id: string;
} }
@ -92,6 +93,36 @@ type LeaveRequestResponse = {
message?: string; message?: string;
}; };
interface ScheduleItem {
id: string;
isActive: boolean;
status: string | null;
ScheduleDate: string;
cutOffTime: string | null;
periodEnd: string | null;
periodStart: string | null;
}
interface PayslipItem {
totalHoursFormatted: string;
payrollSchedule: string;
totalPayAmount: number;
status: string;
date: string | null;
createdDate: string;
name: string;
id: string;
}
interface PayslipResponse {
payrolls: PayslipItem[];
vaPortalId: string;
vaName: string;
id: string;
}
interface Window { interface Window {
electron: { electron: {
login: (credentials: { email: string; password: string }) => Promise<any>; login: (credentials: { email: string; password: string }) => Promise<any>;
@ -174,9 +205,25 @@ interface Window {
payload: LeaveRequestPayload payload: LeaveRequestPayload
) => Promise<LeaveRequestResponse>; ) => Promise<LeaveRequestResponse>;
getSchedules: () => Promise<any>;
getPayslips: (vaId: string) => Promise<{
success: boolean;
data?: PayslipResponse[];
message?: string;
}>;
getLeaveList: () => Promise<{
success: boolean;
data?: any;
}>;
getAdjustmentsList: (vaId: string) => Promise<{
success: boolean;
data?: any;
}>;
}; };
} }