listing all the menus components and add the refresh button with the last time refresh list
This commit is contained in:
parent
17dcdec1c6
commit
0eea767e5e
|
|
@ -42,7 +42,7 @@ api.interceptors.response.use(
|
|||
(response: AxiosResponse) => response.data,
|
||||
async (error) => {
|
||||
// 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');
|
||||
sendTokenExpired();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ ipcMain.handle("get-project-list", async () => {
|
|||
return {
|
||||
success: false,
|
||||
message: error?.message || String(error) || "Unknown error",
|
||||
status:error?.status
|
||||
status: error?.status
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -111,7 +111,7 @@ ipcMain.handle("start-project-tracking", async (event, payload) => {
|
|||
|
||||
console.log(" Tracking start synced:", response);
|
||||
return { success: true, data: response };
|
||||
} catch (error:any) {
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error syncing project start:", error);
|
||||
|
||||
// If API sent a response (like 400)
|
||||
|
|
@ -261,14 +261,14 @@ ipcMain.handle("change-password", async (event, data) => {
|
|||
|
||||
// Build final payload
|
||||
const payload = {
|
||||
userDetails: {
|
||||
oldPassword: data.oldPassword,
|
||||
password: data.password,
|
||||
cPassword: data.cPassword,
|
||||
},
|
||||
};
|
||||
userDetails: {
|
||||
oldPassword: data.oldPassword,
|
||||
password: data.password,
|
||||
cPassword: data.cPassword,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Change Password payload" , payload)
|
||||
console.log("Change Password payload", payload)
|
||||
try {
|
||||
const response = await api.patch("/profile", payload);
|
||||
console.log("Chnage-passowrd-response", response)
|
||||
|
|
@ -303,20 +303,20 @@ ipcMain.handle("get-project-summary", async (_, { projectId, date }) => {
|
|||
|
||||
ipcMain.handle("create-timesheet", async (event, data) => {
|
||||
|
||||
console.log("data" , data)
|
||||
console.log("data", data)
|
||||
try {
|
||||
console.log("📩 Timesheet Payload:", data);
|
||||
|
||||
const res = await api.post("/timesheet/adjustment/request", data);
|
||||
|
||||
console.log(res , " Line 323")
|
||||
console.log(res, " Line 323")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Timesheet created successfully",
|
||||
data: res,
|
||||
};
|
||||
} catch (error:any) {
|
||||
} catch (error: any) {
|
||||
console.error("❌ Timesheet Error:", error);
|
||||
|
||||
return {
|
||||
|
|
@ -402,7 +402,7 @@ ipcMain.handle("get-schedules", async () => {
|
|||
|
||||
|
||||
ipcMain.handle("get-payslips", async (event, vaId: string) => {
|
||||
console.log("get-payslips",vaId)
|
||||
console.log("get-payslips", vaId)
|
||||
try {
|
||||
const response = await api.get(`/paySlip/${vaId}`);
|
||||
|
||||
|
|
@ -433,6 +433,34 @@ ipcMain.handle("get-payslips", async (event, vaId: string) => {
|
|||
|
||||
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('electron', {
|
|||
getSchedules: () => ipcRenderer.invoke("get-schedules"),
|
||||
|
||||
getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId),
|
||||
getLeaveList: () => ipcRenderer.invoke("get-leave-list")
|
||||
|
||||
} satisfies Window['electron']);
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ import HelpSupport from "./pages/HelpSupport/HelpSupport";
|
|||
import Vpn from "./pages/Vpn/Vpn";
|
||||
import Schedule from "./pages/Schedule/Schedule";
|
||||
import PaysSlips from "./pages/PaySlips/PaysSlips";
|
||||
import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime";
|
||||
|
||||
|
||||
const App: React.FC = () => {
|
||||
|
|
@ -138,6 +139,7 @@ const App: React.FC = () => {
|
|||
<Route index element={<Dashboard />} />
|
||||
<Route path="projects" element={<Projects />} />
|
||||
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
|
||||
<Route path="leave-adjustments/apply" element={<ApplyLeaveAndAdjustmentsTime/>} />
|
||||
<Route path="timesheet" element={<Timesheets />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="vpn" element={<Vpn />} />
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -746,7 +746,7 @@ const DashboardLayout = () => {
|
|||
</div>
|
||||
))}
|
||||
|
||||
<div className={`absolute ${!isTracking ? "bottom-13" : "bottom-5"} `}>
|
||||
<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}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ const Dashboard: React.FC = () => {
|
|||
};
|
||||
|
||||
const fetchProjects = async () => {
|
||||
setIsRefreshing(true)
|
||||
try {
|
||||
console.log("📡 Fetching projects...Dashboard");
|
||||
const res = await window.electron.getProjectList();
|
||||
|
|
@ -175,6 +176,8 @@ const Dashboard: React.FC = () => {
|
|||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error fetching projects:", err);
|
||||
} finally {
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -183,9 +186,9 @@ const Dashboard: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
// setIsRefreshing(true);
|
||||
await fetchProjects();
|
||||
setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback
|
||||
// setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback
|
||||
};
|
||||
|
||||
const selectedData = selectedProject
|
||||
|
|
|
|||
|
|
@ -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,103 +1,203 @@
|
|||
import React, { useEffect, useState } 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 React, { 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';
|
||||
|
||||
type TabType = 'pto' | 'sick' | 'adjustment';
|
||||
interface LeaveItem {
|
||||
leaveId: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
totalDays: number;
|
||||
leaveType: string;
|
||||
comment: string;
|
||||
status: string;
|
||||
appliedOn: string;
|
||||
reviewedAt: string | null;
|
||||
rejectionReason: string | null;
|
||||
}
|
||||
|
||||
const LeaveAndAdjustments: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('pto');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false); // 🚀 NEW
|
||||
const projects = useSelector((state: RootState) => state.projects.projectsData);
|
||||
const { isTracking } = useOutletContext<TrackingState>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// ------- COMMON API CALL FOR BOTH TABS -------
|
||||
const submitLeave = async (payload: {
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
leaveType: "PTO" | "SICK";
|
||||
comment: string;
|
||||
}) => {
|
||||
const [leaves, setLeaves] = useState<LeaveItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
|
||||
|
||||
const fetchLeaves = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const res = await window.electron.getLeaveList();
|
||||
|
||||
console.log("Leave /sick Payload:", payload);
|
||||
|
||||
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
|
||||
console.log("Leave Payload:", response)
|
||||
if (response.success) {
|
||||
return { success: true };
|
||||
if (res.success) {
|
||||
setLeaves(res?.data?.leaves || []);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: response.message
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return { success: false, message: "Request failed" };
|
||||
setLastRefreshed(new Date());
|
||||
} catch (e) {
|
||||
console.error("Error fetching leaves:", e);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
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>
|
||||
));
|
||||
};
|
||||
|
||||
const formatLastRefreshed = () => {
|
||||
if (!lastRefreshed) return "Never";
|
||||
|
||||
return DateTime.fromJSDate(lastRefreshed)
|
||||
.toLocal()
|
||||
.toFormat("HH:mm:ss"); // Only time
|
||||
};
|
||||
return (
|
||||
<div className="max-h-screen">
|
||||
<div className="w-full mx-auto">
|
||||
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
||||
<main
|
||||
className="flex flex-col gap-2 overflow-hidden"
|
||||
style={{
|
||||
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex-none mb-2 flex justify-between items-center">
|
||||
<h2 className="text-md font-bold">Leave & Adjust Timesheet</h2>
|
||||
<button className="btn btn-xs btn-primary" onClick={handleApplyLeaveAndAdjust}>
|
||||
Apply
|
||||
</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>
|
||||
{/* 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) ",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
renderSkeletonRows()
|
||||
) : refreshing && leaves.length === 0 ? (
|
||||
renderSkeletonRows()
|
||||
) : leaves.length > 0 ? (
|
||||
leaves.map((leave, index) => (
|
||||
<tr key={leave.leaveId}>
|
||||
<td>{index + 1}</td>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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]">
|
||||
<div className="flex items-center gap-4">
|
||||
{lastRefreshed && (
|
||||
<div className="text-xs text-base-content/60">
|
||||
Last updated: {formatLastRefreshed()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "pto" && (
|
||||
<PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
|
||||
<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" />
|
||||
)}
|
||||
|
||||
{activeTab === "sick" && (
|
||||
<SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
|
||||
)}
|
||||
|
||||
{activeTab === "adjustment" && (
|
||||
<AdjustmentTab projects={projects} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import ReactQuill from 'react-quill-new';
|
|||
import { quillModules } from '../../../utils/quillConfig';
|
||||
import Quill from 'quill';
|
||||
import 'quill/dist/quill.snow.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface Project {
|
||||
projectId: string;
|
||||
|
|
@ -48,6 +49,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
|||
const [comment, setComment] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const quillRef = useRef<any>(null);
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Field validation functions
|
||||
const validateProjectId = (projectId: string): string | undefined => {
|
||||
|
|
@ -323,6 +325,12 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (selectedProjectId && selectedDate) {
|
||||
setProjectSummary(null)
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
setComment("");
|
||||
setFormErrors({});
|
||||
setTotalAdjustment("0h 0m");
|
||||
fetchLogs();
|
||||
} else {
|
||||
setProjectSummary(null);
|
||||
|
|
@ -454,6 +462,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
|||
console.log("Timesheet Created Successfully");
|
||||
alert(res.message);
|
||||
resetForm();
|
||||
navigate('/dashboard/leave-adjustments')
|
||||
} else {
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
|
|
@ -655,11 +664,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
|||
disabled={isSubmitting}
|
||||
className={`btn btn-primary flex-1 hover:shadow-lg transition-all duration-300 ${isSubmitting ? 'loading' : ''}`}
|
||||
>
|
||||
{!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'}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { quillModules } from "../../../utils/quillConfig";
|
|||
import ReactQuill from "react-quill-new";
|
||||
import Quill from "quill";
|
||||
import "quill/dist/quill.snow.css";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface LeaveFormData {
|
||||
fromDate: string;
|
||||
|
|
@ -23,6 +24,7 @@ const PtoLeaveTab: React.FC<{
|
|||
});
|
||||
const [serverError, setServerError] = useState("");
|
||||
const quillRef = useRef<any>(null);
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
|
||||
|
||||
|
|
@ -199,6 +201,7 @@ const PtoLeaveTab: React.FC<{
|
|||
if (result.success) {
|
||||
resetForm();
|
||||
alert("PTO submitted!");
|
||||
navigate('/dashboard/leave-adjustments')
|
||||
} else {
|
||||
setServerError(result.message || "Something went wrong");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { quillModules } from "../../../utils/quillConfig";
|
|||
import ReactQuill from "react-quill-new";
|
||||
import Quill from "quill";
|
||||
import "quill/dist/quill.snow.css"; // correct CSS
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface LeaveFormData {
|
||||
fromDate: string;
|
||||
|
|
@ -22,6 +23,7 @@ const SickLeaveTab: React.FC<{
|
|||
const [serverError, setServerError] = useState("");
|
||||
const quillRef = useRef<any>(null);
|
||||
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const editor = quillRef.current?.getEditor();
|
||||
|
|
@ -189,6 +191,7 @@ const SickLeaveTab: React.FC<{
|
|||
if (result.success) {
|
||||
resetForm();
|
||||
alert("SICK submitted!");
|
||||
navigate('/dashboard/leave-adjustments')
|
||||
} else {
|
||||
setServerError(result.message || "Something went wrong");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
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 {
|
||||
|
|
@ -12,50 +15,58 @@ interface ScheduleItem {
|
|||
}
|
||||
|
||||
const Schedule = () => {
|
||||
const { isTracking } = useOutletContext<TrackingState>();
|
||||
|
||||
const [rows, setRows] = useState<ScheduleItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// const [lastUpdated, setLastUpdated] = useState<DateTime | null>(null);
|
||||
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
|
||||
|
||||
// Format helpers using Luxon
|
||||
const formatDateTime = (input: string | null) => {
|
||||
if (!input) return "-";
|
||||
return DateTime.fromISO(input)
|
||||
.setZone("local")
|
||||
.toLocaleString(DateTime.DATETIME_MED);
|
||||
};
|
||||
|
||||
/** Format: Only Date */
|
||||
const formatOnlyDate = (input: string | null) => {
|
||||
if (!input) return "-";
|
||||
return DateTime.fromISO(input)
|
||||
.setZone("local")
|
||||
.toLocaleString(DateTime.DATE_MED);
|
||||
return DateTime.fromISO(input).toLocaleString(DateTime.DATE_MED);
|
||||
};
|
||||
|
||||
// Fetch schedules
|
||||
useEffect(() => {
|
||||
const loadSchedules = async () => {
|
||||
try {
|
||||
const res = await window.electron.getSchedules();
|
||||
console.log(res.result);
|
||||
/** Format: Date + Time */
|
||||
const formatDateTime = (input: string | null) => {
|
||||
if (!input) return "-";
|
||||
return DateTime.fromISO(input).toLocaleString(DateTime.DATETIME_MED);
|
||||
};
|
||||
|
||||
if (!res.success) {
|
||||
setError(res.message || "Failed to load schedules");
|
||||
return;
|
||||
}
|
||||
/** Fetch Data */
|
||||
const fetchSchedules = async () => {
|
||||
try {
|
||||
const res = await window.electron.getSchedules();
|
||||
|
||||
const list = res.result || []; // Use data array
|
||||
setRows(list);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Unexpected error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!res.success) {
|
||||
setError(res.message || "Failed to load schedules");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loadSchedules();
|
||||
setRows(res.result || []);
|
||||
setLastRefreshed(new Date());
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Unexpected error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** Refresh Button */
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await fetchSchedules();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSchedules();
|
||||
}, []);
|
||||
|
||||
// Determine if current date is within the schedule period
|
||||
/** Determine Active Period */
|
||||
const isActiveRow = (row: ScheduleItem) => {
|
||||
if (!row.periodStart || !row.periodEnd) return false;
|
||||
|
||||
|
|
@ -66,50 +77,69 @@ const Schedule = () => {
|
|||
return now >= start && now <= end;
|
||||
};
|
||||
|
||||
// Get Active Schedule (only one)
|
||||
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 (
|
||||
<div className="flex flex-col gap-2 max-h-screen">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<main
|
||||
className="flex flex-col gap-2 overflow-hidden"
|
||||
style={{
|
||||
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
|
||||
}}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="flex justify-between items-center">
|
||||
<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 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 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>
|
||||
{/* TABLE WRAPPER */}
|
||||
<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)",
|
||||
}}
|
||||
>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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 w-full text-[12px] min-w-[600px]">
|
||||
) : (
|
||||
<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">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
|
|
@ -123,22 +153,59 @@ const Schedule = () => {
|
|||
{rows.map((row, index) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={isActiveRow(row) ? "bg-secondary font-semibold" : ""}
|
||||
className={
|
||||
isActiveRow(row)
|
||||
? "bg-primary text-primary-content font-semibold"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<td>{index + 1}</td>
|
||||
<td>
|
||||
{formatOnlyDate(row.periodStart)} → {formatOnlyDate(row.periodEnd)}
|
||||
{formatOnlyDate(row.periodStart)} →{" "}
|
||||
{formatOnlyDate(row.periodEnd)}
|
||||
</td>
|
||||
<td>{formatDateTime(row.cutOffTime)}</td>
|
||||
<td>{formatOnlyDate(row.ScheduleDate)}</td>
|
||||
</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>
|
||||
</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>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -82,14 +82,6 @@ const Settings: React.FC = () => {
|
|||
</p>
|
||||
</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 */}
|
||||
<div className="p-2 rounded bg-base-200/40">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,22 @@
|
|||
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 { RootState } from "../../store";
|
||||
import ProjectLogsModal from "./ProjectLogsModal";
|
||||
import { TrackingState } from "../../layout/DashboardLayout";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
|
||||
interface TimesheetEntry {
|
||||
id: number;
|
||||
|
|
@ -28,45 +42,78 @@ interface Project {
|
|||
totalWorkTime: string;
|
||||
totalWorkMs: number;
|
||||
totalLogs: number;
|
||||
status:string
|
||||
status: string
|
||||
}
|
||||
|
||||
const Timesheets: React.FC = () => {
|
||||
const { isTracking } = useOutletContext<TrackingState>();
|
||||
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
|
||||
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
|
||||
|
||||
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 [logs, setLogs] = useState([]);
|
||||
const [selectedProject, setSelectedProject] = useState<string | null>(null);
|
||||
const [selectedProjectName, setSelectedProjectName] = useState<string>("");
|
||||
const [modalOpen, setModalOpen] = 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(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
console.log("📡 Fetching projects...");
|
||||
const res = await window.electron.getProjectList();
|
||||
console.log("✅ Project list response:", res);
|
||||
const interval = setInterval(() => {
|
||||
setTick((t) => t + 1); // force re-render every second
|
||||
}, 1000);
|
||||
|
||||
if (res?.success && Array.isArray(res.data)) {
|
||||
// Store project list
|
||||
setProjectsData(res.data);
|
||||
} else {
|
||||
console.warn(" Unexpected response:", res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching projects:", err);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const formatLastRefreshed = () => {
|
||||
if (!lastRefreshed) return "Never";
|
||||
|
||||
return DateTime.fromJSDate(lastRefreshed)
|
||||
.toLocal()
|
||||
.toFormat("HH:mm:ss"); // Only time
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchProjects = async () => {
|
||||
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();
|
||||
}, []);
|
||||
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await fetchProjects();
|
||||
};
|
||||
|
||||
const handleViewLogs = async (projectID: string, projectName: string) => {
|
||||
try {
|
||||
|
|
@ -105,54 +152,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 (
|
||||
<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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-md font-bold">Timesheets</h2>
|
||||
</div>
|
||||
<div className="flex-none mb-2 flex justify-between items-center">
|
||||
<h2 className="text-md font-bold">Timesheets</h2>
|
||||
</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 */}
|
||||
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[80vh]">
|
||||
<table className="table table-zebra w-full text-[12px]">
|
||||
<thead className="sticky top-0 bg-base-200 z-10 ">
|
||||
<div
|
||||
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
|
||||
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>
|
||||
<th>#</th>
|
||||
<th>Project</th>
|
||||
|
|
@ -161,34 +203,83 @@ const Timesheets: React.FC = () => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projectsData.map((entry, index) => (
|
||||
<tr key={entry.projectId} className={`${entry.status ==='INACTIVE' && 'hidden'}`}>
|
||||
<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>
|
||||
{initialLoading ? (
|
||||
// Show skeleton while initially loading
|
||||
renderSkeletonRows()
|
||||
) : refreshing && projectsData.length === 0 ? (
|
||||
// Show skeleton while refreshing with empty data
|
||||
renderSkeletonRows()
|
||||
) : (
|
||||
// Show actual data
|
||||
projectsData.map((entry, index) => (
|
||||
<tr
|
||||
key={entry.projectId}
|
||||
className={`${entry.status === "INACTIVE" && "hidden"}`}
|
||||
>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Footer Summary */}
|
||||
<div className="flex justify-end items-center text-xs mt-2">
|
||||
{/* <span className="text-base-content/60">
|
||||
Showing {timesheetData.length} entries
|
||||
</span> */}
|
||||
<span className="text-base-content/60">
|
||||
Last updated: {new Date().toLocaleDateString()}
|
||||
</span>
|
||||
<div className="flex-none flex justify-between items-center text-xs mt-2">
|
||||
<div className="text-base-content/60">
|
||||
{activeProjects.length} project{activeProjects.length !== 1 ? 's' : ''} shown
|
||||
</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 projects"
|
||||
>
|
||||
{refreshing ? (
|
||||
<Loader2 size={15} className="animate-spin text-base-content" />
|
||||
) : (
|
||||
<RefreshCw size={15} className="text-base-content" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectLogsModal
|
||||
|
|
@ -198,7 +289,7 @@ const Timesheets: React.FC = () => {
|
|||
logs={logs}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,11 @@ interface Window {
|
|||
data?: PayslipResponse[];
|
||||
message?: string;
|
||||
}>;
|
||||
|
||||
getLeaveList: () => Promise<{
|
||||
success: boolean;
|
||||
data?: any;
|
||||
}>;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue