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 ProtectedRoute from "./routes/ProtectedRoute";
|
||||
import PublicRoute from "./routes/PublicRoute";
|
||||
import { ThemeToggle } from "./helper/ThemeToggle";
|
||||
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
||||
import Timesheets from "./pages/Timesheets/Timesheets";
|
||||
import Settings from "./pages/Settings/Settings";
|
||||
|
|
@ -98,15 +97,13 @@ import Vpn from "./pages/Vpn/Vpn";
|
|||
|
||||
|
||||
const App: React.FC = () => {
|
||||
const theme = ThemeToggle();
|
||||
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
// ✅ Get user directly from Redux
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadUserFromStorage());
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
// src/hooks/ThemeToggle.js
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
export const useTheme = () => {
|
||||
const [theme, setTheme] = useState("light");
|
||||
|
||||
useEffect(() => {
|
||||
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
// Initial check
|
||||
setTheme(darkQuery.matches ? "dark" : "light");
|
||||
|
||||
// Listen for changes
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
const handleChange = (e:any) => {
|
||||
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 { setError, setLoading, setProfile } from "../store/profileSlice";
|
||||
import { canStartTracking } from "../utils/projectLimits";
|
||||
import { useTheme } from "../helper/ThemeToggle";
|
||||
import LogoutModal from "../modal/LogoutModal";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
|
|
@ -79,6 +81,11 @@ export interface TrackingState {
|
|||
}
|
||||
|
||||
const DashboardLayout = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
};
|
||||
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -114,6 +121,11 @@ const DashboardLayout = () => {
|
|||
const toggleWindowSize = useRef(true);
|
||||
const [loadingStart, setLoadingStart] = 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
|
||||
|
|
@ -155,9 +167,9 @@ const DashboardLayout = () => {
|
|||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
console.log("⏹️ Timer cleared");
|
||||
console.log("Timer cleared");
|
||||
} else {
|
||||
console.log("ℹ️ No active timer to clear");
|
||||
console.log("No active timer to clear");
|
||||
}
|
||||
|
||||
// STEP 3: Send LOGOUT to backend
|
||||
|
|
@ -222,6 +234,7 @@ const DashboardLayout = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log("Alert Message 1")
|
||||
// Show alert to user
|
||||
alert(
|
||||
`⏰ Daily Limit Reached!\n\n` +
|
||||
|
|
@ -229,7 +242,7 @@ const DashboardLayout = () => {
|
|||
`Dedicated Hours: ${selectedProject.dedicatedHours}\n\n` +
|
||||
`Your tracking has been stopped automatically because you've reached the daily time limit for this project.`
|
||||
);
|
||||
|
||||
console.log("Alert Message 2")
|
||||
// Stop tracking
|
||||
await handleStopTracking();
|
||||
}, [selectedProject, handleStopTracking]);
|
||||
|
|
@ -625,7 +638,7 @@ const DashboardLayout = () => {
|
|||
{/* Layout Body */}
|
||||
<div className={`flex ${!isTracking ? "max-h-screen min-h-screen" : "h-[calc(100vh-3rem)]"} bg-base-200 overflow-hidden`}>
|
||||
{/* 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) => (
|
||||
<div className="tooltip z-100 tooltip-right" key={item.label} data-tip={item.label}>
|
||||
<NavLink
|
||||
|
|
@ -643,9 +656,34 @@ const DashboardLayout = () => {
|
|||
</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 && (
|
||||
<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"
|
||||
data-tip={"Logout"}
|
||||
>
|
||||
|
|
@ -658,7 +696,15 @@ const DashboardLayout = () => {
|
|||
<main className="flex-1 overflow-y-auto p-4 no-scrollbar bg-base-200">
|
||||
<Outlet context={trackingState} />
|
||||
</main>
|
||||
|
||||
<LogoutModal
|
||||
open={showLogoutModal}
|
||||
onClose={() => setShowLogoutModal(false)}
|
||||
onConfirm={handleLogout}
|
||||
/>
|
||||
|
||||
</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 AdjustmentModal from './AdjustmentModal';
|
||||
import { RootState } from '../../store';
|
||||
import React, { useState } from 'react';
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
type TabType = 'pto' | 'sick' | 'adjustment';
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
};
|
||||
console.log("LeaveAndAdjustments", projects);
|
||||
|
||||
return (
|
||||
<div className="max-h-screen">
|
||||
<div className="w-full mx-auto">
|
||||
|
||||
{/* Header */}
|
||||
<div className=" ">
|
||||
<div className="">
|
||||
<h1 className="text-mb font-bold mb-2">Leave & Adjust Timesheet</h1>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
{/* Tabs Navigation */}
|
||||
<div role="tablist" className="tabs tabs-border w-full mb-1">
|
||||
<a
|
||||
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>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Main Container */}
|
||||
<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]">
|
||||
{/* PTO Tab */}
|
||||
{activeTab === 'create_and_adjust_timesheet' && (
|
||||
{activeTab === 'pto' && (
|
||||
<div className="animate-fade-in">
|
||||
<div className="max-w-4xl ">
|
||||
<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>
|
||||
<PtoLeaveTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other tabs remain the same */}
|
||||
{activeTab === 'sick' && (
|
||||
<div className="animate-fade-in">
|
||||
{/* Sick leave content remains the same */}
|
||||
<SickLeaveTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'adjustment' && (
|
||||
<div className="animate-fade-in">
|
||||
{/* Adjustment content remains the same */}
|
||||
<AdjustmentTab projects={projects} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -786,14 +88,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
|||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<AdjustmentModal
|
||||
editingEntry={editingEntry}
|
||||
newHours={newHours}
|
||||
setNewHours={setNewHours}
|
||||
handleCloseModal={handleCloseModal}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
</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 DocVaLogoLight from '../../assets/logo-colored-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';
|
||||
|
||||
const Login = () => {
|
||||
const theme = ThemeToggle();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const inputRef = useRef<HTMLFormElement | null>(null)
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ const Settings: React.FC = () => {
|
|||
|
||||
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 (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Header */}
|
||||
|
|
@ -98,7 +107,7 @@ const Settings: React.FC = () => {
|
|||
💳 Payoneer
|
||||
</p>
|
||||
<p className="text-xs font-medium truncate" title={profile?.payoneerAccount || "N/A"}>
|
||||
{profile?.payoneerAccount || "N/A"}
|
||||
{maskLast5(profile?.payoneerAccount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { RootState } from "../../store";
|
||||
import ProjectLogsModal from "./ProjectLogsModal";
|
||||
|
|
@ -73,7 +73,7 @@ const Timesheets: React.FC = () => {
|
|||
setSelectedProject(projectID);
|
||||
setSelectedProjectName(projectName);
|
||||
|
||||
console.log("projectID" ,projectID)
|
||||
console.log("projectID", projectID)
|
||||
|
||||
const res = await window.electron.getProjectLogs(projectID);
|
||||
console.log("📦 Full Logs:", res);
|
||||
|
|
@ -90,9 +90,9 @@ const Timesheets: React.FC = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const formatWorkTime = (time: string) => {
|
||||
const formatWorkTime = (time: string) => {
|
||||
if (!time) return "0h 0m";
|
||||
|
||||
// Example input: "0h 2m 29s"
|
||||
|
|
@ -100,7 +100,7 @@ const formatWorkTime = (time: string) => {
|
|||
|
||||
// Always return first two parts: ["0h", "2m"]
|
||||
return parts.slice(0, 2).join(" ");
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
@ -162,15 +162,16 @@ const formatWorkTime = (time: string) => {
|
|||
<tbody>
|
||||
{projectsData.map((entry, index) => (
|
||||
<tr key={entry.projectId}>
|
||||
<td>{index+1}</td>
|
||||
<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"
|
||||
>
|
||||
<Eye size={16} />
|
||||
{formatWorkTime(entry.totalWorkTime) === "0h 0m" ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Reference in New Issue