listing all the menus components and add the refresh button with the last time refresh list

This commit is contained in:
JaiPandey425 2025-12-02 17:16:50 +05:30
parent 17dcdec1c6
commit 0eea767e5e
16 changed files with 695 additions and 267 deletions

View File

@ -42,7 +42,7 @@ api.interceptors.response.use(
(response: AxiosResponse) => response.data, (response: AxiosResponse) => response.data,
async (error) => { async (error) => {
// console.error('[Axios Error]', error.message); // console.error('[Axios Error]', error.message);
if (error.response?.status === 401) { if (error.response?.status === 401 || 403) {
console.warn('Token expired — trigger refresh logic if needed'); console.warn('Token expired — trigger refresh logic if needed');
sendTokenExpired(); sendTokenExpired();
} }

View File

@ -29,7 +29,7 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
return { success: true }; return { success: true };
}); });
ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => { ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => {
return handleLogin(credentials); return handleLogin(credentials);
}); });
@ -46,8 +46,8 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
return { success: false, message: "Logout failed" }; return { success: false, message: "Logout failed" };
} }
}); });
@ -96,7 +96,7 @@ ipcMain.handle("get-project-list", async () => {
return { return {
success: false, success: false,
message: error?.message || String(error) || "Unknown error", message: error?.message || String(error) || "Unknown error",
status:error?.status status: error?.status
}; };
} }
}); });
@ -110,8 +110,8 @@ ipcMain.handle("start-project-tracking", async (event, payload) => {
const response = await api.post("/log/project", payload); const response = await api.post("/log/project", payload);
console.log(" Tracking start synced:", response); console.log(" Tracking start synced:", response);
return { success: true, data: response }; return { success: true, data: response };
} catch (error:any) { } catch (error: any) {
console.error("❌ Error syncing project start:", error); console.error("❌ Error syncing project start:", error);
// If API sent a response (like 400) // If API sent a response (like 400)
@ -261,14 +261,14 @@ ipcMain.handle("change-password", async (event, data) => {
// Build final payload // Build final payload
const payload = { const payload = {
userDetails: { userDetails: {
oldPassword: data.oldPassword, oldPassword: data.oldPassword,
password: data.password, password: data.password,
cPassword: data.cPassword, cPassword: data.cPassword,
}, },
}; };
console.log("Change Password payload" , payload) console.log("Change Password payload", payload)
try { try {
const response = await api.patch("/profile", payload); const response = await api.patch("/profile", payload);
console.log("Chnage-passowrd-response", response) console.log("Chnage-passowrd-response", response)
@ -303,20 +303,20 @@ ipcMain.handle("get-project-summary", async (_, { projectId, date }) => {
ipcMain.handle("create-timesheet", async (event, data) => { ipcMain.handle("create-timesheet", async (event, data) => {
console.log("data" , data) console.log("data", data)
try { try {
console.log("📩 Timesheet Payload:", data); console.log("📩 Timesheet Payload:", data);
const res = await api.post("/timesheet/adjustment/request", data); const res = await api.post("/timesheet/adjustment/request", data);
console.log(res , " Line 323") console.log(res, " Line 323")
return { return {
success: true, success: true,
message: "Timesheet created successfully", message: "Timesheet created successfully",
data: res, data: res,
}; };
} catch (error:any) { } catch (error: any) {
console.error("❌ Timesheet Error:", error); console.error("❌ Timesheet Error:", error);
return { return {
@ -402,7 +402,7 @@ ipcMain.handle("get-schedules", async () => {
ipcMain.handle("get-payslips", async (event, vaId: string) => { ipcMain.handle("get-payslips", async (event, vaId: string) => {
console.log("get-payslips",vaId) console.log("get-payslips", vaId)
try { try {
const response = await api.get(`/paySlip/${vaId}`); 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
};
}
});

View File

@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('electron', {
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")
} satisfies Window['electron']); } satisfies Window['electron']);

View File

@ -96,6 +96,7 @@ import HelpSupport from "./pages/HelpSupport/HelpSupport";
import Vpn from "./pages/Vpn/Vpn"; import Vpn from "./pages/Vpn/Vpn";
import Schedule from "./pages/Schedule/Schedule"; import Schedule from "./pages/Schedule/Schedule";
import PaysSlips from "./pages/PaySlips/PaysSlips"; import PaysSlips from "./pages/PaySlips/PaysSlips";
import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime";
const App: React.FC = () => { const App: React.FC = () => {
@ -138,6 +139,7 @@ const App: React.FC = () => {
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="projects" element={<Projects />} /> <Route path="projects" element={<Projects />} />
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} /> <Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="leave-adjustments/apply" element={<ApplyLeaveAndAdjustmentsTime/>} />
<Route path="timesheet" element={<Timesheets />} /> <Route path="timesheet" element={<Timesheets />} />
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
<Route path="vpn" element={<Vpn />} /> <Route path="vpn" element={<Vpn />} />

View File

@ -22,6 +22,21 @@
} }
} }
@layer utilities {
.thin-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.thin-scrollbar::-webkit-scrollbar-thumb {
background-color: hsl(var(--bc) / 0.3); /* base-content/30 */
border-radius: 999px;
}
.thin-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
}
input[type=number] { input[type=number] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }

View File

@ -746,7 +746,7 @@ const DashboardLayout = () => {
</div> </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"> <label className="swap swap-rotate p-2.5">
{/* this hidden checkbox controls the state */} {/* this hidden checkbox controls the state */}
<input type="checkbox" className="theme-controller" onChange={toggleTheme} <input type="checkbox" className="theme-controller" onChange={toggleTheme}

View File

@ -129,6 +129,7 @@ const Dashboard: React.FC = () => {
}; };
const fetchProjects = async () => { const fetchProjects = async () => {
setIsRefreshing(true)
try { try {
console.log("📡 Fetching projects...Dashboard"); console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList(); const res = await window.electron.getProjectList();
@ -175,6 +176,8 @@ const Dashboard: React.FC = () => {
} }
} catch (err) { } catch (err) {
console.error("❌ Error fetching projects:", err); console.error("❌ Error fetching projects:", err);
} finally {
setIsRefreshing(false)
} }
}; };
@ -183,9 +186,9 @@ const Dashboard: React.FC = () => {
}, []); }, []);
const handleRefresh = async () => { const handleRefresh = async () => {
setIsRefreshing(true); // setIsRefreshing(true);
await fetchProjects(); await fetchProjects();
setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback // setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback
}; };
const selectedData = selectedProject const selectedData = selectedProject

View File

@ -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

View File

@ -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'; interface LeaveItem {
import { useSelector } from 'react-redux'; leaveId: string;
import { RootState } from '../../store'; dateFrom: string;
import PtoLeaveTab from './tabs/PtoLeaveTab'; dateTo: string;
import SickLeaveTab from './tabs/SickLeaveTab'; totalDays: number;
import AdjustmentTab from './tabs/AdjustmentTab'; leaveType: string;
comment: string;
type TabType = 'pto' | 'sick' | 'adjustment'; status: string;
appliedOn: string;
reviewedAt: string | null;
rejectionReason: string | null;
}
const LeaveAndAdjustments: React.FC = () => { const LeaveAndAdjustments: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('pto'); const { isTracking } = useOutletContext<TrackingState>();
const [isSubmitting, setIsSubmitting] = useState(false); // 🚀 NEW const navigate = useNavigate();
const projects = useSelector((state: RootState) => state.projects.projectsData);
// ------- COMMON API CALL FOR BOTH TABS ------- const [leaves, setLeaves] = useState<LeaveItem[]>([]);
const submitLeave = async (payload: { const [loading, setLoading] = useState(true);
dateFrom: string; const [refreshing, setRefreshing] = useState(false);
dateTo: string; const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
leaveType: "PTO" | "SICK";
comment: string; const fetchLeaves = async () => {
}) => {
try { try {
setIsSubmitting(true); const res = await window.electron.getLeaveList();
console.log("Leave /sick Payload:", payload); if (res.success) {
setLeaves(res?.data?.leaves || []);
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
console.log("Leave Payload:", response)
if (response.success) {
return { success: true };
} }
setLastRefreshed(new Date());
return { } catch (e) {
success: false, console.error("Error fetching leaves:", e);
message: response.message
};
} catch (err) {
console.error(err);
return { success: false, message: "Request failed" };
} finally { } 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 ( return (
<div className="max-h-screen"> <main
<div className="w-full mx-auto"> className="flex flex-col gap-2 overflow-hidden"
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1> style={{
height: isTracking ? "calc(100vh - 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 */} {/* Table Container */}
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100"> <div
<a className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
role="tab" style={{
className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : "" height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px) ",
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} }}
onClick={() => setActiveTab("pto")} >
> <table className="table table-zebra w-full text-[11px]">
PTO Leave <thead className="sticky top-0 bg-base-200 z-10 text-[12px]">
</a> <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 <tbody>
role="tab" {loading ? (
className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : "" renderSkeletonRows()
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`} ) : refreshing && leaves.length === 0 ? (
onClick={() => setActiveTab("sick")} renderSkeletonRows()
> ) : leaves.length > 0 ? (
Sick Leave leaves.map((leave, index) => (
</a> <tr key={leave.leaveId}>
<td>{index + 1}</td>
<a <td>
role="tab" <span className="badge badge-xs badge-primary">{leave.leaveType}</span>
className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : "" </td>
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")} <td>
> <span>{leave.dateFrom}</span>
Adjustment Submission <br />
</a> <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>
{/* Main Container */} <div className="flex items-center gap-4">
<div className="rounded-lg bg-base-100 overflow-hidden"> {lastRefreshed && (
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]"> <div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
{activeTab === "pto" && ( <button
<PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} /> 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>
{activeTab === "sick" && (
<SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "adjustment" && (
<AdjustmentTab projects={projects} />
)}
</div>
</div> </div>
</div> </div>
</div> </main>
); );
}; };

View File

@ -4,6 +4,7 @@ import ReactQuill from 'react-quill-new';
import { quillModules } from '../../../utils/quillConfig'; import { quillModules } from '../../../utils/quillConfig';
import Quill from 'quill'; import Quill from 'quill';
import 'quill/dist/quill.snow.css'; import 'quill/dist/quill.snow.css';
import { useNavigate } from 'react-router-dom';
interface Project { interface Project {
projectId: string; projectId: string;
@ -48,6 +49,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const navigate = useNavigate()
// Field validation functions // Field validation functions
const validateProjectId = (projectId: string): string | undefined => { const validateProjectId = (projectId: string): string | undefined => {
@ -323,6 +325,12 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
useEffect(() => { useEffect(() => {
if (selectedProjectId && selectedDate) { if (selectedProjectId && selectedDate) {
setProjectSummary(null)
setStartTime("");
setEndTime("");
setComment("");
setFormErrors({});
setTotalAdjustment("0h 0m");
fetchLogs(); fetchLogs();
} else { } else {
setProjectSummary(null); setProjectSummary(null);
@ -454,6 +462,7 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
console.log("Timesheet Created Successfully"); console.log("Timesheet Created Successfully");
alert(res.message); alert(res.message);
resetForm(); resetForm();
navigate('/dashboard/leave-adjustments')
} else { } else {
setFormErrors(prev => ({ setFormErrors(prev => ({
...prev, ...prev,
@ -655,11 +664,6 @@ const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
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 ? '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'} {isSubmitting ? 'Submitting...' : 'Submit Request'}
</button> </button>
</div> </div>

View File

@ -4,6 +4,7 @@ import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new"; import ReactQuill from "react-quill-new";
import Quill from "quill"; import Quill from "quill";
import "quill/dist/quill.snow.css"; import "quill/dist/quill.snow.css";
import { useNavigate } from "react-router-dom";
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -23,6 +24,7 @@ const PtoLeaveTab: React.FC<{
}); });
const [serverError, setServerError] = useState(""); const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const navigate = useNavigate()
const [errors, setErrors] = useState<Partial<LeaveFormData>>({}); const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
@ -199,6 +201,7 @@ const PtoLeaveTab: React.FC<{
if (result.success) { if (result.success) {
resetForm(); resetForm();
alert("PTO submitted!"); alert("PTO submitted!");
navigate('/dashboard/leave-adjustments')
} else { } else {
setServerError(result.message || "Something went wrong"); setServerError(result.message || "Something went wrong");
} }

View File

@ -3,6 +3,7 @@ import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new"; import ReactQuill from "react-quill-new";
import Quill from "quill"; import Quill from "quill";
import "quill/dist/quill.snow.css"; // correct CSS import "quill/dist/quill.snow.css"; // correct CSS
import { useNavigate } from 'react-router-dom';
interface LeaveFormData { interface LeaveFormData {
fromDate: string; fromDate: string;
@ -22,6 +23,7 @@ const SickLeaveTab: React.FC<{
const [serverError, setServerError] = useState(""); const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null); const quillRef = useRef<any>(null);
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({}); const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
const editor = quillRef.current?.getEditor(); const editor = quillRef.current?.getEditor();
@ -189,6 +191,7 @@ const SickLeaveTab: React.FC<{
if (result.success) { if (result.success) {
resetForm(); resetForm();
alert("SICK submitted!"); alert("SICK submitted!");
navigate('/dashboard/leave-adjustments')
} else { } else {
setServerError(result.message || "Something went wrong"); setServerError(result.message || "Something went wrong");
} }

View File

@ -1,4 +1,7 @@
import React, { useEffect, useState } from "react"; 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"; import { DateTime } from "luxon";
interface ScheduleItem { interface ScheduleItem {
@ -12,50 +15,58 @@ interface ScheduleItem {
} }
const Schedule = () => { const Schedule = () => {
const { isTracking } = useOutletContext<TrackingState>();
const [rows, setRows] = useState<ScheduleItem[]>([]); const [rows, setRows] = useState<ScheduleItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null); 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 /** Format: Only Date */
const formatDateTime = (input: string | null) => {
if (!input) return "-";
return DateTime.fromISO(input)
.setZone("local")
.toLocaleString(DateTime.DATETIME_MED);
};
const formatOnlyDate = (input: string | null) => { const formatOnlyDate = (input: string | null) => {
if (!input) return "-"; if (!input) return "-";
return DateTime.fromISO(input) return DateTime.fromISO(input).toLocaleString(DateTime.DATE_MED);
.setZone("local")
.toLocaleString(DateTime.DATE_MED);
}; };
// Fetch schedules /** Format: Date + Time */
useEffect(() => { const formatDateTime = (input: string | null) => {
const loadSchedules = async () => { if (!input) return "-";
try { return DateTime.fromISO(input).toLocaleString(DateTime.DATETIME_MED);
const res = await window.electron.getSchedules(); };
console.log(res.result);
if (!res.success) { /** Fetch Data */
setError(res.message || "Failed to load schedules"); const fetchSchedules = async () => {
return; try {
} const res = await window.electron.getSchedules();
const list = res.result || []; // Use data array if (!res.success) {
setRows(list); setError(res.message || "Failed to load schedules");
} catch (err: any) { return;
setError(err.message || "Unexpected error");
} finally {
setLoading(false);
} }
};
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) => { const isActiveRow = (row: ScheduleItem) => {
if (!row.periodStart || !row.periodEnd) return false; if (!row.periodStart || !row.periodEnd) return false;
@ -66,50 +77,69 @@ const Schedule = () => {
return now >= start && now <= end; return now >= start && now <= end;
}; };
// Get Active Schedule (only one)
const activeSchedule = rows.find((r) => isActiveRow(r)); const activeSchedule = rows.find((r) => isActiveRow(r));
/** Skeleton Rows */
const renderSkeletonRows = () =>
Array.from({ length: 5 }).map((_, idx) => (
<tr key={`sk-${idx}`}>
<td><div className="skeleton h-4 w-6"></div></td>
<td><div className="skeleton h-4 w-32"></div></td>
<td><div className="skeleton h-4 w-28"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
</tr>
));
const formatLastRefreshed = () => {
if (!lastRefreshed) return "Never";
return DateTime.fromJSDate(lastRefreshed)
.toLocal()
.toFormat("HH:mm:ss"); // Only time
};
return ( return (
<div className="flex flex-col gap-2 max-h-screen"> <main
{/* Header */} className="flex flex-col gap-2 overflow-hidden"
<div className="flex items-center justify-between"> 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> <h2 className="text-md font-bold">Schedules</h2>
{activeSchedule && (
<div className="p-1 bg-secondary text-secondary-content rounded-lg"> <div className="flex items-center gap-3">
<p className="text-xs"> {activeSchedule && (
Pay Date: <span className="font-semibold"> <div className="px-2 py-1 bg-primary text-primary-content rounded-lg text-xs">
{formatOnlyDate(activeSchedule.ScheduleDate)} Pay Date: <b>{formatOnlyDate(activeSchedule.ScheduleDate)}</b>
</span> </div>
</p> )}
</div>
)} </div>
</div> </div>
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh] no-scrollbar"> {/* TABLE WRAPPER */}
{/* Loader */} <div
{loading && ( className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
<div className="flex justify-center items-center h-full"> style={{
<span className="loading loading-spinner text-primary"></span> height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
{/* Loading */}
{loading ? (
<table className="table w-full text-[12px]">
<tbody>{renderSkeletonRows()}</tbody>
</table>
) : error ? (
<div className="text-center text-red-500 py-8">{error}</div>
) : rows.length === 0 ? (
<div className="text-center py-8 text-base-content/50">
No schedules found
</div> </div>
)} ) : (
<div className="overflow-x-auto no-scrollbar">
{/* Error */} <table className="table w-full text-[12px] min-w-[600px]">
{!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]">
<thead className="sticky top-0 bg-base-200 z-10"> <thead className="sticky top-0 bg-base-200 z-10">
<tr> <tr>
<th>#</th> <th>#</th>
@ -123,22 +153,59 @@ const Schedule = () => {
{rows.map((row, index) => ( {rows.map((row, index) => (
<tr <tr
key={row.id} 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>{index + 1}</td>
<td> <td>
{formatOnlyDate(row.periodStart)} → {formatOnlyDate(row.periodEnd)} {formatOnlyDate(row.periodStart)} →{" "}
{formatOnlyDate(row.periodEnd)}
</td> </td>
<td>{formatDateTime(row.cutOffTime)}</td> <td>{formatDateTime(row.cutOffTime)}</td>
<td>{formatOnlyDate(row.ScheduleDate)}</td> <td>{formatOnlyDate(row.ScheduleDate)}</td>
</tr> </tr>
))} ))}
{refreshing && (
<tr>
<td colSpan={4} className="bg-base-200/40 text-center py-2">
<Loader2 className="animate-spin inline mr-2" size={16} />
Refreshing...
</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>
)} )}
</div> </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>
); );
}; };

View File

@ -82,14 +82,6 @@ const Settings: React.FC = () => {
</p> </p>
</div> </div>
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
💬 Skype
</p>
<p className="text-xs font-medium truncate" title={profile?.skype || "Not Linked"}>
{profile?.skype || "Not Linked"}
</p>
</div>
{/* Payment Info */} {/* Payment Info */}
<div className="p-2 rounded bg-base-200/40"> <div className="p-2 rounded bg-base-200/40">

View File

@ -1,8 +1,22 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { CalendarDays, Clock, FileText, CheckCircle, XCircle, Eye, EyeOff } from "lucide-react"; import {
CalendarDays,
Clock,
FileText,
CheckCircle,
XCircle,
Eye,
EyeOff,
RefreshCw,
Loader2
} from "lucide-react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../store"; import { RootState } from "../../store";
import ProjectLogsModal from "./ProjectLogsModal"; import ProjectLogsModal from "./ProjectLogsModal";
import { TrackingState } from "../../layout/DashboardLayout";
import { useOutletContext } from "react-router-dom";
import { DateTime } from "luxon";
interface TimesheetEntry { interface TimesheetEntry {
id: number; id: number;
@ -28,45 +42,78 @@ interface Project {
totalWorkTime: string; totalWorkTime: string;
totalWorkMs: number; totalWorkMs: number;
totalLogs: number; totalLogs: number;
status:string status: string
} }
const Timesheets: React.FC = () => { const Timesheets: React.FC = () => {
const { isTracking } = useOutletContext<TrackingState>();
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0); const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length; const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
const { windowLogs } = useSelector((state: RootState) => state.window); const { windowLogs } = useSelector((state: RootState) => state.window);
console.log("🧾 Timesheet in component:", windowLogs); console.log(" Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]); const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [selectedProject, setSelectedProject] = useState<string | null>(null); const [selectedProject, setSelectedProject] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>(""); const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [initialLoading, setInitialLoading] = useState(true);
const [tick, setTick] = useState(0);
const activeProjects = projectsData.filter(p => p.status === "ACTIVE");
useEffect(() => { useEffect(() => {
const fetchProjects = async () => { const interval = setInterval(() => {
try { setTick((t) => t + 1); // force re-render every second
console.log("📡 Fetching projects..."); }, 1000);
const res = await window.electron.getProjectList();
console.log("✅ Project list response:", res);
if (res?.success && Array.isArray(res.data)) { return () => clearInterval(interval);
// Store project list }, []);
setProjectsData(res.data);
} else { const formatLastRefreshed = () => {
console.warn(" Unexpected response:", res); if (!lastRefreshed) return "Never";
}
} catch (err) { return DateTime.fromJSDate(lastRefreshed)
console.error("Error fetching projects:", err); .toLocal()
.toFormat("HH:mm:ss"); // Only time
};
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...");
const res = await window.electron.getProjectList();
console.log("✅ Project list response:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
setProjectsData(res.data);
setLastRefreshed(new Date());
} else {
console.warn(" Unexpected response:", res);
} }
}; } catch (err) {
console.error("Error fetching projects:", err);
} finally {
setInitialLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchProjects(); fetchProjects();
}, []); }, []);
const handleRefresh = async () => {
setRefreshing(true);
await fetchProjects();
};
const handleViewLogs = async (projectID: string, projectName: string) => { const handleViewLogs = async (projectID: string, projectName: string) => {
try { try {
@ -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 ( return (
<div className="flex flex-col gap-2 max-h-screen "> <main
className="flex flex-col gap-2 overflow-hidden"
style={{
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
}}
>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex-none mb-2 flex justify-between items-center">
<div className="flex items-center gap-2"> <h2 className="text-md font-bold">Timesheets</h2>
<h2 className="text-md font-bold">Timesheets</h2>
</div>
</div> </div>
{/* Summary Cards */}
{/* <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Total Logged Hours</p>
<h3 className="text-lg font-semibold">{totalHours} hrs</h3>
</div>
<Clock size={20} className="text-primary" />
</div>
</div>
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Approved Days</p>
<h3 className="text-lg font-semibold">{approvedCount}</h3>
</div>
<CheckCircle size={20} className="text-success" />
</div>
</div>
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Pending Days</p>
<h3 className="text-lg font-semibold">
{timesheetData.length - approvedCount}
</h3>
</div>
<FileText size={20} className="text-warning" />
</div>
</div>
</div> */}
{/* Timesheet Table */} {/* Timesheet Table */}
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[80vh]"> <div
<table className="table table-zebra w-full text-[12px]"> className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
<thead className="sticky top-0 bg-base-200 z-10 "> style={{
height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr> <tr>
<th>#</th> <th>#</th>
<th>Project</th> <th>Project</th>
@ -161,34 +203,83 @@ const Timesheets: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{projectsData.map((entry, index) => ( {initialLoading ? (
<tr key={entry.projectId} className={`${entry.status ==='INACTIVE' && 'hidden'}`}> // Show skeleton while initially loading
<td>{index + 1}</td> renderSkeletonRows()
<td>{entry.projectName}</td> ) : refreshing && projectsData.length === 0 ? (
<td>{formatWorkTime(entry.totalWorkTime)}</td> // Show skeleton while refreshing with empty data
<td> renderSkeletonRows()
<button ) : (
disabled={formatWorkTime(entry.totalWorkTime) === "0h 0m"} // Show actual data
onClick={() => handleViewLogs(entry.projectId, entry.projectName)} projectsData.map((entry, index) => (
className="btn btn-ghost btn-xs" <tr
> key={entry.projectId}
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? <EyeOff size={16} /> : <Eye size={16} />} className={`${entry.status === "INACTIVE" && "hidden"}`}
</button> >
<td>{index + 1}</td>
<td>{entry.projectName}</td>
<td>{formatWorkTime(entry.totalWorkTime)}</td>
<td>
<button
disabled={formatWorkTime(entry.totalWorkTime) === "0h 0m"}
onClick={() =>
handleViewLogs(entry.projectId, entry.projectName)
}
className="btn btn-ghost btn-xs"
>
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
</td>
</tr>
))
)}
{refreshing && projectsData.length > 0 && (
// Show a single skeleton row at the top while refreshing with existing data
<tr>
<td colSpan={4} className="bg-base-200/50">
<div className="flex justify-center py-2">
<Loader2 size={20} className="animate-spin" />
<span className="ml-2 text-sm">Refreshing...</span>
</div>
</td> </td>
</tr> </tr>
))} )}
</tbody> </tbody>
</table> </table>
</div> </div>
{/* Footer Summary */} {/* Footer Summary */}
<div className="flex justify-end items-center text-xs mt-2"> <div className="flex-none flex justify-between items-center text-xs mt-2">
{/* <span className="text-base-content/60"> <div className="text-base-content/60">
Showing {timesheetData.length} entries {activeProjects.length} project{activeProjects.length !== 1 ? 's' : ''} shown
</span> */} </div>
<span className="text-base-content/60">
Last updated: {new Date().toLocaleDateString()} <div className="flex items-center gap-4">
</span> {lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh projects"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div> </div>
<ProjectLogsModal <ProjectLogsModal
@ -198,8 +289,8 @@ const Timesheets: React.FC = () => {
logs={logs} logs={logs}
loading={loading} loading={loading}
/> />
</div> </main>
); );
}; };
export default Timesheets; export default Timesheets;

5
types.d.ts vendored
View File

@ -211,6 +211,11 @@ interface Window {
data?: PayslipResponse[]; data?: PayslipResponse[];
message?: string; message?: string;
}>; }>;
getLeaveList: () => Promise<{
success: boolean;
data?: any;
}>;
}; };
} }