Compare commits

...

9 Commits

106 changed files with 2137 additions and 172233 deletions

13
package-lock.json generated
View File

@ -10,11 +10,10 @@
"dependencies": {
"@reduxjs/toolkit": "^2.10.1",
"@tailwindcss/vite": "^4.1.16",
"active-win": "^7.7.2",
"axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2",
"get-windows": "^9.2.3",
"https": "^1.0.0",
"lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14",
@ -23,10 +22,8 @@
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.5",
"recharts": "^2.12.7",
"sharp": "^0.34.5",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.16",
"windowmanager": "^0.14.8"
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@playwright/test": "^1.47.2",
@ -7030,6 +7027,12 @@
"node": ">=10.19.0"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",

View File

@ -7,7 +7,7 @@
"scripts": {
"dev": "npm-run-all --parallel transpile:electron dev:react dev:electron",
"dev:react": "vite",
"dev:electron": "npm run build && cross-env NODE_ENV=development electron .",
"dev:electron": "npm run build && cross-env NODE_ENV=development electron --inspect=5858 .",
"prod:electron": "npm run build && cross-env NODE_ENV=production electron .",
"build": "tsc && vite build",
"preview": "vite preview",
@ -24,6 +24,7 @@
"axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2",
"https": "^1.0.0",
"lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14",

Binary file not shown.

View File

@ -1,223 +0,0 @@
x64:
firstOrDefaultFilePatterns:
- '!**/node_modules'
- '!build{,/**/*}'
- '!release{,/**/*}'
- dist-electron/**/*
- dist-react/**/*
- package.json
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,forge-meta,pdb}'
- '!**/._*'
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
- '!.yarn{,/**/*}'
- '!.editorconfig'
- '!.yarnrc.yml'
nodeModuleFilePatterns:
- '**/*'
- dist-electron/**/*
- dist-react/**/*
- package.json
nsis:
script: |-
!include "C:\Users\pc\Desktop\va-tracker\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
!addincludedir "C:\Users\pc\Desktop\va-tracker\node_modules\app-builder-lib\templates\nsis\include"
!macro _isUpdated _a _b _t _f
${StdUtils.TestParameter} $R9 "updated"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isUpdated `"" isUpdated ""`
!macro _isForceRun _a _b _t _f
${StdUtils.TestParameter} $R9 "force-run"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isForceRun `"" isForceRun ""`
!macro _isKeepShortcuts _a _b _t _f
${StdUtils.TestParameter} $R9 "keep-shortcuts"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isKeepShortcuts `"" isKeepShortcuts ""`
!macro _isNoDesktopShortcut _a _b _t _f
${StdUtils.TestParameter} $R9 "no-desktop-shortcut"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isNoDesktopShortcut `"" isNoDesktopShortcut ""`
!macro _isDeleteAppData _a _b _t _f
${StdUtils.TestParameter} $R9 "delete-app-data"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isDeleteAppData `"" isDeleteAppData ""`
!macro _isForAllUsers _a _b _t _f
${StdUtils.TestParameter} $R9 "allusers"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isForAllUsers `"" isForAllUsers ""`
!macro _isForCurrentUser _a _b _t _f
${StdUtils.TestParameter} $R9 "currentuser"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isForCurrentUser `"" isForCurrentUser ""`
!macro addLangs
!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_LANGUAGE "German"
!insertmacro MUI_LANGUAGE "French"
!insertmacro MUI_LANGUAGE "SpanishInternational"
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "TradChinese"
!insertmacro MUI_LANGUAGE "Japanese"
!insertmacro MUI_LANGUAGE "Korean"
!insertmacro MUI_LANGUAGE "Italian"
!insertmacro MUI_LANGUAGE "Dutch"
!insertmacro MUI_LANGUAGE "Danish"
!insertmacro MUI_LANGUAGE "Swedish"
!insertmacro MUI_LANGUAGE "Norwegian"
!insertmacro MUI_LANGUAGE "Finnish"
!insertmacro MUI_LANGUAGE "Russian"
!insertmacro MUI_LANGUAGE "Portuguese"
!insertmacro MUI_LANGUAGE "PortugueseBR"
!insertmacro MUI_LANGUAGE "Polish"
!insertmacro MUI_LANGUAGE "Ukrainian"
!insertmacro MUI_LANGUAGE "Czech"
!insertmacro MUI_LANGUAGE "Slovak"
!insertmacro MUI_LANGUAGE "Hungarian"
!insertmacro MUI_LANGUAGE "Arabic"
!insertmacro MUI_LANGUAGE "Turkish"
!insertmacro MUI_LANGUAGE "Thai"
!insertmacro MUI_LANGUAGE "Vietnamese"
!macroend
!addplugindir /x86-unicode "C:\Users\pc\AppData\Local\electron-builder\Cache\nsis\nsis-resources-3.4.1\plugins\x86-unicode"
!include "C:\Users\pc\AppData\Local\Temp\t-OkeCZT\0-messages.nsh"
Var newStartMenuLink
Var oldStartMenuLink
Var newDesktopLink
Var oldDesktopLink
Var oldShortcutName
Var oldMenuDirectory
!include "common.nsh"
!include "MUI2.nsh"
!include "multiUser.nsh"
!include "allowOnlyOneInstallerInstance.nsh"
!ifdef INSTALL_MODE_PER_ALL_USERS
!ifdef BUILD_UNINSTALLER
RequestExecutionLevel user
!else
RequestExecutionLevel admin
!endif
!else
RequestExecutionLevel user
!endif
!ifdef BUILD_UNINSTALLER
SilentInstall silent
!else
Var appExe
Var launchLink
!endif
!ifdef ONE_CLICK
!include "oneClick.nsh"
!else
!include "assistedInstaller.nsh"
!endif
!insertmacro addLangs
!ifmacrodef customHeader
!insertmacro customHeader
!endif
Function .onInit
Call setInstallSectionSpaceRequired
SetOutPath $INSTDIR
${LogSet} on
!ifmacrodef preInit
!insertmacro preInit
!endif
!ifdef DISPLAY_LANG_SELECTOR
!insertmacro MUI_LANGDLL_DISPLAY
!endif
!ifdef BUILD_UNINSTALLER
WriteUninstaller "${UNINSTALLER_OUT_FILE}"
!insertmacro quitSuccess
!else
!insertmacro check64BitAndSetRegView
!ifdef ONE_CLICK
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
!else
${IfNot} ${UAC_IsInnerInstance}
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
${EndIf}
!endif
!insertmacro initMultiUser
!ifmacrodef customInit
!insertmacro customInit
!endif
!ifmacrodef addLicenseFiles
InitPluginsDir
!insertmacro addLicenseFiles
!endif
!endif
FunctionEnd
!ifndef BUILD_UNINSTALLER
!include "installUtil.nsh"
!endif
Section "install" INSTALL_SECTION_ID
!ifndef BUILD_UNINSTALLER
# If we're running a silent upgrade of a per-machine installation, elevate so extracting the new app will succeed.
# For a non-silent install, the elevation will be triggered when the install mode is selected in the UI,
# but that won't be executed when silent.
!ifndef INSTALL_MODE_PER_ALL_USERS
!ifndef ONE_CLICK
${if} $hasPerMachineInstallation == "1" # set in onInit by initMultiUser
${andIf} ${Silent}
${ifNot} ${UAC_IsAdmin}
ShowWindow $HWNDPARENT ${SW_HIDE}
!insertmacro UAC_RunElevated
${Switch} $0
${Case} 0
${Break}
${Case} 1223 ;user aborted
${Break}
${Default}
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
${Break}
${EndSwitch}
Quit
${else}
!insertmacro setInstallModePerAllUsers
${endIf}
${endIf}
!endif
!endif
!include "installSection.nsh"
!endif
SectionEnd
Function setInstallSectionSpaceRequired
!insertmacro setSpaceRequired ${INSTALL_SECTION_ID}
FunctionEnd
!ifdef BUILD_UNINSTALLER
!include "uninstaller.nsh"
!endif

View File

@ -1,35 +0,0 @@
directories:
output: release
buildResources: build
appId: com.vatracker.app
productName: VA Tracker
npmRebuild: false
buildDependenciesFromSource: false
nodeGypRebuild: false
files:
- filter:
- dist-electron/**/*
- dist-react/**/*
- package.json
extraResources:
- from: dist-electron
to: dist-electron
- from: src/assets
to: assets
win:
target:
- target: nsis
arch:
- x64
icon: src/assets/icons/appIcon.ico
signAndEditExecutable: false
signingHashAlgorithms: []
verifyUpdateCodeSignature: false
nsis:
oneClick: true
perMachine: false
createDesktopShortcut: always
createStartMenuShortcut: true
runAfterFinish: true
artifactName: ${productName}-Setup-${version}.${ext}
electronVersion: 32.3.3

View File

@ -1,21 +0,0 @@
Copyright (c) Electron contributors
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
background: #ffffff;
font-family: Arial, sans-serif;
}
.loader {
width: 50px;
height: 50px;
border: 5px solid #e0e0e0;
border-top-color: #6a5acd;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="loader"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1 +0,0 @@
{"file_format_version": "1.0.0", "ICD": {"library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5"}}

Binary file not shown.

View File

@ -3,11 +3,17 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken } from '../utils/tokenHelper.js';
import chalk from "chalk";
import { sendTokenExpired } from '../ipc/ipcHandler.js';
import https from 'https';
const TOKEN_KEY = 'ACCESS_TOKEN';
const httpsAgent = new https.Agent({
rejectUnauthorized: false
});
const api: AxiosInstance = axios.create({
baseURL: 'https://dreadingly-unfighting-pandora.ngrok-free.dev/api/v1/va', // 🔁 your backend
baseURL: 'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va', // 🔁 your backend
httpsAgent
// timeout: 10000,
});
@ -35,7 +41,7 @@ api.interceptors.request.use(
api.interceptors.response.use(
(response: AxiosResponse) => response.data,
async (error) => {
console.error('[Axios Error]', error.message);
// console.error('[Axios Error]', error.message);
if (error.response?.status === 401) {
console.warn('Token expired — trigger refresh logic if needed');
sendTokenExpired();

View File

@ -110,14 +110,29 @@ ipcMain.handle("start-project-tracking", async (event, payload) => {
const response = await api.post("/log/project", payload);
console.log(" Tracking start synced:", response);
return response;
} catch (error) {
console.error(" Error syncing project start:", error);
throw error;
return { success: true, data: response };
} catch (error:any) {
console.error("❌ Error syncing project start:", error);
// If API sent a response (like 400)
if (error.response) {
return {
success: false,
message: error.response.data?.message || "Request failed",
status: error.response.status
};
}
return {
success: false,
message: "Something went wrong",
status: 500
};
}
});
// When project tracking stop
ipcMain.handle("stop-project-tracking", async (event, payload) => {
try {
@ -128,7 +143,7 @@ ipcMain.handle("stop-project-tracking", async (event, payload) => {
console.log(" Tracking stop synced:", response);
return response;
} catch (error) {
console.error(" Error syncing project start:", error);
console.error(" Error syncing project stop:", error);
throw error;
}
});
@ -140,16 +155,16 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
let response;
if (!payload.id) {
// 🟢 POST new log (new tab opened)
// POST new log (new tab opened)
response = await api.post("/log/project/track", {
logId: payload.logId,
projectId: payload.projectId,
title: payload.title,
appName: payload.appName,
});
console.log("✅ New log created:", response.data);
console.log(" New log created:", response);
} else {
// 🟡 PATCH existing log (same tab, update)
// PATCH existing log (same tab, update)
response = await api.patch("/log/project/track", {
id: payload.id,
logId: payload.logId,
@ -157,12 +172,12 @@ ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
title: payload.title,
appName: payload.appName,
});
console.log("🟨 Log updated:", response.data);
console.log("Log updated:", response);
}
return response.data;
return response;
} catch (error) {
console.error("❌ Error syncing project tab tracking:", error);
console.error("Error syncing project tab tracking:", error);
throw error;
}
});

View File

@ -160,9 +160,9 @@ app.on("ready", async () => {
// });
// });
app.on('browser-window-blur', () => {
globalShortcut.unregisterAll();
});
// app.on('browser-window-blur', () => {
// globalShortcut.unregisterAll();
// });
// ---------------------------

View File

@ -2,12 +2,18 @@ import { app, BrowserWindow, desktopCapturer } from "electron";
import * as fs from "fs";
import path from "path";
import axios from 'axios';
import https from 'https';
import sharp from 'sharp';
import { saveToken } from "./tokenHelper.js";
import { activeWindow } from "get-windows";
import { exec } from "child_process";
import { getUIPath } from "../pathResolver.js";
const httpsAgent = new https.Agent({
rejectUnauthorized: false
});
// Blur using an offscreen canvas
async function blurAndCompressBase64Image(base64: string): Promise<Buffer> {
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
@ -55,7 +61,6 @@ export const handleLogin = async (credentials: { email: string, password: string
// Call your login API
console.log("🟡 login Request received", credentials);
console.log("Doom");
console.log("app.getAppPath()", app.getAppPath());
console.log("UIPATH", getUIPath());
console.log("Doom 1");
@ -64,9 +69,9 @@ export const handleLogin = async (credentials: { email: string, password: string
try {
console.log("➡️ Calling API...");
const response = await axios.post(
'https://dreadingly-unfighting-pandora.ngrok-free.dev/api/v1/va/login',
'https://quality-friendly-goldfish.ngrok-free.app/api/v1/va/login',
{ userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 } }
{ headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent }
);
console.log("API CAlled", response)

View File

@ -89,22 +89,21 @@ 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";
import HelpSupport from "./pages/HelpSupport/HelpSupport";
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());
@ -139,6 +138,7 @@ const App: React.FC = () => {
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="timesheet" element={<Timesheets />} />
<Route path="settings" element={<Settings />} />
<Route path="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} />
</Route>

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

@ -2,11 +2,20 @@ import { useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice";
interface DailyHours {
todayTotal: string;
dailyLimit: string;
remaining: string;
}
export const useActiveAppTracker = (
isTracking: boolean,
activeProjectLogId: string | null,
projectId: string | null,
projectName: string | null
projectName: string | null,
onLimitExceeded?: () => void // Callback when limit is reached
) => {
const dispatch = useDispatch();
const lastActiveRef = useRef<WindowInfo | null>(null);
@ -14,10 +23,44 @@ export const useActiveAppTracker = (
const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<number | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null);
const limitCheckedRef = useRef<boolean>(false);
// ✅ Common guard to check if we can send
const canSend = !!(activeProjectLogId && projectId);
// ✅ Parse time string to minutes
const parseTimeToMinutes = (timeStr: string): number => {
const hourMatch = timeStr.match(/(\d+)h/);
const minMatch = timeStr.match(/(\d+)m/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
return hours * 60 + minutes;
};
// ✅ Check if daily limit is exceeded
const checkDailyLimit = (dailyHours?: DailyHours): boolean => {
if (!dailyHours) return false;
const todayTotalMins = parseTimeToMinutes(dailyHours.todayTotal);
const dailyLimitMins = parseTimeToMinutes(dailyHours.dailyLimit);
const remainingMins = parseTimeToMinutes(dailyHours.remaining);
console.log("⏰ Daily Limit Check:", {
todayTotal: `${todayTotalMins} mins`,
dailyLimit: `${dailyLimitMins} mins`,
remaining: `${remainingMins} mins`,
exceeded: remainingMins <= 0
});
// If remaining time is 0 or negative, limit is exceeded
if (remainingMins <= 0) {
return true;
}
return false;
};
const sendActivityLog = async (
projectLogId: string,
projectId: string,
@ -44,17 +87,40 @@ export const useActiveAppTracker = (
appName,
};
// ✅ only attach `id` if it’s non-null and not undefined
if (!isNew && lastLogIdRef.current) {
payload.id = lastLogIdRef.current;
}
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
const res = await window.electron.trackingTabsOpeningClosing(payload);
if (isNew && res?._id) lastLogIdRef.current = res._id;
console.log("📊 Activity log response:", res);
// Store the log ID for future updates
if (isNew && res?.data?._id) {
console.log("res?.data?._id", res?.data?._id)
lastLogIdRef.current = res.data._id;
}
// ✅ Check daily limit after every API call
if (res?.dailyHours) {
console.log("res?.data?.dailyHours", res?.dailyHours)
const limitExceeded = checkDailyLimit(res?.dailyHours);
if (limitExceeded && !limitCheckedRef.current) {
limitCheckedRef.current = true;
console.warn("🚨 Daily limit exceeded! Stopping tracking...");
// Trigger stop tracking callback
if (onLimitExceeded) {
setTimeout(() => {
onLimitExceeded();
}, 500);
}
}
}
return res;
} catch (error) {
console.error("❌ Activity log error:", error);
@ -69,27 +135,54 @@ export const useActiveAppTracker = (
return;
}
console.log("🛑 Sending final PATCH before stopping...");
console.log("=".repeat(60));
console.log("🛑 FINAL PATCH INITIATED - Recording last activity");
console.log("=".repeat(60));
if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) {
try {
await sendActivityLog(
console.log("📊 Final Activity Details:");
console.log(` • Window Title: ${lastTitleRef.current}`);
console.log(` • App Name: ${lastActiveRef.current.owner.name}`);
console.log(` • Log ID: ${lastLogIdRef.current}`);
console.log(` • Project ID: ${projectId}`);
const result = await sendActivityLog(
activeProjectLogId!,
projectId!,
lastTitleRef.current,
lastActiveRef.current.owner.name || "Unknown",
false
false // This is a PATCH (update), not a POST (new)
);
console.log("✅ Final PATCH sent for", lastTitleRef.current);
console.log("✅ Final PATCH sent successfully!");
if (result?.dailyHours) {
console.log("📈 Final Daily Hours Summary:");
console.log(` • Today Total: ${result.dailyHours.todayTotal}`);
console.log(` • Daily Limit: ${result.dailyHours.dailyLimit}`);
console.log(` • Remaining: ${result.dailyHours.remaining}`);
}
} catch (error) {
console.error("❌ Final PATCH failed:", error);
throw error; // Re-throw to let caller handle
}
} else {
console.log("ℹ️ No active log to finalize");
console.log("ℹ️ No active window to finalize");
console.log(" • Last Log ID:", lastLogIdRef.current || "None");
console.log(" • Last Title:", lastTitleRef.current || "None");
console.log(" • Last Active Window:", lastActiveRef.current ? "Yes" : "No");
}
// Clean up all refs
console.log("🧹 Cleaning up tracking references...");
lastActiveRef.current = null;
lastTitleRef.current = null;
lastLogIdRef.current = null;
limitCheckedRef.current = false;
console.log("✅ Cleanup complete");
console.log("=".repeat(60));
}, [activeProjectLogId, projectId, canSend]);
useEffect(() => {
@ -97,7 +190,11 @@ export const useActiveAppTracker = (
}, [sendFinalPatch]);
useEffect(() => {
// 🚫 If not tracking OR missing IDs, skip everything
// Reset limit check when tracking starts
if (isTracking) {
limitCheckedRef.current = false;
}
if (!isTracking || !canSend) {
console.log("🟡 Tracking inactive or missing IDs — skipping tracker setup");
return;
@ -150,4 +247,4 @@ export const useActiveAppTracker = (
return {
finalizeTracking: finalizeTrackingRef.current,
};
};
};

File diff suppressed because it is too large Load Diff

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

@ -2,6 +2,10 @@ import React, { useEffect, useRef, useState } from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import { useOutletContext } from "react-router-dom";
import NoData from "./NoData";
import { useDispatch, useSelector } from "react-redux";
import { initializeProjectTracking, setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, updateProjectTracking } from "../store/projectSlice";
import { setError, setLoading, setProfile } from "../store/profileSlice";
import { getFormattedRemainingTime, isProjectLimitReached } from '../utils/projectLimits';
// Updated Project interface to match JSON structure
interface Project {
@ -11,6 +15,7 @@ interface Project {
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
status: string
}
interface ProjectTrackingData {
@ -23,7 +28,7 @@ interface ProjectTrackingData {
interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<string, ProjectTrackingData>;
// projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
@ -34,6 +39,8 @@ interface TrackingState {
handleStopTracking: () => void;
handleProjectSelect: (projectId: string) => void;
projectsData: Project[];
loadingStart: boolean,
loadingStop: boolean
}
// Helper function
@ -41,8 +48,8 @@ const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
// return `${hrs}h ${mins}m ${secs}s`;
return `${hrs}h ${mins}m `;
return `${hrs}h ${mins}m ${secs}s`;
// return `${hrs}h ${mins}m `;
};
// Widget Component
@ -79,8 +86,8 @@ const Widget: React.FC<{
// Dashboard Component
const Dashboard: React.FC = () => {
const {
selectedProject,
projectTrackingMap,
// selectedProject,
// projectTrackingMap,
isTracking,
isPaused,
elapsedTime,
@ -89,7 +96,9 @@ const Dashboard: React.FC = () => {
handlePauseResume,
handleStopTracking,
handleProjectSelect,
projectsData,
// projectsData,
loadingStart,
loadingStop
} = useOutletContext<TrackingState>();
// interface AppsListInt {
@ -100,10 +109,112 @@ const Dashboard: React.FC = () => {
// const [runningApps, setRunningApps] = useState<AppsListInt[]>([]);
const projectsData = useSelector((state: any) => state.projects.projectsData);
const selectedProject = useSelector((state: any) => state.projects.selectedProject);
const projectSummary = useSelector((state: any) => state.projects.projectSummary);
const projectTrackingMap = useSelector((state: any) => state.projects.projectTrackingMap);
console.log("Line 111", projectsData, selectedProject, projectTrackingMap)
const dispatch = useDispatch();
const fetchProfile = async () => {
try {
dispatch(setLoading(true));
const res = await window.electron.getProfile();
console.log("Profile", res)
if (res.success && res.data) {
dispatch(setProfile(res.data));
} else {
dispatch(setError("Failed to fetch profile"));
}
} catch (err: any) {
dispatch(setError(err.message));
} finally {
dispatch(setLoading(false));
}
};
useEffect(() => {
const fetchProjects = async () => {
try {
console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList();
console.log(" Project list response in dashboard:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
// setProjectsData(res.data);
// Store project summary info
// setProjectSummary({
// totalProjects: res.totalProjects || 0,
// activeProjects: res.activeProjects || 0,
// awayProjects: res.awayProjects || 0,
// totalWorkTime: res.totalWorkTime || "0h 0m 0s",
// totalWorkMs: res.totalWorkMs || 0,
// vaName: res.vaName || "",
// vaId: res.vaId || "",
// });
// Store data redux store
dispatch(setProjectsDataSlice(res.data));
dispatch(
setProjectSummarySlice({
totalProjects: res.totalProjects || 0,
activeProjects: res.activeProjects || 0,
awayProjects: res.awayProjects || 0,
totalWorkTime: res.totalWorkTime || "0h 0m 0s",
totalWorkMs: res.totalWorkMs || 0,
vaName: res.vaName || "",
vaId: res.vaId || "",
})
);
// Auto-select first project
if (res.data.length > 0 && !selectedProject) {
const firstProject = res.data[0];
// Select the project
dispatch(setSelectedProjectSlice(firstProject));
// Convert total work ms → seconds
const initialElapsed = Math.floor((firstProject.totalWorkMs || 0) / 1000);
// Initialize tracking data for first project (your slice matches this)
dispatch(
initializeProjectTracking({
projectId: firstProject.projectId,
initialTime: initialElapsed,
})
);
}
fetchProfile()
} else {
console.warn("⚠️ Unexpected response:", res);
// if (res?.status) {
// // handleLogout()
// }
}
} catch (err) {
console.error("❌ Error fetching projects:", err);
}
};
fetchProjects();
}, []);
const selectedData = selectedProject
? projectTrackingMap.get(selectedProject.projectId)
? projectTrackingMap[selectedProject.projectId]
: null;
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
@ -115,49 +226,40 @@ const Dashboard: React.FC = () => {
};
const handleProjectSelectLocal = (projectId: string) => {
const project = projectsData.find((p: any) => p.projectId === projectId);
if (!project) return;
dispatch(setSelectedProjectSlice(project));
const existingData = projectTrackingMap[projectId];
if (!existingData) {
const initialElapsed = Math.floor((project.totalWorkMs || 0) / 1000);
dispatch(initializeProjectTracking({
projectId,
initialTime: initialElapsed
}));
} else {
// Restore saved tracking state
dispatch(updateProjectTracking({
projectId,
data: existingData
}));
}
};
return (
<main className="flex-1 overflow-hidden no-scrollbar max-h-screen">
{/* Header */}
<div className="mb-2">
<h1 className="text-md font-bold">Dashboard</h1>
{/* <button
onClick={async () => {
await window.electron.uploadScreenshot();
}}
className="btn btn-xs btn-outline mt-2"
>
Screenshot Test
</button> */}
</div>
{/* Widgets */}
{/* <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<Widget
title="Logged Hours Today"
value="6h 24m"
icon={<Clock size={24} />}
color="primary"
/>
<Widget
title="Logged Hours This Week"
value="4h 48m"
icon={<TrendingUp size={24} />}
color="success"
/>
<Widget
title="Active Project"
value={selectedProject ? selectedProject.projectName : "—"}
icon={<ProjectorIcon size={24} />}
color="warning"
/>
</div> */}
{/* Tracking Section */}
<div className="w-full mb-4">
{/* <div className="flex justify-between items-center mb-2">
<span className="text-md font-bold">Tracking</span>
</div> */}
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body">
{selectedProject ? (
@ -183,60 +285,33 @@ const Dashboard: React.FC = () => {
<button
onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
disabled={loadingStart}
>
<Play className="w-3 h-3" /> Start Tracking
{loadingStart ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
<Play className="w-3 h-3" />
)}
{loadingStart ? "Starting..." : "Start Tracking"}
</button>
) : (
<button
className="btn btn-xs btn-error rounded-full"
onClick={handleStopTracking}
disabled={loadingStop}
>
<Square className="w-3 h-3" />
Stop Tracking
{loadingStop ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
<Square className="w-3 h-3" />
)}
{loadingStop ? "Stopping..." : "Stop Tracking"}
</button>
)}
</div>
<div className="divider my-1" />
{/* <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
<div>
<span className="text-base-content/70">Status:</span>{" "}
<span
className={`badge ${selectedProject.activityStatus === "ACTIVE"
? "badge-success"
: "badge-warning"
}`}
>
{selectedProject.activityStatus}
</span>
</div>
<div>
<span className="text-base-content/70">Tracking:</span>{" "}
<span
className={`badge ${selectedData?.isPaused
? "badge-warning"
: selectedData?.isTracking
? "badge-success"
: "badge-neutral"
}`}
>
{selectedData?.isPaused
? "Paused"
: selectedData?.isTracking
? "Active"
: "Ready"}
</span>
</div>
<div>
<span className="text-base-content/70">Start:</span>{" "}
<span>
{selectedData?.startTime
? selectedData.startTime.toLocaleTimeString()
: "—"}
</span>
</div>
</div> */}
</>
) : (
<p className="text-sm text-base-content/60 text-center">
@ -257,7 +332,7 @@ const Dashboard: React.FC = () => {
{/* 🔹 Table or Empty State */}
{projectsData.length > 0 ? (
<div
className="overflow-y-auto"
className="overflow-y-auto no-scrollbar"
style={{
maxHeight: "50vh", // scroll only after this height
minHeight: "auto", // auto adjusts if few rows
@ -268,32 +343,53 @@ const Dashboard: React.FC = () => {
<tr>
<th>#</th>
<th>Name</th>
{/* <th>Logs Hours</th> */}
<th>Logs Hours</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{projectsData.map((project, index) => {
{projectsData.map((project: any, index: any) => {
const isCurrentlySelected =
selectedProject?.projectId === project.projectId;
const isDisabled = isTracking && !isCurrentlySelected;
const limitReached = isProjectLimitReached(project);
const remainingTime = getFormattedRemainingTime(project);
return (
<tr key={project.projectId}>
<tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
<td className="text-xs opacity-70">{index + 1}</td>
<td className="font-medium">{project.projectName}</td>
{/* <td className="font-mono text-xs">
{formatWorkTime(project.totalWorkTime)}
</td> */}
<td className="font-medium"><div className="flex flex-col">
<span className="font-medium">{project.projectName}</span>
{limitReached && (
<span className="text-xs text-error">Daily limit reached</span>
)}
</div></td>
<td className="font-mono text-xs">
<div className="flex flex-col">
<span className="font-mono text-xs">
{formatWorkTime(project.totalWorkTime)}
</span>
{/* <span className="text-xs opacity-60">
of {project.dedicatedHours}
</span> */}
</div>
</td>
<td>
<button
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
}`}
onClick={() => handleProjectSelect(project.projectId)}
disabled={isDisabled}
>
{isCurrentlySelected ? "Selected" : "Select"}
</button>
<div className="flex flex-col gap-1">
<button
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
}`}
onClick={() => handleProjectSelectLocal(project.projectId)}
disabled={isDisabled || limitReached}
>
{isCurrentlySelected ? "Selected" : "Select"}
</button>
{/* {!limitReached && (
<span className="text-xs text-success">
{remainingTime} left
</span>
)} */}
</div>
</td>
</tr>
);

View File

@ -1,766 +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=" ">
<h1 className="text-mb font-bold mb-2">Adjust Timesheet</h1>
<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>
@ -783,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,10 +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();
@ -41,7 +43,8 @@ const Login = () => {
email: response.user.email,
}));
navigate("/")
} else if (response.success == false) {
}
else if (response.success == false) {
console.log(response.error.message)
setErrorMessage(response.error.message)
}
@ -112,15 +115,8 @@ const Login = () => {
<label className="label">
<span className="label-text font-medium">Email Address</span>
</label>
<label className="input input-bordered flex items-center gap-2">
<svg
viewBox="0 0 16 16"
fill="currentColor"
className="w-4 h-4 opacity-70"
>
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
<path d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<label className="input input-bordered flex items-center gap-2" tabIndex={0}>
<Mail size={18} />
<input
type="email"
name="email"
@ -137,18 +133,8 @@ const Login = () => {
<label className="label">
<span className="label-text font-medium">Password</span>
</label>
<label className="input input-bordered flex items-center gap-2">
<svg
viewBox="0 0 16 16"
fill="currentColor"
className="w-4 h-4 opacity-70"
>
<path
fillRule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clipRule="evenodd"
/>
</svg>
<label className="input input-bordered flex items-center gap-2" tabIndex={0}>
<KeyRound size={18} />
<input
type="password"
name="password"

Some files were not shown because too many files have changed in this diff Show More