Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 04-Dec - Mac
This commit is contained in:
commit
ec566bb14a
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = () => {
|
||||||
|
|
|
||||||
|
|
@ -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']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,4 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectLogsModal;
|
export default ProjectLogsModal;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue