Compare commits

..

2 Commits

8 changed files with 363 additions and 88 deletions

View File

@ -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 }) => {
try {
@ -378,6 +365,75 @@ 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
};
}
});
export const sendTokenExpired = () => {

View File

@ -53,6 +53,9 @@ contextBridge.exposeInMainWorld('electron', {
createPTOAndSickLeaveRequest: (data) =>
ipcRenderer.invoke("create-leave-request", data),
getSchedules: () => ipcRenderer.invoke("get-schedules"),
getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId),
} satisfies Window['electron']);

View File

@ -95,6 +95,7 @@ import Settings from "./pages/Settings/Settings";
import HelpSupport from "./pages/HelpSupport/HelpSupport";
import Vpn from "./pages/Vpn/Vpn";
import Schedule from "./pages/Schedule/Schedule";
import PaysSlips from "./pages/PaySlips/PaysSlips";
const App: React.FC = () => {
@ -142,6 +143,7 @@ const App: React.FC = () => {
<Route path="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} />
<Route path="schedule" element={<Schedule/>} />
<Route path="pays-slips" element={<PaysSlips/>} />
</Route>
{/* Default Redirect */}

View File

@ -27,6 +27,7 @@ import {
ChevronUp,
GlobeLock,
ClipboardClock,
CircleDollarSign,
} from "lucide-react";
import { useActiveAppTracker } from "../hooks/useActiveAppTracker";
import { useDispatch, useSelector } from "react-redux";
@ -113,6 +114,7 @@ const DashboardLayout = () => {
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
{ label: "Leave & Adjust", path: "/dashboard/leave-adjustments", icon: <CalendarDays 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: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
@ -297,7 +299,7 @@ const DashboardLayout = () => {
);
console.log("Alert Message 2")
// Stop tracking
await handleStopTracking();
// await handleStopTracking();
}, [selectedProject, handleStopTracking]);
// Initialize useActiveAppTracker and store finalizeTracking in ref
@ -743,7 +745,7 @@ const DashboardLayout = () => {
</div>
))}
<div className="absolute bottom-13 ">
<div className={`bsolute ${!isTracking ? "bottom-13" : "bottom-5"} `}>
<label className="swap swap-rotate p-2.5">
{/* this hidden checkbox controls the state */}
<input type="checkbox" className="theme-controller" onChange={toggleTheme}

View File

@ -258,8 +258,8 @@ const PtoLeaveTab: React.FC<{
onChange={(val) => handleChange('comment', val)}
className="h-60"
/>
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div>
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div>
{serverError && (

View File

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

View File

@ -1,70 +1,115 @@
import React, { useEffect, useState } from "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 rows = [
{
period: "Oct 25, 2025 to Nov 07, 2025",
cutoff: "Nov 11, 2025 05:29:59 AM",
payDate: "Nov 14, 2025"
},
{
period: "Nov 08, 2025 to Nov 21, 2025",
cutoff: "Nov 25, 2025 05:29:59 AM",
payDate: "Nov 28, 2025"
},
{
period: "Nov 22, 2025 to Dec 05, 2025",
cutoff: "Dec 09, 2025 05:29:59 AM",
payDate: "Dec 12, 2025"
},
{
period: "Dec 06, 2025 to Dec 19, 2025",
cutoff: "Dec 23, 2025 05:29:59 AM",
payDate: "Dec 26, 2025"
},
{
period: "Dec 20, 2025 to Jan 02, 2026",
cutoff: "Jan 06, 2026 05:29:59 AM",
payDate: "Jan 09, 2026"
},
{
period: "Jan 03, 2026 to Jan 16, 2026",
cutoff: "Jan 20, 2026 05:29:59 AM",
payDate: "Jan 23, 2026"
},
{
period: "Jan 17, 2026 to Jan 30, 2026",
cutoff: "Feb 03, 2026 05:29:59 AM",
payDate: "Feb 06, 2026"
},
{
period: "Jan 31, 2026 to Feb 13, 2026",
cutoff: "Feb 17, 2026 05:29:59 AM",
payDate: "Feb 20, 2026"
},
{
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"
}
];
const [rows, setRows] = useState<ScheduleItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Format helpers using Luxon
const formatDateTime = (input: string | null) => {
if (!input) return "-";
return DateTime.fromISO(input)
.setZone("local")
.toLocaleString(DateTime.DATETIME_MED);
};
const formatOnlyDate = (input: string | null) => {
if (!input) return "-";
return DateTime.fromISO(input)
.setZone("local")
.toLocaleString(DateTime.DATE_MED);
};
// Fetch schedules
useEffect(() => {
const loadSchedules = async () => {
try {
const res = await window.electron.getSchedules();
console.log(res.result);
if (!res.success) {
setError(res.message || "Failed to load schedules");
return;
}
const list = res.result || []; // Use data array
setRows(list);
} catch (err: any) {
setError(err.message || "Unexpected error");
} finally {
setLoading(false);
}
};
loadSchedules();
}, []);
// Determine if current date is within the schedule 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;
};
// Get Active Schedule (only one)
const activeSchedule = rows.find((r) => isActiveRow(r));
return (
<>
<div className="flex flex-col gap-2 max-h-screen">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-md font-bold">Schedules</h2>
</div>
<div className="flex flex-col gap-2 max-h-screen">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-md font-bold">Schedules</h2>
{activeSchedule && (
<div className="p-1 bg-secondary text-secondary-content rounded-lg">
<p className="text-xs">
Pay Date: <span className="font-semibold">
{formatOnlyDate(activeSchedule.ScheduleDate)}
</span>
</p>
</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">
<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">
{/* Loader */}
{loading && (
<div className="flex justify-center items-center h-full">
<span className="loading loading-spinner text-primary"></span>
</div>
)}
{/* ⭐ FIXED: Horizontal scroll wrapper */}
{/* Error */}
{!loading && error && (
<div className="text-center text-red-500 py-10">❌ {error}</div>
)}
{/* No Data */}
{!loading && !error && rows.length === 0 && (
<div className="text-center text-gray-500 py-10">No schedules found.</div>
)}
{/* Table */}
{!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-[600px]">
<table className="table w-full text-[12px] min-w-[600px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
@ -76,24 +121,24 @@ const Schedule = () => {
<tbody>
{rows.map((row, index) => (
<tr key={index}>
<tr
key={row.id}
className={isActiveRow(row) ? "bg-secondary font-semibold" : ""}
>
<td>{index + 1}</td>
<td>{row.period}</td>
<td>
{row.cutoff}
<br />
<span className="text-gray-500 text-[11px]">(Asia/Calcutta)</span>
{formatOnlyDate(row.periodStart)} → {formatOnlyDate(row.periodEnd)}
</td>
<td>{row.payDate}</td>
<td>{formatDateTime(row.cutOffTime)}</td>
<td>{formatOnlyDate(row.ScheduleDate)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</>
</div>
);
};

36
types.d.ts vendored
View File

@ -92,6 +92,36 @@ type LeaveRequestResponse = {
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 {
electron: {
login: (credentials: { email: string; password: string }) => Promise<any>;
@ -174,7 +204,13 @@ interface Window {
payload: LeaveRequestPayload
) => Promise<LeaveRequestResponse>;
getSchedules: () => Promise<any>;
getPayslips: (vaId: string) => Promise<{
success: boolean;
data?: PayslipResponse[];
message?: string;
}>;
};
}