leave and adjut. show both the table and tabs based table

This commit is contained in:
JaiPandey425 2025-12-03 16:17:10 +05:30
parent 70193501d3
commit df2b35a4e4
8 changed files with 533 additions and 163 deletions

View File

@ -462,6 +462,35 @@ ipcMain.handle("get-leave-list", async () => {
}
});
ipcMain.handle("get-adjustments-list", async () => {
try {
const res = await api.get("/timesheet/adjustments/list");
console.log("/timesheet/adjustments/list", res.data);
return {
success: true,
data: res.data,
};
} catch (error: any) {
let message = "Unknown error occurred";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return {
success: false,
message
};
}
});
export const sendTokenExpired = () => {

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from "react";
import { useNavigate, useOutletContext } from "react-router-dom";
import { TrackingState } from "../../layout/DashboardLayout";
import { Loader2, RefreshCw } from "lucide-react";
import { DateTime } from "luxon";
import LeaveTable from "./LeaveTable";
import AdjustmentsTable from "./AdjustmentsTable";
import { useSelector } from "react-redux";
interface LeaveItem {
leaveId: string;
@ -17,70 +18,104 @@ interface LeaveItem {
rejectionReason: string | null;
}
interface AdjustmentItem {
id: string;
date: string;
loginTime: string;
logoutTime: string;
comment: string;
status: string;
requestedAt: string;
adjustedHours: string;
project: {
id: string;
name: string;
};
}
interface ApiResponse<T> {
success: boolean;
data?: T;
}
const LeaveAndAdjustments: React.FC = () => {
const { isTracking } = useOutletContext<TrackingState>();
const navigate = useNavigate();
const { projectSummary } = useSelector((state: any) => state.projects);
console.log("projectSummary46" , projectSummary)
// Tabs state
const [activeTab, setActiveTab] = useState<"leave" | "adjustments">("leave");
// Data states
const [leaves, setLeaves] = useState<LeaveItem[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
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 fetchLeaves = async () => {
try {
const res = await window.electron.getLeaveList();
if (res.success) {
setLeaves(res?.data?.leaves || []);
}
setLastRefreshed(new Date());
} catch (e) {
console.error("Error fetching leaves:", e);
} finally {
setLoading(false);
setRefreshing(false);
}
};
const handleRefresh = async () => {
setRefreshing(true);
await fetchLeaves();
};
useEffect(() => {
fetchLeaves();
}, []);
const handleApplyLeaveAndAdjust = () => {
navigate("/dashboard/leave-adjustments/apply");
};
const renderSkeletonRows = () => {
return Array.from({ length: 5 }).map((_, index) => (
<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>
));
// Fetch leaves data
const fetchLeaves = async () => {
setLoadingLeaves(true);
try {
const res: ApiResponse<{ leaves: LeaveItem[] }> = await window.electron.getLeaveList();
if (res.success) {
setLeaves(res?.data?.leaves || []);
}
} catch (e) {
console.error("Error fetching leaves:", e);
} finally {
setLoadingLeaves(false);
}
};
const formatLastRefreshed = () => {
if (!lastRefreshed) return "Never";
return DateTime.fromJSDate(lastRefreshed)
.toLocal()
.toFormat("HH:mm:ss"); // Only time
// Fetch adjustments data
const fetchAdjustments = async () => {
setLoadingAdjustments(true);
try {
const res: ApiResponse<AdjustmentItem[]> = await window.electron.getAdjustmentsList();
if (res.success) {
setAdjustments(res.data || []);
}
} catch (err) {
console.error("Error fetching adjustments:", err);
} finally {
setLoadingAdjustments(false);
}
};
// Combined refresh function
const handleRefresh = async () => {
if (activeTab === "leave") {
await fetchLeaves();
} else {
await fetchAdjustments();
}
setLastRefreshed(new Date());
};
// Initial data fetch
useEffect(() => {
fetchLeaves();
fetchAdjustments();
setLastRefreshed(new Date());
}, []);
return (
<main
className="flex flex-col gap-2 overflow-hidden"
style={{
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
}}
// style={{
// height: isTracking ? "calc(100vh - 40px)" : "",
// }}
>
{/* Header */}
<div className="flex-none mb-2 flex justify-between items-center">
@ -90,112 +125,45 @@ const LeaveAndAdjustments: React.FC = () => {
</button>
</div>
{/* 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 - 180px)" : "calc(100vh - 130px) ",
}}
{/* 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")}
>
<table className="table table-zebra w-full text-[11px]">
<thead className="sticky top-0 bg-base-200 z-10 text-[12px]">
<tr>
<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>
{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>
<span>{leave.dateFrom}</span>
<br />
<span>{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>{new Date(leave.appliedOn).toLocaleString()}</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">
<Loader2 size={20} className="animate-spin" />
<span className="ml-2 text-sm">Refreshing...</span>
</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>
)}
Leave
</button>
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh Leaves"
role="tab"
className={`tab ${activeTab === "adjustments" ? "tab-active border-primary border-b-2" : ""}`}
onClick={() => setActiveTab("adjustments")}
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
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>
);

View File

@ -0,0 +1,184 @@
import React from "react";
import { Loader2, RefreshCw } from "lucide-react";
import { DateTime } from "luxon";
interface LeaveItem {
leaveId: string;
dateFrom: string;
dateTo: string;
totalDays: number;
leaveType: string;
comment: string;
status: string;
appliedOn: string;
reviewedAt: string | null;
rejectionReason: string | null;
}
interface LeaveTableProps {
isTracking: boolean;
leaves: LeaveItem[];
loading: boolean;
lastRefreshed: Date | null;
onRefresh: () => Promise<void>;
}
const LeaveTable: React.FC<LeaveTableProps> = ({
isTracking,
leaves,
loading,
lastRefreshed,
onRefresh
}) => {
const [refreshing, setRefreshing] = React.useState(false);
// 🌍 Convert UTC → Local System Time
const toLocal = (utcString: string, format: string) => {
return DateTime.fromISO(utcString, { zone: "utc" })
.setZone(DateTime.local().zoneName)
.toFormat(format);
};
const formatDate = (d: string) => toLocal(d, "yyyy-MM-dd");
const formatDateTime = (d: string) => toLocal(d, "yyyy-MM-dd HH:mm:ss");
const handleRefresh = async () => {
setRefreshing(true);
await onRefresh();
setRefreshing(false);
};
const renderSkeletonRows = () =>
Array.from({ length: 5 }).map((_, index) => (
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
<td><div className="skeleton h-4 w-6 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-20 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-28 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-16 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-10 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-24 bg-base-300"></div></td>
</tr>
));
const formatLastRefreshed = () =>
lastRefreshed
? DateTime.fromJSDate(lastRefreshed)
.setZone(DateTime.local().zoneName)
.toFormat("HH:mm:ss")
: "Never";
return (
<>
{/* Table Container */}
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
style={{
height: isTracking ? "calc(100vh - 200px)" : "calc(100vh - 160px)",
}}
>
<table className="table table-zebra w-full text-[11px]">
<thead className="sticky top-0 bg-base-200 z-10 text-[12px]">
<tr className="text-center">
<th>#</th>
<th>Leave Type</th>
<th>Date From → To</th>
<th>Status</th>
<th>Total Days</th>
<th>Applied On</th>
</tr>
</thead>
<tbody className="text-center">
{loading ? (
renderSkeletonRows()
) : refreshing && leaves.length === 0 ? (
renderSkeletonRows()
) : leaves.length > 0 ? (
leaves.map((leave, index) => (
<tr key={leave.leaveId}>
<td>{index + 1}</td>
<td>
<span className="badge badge-xs badge-primary">
{leave.leaveType}
</span>
</td>
<td className="whitespace-nowrap">
<span className="">{formatDate(leave.dateFrom)} </span>
→ {" "}
<span className="">{formatDate(leave.dateTo)}</span>
</td>
<td>
<span
className={`badge badge-xs ${
leave.status === "PENDING"
? "badge-warning"
: leave.status === "APPROVED"
? "badge-success"
: "badge-error"
}`}
>
{leave.status}
</span>
</td>
<td>{leave.totalDays}</td>
<td className="whitespace-nowrap">{formatDateTime(leave.appliedOn)}</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="text-center py-4 text-base-content/50">
No leaves found
</td>
</tr>
)}
{/* {!refreshing && leaves.length > 0 && (
<tr>
<td colSpan={6} className="bg-base-200/50">
<div className="flex justify-center py-2">
{renderSkeletonRows()}
</div>
</td>
</tr>
)} */}
</tbody>
</table>
</div>
{/* Footer */}
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{leaves.length} leave{leaves.length !== 1 ? "s" : ""} found
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh Leaves"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div>
</>
);
};
export default LeaveTable;

View File

@ -662,7 +662,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
<button
onClick={handleSubmit}
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 ? 'Submitting...' : 'Submit Request'}
</button>

7
types.d.ts vendored
View File

@ -83,7 +83,7 @@ interface ProfileData {
paymentMethod: string;
payoneerAccount: string;
veemAccount: string;
wiseEmail:string
wiseEmail: string
_id: string;
}
@ -217,6 +217,11 @@ interface Window {
success: boolean;
data?: any;
}>;
getAdjustmentsList: () => Promise<{
success: boolean;
data?: any;
}>;
};
}