diff --git a/src/ui/App.tsx b/src/ui/App.tsx index a792f6c..fbbf871 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -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(); // ✅ Get user directly from Redux const user = useSelector((state: RootState) => state.user.user); - useEffect(() => { - document.documentElement.setAttribute("data-theme", theme); - }, [theme]); + useEffect(() => { dispatch(loadUserFromStorage()); diff --git a/src/ui/helper/ThemeToggle.tsx b/src/ui/helper/ThemeToggle.tsx index 050d963..3ecd3b6 100644 --- a/src/ui/helper/ThemeToggle.tsx +++ b/src/ui/helper/ThemeToggle.tsx @@ -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 + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); + + return { theme, setTheme }; }; diff --git a/src/ui/layout/DashboardLayout.tsx b/src/ui/layout/DashboardLayout.tsx index 26b6c27..5953436 100644 --- a/src/ui/layout/DashboardLayout.tsx +++ b/src/ui/layout/DashboardLayout.tsx @@ -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 */}
{/* Sidebar */} -
+ ); }; diff --git a/src/ui/modal/LogoutModal.tsx b/src/ui/modal/LogoutModal.tsx new file mode 100644 index 0000000..5beac1e --- /dev/null +++ b/src/ui/modal/LogoutModal.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +interface LogoutModalProps { + open: boolean; + onClose: () => void; + onConfirm: () => void | Promise; +} + +const LogoutModal: React.FC = ({ open, onClose, onConfirm }) => { + if (!open) return null; + + return ( + +
+

Confirm Logout

+ +

+ Are you sure you want to logout? You will need to login again to access your dashboard. +

+ +
+ + {/* Cancel */} + + + {/* Logout */} + + +
+
+
+ ); +}; + +export default LogoutModal; diff --git a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx index 6bf360f..9d8471c 100644 --- a/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx +++ b/src/ui/pages/LeaveAndAdjustments/LeaveAndAdjustments.tsx @@ -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('create_and_adjust_timesheet'); + const [activeTab, setActiveTab] = useState('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>({}); - - // Sick Leave State - const [sickLeave, setSickLeave] = useState({ - fromDate: '', - toDate: '', - }); - const [sickErrors, setSickErrors] = useState>({}); - const [selectedProjectId, setSelectedProjectId] = useState("") - const [selectedDate, setSelectedDate] = useState("") - const [totalAdjustment, setTotalAdjustment] = useState("0h 0m"); - const [pendingOverride, setPendingOverride] = useState(null); - const [loadingSummary, setLoadingSummary] = useState(false); - - // Form Validation State - const [formErrors, setFormErrors] = useState({}); - - // Project Summary State - const [projectSummary, setProjectSummary] = useState(null); - const [startTime, setStartTime] = useState(""); - const [endTime, setEndTime] = useState(""); - - // Adjustment State - const [selectedPeriod, setSelectedPeriod] = useState('week'); - const [adjustmentData, setAdjustmentData] = useState([ - { 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(null); - const [editHours, setEditHours] = useState(''); - - // Validation function - const validateLeaveForm = (data: LeaveFormData): Partial => { - const errors: Partial = {}; - - 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(null); - const [newHours, setNewHours] = useState(0); - const [comment, setComment] = useState("") - 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 (
- {/* Header */} -
+

Leave & Adjust Timesheet

- {/* General Error Alert */} - {formErrors.general && ( -
- {formErrors.general} -
- )} + {/* Tabs Navigation */} + {/* Main Container */}
- {/* Tabs Navigation */} -
- -
- - {/* Tab Content */}
- {/* PTO Tab */} - {activeTab === 'create_and_adjust_timesheet' && ( + {activeTab === 'pto' && (
-
-
-
- {/* Project Selection */} -
- - - {formErrors.selectedProjectId && ( - - )} -
- - {/* Date Selection */} -
- - handleSelectedDate(e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`} - /> - {formErrors.selectedDate && ( - - )} -
- - {/* Current Total Time */} -
- - {loadingSummary ? ( -
- ) : - - } -
- - {/* Current Pending Time */} -
- - {loadingSummary ? ( -
- ) : - - } -
- - {/* Start Time */} -
- - {loadingSummary ? ( -
- ) : - handleStartTimeChange(e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`} - /> - } - {formErrors.startTime && ( - - )} -
- - {/* End Time */} -
- - {loadingSummary ? ( -
- ) : - handleEndTimeChange(e.target.value)} - className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`} - /> - } - {formErrors.endTime && ( - - )} -
- - {/* Total Time Adjustment */} -
- - -
-
- - {/* Description */} -
- -