leave and adjut. show both the table and tabs based table
This commit is contained in:
parent
70193501d3
commit
df2b35a4e4
|
|
@ -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 = () => {
|
export const sendTokenExpired = () => {
|
||||||
|
|
|
||||||
|
|
@ -51,12 +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"),
|
getSchedules: () => ipcRenderer.invoke("get-schedules"),
|
||||||
|
|
||||||
getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId),
|
getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId),
|
||||||
getLeaveList: () => ipcRenderer.invoke("get-leave-list")
|
getLeaveList: () => ipcRenderer.invoke("get-leave-list"),
|
||||||
|
getAdjustmentsList:() => ipcRenderer.invoke("get-adjustments-list"),
|
||||||
|
|
||||||
} satisfies Window['electron']);
|
} satisfies Window['electron']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||||
import { TrackingState } from "../../layout/DashboardLayout";
|
import { TrackingState } from "../../layout/DashboardLayout";
|
||||||
import { Loader2, RefreshCw } from "lucide-react";
|
import LeaveTable from "./LeaveTable";
|
||||||
import { DateTime } from "luxon";
|
import AdjustmentsTable from "./AdjustmentsTable";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
interface LeaveItem {
|
interface LeaveItem {
|
||||||
leaveId: string;
|
leaveId: string;
|
||||||
|
|
@ -17,70 +18,104 @@ interface LeaveItem {
|
||||||
rejectionReason: string | null;
|
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 LeaveAndAdjustments: React.FC = () => {
|
||||||
const { isTracking } = useOutletContext<TrackingState>();
|
const { isTracking } = useOutletContext<TrackingState>();
|
||||||
const navigate = useNavigate();
|
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 [leaves, setLeaves] = useState<LeaveItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [adjustments, setAdjustments] = useState<AdjustmentItem[]>([]);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
|
// Loading states
|
||||||
|
const [loadingLeaves, setLoadingLeaves] = useState(true);
|
||||||
|
const [loadingAdjustments, setLoadingAdjustments] = useState(true);
|
||||||
|
|
||||||
|
// Refresh state
|
||||||
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
|
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 = () => {
|
const handleApplyLeaveAndAdjust = () => {
|
||||||
navigate("/dashboard/leave-adjustments/apply");
|
navigate("/dashboard/leave-adjustments/apply");
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSkeletonRows = () => {
|
// Fetch leaves data
|
||||||
return Array.from({ length: 5 }).map((_, index) => (
|
const fetchLeaves = async () => {
|
||||||
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
|
setLoadingLeaves(true);
|
||||||
<td><div className="skeleton h-4 w-6 bg-base-300"></div></td>
|
try {
|
||||||
<td><div className="skeleton h-4 w-20 bg-base-300"></div></td>
|
const res: ApiResponse<{ leaves: LeaveItem[] }> = await window.electron.getLeaveList();
|
||||||
<td><div className="skeleton h-4 w-28 bg-base-300"></div></td>
|
if (res.success) {
|
||||||
<td><div className="skeleton h-4 w-16 bg-base-300"></div></td>
|
setLeaves(res?.data?.leaves || []);
|
||||||
<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>
|
} catch (e) {
|
||||||
</tr>
|
console.error("Error fetching leaves:", e);
|
||||||
));
|
} finally {
|
||||||
|
setLoadingLeaves(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLastRefreshed = () => {
|
// Fetch adjustments data
|
||||||
if (!lastRefreshed) return "Never";
|
const fetchAdjustments = async () => {
|
||||||
|
setLoadingAdjustments(true);
|
||||||
return DateTime.fromJSDate(lastRefreshed)
|
try {
|
||||||
.toLocal()
|
const res: ApiResponse<AdjustmentItem[]> = await window.electron.getAdjustmentsList();
|
||||||
.toFormat("HH:mm:ss"); // Only time
|
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 (
|
||||||
<main
|
<main
|
||||||
className="flex flex-col gap-2 overflow-hidden"
|
className="flex flex-col gap-2 overflow-hidden"
|
||||||
style={{
|
// style={{
|
||||||
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
|
// height: isTracking ? "calc(100vh - 40px)" : "",
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-none mb-2 flex justify-between items-center">
|
<div className="flex-none mb-2 flex justify-between items-center">
|
||||||
|
|
@ -90,115 +125,48 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Container */}
|
{/* Tabs */}
|
||||||
<div
|
<div role="tablist" className="tabs tabs-bordered w-full">
|
||||||
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
|
<button
|
||||||
style={{
|
role="tab"
|
||||||
height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px) ",
|
className={`tab ${activeTab === "leave" ? "tab-active border-b-2 border-primary" : ""}`}
|
||||||
}}
|
onClick={() => setActiveTab("leave")}
|
||||||
>
|
>
|
||||||
<table className="table table-zebra w-full text-[11px]">
|
Leave
|
||||||
<thead className="sticky top-0 bg-base-200 z-10 text-[12px]">
|
</button>
|
||||||
<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>
|
<button
|
||||||
{loading ? (
|
role="tab"
|
||||||
renderSkeletonRows()
|
className={`tab ${activeTab === "adjustments" ? "tab-active border-primary border-b-2" : ""}`}
|
||||||
) : refreshing && leaves.length === 0 ? (
|
onClick={() => setActiveTab("adjustments")}
|
||||||
renderSkeletonRows()
|
>
|
||||||
) : leaves.length > 0 ? (
|
Adjustments
|
||||||
leaves.map((leave, index) => (
|
</button>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* TABLE AREA - Scrollable */}
|
||||||
<div className="flex-none flex justify-between items-center text-xs mt-2">
|
<div className="flex-1 overflow-y-auto no-scrollbar">
|
||||||
<div className="text-base-content/60">
|
{activeTab === "leave" && (
|
||||||
{leaves.length} leave{leaves.length !== 1 ? "s" : ""} found
|
<LeaveTable
|
||||||
</div>
|
isTracking={isTracking}
|
||||||
|
leaves={leaves}
|
||||||
<div className="flex items-center gap-4">
|
loading={loadingLeaves}
|
||||||
{lastRefreshed && (
|
lastRefreshed={lastRefreshed}
|
||||||
<div className="text-xs text-base-content/60">
|
onRefresh={handleRefresh}
|
||||||
Last updated: {formatLastRefreshed()}
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
{activeTab === "adjustments" && (
|
||||||
|
<AdjustmentsTable
|
||||||
<button
|
isTracking={isTracking}
|
||||||
onClick={handleRefresh}
|
adjustments={adjustments}
|
||||||
disabled={refreshing}
|
loading={loadingAdjustments}
|
||||||
className="btn btn-xs btn-circle bg-warning"
|
lastRefreshed={lastRefreshed}
|
||||||
title="Refresh Leaves"
|
onRefresh={handleRefresh}
|
||||||
>
|
/>
|
||||||
{refreshing ? (
|
)}
|
||||||
<Loader2 size={15} className="animate-spin text-base-content" />
|
|
||||||
) : (
|
|
||||||
<RefreshCw size={15} className="text-base-content" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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;
|
||||||
|
|
@ -662,7 +662,7 @@ 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 ? 'Submitting...' : 'Submit Request'}
|
{isSubmitting ? 'Submitting...' : 'Submit Request'}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -133,13 +133,13 @@ const Timesheets: React.FC = () => {
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching logs:", error);
|
console.error("Error fetching logs:", error);
|
||||||
setModalOpen(false)
|
setModalOpen(false)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ interface ProfileData {
|
||||||
paymentMethod: string;
|
paymentMethod: string;
|
||||||
payoneerAccount: string;
|
payoneerAccount: string;
|
||||||
veemAccount: string;
|
veemAccount: string;
|
||||||
wiseEmail:string
|
wiseEmail: string
|
||||||
_id: string;
|
_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,6 +217,11 @@ interface Window {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: any;
|
data?: any;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
getAdjustmentsList: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: any;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue