pto-and-leave-dark-light-mode-toggle

This commit is contained in:
JaiPandey425 2025-11-21 12:22:02 +05:30
parent e7696925e7
commit 3a34520152
11 changed files with 1034 additions and 799 deletions

View File

@ -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());

View File

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

View File

@ -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>
</>
);
};

View File

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

View File

@ -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>
</div>
)}
{/* 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>
);
};

View File

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

View File

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

View File

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

View File

@ -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();
@ -42,7 +43,7 @@ const Login = () => {
email: response.user.email,
}));
navigate("/")
}
}
else if (response.success == false) {
console.log(response.error.message)
setErrorMessage(response.error.message)

View File

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

View File

@ -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";
@ -67,40 +67,40 @@ const Timesheets: React.FC = () => {
const handleViewLogs = async (projectID: string, projectName: string) => {
try {
setLoading(true);
setSelectedProject(projectID);
setSelectedProjectName(projectName);
const handleViewLogs = async (projectID: string, projectName: string) => {
try {
setLoading(true);
setSelectedProject(projectID);
setSelectedProjectName(projectName);
console.log("projectID" ,projectID)
console.log("projectID", projectID)
const res = await window.electron.getProjectLogs(projectID);
console.log("📦 Full Logs:", res);
const res = await window.electron.getProjectLogs(projectID);
console.log("📦 Full Logs:", res);
if (res?.success && Array.isArray(res.data)) {
setLogs(res.data[0].sessions || []);
} else {
setLogs([]);
if (res?.success && Array.isArray(res.data)) {
setLogs(res.data[0].sessions || []);
} else {
setLogs([]);
}
setModalOpen(true);
} catch (error) {
console.error("Error fetching logs:", error);
} finally {
setLoading(false);
}
};
setModalOpen(true);
} catch (error) {
console.error("Error fetching logs:", error);
} finally {
setLoading(false);
}
};
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
@ -162,17 +162,18 @@ 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
<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>
</td>
</tr>
))}
</tbody>