pto-and-leave-dark-light-mode-toggle
This commit is contained in:
parent
e7696925e7
commit
3a34520152
|
|
@ -89,7 +89,6 @@ import Projects from "./pages/Projects/Projects";
|
||||||
import DashboardLayout from "./layout/DashboardLayout";
|
import DashboardLayout from "./layout/DashboardLayout";
|
||||||
import ProtectedRoute from "./routes/ProtectedRoute";
|
import ProtectedRoute from "./routes/ProtectedRoute";
|
||||||
import PublicRoute from "./routes/PublicRoute";
|
import PublicRoute from "./routes/PublicRoute";
|
||||||
import { ThemeToggle } from "./helper/ThemeToggle";
|
|
||||||
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
||||||
import Timesheets from "./pages/Timesheets/Timesheets";
|
import Timesheets from "./pages/Timesheets/Timesheets";
|
||||||
import Settings from "./pages/Settings/Settings";
|
import Settings from "./pages/Settings/Settings";
|
||||||
|
|
@ -98,15 +97,13 @@ import Vpn from "./pages/Vpn/Vpn";
|
||||||
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const theme = ThemeToggle();
|
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
// ✅ Get user directly from Redux
|
// ✅ Get user directly from Redux
|
||||||
const user = useSelector((state: RootState) => state.user.user);
|
const user = useSelector((state: RootState) => state.user.user);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(loadUserFromStorage());
|
dispatch(loadUserFromStorage());
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
// src/hooks/ThemeToggle.js
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
export const ThemeToggle = () => {
|
export const useTheme = () => {
|
||||||
const [theme, setTheme] = useState("light");
|
const [theme, setTheme] = useState("light");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
// Initial check
|
|
||||||
setTheme(darkQuery.matches ? "dark" : "light");
|
setTheme(darkQuery.matches ? "dark" : "light");
|
||||||
|
|
||||||
// Listen for changes
|
const handleChange = (e:any) => {
|
||||||
const handleChange = (e: MediaQueryListEvent) => {
|
|
||||||
setTheme(e.matches ? "dark" : "light");
|
setTheme(e.matches ? "dark" : "light");
|
||||||
};
|
};
|
||||||
|
|
||||||
darkQuery.addEventListener("change", handleChange);
|
darkQuery.addEventListener?.("change", handleChange);
|
||||||
|
darkQuery.addListener?.(handleChange); // For older Electron
|
||||||
|
|
||||||
return () => darkQuery.removeEventListener("change", handleChange);
|
return () => {
|
||||||
|
darkQuery.removeEventListener?.("change", handleChange);
|
||||||
|
darkQuery.removeListener?.(handleChange);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return theme;
|
// Apply theme to <html>
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return { theme, setTheme };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice
|
||||||
import { clearUser } from "../store/userSlice";
|
import { clearUser } from "../store/userSlice";
|
||||||
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
import { setError, setLoading, setProfile } from "../store/profileSlice";
|
||||||
import { canStartTracking } from "../utils/projectLimits";
|
import { canStartTracking } from "../utils/projectLimits";
|
||||||
|
import { useTheme } from "../helper/ThemeToggle";
|
||||||
|
import LogoutModal from "../modal/LogoutModal";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -79,6 +81,11 @@ export interface TrackingState {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardLayout = () => {
|
const DashboardLayout = () => {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(theme === "dark" ? "light" : "dark");
|
||||||
|
};
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -114,6 +121,11 @@ const DashboardLayout = () => {
|
||||||
const toggleWindowSize = useRef(true);
|
const toggleWindowSize = useRef(true);
|
||||||
const [loadingStart, setLoadingStart] = useState(false);
|
const [loadingStart, setLoadingStart] = useState(false);
|
||||||
const [loadingStop, setLoadingStop] = useState(false);
|
const [loadingStop, setLoadingStop] = useState(false);
|
||||||
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
|
||||||
// Create a ref to store the stop function so it can be called from the hook
|
// Create a ref to store the stop function so it can be called from the hook
|
||||||
|
|
@ -155,9 +167,9 @@ const DashboardLayout = () => {
|
||||||
if (timerRef.current) {
|
if (timerRef.current) {
|
||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
console.log("⏹️ Timer cleared");
|
console.log("Timer cleared");
|
||||||
} else {
|
} else {
|
||||||
console.log("ℹ️ No active timer to clear");
|
console.log("No active timer to clear");
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 3: Send LOGOUT to backend
|
// STEP 3: Send LOGOUT to backend
|
||||||
|
|
@ -222,6 +234,7 @@ const DashboardLayout = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Alert Message 1")
|
||||||
// Show alert to user
|
// Show alert to user
|
||||||
alert(
|
alert(
|
||||||
`⏰ Daily Limit Reached!\n\n` +
|
`⏰ Daily Limit Reached!\n\n` +
|
||||||
|
|
@ -229,7 +242,7 @@ const DashboardLayout = () => {
|
||||||
`Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` +
|
`Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` +
|
||||||
`Your tracking has been stopped automatically because you've reached the daily time limit for this project.`
|
`Your tracking has been stopped automatically because you've reached the daily time limit for this project.`
|
||||||
);
|
);
|
||||||
|
console.log("Alert Message 2")
|
||||||
// Stop tracking
|
// Stop tracking
|
||||||
await handleStopTracking();
|
await handleStopTracking();
|
||||||
}, [selectedProject, handleStopTracking]);
|
}, [selectedProject, handleStopTracking]);
|
||||||
|
|
@ -625,7 +638,7 @@ const DashboardLayout = () => {
|
||||||
{/* Layout Body */}
|
{/* Layout Body */}
|
||||||
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : "h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : "h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4">
|
<aside className="w-20 bg-base-100 border-r border-base-300 flex flex-col items-center py-4 space-y-4 relative">
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}>
|
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}>
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|
@ -643,9 +656,34 @@ const DashboardLayout = () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<div className="absolute bottom-13 ">
|
||||||
|
<label className="swap swap-rotate p-2.5">
|
||||||
|
{/* this hidden checkbox controls the state */}
|
||||||
|
<input type="checkbox" className="theme-controller" onChange={toggleTheme}
|
||||||
|
checked={theme === "dark"} />
|
||||||
|
|
||||||
|
{/* sun icon */}
|
||||||
|
<svg
|
||||||
|
className="swap-off h-5 w-5 fill-current"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
||||||
|
</svg>
|
||||||
|
{/* moon icon */}
|
||||||
|
<svg
|
||||||
|
className="swap-on h-5 w-5 fill-current"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!isTracking && (
|
{!isTracking && (
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={() => setShowLogoutModal(true)}
|
||||||
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right"
|
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-150 hover:bg-error/10 text-error hover:text-error mt-auto cursor-pointer tooltip tooltip-right"
|
||||||
data-tip={"Logout"}
|
data-tip={"Logout"}
|
||||||
>
|
>
|
||||||
|
|
@ -658,7 +696,15 @@ const DashboardLayout = () => {
|
||||||
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
|
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
|
||||||
<Outlet context={trackingState} />
|
<Outlet context={trackingState} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<LogoutModal
|
||||||
|
open={showLogoutModal}
|
||||||
|
onClose={() => setShowLogoutModal(false)}
|
||||||
|
onConfirm={handleLogout}
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface LogoutModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogoutModal: React.FC<LogoutModalProps> = ({ open, onClose, onConfirm }) => {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog open className="modal">
|
||||||
|
<div className="modal-box max-w-[75vw]">
|
||||||
|
<h3 className="font-bold text-sm text-error">Confirm Logout</h3>
|
||||||
|
|
||||||
|
<p className="py-2 text-xs font-medium">
|
||||||
|
Are you sure you want to logout? You will need to login again to access your dashboard.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="modal-action">
|
||||||
|
|
||||||
|
{/* Cancel */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-xs"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Logout */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-error btn-xs"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogoutModal;
|
||||||
|
|
@ -1,769 +1,71 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import AdjustmentModal from './AdjustmentModal';
|
|
||||||
import { RootState } from '../../store';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { DateTime } from 'luxon';
|
import { RootState } from '../../store';
|
||||||
|
import PtoLeaveTab from './tabs/PtoLeaveTab';
|
||||||
|
import SickLeaveTab from './tabs/SickLeaveTab';
|
||||||
|
import AdjustmentTab from './tabs/AdjustmentTab';
|
||||||
|
|
||||||
interface LeaveFormData {
|
type TabType = 'pto' | 'sick' | 'adjustment';
|
||||||
fromDate: string;
|
|
||||||
toDate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdjustmentEntry {
|
|
||||||
id: number;
|
|
||||||
date: string;
|
|
||||||
loggedHours: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LogEntry {
|
|
||||||
id: number;
|
|
||||||
date: string;
|
|
||||||
hours: number;
|
|
||||||
task: string;
|
|
||||||
status: "Completed" | "In Progress" | "Pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectSummaryResponse {
|
|
||||||
totalWorkedHours: string;
|
|
||||||
totalWorkedHoursDetailed: string;
|
|
||||||
totalWorkedHoursDecimal: string;
|
|
||||||
totalWorkedMinutes: string;
|
|
||||||
dedicatedHours: number;
|
|
||||||
pendingHours: string;
|
|
||||||
firstLoginTime: string;
|
|
||||||
lastLogoutTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: LogEntry[] = [
|
|
||||||
{ id: 1, date: "2025-11-01", hours: 8, task: "Frontend Development", status: "Completed" },
|
|
||||||
{ id: 2, date: "2025-11-02", hours: 6, task: "API Integration", status: "In Progress" },
|
|
||||||
{ id: 3, date: "2025-11-03", hours: 7.5, task: "UI Testing", status: "Pending" },
|
|
||||||
{ id: 4, date: "2025-11-04", hours: 9, task: "Bug Fixing", status: "Completed" },
|
|
||||||
{ id: 5, date: "2025-11-05", hours: 5.5, task: "Code Review", status: "Completed" },
|
|
||||||
];
|
|
||||||
|
|
||||||
type TabType = 'create_and_adjust_timesheet' | 'sick' | 'adjustment';
|
|
||||||
|
|
||||||
// Validation interface
|
|
||||||
interface FormErrors {
|
|
||||||
selectedProjectId?: string;
|
|
||||||
selectedDate?: string;
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
general?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LeaveAndAdjustments: React.FC = () => {
|
const LeaveAndAdjustments: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('create_and_adjust_timesheet');
|
const [activeTab, setActiveTab] = useState<TabType>('pto');
|
||||||
const projects = useSelector((state: RootState) => state.projects.projectsData);
|
const projects = useSelector((state: RootState) => state.projects.projectsData);
|
||||||
|
|
||||||
console.log("LeaveAndAdjustments", projects)
|
console.log("LeaveAndAdjustments", projects);
|
||||||
|
|
||||||
// PTO State
|
|
||||||
const [ptoLeave, setPtoLeave] = useState({
|
|
||||||
fromDate: '',
|
|
||||||
toDate: '',
|
|
||||||
});
|
|
||||||
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
|
||||||
|
|
||||||
// Sick Leave State
|
|
||||||
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
|
||||||
fromDate: '',
|
|
||||||
toDate: '',
|
|
||||||
});
|
|
||||||
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState("")
|
|
||||||
const [selectedDate, setSelectedDate] = useState("")
|
|
||||||
const [totalAdjustment, setTotalAdjustment] = useState("0h 0m");
|
|
||||||
const [pendingOverride, setPendingOverride] = useState<string | null>(null);
|
|
||||||
const [loadingSummary, setLoadingSummary] = useState(false);
|
|
||||||
|
|
||||||
// Form Validation State
|
|
||||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
|
||||||
|
|
||||||
// Project Summary State
|
|
||||||
const [projectSummary, setProjectSummary] = useState<ProjectSummaryResponse | null>(null);
|
|
||||||
const [startTime, setStartTime] = useState("");
|
|
||||||
const [endTime, setEndTime] = useState("");
|
|
||||||
|
|
||||||
// Adjustment State
|
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState<string>('week');
|
|
||||||
const [adjustmentData, setAdjustmentData] = useState<AdjustmentEntry[]>([
|
|
||||||
{ id: 1, date: '2025-11-01', loggedHours: 8 },
|
|
||||||
{ id: 2, date: '2025-11-02', loggedHours: 7.5 },
|
|
||||||
{ id: 3, date: '2025-11-03', loggedHours: 8 },
|
|
||||||
]);
|
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
|
||||||
const [editHours, setEditHours] = useState<string>('');
|
|
||||||
|
|
||||||
// Validation function
|
|
||||||
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
|
||||||
const errors: Partial<LeaveFormData> = {};
|
|
||||||
|
|
||||||
if (!data.fromDate) {
|
|
||||||
errors.fromDate = 'From date is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.toDate) {
|
|
||||||
errors.toDate = 'To date is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) {
|
|
||||||
errors.toDate = 'To date must be after from date';
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to convert UTC time to local time in HH:mm format
|
|
||||||
const convertToLocalTime = (utcTimeString: string): string => {
|
|
||||||
if (!utcTimeString) return '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse the UTC time and convert to local timezone
|
|
||||||
const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal();
|
|
||||||
|
|
||||||
// Format to HH:mm (24-hour format)
|
|
||||||
return localTime.toFormat('HH:mm');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error converting time:', error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Field validation functions
|
|
||||||
const validateProjectId = (projectId: string): string | undefined => {
|
|
||||||
if (!projectId) return 'Project selection is required';
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateDate = (date: string): string | undefined => {
|
|
||||||
if (!date) return 'Date is required';
|
|
||||||
|
|
||||||
const selectedDateObj = new Date(date);
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
if (selectedDateObj > today) return 'Cannot select future dates';
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateTime = (time: string, fieldName: string): string | undefined => {
|
|
||||||
if (!time) return `${fieldName} is required`;
|
|
||||||
|
|
||||||
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
|
||||||
if (!timeRegex.test(time)) return 'Invalid time format';
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateTimeRange = (start: string, end: string): string | undefined => {
|
|
||||||
if (!start || !end) return undefined;
|
|
||||||
|
|
||||||
const startTime = DateTime.fromFormat(start, "HH:mm");
|
|
||||||
const endTime = DateTime.fromFormat(end, "HH:mm");
|
|
||||||
|
|
||||||
if (!startTime.isValid || !endTime.isValid) return 'Invalid time values';
|
|
||||||
if (endTime <= startTime) return 'End time must be after start time';
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateDedicatedHours = (start: string, end: string): string | undefined => {
|
|
||||||
if (!start || !end || !projectSummary) return undefined;
|
|
||||||
|
|
||||||
const startTime = DateTime.fromFormat(start, "HH:mm");
|
|
||||||
const endTime = DateTime.fromFormat(end, "HH:mm");
|
|
||||||
|
|
||||||
if (!startTime.isValid || !endTime.isValid) return undefined;
|
|
||||||
|
|
||||||
const diff = endTime.diff(startTime, ["hours", "minutes"]).toObject();
|
|
||||||
const workedHours = (diff.hours || 0) + (diff.minutes || 0) / 60;
|
|
||||||
|
|
||||||
if (workedHours > projectSummary.dedicatedHours) {
|
|
||||||
return `Cannot exceed project's dedicated hours (${projectSummary.dedicatedHours} hrs)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Real-time field validation
|
|
||||||
const validateField = (field: keyof FormErrors, value: string) => {
|
|
||||||
let error: string | undefined;
|
|
||||||
|
|
||||||
switch (field) {
|
|
||||||
case 'selectedProjectId':
|
|
||||||
error = validateProjectId(value);
|
|
||||||
break;
|
|
||||||
case 'selectedDate':
|
|
||||||
error = validateDate(value);
|
|
||||||
break;
|
|
||||||
case 'startTime':
|
|
||||||
error = validateTime(value, 'Start time');
|
|
||||||
if (!error && endTime) {
|
|
||||||
error = validateTimeRange(value, endTime) || validateDedicatedHours(value, endTime);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'endTime':
|
|
||||||
error = validateTime(value, 'End time');
|
|
||||||
if (!error && startTime) {
|
|
||||||
error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: error
|
|
||||||
}));
|
|
||||||
|
|
||||||
return !error; // Return true if valid
|
|
||||||
};
|
|
||||||
|
|
||||||
// Comprehensive form validation
|
|
||||||
const validateForm = (): boolean => {
|
|
||||||
const errors: FormErrors = {};
|
|
||||||
|
|
||||||
// Validate individual fields
|
|
||||||
errors.selectedProjectId = validateProjectId(selectedProjectId);
|
|
||||||
errors.selectedDate = validateDate(selectedDate);
|
|
||||||
errors.startTime = validateTime(startTime, 'Start time');
|
|
||||||
errors.endTime = validateTime(endTime, 'End time');
|
|
||||||
|
|
||||||
// Validate time range and dedicated hours
|
|
||||||
if (!errors.startTime && !errors.endTime) {
|
|
||||||
errors.startTime = validateTimeRange(startTime, endTime);
|
|
||||||
if (!errors.startTime) {
|
|
||||||
errors.startTime = validateDedicatedHours(startTime, endTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out undefined errors
|
|
||||||
const filteredErrors = Object.fromEntries(
|
|
||||||
Object.entries(errors).filter(([_, value]) => value !== undefined)
|
|
||||||
) as FormErrors;
|
|
||||||
|
|
||||||
setFormErrors(filteredErrors);
|
|
||||||
return Object.keys(filteredErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePtoSubmit = async () => {
|
|
||||||
const errors = validateLeaveForm(ptoLeave);
|
|
||||||
|
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
setPtoErrors(errors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Submitting PTO:', ptoLeave);
|
|
||||||
alert('PTO request submitted successfully!');
|
|
||||||
|
|
||||||
setPtoLeave({ fromDate: '', toDate: '' });
|
|
||||||
setPtoErrors({});
|
|
||||||
} catch (error) {
|
|
||||||
alert('Failed to submit request. Please try again.');
|
|
||||||
console.error('Error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sick Leave Handlers
|
|
||||||
const handleSickChange = (field: keyof LeaveFormData, value: string) => {
|
|
||||||
setSickLeave(prev => ({ ...prev, [field]: value }));
|
|
||||||
if (sickErrors[field]) {
|
|
||||||
setSickErrors(prev => ({ ...prev, [field]: undefined }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSickSubmit = async () => {
|
|
||||||
const errors = validateLeaveForm(sickLeave);
|
|
||||||
|
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
setSickErrors(errors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Submitting Sick Leave:', sickLeave);
|
|
||||||
alert('Sick Leave request submitted successfully!');
|
|
||||||
|
|
||||||
setSickLeave({ fromDate: '', toDate: '' });
|
|
||||||
setSickErrors({});
|
|
||||||
} catch (error) {
|
|
||||||
alert('Failed to submit request. Please try again.');
|
|
||||||
console.error('Error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [editingEntry, setEditingEntry] = useState<AdjustmentEntry | null>(null);
|
|
||||||
const [newHours, setNewHours] = useState<number>(0);
|
|
||||||
const [comment, setComment] = useState<string>("")
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const handlePeriodChange = (value: string) => setSelectedPeriod(value);
|
|
||||||
|
|
||||||
const handleEditClick = (entry: AdjustmentEntry) => {
|
|
||||||
setEditingEntry(entry);
|
|
||||||
const modal = document.getElementById("editModal") as HTMLDialogElement;
|
|
||||||
modal?.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
|
||||||
setEditingEntry(null);
|
|
||||||
setNewHours(0)
|
|
||||||
const modal = document.getElementById("editModal") as HTMLDialogElement;
|
|
||||||
modal?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
console.log("Saving:", { ...editingEntry, updatedHours: newHours });
|
|
||||||
handleCloseModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProjectSelect = (projectId: string) => {
|
|
||||||
console.log("handleProjectSelect", projectId)
|
|
||||||
setSelectedProjectId(projectId);
|
|
||||||
validateField('selectedProjectId', projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectedDate = (value: string) => {
|
|
||||||
setSelectedDate(value);
|
|
||||||
validateField('selectedDate', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const debugTimezoneInfo = (utcTimeString: string, check: string) => {
|
|
||||||
const utcTime = DateTime.fromISO(utcTimeString, { zone: 'utc' });
|
|
||||||
const localTime = utcTime.toLocal();
|
|
||||||
|
|
||||||
console.log(`-------------------------Timezone Debug Info: ${check}----------------------`);
|
|
||||||
console.log('UTC Time:', utcTime.toString());
|
|
||||||
console.log('Local Time:', localTime.toString());
|
|
||||||
console.log('System Timezone:', localTime.zoneName);
|
|
||||||
console.log('Formatted Local Time:', localTime.toFormat('HH:mm'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
|
||||||
try {
|
|
||||||
console.log("Sending =>", selectedProjectId, selectedDate);
|
|
||||||
setLoadingSummary(true);
|
|
||||||
|
|
||||||
const res = await window.electron.getProjectSummary(
|
|
||||||
selectedProjectId,
|
|
||||||
selectedDate
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("📥 Summary Response:", res);
|
|
||||||
|
|
||||||
if (res.success) {
|
|
||||||
setProjectSummary(res?.data?.summary);
|
|
||||||
|
|
||||||
// Convert UTC times to local timezone and set them
|
|
||||||
if (res?.data?.summary?.firstLoginTime) {
|
|
||||||
const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime);
|
|
||||||
debugTimezoneInfo(localStartTime, "start");
|
|
||||||
setStartTime(localStartTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res?.data?.summary?.lastLogoutTime) {
|
|
||||||
const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime);
|
|
||||||
debugTimezoneInfo(localEndTime, "end");
|
|
||||||
setEndTime(localEndTime);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("⚠️ Summary API failed:", res.message);
|
|
||||||
setProjectSummary(null);
|
|
||||||
setStartTime("");
|
|
||||||
setEndTime("");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Error fetching project summary:", err);
|
|
||||||
setProjectSummary(null);
|
|
||||||
setStartTime("");
|
|
||||||
setEndTime("");
|
|
||||||
} finally {
|
|
||||||
setLoadingSummary(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedProjectId && selectedDate) {
|
|
||||||
fetchLogs();
|
|
||||||
} else {
|
|
||||||
setProjectSummary(null);
|
|
||||||
setStartTime("");
|
|
||||||
setEndTime("");
|
|
||||||
}
|
|
||||||
}, [selectedProjectId, selectedDate]);
|
|
||||||
|
|
||||||
// Auto calculations --- derived state
|
|
||||||
useEffect(() => {
|
|
||||||
if (!projectSummary) return; // No API summary yet
|
|
||||||
if (!startTime || !endTime) return;
|
|
||||||
|
|
||||||
// Clear previous time range errors when times change
|
|
||||||
if (formErrors.startTime?.includes('time') || formErrors.startTime?.includes('dedicated')) {
|
|
||||||
setFormErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
startTime: undefined
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Validation: prevent end < start
|
|
||||||
const start = DateTime.fromFormat(startTime, "HH:mm");
|
|
||||||
const end = DateTime.fromFormat(endTime, "HH:mm");
|
|
||||||
|
|
||||||
if (!start.isValid || !end.isValid) return;
|
|
||||||
|
|
||||||
if (end < start) {
|
|
||||||
setTotalAdjustment("0h 0m");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Calculate worked minutes from UI input
|
|
||||||
const diff = end.diff(start, ["hours", "minutes"]).toObject();
|
|
||||||
const workedHours = diff.hours || 0;
|
|
||||||
const workedMinutes = diff.minutes || 0;
|
|
||||||
|
|
||||||
const totalWorkedDecimal = workedHours + workedMinutes / 60;
|
|
||||||
|
|
||||||
// 3. Compare with server original total
|
|
||||||
const originalWorkedDecimal = Number(projectSummary.totalWorkedHoursDecimal);
|
|
||||||
const adjustment = totalWorkedDecimal - originalWorkedDecimal;
|
|
||||||
|
|
||||||
// 4. Build readable format (always positive for display)
|
|
||||||
const adjH = Math.floor(Math.abs(adjustment));
|
|
||||||
const adjM = Math.round((Math.abs(adjustment) % 1) * 60);
|
|
||||||
|
|
||||||
const formatted = `${adjustment >= 0 ? "+" : "-"}${adjH}h ${adjM}m`;
|
|
||||||
setTotalAdjustment(formatted);
|
|
||||||
|
|
||||||
// 5. Update Pending Hours -> dedicated - new worked
|
|
||||||
const newPending = projectSummary.dedicatedHours - totalWorkedDecimal;
|
|
||||||
const newPendingRounded = newPending.toFixed(2);
|
|
||||||
|
|
||||||
setPendingOverride(newPendingRounded);
|
|
||||||
|
|
||||||
}, [startTime, endTime, projectSummary]);
|
|
||||||
|
|
||||||
const handleStartTimeChange = (value: string) => {
|
|
||||||
console.log("Start changed:", value);
|
|
||||||
setStartTime(value);
|
|
||||||
validateField('startTime', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEndTimeChange = (value: string) => {
|
|
||||||
console.log("End changed:", value);
|
|
||||||
setEndTime(value);
|
|
||||||
validateField('endTime', value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
|
|
||||||
if (isSubmitting) return;
|
|
||||||
|
|
||||||
// Clear previous errors
|
|
||||||
setFormErrors({});
|
|
||||||
|
|
||||||
// Validate all fields
|
|
||||||
if (!validateForm()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final dedicated hours check
|
|
||||||
const dedicatedHoursError = validateDedicatedHours(startTime, endTime);
|
|
||||||
if (dedicatedHoursError) {
|
|
||||||
setFormErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
startTime: dedicatedHoursError
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
projectId: selectedProjectId,
|
|
||||||
date: selectedDate,
|
|
||||||
loginTime: `${startTime}:00`,
|
|
||||||
logoutTime: `${endTime}:00`,
|
|
||||||
comment
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Submitting payload:", payload);
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Add await here to properly wait for the response
|
|
||||||
const res = await window.electron.createTimesheet(payload);
|
|
||||||
console.log("API Response:", res);
|
|
||||||
|
|
||||||
if (res?.success) {
|
|
||||||
console.log("Timesheet Created Successfully");
|
|
||||||
|
|
||||||
// Show success message
|
|
||||||
alert(res.message);
|
|
||||||
|
|
||||||
// Reset form or navigate away
|
|
||||||
setSelectedProjectId("");
|
|
||||||
setSelectedDate("");
|
|
||||||
setStartTime("");
|
|
||||||
setEndTime("");
|
|
||||||
setComment("");
|
|
||||||
setFormErrors({});
|
|
||||||
setTotalAdjustment("0h 0m");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
setFormErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
general: res?.message || "Failed to create timesheet"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("Submission error:", err);
|
|
||||||
setFormErrors(prev => ({
|
|
||||||
...prev,
|
|
||||||
general: err.message || "Something went wrong while saving timesheet"
|
|
||||||
}));
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDescription = (value: string) => {
|
|
||||||
setComment(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-h-screen">
|
<div className="max-h-screen">
|
||||||
<div className="w-full mx-auto">
|
<div className="w-full mx-auto">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className=" ">
|
<div className="">
|
||||||
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* General Error Alert */}
|
{/* Tabs Navigation */}
|
||||||
{formErrors.general && (
|
<div role="tablist" className="tabs tabs-border w-full mb-1">
|
||||||
<div className="alert alert-error shadow-sm my-1 px-6 py-1 text-xs text-base-100">
|
<a
|
||||||
<span>{formErrors.general}</span>
|
role="tab"
|
||||||
|
className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : ""}`}
|
||||||
|
onClick={() => setActiveTab("pto")}
|
||||||
|
>
|
||||||
|
PTO Leave
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
role="tab"
|
||||||
|
className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""}`}
|
||||||
|
onClick={() => setActiveTab("sick")}
|
||||||
|
>
|
||||||
|
Sick Leave
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
role="tab"
|
||||||
|
className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""}`}
|
||||||
|
onClick={() => setActiveTab("adjustment")}
|
||||||
|
>
|
||||||
|
Adjustment Submission
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<div className="rounded-lg bg-base-100 overflow-hidden">
|
<div className="rounded-lg bg-base-100 overflow-hidden">
|
||||||
{/* Tabs Navigation */}
|
|
||||||
<div className="flex ">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('create_and_adjust_timesheet')}
|
|
||||||
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'create_and_adjust_timesheet'
|
|
||||||
? ' text-primary border-b-2 border-primary'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-center gap-2 cursor-pointer">
|
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
Create or Adjust Timesheet
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
|
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
|
||||||
{/* PTO Tab */}
|
{activeTab === 'pto' && (
|
||||||
{activeTab === 'create_and_adjust_timesheet' && (
|
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="max-w-4xl ">
|
<PtoLeaveTab />
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{/* Project Selection */}
|
|
||||||
<div className="form-control col-span-2">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm">Select project</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className={`select select-sm ${formErrors.selectedProjectId ? 'select-error' : ''}`}
|
|
||||||
value={selectedProjectId}
|
|
||||||
onChange={(e) => handleProjectSelect(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="" disabled>
|
|
||||||
Pick a project
|
|
||||||
</option>
|
|
||||||
{projects.map((item, index) => (
|
|
||||||
<option key={item.projectId || index} value={item.projectId}>
|
|
||||||
{item.projectName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{formErrors.selectedProjectId && (
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-error font-medium">{formErrors.selectedProjectId}</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Date Selection */}
|
|
||||||
<div className="form-control col-span-2">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm">Select timesheet date</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={selectedDate}
|
|
||||||
onChange={(e) => handleSelectedDate(e.target.value)}
|
|
||||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`}
|
|
||||||
/>
|
|
||||||
{formErrors.selectedDate && (
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-error font-medium">{formErrors.selectedDate}</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Total Time */}
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm ">Current total time</span>
|
|
||||||
</label>
|
|
||||||
{loadingSummary ? (
|
|
||||||
<div className="skeleton h-9 w-full"></div>
|
|
||||||
) :
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={
|
|
||||||
projectSummary
|
|
||||||
? `${pendingOverride ? (projectSummary.dedicatedHours - Number(pendingOverride)).toFixed(2) : projectSummary.totalWorkedHoursDecimal} hrs`
|
|
||||||
: "0 hrs"
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
className="input input-bordered input-sm w-full bg-base-200"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Pending Time */}
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm">Current pending time</span>
|
|
||||||
</label>
|
|
||||||
{loadingSummary ? (
|
|
||||||
<div className="skeleton h-9 w-full"></div>
|
|
||||||
) :
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={
|
|
||||||
projectSummary
|
|
||||||
? `${pendingOverride ?? projectSummary.pendingHours} hrs`
|
|
||||||
: "0 hrs"
|
|
||||||
}
|
|
||||||
readOnly
|
|
||||||
className="input input-bordered input-sm w-full bg-base-200"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Start Time */}
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm">Start time</span>
|
|
||||||
</label>
|
|
||||||
{loadingSummary ? (
|
|
||||||
<div className="skeleton h-9 w-full"></div>
|
|
||||||
) :
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={startTime}
|
|
||||||
onChange={(e) => handleStartTimeChange(e.target.value)}
|
|
||||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{formErrors.startTime && (
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-error font-medium">{formErrors.startTime}</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* End Time */}
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm ">End time</span>
|
|
||||||
</label>
|
|
||||||
{loadingSummary ? (
|
|
||||||
<div className="skeleton h-9 w-full"></div>
|
|
||||||
) :
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
value={endTime}
|
|
||||||
onChange={(e) => handleEndTimeChange(e.target.value)}
|
|
||||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{formErrors.endTime && (
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text-alt text-error font-medium">{formErrors.endTime}</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Total Time Adjustment */}
|
|
||||||
<div className="form-control col-span-2">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm">Total time adjustment</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Calculated automatically"
|
|
||||||
value={totalAdjustment}
|
|
||||||
readOnly
|
|
||||||
className="input input-bordered input-sm w-full bg-base-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div className="form-control flex flex-col gap-1">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all"
|
|
||||||
placeholder="Please enter the task name or leave notes to help us better understand your request."
|
|
||||||
value={comment}
|
|
||||||
onChange={(e) => handleDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Other tabs remain the same */}
|
|
||||||
{activeTab === 'sick' && (
|
{activeTab === 'sick' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
{/* Sick leave content remains the same */}
|
<SickLeaveTab />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'adjustment' && (
|
{activeTab === 'adjustment' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
{/* Adjustment content remains the same */}
|
<AdjustmentTab projects={projects} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -786,14 +88,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
animation: fade-in 0.3s ease-out;
|
animation: fade-in 0.3s ease-out;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<AdjustmentModal
|
|
||||||
editingEntry={editingEntry}
|
|
||||||
newHours={newHours}
|
|
||||||
setNewHours={setNewHours}
|
|
||||||
handleCloseModal={handleCloseModal}
|
|
||||||
handleSave={handleSave}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,537 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectSummaryResponse {
|
||||||
|
totalWorkedHours: string;
|
||||||
|
totalWorkedHoursDetailed: string;
|
||||||
|
totalWorkedHoursDecimal: string;
|
||||||
|
totalWorkedMinutes: string;
|
||||||
|
dedicatedHours: number;
|
||||||
|
pendingHours: string;
|
||||||
|
firstLoginTime: string;
|
||||||
|
lastLogoutTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
selectedProjectId?: string;
|
||||||
|
selectedDate?: string;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
general?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdjustmentTabProps {
|
||||||
|
projects: Project[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||||
|
const [selectedDate, setSelectedDate] = useState("");
|
||||||
|
const [totalAdjustment, setTotalAdjustment] = useState("0h 0m");
|
||||||
|
const [pendingOverride, setPendingOverride] = useState<string | null>(null);
|
||||||
|
const [loadingSummary, setLoadingSummary] = useState(false);
|
||||||
|
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||||
|
const [projectSummary, setProjectSummary] = useState<ProjectSummaryResponse | null>(null);
|
||||||
|
const [startTime, setStartTime] = useState("");
|
||||||
|
const [endTime, setEndTime] = useState("");
|
||||||
|
const [comment, setComment] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Field validation functions
|
||||||
|
const validateProjectId = (projectId: string): string | undefined => {
|
||||||
|
if (!projectId) return 'Project selection is required';
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateDate = (date: string): string | undefined => {
|
||||||
|
if (!date) return 'Date is required';
|
||||||
|
|
||||||
|
const selectedDateObj = new Date(date);
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (selectedDateObj > today) return 'Cannot select future dates';
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateTime = (time: string, fieldName: string): string | undefined => {
|
||||||
|
if (!time) return `${fieldName} is required`;
|
||||||
|
|
||||||
|
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
||||||
|
if (!timeRegex.test(time)) return 'Invalid time format';
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateTimeRange = (start: string, end: string): string | undefined => {
|
||||||
|
if (!start || !end) return undefined;
|
||||||
|
|
||||||
|
const startTime = DateTime.fromFormat(start, "HH:mm");
|
||||||
|
const endTime = DateTime.fromFormat(end, "HH:mm");
|
||||||
|
|
||||||
|
if (!startTime.isValid || !endTime.isValid) return 'Invalid time values';
|
||||||
|
if (endTime <= startTime) return 'End time must be after start time';
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateDedicatedHours = (start: string, end: string): string | undefined => {
|
||||||
|
if (!start || !end || !projectSummary) return undefined;
|
||||||
|
|
||||||
|
const startTime = DateTime.fromFormat(start, "HH:mm");
|
||||||
|
const endTime = DateTime.fromFormat(end, "HH:mm");
|
||||||
|
|
||||||
|
if (!startTime.isValid || !endTime.isValid) return undefined;
|
||||||
|
|
||||||
|
const diff = endTime.diff(startTime, ["hours", "minutes"]).toObject();
|
||||||
|
const workedHours = (diff.hours || 0) + (diff.minutes || 0) / 60;
|
||||||
|
|
||||||
|
if (workedHours > projectSummary.dedicatedHours) {
|
||||||
|
return `Cannot exceed project's dedicated hours (${projectSummary.dedicatedHours} hrs)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Real-time field validation
|
||||||
|
const validateField = (field: keyof FormErrors, value: string) => {
|
||||||
|
let error: string | undefined;
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case 'selectedProjectId':
|
||||||
|
error = validateProjectId(value);
|
||||||
|
break;
|
||||||
|
case 'selectedDate':
|
||||||
|
error = validateDate(value);
|
||||||
|
break;
|
||||||
|
case 'startTime':
|
||||||
|
error = validateTime(value, 'Start time');
|
||||||
|
if (!error && endTime) {
|
||||||
|
error = validateTimeRange(value, endTime) || validateDedicatedHours(value, endTime);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'endTime':
|
||||||
|
error = validateTime(value, 'End time');
|
||||||
|
if (!error && startTime) {
|
||||||
|
error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: error
|
||||||
|
}));
|
||||||
|
|
||||||
|
return !error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Comprehensive form validation
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const errors: FormErrors = {};
|
||||||
|
|
||||||
|
errors.selectedProjectId = validateProjectId(selectedProjectId);
|
||||||
|
errors.selectedDate = validateDate(selectedDate);
|
||||||
|
errors.startTime = validateTime(startTime, 'Start time');
|
||||||
|
errors.endTime = validateTime(endTime, 'End time');
|
||||||
|
|
||||||
|
if (!errors.startTime && !errors.endTime) {
|
||||||
|
errors.startTime = validateTimeRange(startTime, endTime);
|
||||||
|
if (!errors.startTime) {
|
||||||
|
errors.startTime = validateDedicatedHours(startTime, endTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredErrors = Object.fromEntries(
|
||||||
|
Object.entries(errors).filter(([_, value]) => value !== undefined)
|
||||||
|
) as FormErrors;
|
||||||
|
|
||||||
|
setFormErrors(filteredErrors);
|
||||||
|
return Object.keys(filteredErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToLocalTime = (utcTimeString: string): string => {
|
||||||
|
if (!utcTimeString) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal();
|
||||||
|
return localTime.toFormat('HH:mm');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting time:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Sending =>", selectedProjectId, selectedDate);
|
||||||
|
setLoadingSummary(true);
|
||||||
|
|
||||||
|
const res = await window.electron.getProjectSummary(
|
||||||
|
selectedProjectId,
|
||||||
|
selectedDate
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("📥 Summary Response:", res);
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
setProjectSummary(res?.data?.summary);
|
||||||
|
|
||||||
|
if (res?.data?.summary?.firstLoginTime) {
|
||||||
|
const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime);
|
||||||
|
setStartTime(localStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res?.data?.summary?.lastLogoutTime) {
|
||||||
|
const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime);
|
||||||
|
setEndTime(localEndTime);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ Summary API failed:", res.message);
|
||||||
|
setProjectSummary(null);
|
||||||
|
setStartTime("");
|
||||||
|
setEndTime("");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Error fetching project summary:", err);
|
||||||
|
setProjectSummary(null);
|
||||||
|
setStartTime("");
|
||||||
|
setEndTime("");
|
||||||
|
} finally {
|
||||||
|
setLoadingSummary(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedProjectId && selectedDate) {
|
||||||
|
fetchLogs();
|
||||||
|
} else {
|
||||||
|
setProjectSummary(null);
|
||||||
|
setStartTime("");
|
||||||
|
setEndTime("");
|
||||||
|
}
|
||||||
|
}, [selectedProjectId, selectedDate]);
|
||||||
|
|
||||||
|
// Auto calculations
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectSummary) return;
|
||||||
|
if (!startTime || !endTime) return;
|
||||||
|
|
||||||
|
if (formErrors.startTime?.includes('time') || formErrors.startTime?.includes('dedicated')) {
|
||||||
|
setFormErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
startTime: undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = DateTime.fromFormat(startTime, "HH:mm");
|
||||||
|
const end = DateTime.fromFormat(endTime, "HH:mm");
|
||||||
|
|
||||||
|
if (!start.isValid || !end.isValid) return;
|
||||||
|
|
||||||
|
if (end < start) {
|
||||||
|
setTotalAdjustment("0h 0m");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = end.diff(start, ["hours", "minutes"]).toObject();
|
||||||
|
const workedHours = diff.hours || 0;
|
||||||
|
const workedMinutes = diff.minutes || 0;
|
||||||
|
|
||||||
|
const totalWorkedDecimal = workedHours + workedMinutes / 60;
|
||||||
|
const originalWorkedDecimal = Number(projectSummary.totalWorkedHoursDecimal);
|
||||||
|
const adjustment = totalWorkedDecimal - originalWorkedDecimal;
|
||||||
|
|
||||||
|
const adjH = Math.floor(Math.abs(adjustment));
|
||||||
|
const adjM = Math.round((Math.abs(adjustment) % 1) * 60);
|
||||||
|
|
||||||
|
const formatted = `${adjustment >= 0 ? "+" : "-"}${adjH}h ${adjM}m`;
|
||||||
|
setTotalAdjustment(formatted);
|
||||||
|
|
||||||
|
const newPending = projectSummary.dedicatedHours - totalWorkedDecimal;
|
||||||
|
const newPendingRounded = newPending.toFixed(2);
|
||||||
|
|
||||||
|
setPendingOverride(newPendingRounded);
|
||||||
|
}, [startTime, endTime, projectSummary]);
|
||||||
|
|
||||||
|
const handleProjectSelect = (projectId: string) => {
|
||||||
|
console.log("handleProjectSelect", projectId);
|
||||||
|
setSelectedProjectId(projectId);
|
||||||
|
validateField('selectedProjectId', projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectedDate = (value: string) => {
|
||||||
|
setSelectedDate(value);
|
||||||
|
validateField('selectedDate', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartTimeChange = (value: string) => {
|
||||||
|
console.log("Start changed:", value);
|
||||||
|
setStartTime(value);
|
||||||
|
validateField('startTime', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndTimeChange = (value: string) => {
|
||||||
|
console.log("End changed:", value);
|
||||||
|
setEndTime(value);
|
||||||
|
validateField('endTime', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDescription = (value: string) => {
|
||||||
|
setComment(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
setFormErrors({});
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dedicatedHoursError = validateDedicatedHours(startTime, endTime);
|
||||||
|
if (dedicatedHoursError) {
|
||||||
|
setFormErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
startTime: dedicatedHoursError
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
projectId: selectedProjectId,
|
||||||
|
date: selectedDate,
|
||||||
|
loginTime: `${startTime}:00`,
|
||||||
|
logoutTime: `${endTime}:00`,
|
||||||
|
comment
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Submitting payload:", payload);
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await window.electron.createTimesheet(payload);
|
||||||
|
console.log("API Response:", res);
|
||||||
|
|
||||||
|
if (res?.success) {
|
||||||
|
console.log("Timesheet Created Successfully");
|
||||||
|
alert(res.message);
|
||||||
|
|
||||||
|
setSelectedProjectId("");
|
||||||
|
setSelectedDate("");
|
||||||
|
setStartTime("");
|
||||||
|
setEndTime("");
|
||||||
|
setComment("");
|
||||||
|
setFormErrors({});
|
||||||
|
setTotalAdjustment("0h 0m");
|
||||||
|
} else {
|
||||||
|
setFormErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
general: res?.message || "Failed to create timesheet"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Submission error:", err);
|
||||||
|
setFormErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
general: err.message || "Something went wrong while saving timesheet"
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl ">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* General Error Alert */}
|
||||||
|
{formErrors.general && (
|
||||||
|
<div className="alert alert-error shadow-sm my-1 px-6 py-1 text-xs text-base-100">
|
||||||
|
<span>{formErrors.general}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* Project Selection */}
|
||||||
|
<div className="form-control col-span-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Select project</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={`select select-sm ${formErrors.selectedProjectId ? 'select-error' : ''}`}
|
||||||
|
value={selectedProjectId}
|
||||||
|
onChange={(e) => handleProjectSelect(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Pick a project
|
||||||
|
</option>
|
||||||
|
{projects.map((item, index) => (
|
||||||
|
<option key={item.projectId || index} value={item.projectId}>
|
||||||
|
{item.projectName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{formErrors.selectedProjectId && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{formErrors.selectedProjectId}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date Selection */}
|
||||||
|
<div className="form-control col-span-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Select timesheet date</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={(e) => handleSelectedDate(e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`}
|
||||||
|
/>
|
||||||
|
{formErrors.selectedDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{formErrors.selectedDate}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Total Time */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm ">Current total time</span>
|
||||||
|
</label>
|
||||||
|
{loadingSummary ? (
|
||||||
|
<div className="skeleton h-9 w-full"></div>
|
||||||
|
) :
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={
|
||||||
|
projectSummary
|
||||||
|
? `${pendingOverride ? (projectSummary.dedicatedHours - Number(pendingOverride)).toFixed(2) : projectSummary.totalWorkedHoursDecimal} hrs`
|
||||||
|
: "0 hrs"
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
className="input input-bordered input-sm w-full bg-base-200"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current Pending Time */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Current pending time</span>
|
||||||
|
</label>
|
||||||
|
{loadingSummary ? (
|
||||||
|
<div className="skeleton h-9 w-full"></div>
|
||||||
|
) :
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={
|
||||||
|
projectSummary
|
||||||
|
? `${pendingOverride ?? projectSummary.pendingHours} hrs`
|
||||||
|
: "0 hrs"
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
className="input input-bordered input-sm w-full bg-base-200"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Start Time */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Start time</span>
|
||||||
|
</label>
|
||||||
|
{loadingSummary ? (
|
||||||
|
<div className="skeleton h-9 w-full"></div>
|
||||||
|
) :
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={startTime}
|
||||||
|
onChange={(e) => handleStartTimeChange(e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{formErrors.startTime && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{formErrors.startTime}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* End Time */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm ">End time</span>
|
||||||
|
</label>
|
||||||
|
{loadingSummary ? (
|
||||||
|
<div className="skeleton h-9 w-full"></div>
|
||||||
|
) :
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={endTime}
|
||||||
|
onChange={(e) => handleEndTimeChange(e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{formErrors.endTime && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-error font-medium">{formErrors.endTime}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Total Time Adjustment */}
|
||||||
|
<div className="form-control col-span-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Total time adjustment</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Calculated automatically"
|
||||||
|
value={totalAdjustment}
|
||||||
|
readOnly
|
||||||
|
className="input input-bordered input-sm w-full bg-base-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="form-control flex flex-col gap-1">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all"
|
||||||
|
placeholder="Please enter the task name or leave notes to help us better understand your request."
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => handleDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdjustmentTab;
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
interface LeaveFormData {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PtoLeaveTab: React.FC = () => {
|
||||||
|
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
comment: ''
|
||||||
|
});
|
||||||
|
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
|
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
||||||
|
const errors: Partial<LeaveFormData> = {};
|
||||||
|
|
||||||
|
if (!data.fromDate) {
|
||||||
|
errors.fromDate = 'From date is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.toDate) {
|
||||||
|
errors.toDate = 'To date is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.comment.trim()) {
|
||||||
|
errors.comment = 'Comment is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) {
|
||||||
|
errors.toDate = 'To date must be after from date';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePtoChange = (field: keyof LeaveFormData, value: string) => {
|
||||||
|
setPtoLeave(prev => ({ ...prev, [field]: value }));
|
||||||
|
// Clear error for the field being changed
|
||||||
|
if (ptoErrors[field]) {
|
||||||
|
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePtoSubmit = async () => {
|
||||||
|
const errors = validateLeaveForm(ptoLeave);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setPtoErrors(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Submitting PTO:', ptoLeave);
|
||||||
|
alert('PTO request submitted successfully!');
|
||||||
|
|
||||||
|
setPtoLeave({ fromDate: '', toDate: '', comment: "" });
|
||||||
|
setPtoErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to submit request. Please try again.');
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* From Date */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">From Date</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={ptoLeave.fromDate}
|
||||||
|
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||||
|
ptoErrors.fromDate ? 'input-error' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{ptoErrors.fromDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.fromDate}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* To Date */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">To Date</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={ptoLeave.toDate}
|
||||||
|
onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||||
|
ptoErrors.toDate ? 'input-error' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{ptoErrors.toDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.toDate}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description/Comment Field */}
|
||||||
|
<div className="form-control flex flex-col gap-1 col-span-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Description / Reason</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${
|
||||||
|
ptoErrors.comment ? 'textarea-error' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="Please enter the reason for your PTO request to help us better understand your needs."
|
||||||
|
value={ptoLeave.comment}
|
||||||
|
onChange={(e) => handlePtoChange('comment', e.target.value)}
|
||||||
|
/>
|
||||||
|
{ptoErrors.comment && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.comment}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handlePtoSubmit}
|
||||||
|
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
Submit PTO Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PtoLeaveTab;
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
interface LeaveFormData {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SickLeaveTab: React.FC = () => {
|
||||||
|
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
comment: ''
|
||||||
|
});
|
||||||
|
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||||
|
|
||||||
|
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
||||||
|
const errors: Partial<LeaveFormData> = {};
|
||||||
|
|
||||||
|
if (!data.fromDate) {
|
||||||
|
errors.fromDate = 'From date is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.toDate) {
|
||||||
|
errors.toDate = 'To date is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.comment.trim()) {
|
||||||
|
errors.comment = 'Comment is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) {
|
||||||
|
errors.toDate = 'To date must be after from date';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSickChange = (field: keyof LeaveFormData, value: string) => {
|
||||||
|
setSickLeave(prev => ({ ...prev, [field]: value }));
|
||||||
|
if (sickErrors[field]) {
|
||||||
|
setSickErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSickSubmit = async () => {
|
||||||
|
const errors = validateLeaveForm(sickLeave);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setSickErrors(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Submitting Sick Leave:', sickLeave);
|
||||||
|
alert('Sick Leave request submitted successfully!');
|
||||||
|
|
||||||
|
setSickLeave({ fromDate: '', toDate: '', comment: '' });
|
||||||
|
setSickErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to submit request. Please try again.');
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* From Date */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">From Date</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={sickLeave.fromDate}
|
||||||
|
onChange={(e) => handleSickChange('fromDate', e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||||
|
sickErrors.fromDate ? 'input-error' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{sickErrors.fromDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.fromDate}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* To Date */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">To Date</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={sickLeave.toDate}
|
||||||
|
onChange={(e) => handleSickChange('toDate', e.target.value)}
|
||||||
|
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||||
|
sickErrors.toDate ? 'input-error' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{sickErrors.toDate && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.toDate}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description/Comment Field */}
|
||||||
|
<div className="form-control flex flex-col gap-1 col-span-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold text-sm">Description / Reason</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${
|
||||||
|
sickErrors.comment ? 'textarea-error' : ''
|
||||||
|
}`}
|
||||||
|
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
|
||||||
|
value={sickLeave.comment}
|
||||||
|
onChange={(e) => handleSickChange('comment', e.target.value)}
|
||||||
|
/>
|
||||||
|
{sickErrors.comment && (
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleSickSubmit}
|
||||||
|
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
Submit Sick Leave Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SickLeaveTab;
|
||||||
|
|
@ -4,11 +4,12 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import { setUserSlice } from '../store/userSlice';
|
import { setUserSlice } from '../store/userSlice';
|
||||||
import DocVaLogoLight from '../../assets/logo-colored-vertical.svg'
|
import DocVaLogoLight from '../../assets/logo-colored-vertical.svg'
|
||||||
import DocVaLogoDark from '../../assets/logo-white-vertical.svg'
|
import DocVaLogoDark from '../../assets/logo-white-vertical.svg'
|
||||||
import { ThemeToggle } from '../helper/ThemeToggle';
|
import { useTheme } from '../helper/ThemeToggle';
|
||||||
import { KeyRound, Mail } from 'lucide-react';
|
import { KeyRound, Mail } from 'lucide-react';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const theme = ThemeToggle();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
const inputRef = useRef<HTMLFormElement | null>(null)
|
const inputRef = useRef<HTMLFormElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,15 @@ const Settings: React.FC = () => {
|
||||||
|
|
||||||
const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim();
|
const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim();
|
||||||
|
|
||||||
|
const maskLast5 = (value: string) => {
|
||||||
|
if (!value) return "N/A";
|
||||||
|
const str = value.toString();
|
||||||
|
const last5 = str.slice(-5);
|
||||||
|
const masked = "X".repeat(str.length - 5) + last5;
|
||||||
|
return masked;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -98,7 +107,7 @@ const Settings: React.FC = () => {
|
||||||
💳 Payoneer
|
💳 Payoneer
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-medium truncate" title={profile?.payoneerAccount || "N/A"}>
|
<p className="text-xs font-medium truncate" title={profile?.payoneerAccount || "N/A"}>
|
||||||
{profile?.payoneerAccount || "N/A"}
|
{maskLast5(profile?.payoneerAccount)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CalendarDays, Clock, FileText, CheckCircle, XCircle, Eye } from "lucide-react";
|
import { CalendarDays, Clock, FileText, CheckCircle, XCircle, Eye, EyeOff } 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";
|
||||||
|
|
@ -73,7 +73,7 @@ const Timesheets: React.FC = () => {
|
||||||
setSelectedProject(projectID);
|
setSelectedProject(projectID);
|
||||||
setSelectedProjectName(projectName);
|
setSelectedProjectName(projectName);
|
||||||
|
|
||||||
console.log("projectID" ,projectID)
|
console.log("projectID", projectID)
|
||||||
|
|
||||||
const res = await window.electron.getProjectLogs(projectID);
|
const res = await window.electron.getProjectLogs(projectID);
|
||||||
console.log("📦 Full Logs:", res);
|
console.log("📦 Full Logs:", res);
|
||||||
|
|
@ -90,9 +90,9 @@ const Timesheets: React.FC = () => {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatWorkTime = (time: string) => {
|
const formatWorkTime = (time: string) => {
|
||||||
if (!time) return "0h 0m";
|
if (!time) return "0h 0m";
|
||||||
|
|
||||||
// Example input: "0h 2m 29s"
|
// Example input: "0h 2m 29s"
|
||||||
|
|
@ -100,7 +100,7 @@ const formatWorkTime = (time: string) => {
|
||||||
|
|
||||||
// Always return first two parts: ["0h", "2m"]
|
// Always return first two parts: ["0h", "2m"]
|
||||||
return parts.slice(0, 2).join(" ");
|
return parts.slice(0, 2).join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -162,15 +162,16 @@ const formatWorkTime = (time: string) => {
|
||||||
<tbody>
|
<tbody>
|
||||||
{projectsData.map((entry, index) => (
|
{projectsData.map((entry, index) => (
|
||||||
<tr key={entry.projectId}>
|
<tr key={entry.projectId}>
|
||||||
<td>{index+1}</td>
|
<td>{index + 1}</td>
|
||||||
<td>{entry.projectName}</td>
|
<td>{entry.projectName}</td>
|
||||||
<td>{formatWorkTime(entry.totalWorkTime)}</td>
|
<td>{formatWorkTime(entry.totalWorkTime)}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
disabled={formatWorkTime(entry.totalWorkTime) === "0h 0m"}
|
||||||
onClick={() => handleViewLogs(entry.projectId, entry.projectName)}
|
onClick={() => handleViewLogs(entry.projectId, entry.projectName)}
|
||||||
className="btn btn-ghost btn-xs"
|
className="btn btn-ghost btn-xs"
|
||||||
>
|
>
|
||||||
<Eye size={16} />
|
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue