Compare commits
9 Commits
e324beca50
...
6a122f3d51
| Author | SHA1 | Date |
|---|---|---|
|
|
6a122f3d51 | |
|
|
3a34520152 | |
|
|
e7696925e7 | |
|
|
558ea81fc2 | |
|
|
4ab4164f65 | |
|
|
45a3040614 | |
|
|
2212359c96 | |
|
|
deb66976e9 | |
|
|
1d71f8f369 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
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 |
|
|
@ -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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
{"file_format_version": "1.0.0", "ICD": {"library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5"}}
|
||||
Binary file not shown.
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -160,9 +160,9 @@ app.on("ready", async () => {
|
|||
// });
|
||||
// });
|
||||
|
||||
app.on('browser-window-blur', () => {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
// app.on('browser-window-blur', () => {
|
||||
// globalShortcut.unregisterAll();
|
||||
// });
|
||||
|
||||
|
||||
// ---------------------------
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,537 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface Project {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
interface ProjectSummaryResponse {
|
||||
totalWorkedHours: string;
|
||||
totalWorkedHoursDetailed: string;
|
||||
totalWorkedHoursDecimal: string;
|
||||
totalWorkedMinutes: string;
|
||||
dedicatedHours: number;
|
||||
pendingHours: string;
|
||||
firstLoginTime: string;
|
||||
lastLogoutTime: string;
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
selectedProjectId?: string;
|
||||
selectedDate?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
general?: string;
|
||||
}
|
||||
|
||||
interface AdjustmentTabProps {
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
const AdjustmentTab: React.FC<AdjustmentTabProps> = ({ projects }) => {
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||
const [selectedDate, setSelectedDate] = useState("");
|
||||
const [totalAdjustment, setTotalAdjustment] = useState("0h 0m");
|
||||
const [pendingOverride, setPendingOverride] = useState<string | null>(null);
|
||||
const [loadingSummary, setLoadingSummary] = useState(false);
|
||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||
const [projectSummary, setProjectSummary] = useState<ProjectSummaryResponse | null>(null);
|
||||
const [startTime, setStartTime] = useState("");
|
||||
const [endTime, setEndTime] = useState("");
|
||||
const [comment, setComment] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Field validation functions
|
||||
const validateProjectId = (projectId: string): string | undefined => {
|
||||
if (!projectId) return 'Project selection is required';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDate = (date: string): string | undefined => {
|
||||
if (!date) return 'Date is required';
|
||||
|
||||
const selectedDateObj = new Date(date);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (selectedDateObj > today) return 'Cannot select future dates';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateTime = (time: string, fieldName: string): string | undefined => {
|
||||
if (!time) return `${fieldName} is required`;
|
||||
|
||||
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
||||
if (!timeRegex.test(time)) return 'Invalid time format';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateTimeRange = (start: string, end: string): string | undefined => {
|
||||
if (!start || !end) return undefined;
|
||||
|
||||
const startTime = DateTime.fromFormat(start, "HH:mm");
|
||||
const endTime = DateTime.fromFormat(end, "HH:mm");
|
||||
|
||||
if (!startTime.isValid || !endTime.isValid) return 'Invalid time values';
|
||||
if (endTime <= startTime) return 'End time must be after start time';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDedicatedHours = (start: string, end: string): string | undefined => {
|
||||
if (!start || !end || !projectSummary) return undefined;
|
||||
|
||||
const startTime = DateTime.fromFormat(start, "HH:mm");
|
||||
const endTime = DateTime.fromFormat(end, "HH:mm");
|
||||
|
||||
if (!startTime.isValid || !endTime.isValid) return undefined;
|
||||
|
||||
const diff = endTime.diff(startTime, ["hours", "minutes"]).toObject();
|
||||
const workedHours = (diff.hours || 0) + (diff.minutes || 0) / 60;
|
||||
|
||||
if (workedHours > projectSummary.dedicatedHours) {
|
||||
return `Cannot exceed project's dedicated hours (${projectSummary.dedicatedHours} hrs)`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Real-time field validation
|
||||
const validateField = (field: keyof FormErrors, value: string) => {
|
||||
let error: string | undefined;
|
||||
|
||||
switch (field) {
|
||||
case 'selectedProjectId':
|
||||
error = validateProjectId(value);
|
||||
break;
|
||||
case 'selectedDate':
|
||||
error = validateDate(value);
|
||||
break;
|
||||
case 'startTime':
|
||||
error = validateTime(value, 'Start time');
|
||||
if (!error && endTime) {
|
||||
error = validateTimeRange(value, endTime) || validateDedicatedHours(value, endTime);
|
||||
}
|
||||
break;
|
||||
case 'endTime':
|
||||
error = validateTime(value, 'End time');
|
||||
if (!error && startTime) {
|
||||
error = validateTimeRange(startTime, value) || validateDedicatedHours(startTime, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
[field]: error
|
||||
}));
|
||||
|
||||
return !error;
|
||||
};
|
||||
|
||||
// Comprehensive form validation
|
||||
const validateForm = (): boolean => {
|
||||
const errors: FormErrors = {};
|
||||
|
||||
errors.selectedProjectId = validateProjectId(selectedProjectId);
|
||||
errors.selectedDate = validateDate(selectedDate);
|
||||
errors.startTime = validateTime(startTime, 'Start time');
|
||||
errors.endTime = validateTime(endTime, 'End time');
|
||||
|
||||
if (!errors.startTime && !errors.endTime) {
|
||||
errors.startTime = validateTimeRange(startTime, endTime);
|
||||
if (!errors.startTime) {
|
||||
errors.startTime = validateDedicatedHours(startTime, endTime);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredErrors = Object.fromEntries(
|
||||
Object.entries(errors).filter(([_, value]) => value !== undefined)
|
||||
) as FormErrors;
|
||||
|
||||
setFormErrors(filteredErrors);
|
||||
return Object.keys(filteredErrors).length === 0;
|
||||
};
|
||||
|
||||
const convertToLocalTime = (utcTimeString: string): string => {
|
||||
if (!utcTimeString) return '';
|
||||
|
||||
try {
|
||||
const localTime = DateTime.fromISO(utcTimeString, { zone: 'utc' }).toLocal();
|
||||
return localTime.toFormat('HH:mm');
|
||||
} catch (error) {
|
||||
console.error('Error converting time:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
console.log("Sending =>", selectedProjectId, selectedDate);
|
||||
setLoadingSummary(true);
|
||||
|
||||
const res = await window.electron.getProjectSummary(
|
||||
selectedProjectId,
|
||||
selectedDate
|
||||
);
|
||||
|
||||
console.log("📥 Summary Response:", res);
|
||||
|
||||
if (res.success) {
|
||||
setProjectSummary(res?.data?.summary);
|
||||
|
||||
if (res?.data?.summary?.firstLoginTime) {
|
||||
const localStartTime = convertToLocalTime(res?.data?.summary?.firstLoginTime);
|
||||
setStartTime(localStartTime);
|
||||
}
|
||||
|
||||
if (res?.data?.summary?.lastLogoutTime) {
|
||||
const localEndTime = convertToLocalTime(res?.data?.summary?.lastLogoutTime);
|
||||
setEndTime(localEndTime);
|
||||
}
|
||||
} else {
|
||||
console.warn("⚠️ Summary API failed:", res.message);
|
||||
setProjectSummary(null);
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error fetching project summary:", err);
|
||||
setProjectSummary(null);
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
} finally {
|
||||
setLoadingSummary(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProjectId && selectedDate) {
|
||||
fetchLogs();
|
||||
} else {
|
||||
setProjectSummary(null);
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
}
|
||||
}, [selectedProjectId, selectedDate]);
|
||||
|
||||
// Auto calculations
|
||||
useEffect(() => {
|
||||
if (!projectSummary) return;
|
||||
if (!startTime || !endTime) return;
|
||||
|
||||
if (formErrors.startTime?.includes('time') || formErrors.startTime?.includes('dedicated')) {
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
startTime: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
const start = DateTime.fromFormat(startTime, "HH:mm");
|
||||
const end = DateTime.fromFormat(endTime, "HH:mm");
|
||||
|
||||
if (!start.isValid || !end.isValid) return;
|
||||
|
||||
if (end < start) {
|
||||
setTotalAdjustment("0h 0m");
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = end.diff(start, ["hours", "minutes"]).toObject();
|
||||
const workedHours = diff.hours || 0;
|
||||
const workedMinutes = diff.minutes || 0;
|
||||
|
||||
const totalWorkedDecimal = workedHours + workedMinutes / 60;
|
||||
const originalWorkedDecimal = Number(projectSummary.totalWorkedHoursDecimal);
|
||||
const adjustment = totalWorkedDecimal - originalWorkedDecimal;
|
||||
|
||||
const adjH = Math.floor(Math.abs(adjustment));
|
||||
const adjM = Math.round((Math.abs(adjustment) % 1) * 60);
|
||||
|
||||
const formatted = `${adjustment >= 0 ? "+" : "-"}${adjH}h ${adjM}m`;
|
||||
setTotalAdjustment(formatted);
|
||||
|
||||
const newPending = projectSummary.dedicatedHours - totalWorkedDecimal;
|
||||
const newPendingRounded = newPending.toFixed(2);
|
||||
|
||||
setPendingOverride(newPendingRounded);
|
||||
}, [startTime, endTime, projectSummary]);
|
||||
|
||||
const handleProjectSelect = (projectId: string) => {
|
||||
console.log("handleProjectSelect", projectId);
|
||||
setSelectedProjectId(projectId);
|
||||
validateField('selectedProjectId', projectId);
|
||||
};
|
||||
|
||||
const handleSelectedDate = (value: string) => {
|
||||
setSelectedDate(value);
|
||||
validateField('selectedDate', value);
|
||||
};
|
||||
|
||||
const handleStartTimeChange = (value: string) => {
|
||||
console.log("Start changed:", value);
|
||||
setStartTime(value);
|
||||
validateField('startTime', value);
|
||||
};
|
||||
|
||||
const handleEndTimeChange = (value: string) => {
|
||||
console.log("End changed:", value);
|
||||
setEndTime(value);
|
||||
validateField('endTime', value);
|
||||
};
|
||||
|
||||
const handleDescription = (value: string) => {
|
||||
setComment(value);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
setFormErrors({});
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dedicatedHoursError = validateDedicatedHours(startTime, endTime);
|
||||
if (dedicatedHoursError) {
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
startTime: dedicatedHoursError
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
projectId: selectedProjectId,
|
||||
date: selectedDate,
|
||||
loginTime: `${startTime}:00`,
|
||||
logoutTime: `${endTime}:00`,
|
||||
comment
|
||||
};
|
||||
|
||||
console.log("Submitting payload:", payload);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await window.electron.createTimesheet(payload);
|
||||
console.log("API Response:", res);
|
||||
|
||||
if (res?.success) {
|
||||
console.log("Timesheet Created Successfully");
|
||||
alert(res.message);
|
||||
|
||||
setSelectedProjectId("");
|
||||
setSelectedDate("");
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
setComment("");
|
||||
setFormErrors({});
|
||||
setTotalAdjustment("0h 0m");
|
||||
} else {
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
general: res?.message || "Failed to create timesheet"
|
||||
}));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Submission error:", err);
|
||||
setFormErrors(prev => ({
|
||||
...prev,
|
||||
general: err.message || "Something went wrong while saving timesheet"
|
||||
}));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl ">
|
||||
<div className="space-y-3">
|
||||
{/* General Error Alert */}
|
||||
{formErrors.general && (
|
||||
<div className="alert alert-error shadow-sm my-1 px-6 py-1 text-xs text-base-100">
|
||||
<span>{formErrors.general}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Project Selection */}
|
||||
<div className="form-control col-span-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Select project</span>
|
||||
</label>
|
||||
<select
|
||||
className={`select select-sm ${formErrors.selectedProjectId ? 'select-error' : ''}`}
|
||||
value={selectedProjectId}
|
||||
onChange={(e) => handleProjectSelect(e.target.value)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Pick a project
|
||||
</option>
|
||||
{projects.map((item, index) => (
|
||||
<option key={item.projectId || index} value={item.projectId}>
|
||||
{item.projectName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{formErrors.selectedProjectId && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error font-medium">{formErrors.selectedProjectId}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date Selection */}
|
||||
<div className="form-control col-span-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Select timesheet date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => handleSelectedDate(e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.selectedDate ? 'input-error' : ''}`}
|
||||
/>
|
||||
{formErrors.selectedDate && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error font-medium">{formErrors.selectedDate}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Current Total Time */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm ">Current total time</span>
|
||||
</label>
|
||||
{loadingSummary ? (
|
||||
<div className="skeleton h-9 w-full"></div>
|
||||
) :
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
projectSummary
|
||||
? `${pendingOverride ? (projectSummary.dedicatedHours - Number(pendingOverride)).toFixed(2) : projectSummary.totalWorkedHoursDecimal} hrs`
|
||||
: "0 hrs"
|
||||
}
|
||||
readOnly
|
||||
className="input input-bordered input-sm w-full bg-base-200"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Current Pending Time */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Current pending time</span>
|
||||
</label>
|
||||
{loadingSummary ? (
|
||||
<div className="skeleton h-9 w-full"></div>
|
||||
) :
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
projectSummary
|
||||
? `${pendingOverride ?? projectSummary.pendingHours} hrs`
|
||||
: "0 hrs"
|
||||
}
|
||||
readOnly
|
||||
className="input input-bordered input-sm w-full bg-base-200"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Start Time */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Start time</span>
|
||||
</label>
|
||||
{loadingSummary ? (
|
||||
<div className="skeleton h-9 w-full"></div>
|
||||
) :
|
||||
<input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => handleStartTimeChange(e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.startTime ? 'input-error' : ''}`}
|
||||
/>
|
||||
}
|
||||
{formErrors.startTime && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error font-medium">{formErrors.startTime}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* End Time */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm ">End time</span>
|
||||
</label>
|
||||
{loadingSummary ? (
|
||||
<div className="skeleton h-9 w-full"></div>
|
||||
) :
|
||||
<input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => handleEndTimeChange(e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${formErrors.endTime ? 'input-error' : ''}`}
|
||||
/>
|
||||
}
|
||||
{formErrors.endTime && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error font-medium">{formErrors.endTime}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Total Time Adjustment */}
|
||||
<div className="form-control col-span-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Total time adjustment</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Calculated automatically"
|
||||
value={totalAdjustment}
|
||||
readOnly
|
||||
className="input input-bordered input-sm w-full bg-base-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="form-control flex flex-col gap-1">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm ">Description / Reason</span>
|
||||
</label>
|
||||
<textarea
|
||||
className="textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all"
|
||||
placeholder="Please enter the task name or leave notes to help us better understand your request."
|
||||
value={comment}
|
||||
onChange={(e) => handleDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
className={`btn btn-primary flex-1 hover:shadow-lg transition-all duration-300 ${isSubmitting ? 'loading' : ''}`}
|
||||
>
|
||||
{!isSubmitting && (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Request'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdjustmentTab;
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
interface LeaveFormData {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
const PtoLeaveTab: React.FC = () => {
|
||||
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
comment: ''
|
||||
});
|
||||
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
|
||||
|
||||
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
||||
const errors: Partial<LeaveFormData> = {};
|
||||
|
||||
if (!data.fromDate) {
|
||||
errors.fromDate = 'From date is required';
|
||||
}
|
||||
|
||||
if (!data.toDate) {
|
||||
errors.toDate = 'To date is required';
|
||||
}
|
||||
|
||||
if (!data.comment.trim()) {
|
||||
errors.comment = 'Comment is required';
|
||||
}
|
||||
|
||||
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) {
|
||||
errors.toDate = 'To date must be after from date';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handlePtoChange = (field: keyof LeaveFormData, value: string) => {
|
||||
setPtoLeave(prev => ({ ...prev, [field]: value }));
|
||||
// Clear error for the field being changed
|
||||
if (ptoErrors[field]) {
|
||||
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePtoSubmit = async () => {
|
||||
const errors = validateLeaveForm(ptoLeave);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setPtoErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Submitting PTO:', ptoLeave);
|
||||
alert('PTO request submitted successfully!');
|
||||
|
||||
setPtoLeave({ fromDate: '', toDate: '', comment: "" });
|
||||
setPtoErrors({});
|
||||
} catch (error) {
|
||||
alert('Failed to submit request. Please try again.');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* From Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">From Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={ptoLeave.fromDate}
|
||||
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
ptoErrors.fromDate ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
{ptoErrors.fromDate && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.fromDate}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* To Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">To Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={ptoLeave.toDate}
|
||||
onChange={(e) => handlePtoChange('toDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
ptoErrors.toDate ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
{ptoErrors.toDate && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.toDate}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description/Comment Field */}
|
||||
<div className="form-control flex flex-col gap-1 col-span-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Description / Reason</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${
|
||||
ptoErrors.comment ? 'textarea-error' : ''
|
||||
}`}
|
||||
placeholder="Please enter the reason for your PTO request to help us better understand your needs."
|
||||
value={ptoLeave.comment}
|
||||
onChange={(e) => handlePtoChange('comment', e.target.value)}
|
||||
/>
|
||||
{ptoErrors.comment && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-xs text-error font-medium">{ptoErrors.comment}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handlePtoSubmit}
|
||||
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Submit PTO Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PtoLeaveTab;
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
interface LeaveFormData {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
const SickLeaveTab: React.FC = () => {
|
||||
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
comment: ''
|
||||
});
|
||||
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
|
||||
|
||||
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
|
||||
const errors: Partial<LeaveFormData> = {};
|
||||
|
||||
if (!data.fromDate) {
|
||||
errors.fromDate = 'From date is required';
|
||||
}
|
||||
|
||||
if (!data.toDate) {
|
||||
errors.toDate = 'To date is required';
|
||||
}
|
||||
|
||||
if (!data.comment.trim()) {
|
||||
errors.comment = 'Comment is required';
|
||||
}
|
||||
|
||||
if (data.fromDate && data.toDate && new Date(data.fromDate) > new Date(data.toDate)) {
|
||||
errors.toDate = 'To date must be after from date';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleSickChange = (field: keyof LeaveFormData, value: string) => {
|
||||
setSickLeave(prev => ({ ...prev, [field]: value }));
|
||||
if (sickErrors[field]) {
|
||||
setSickErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSickSubmit = async () => {
|
||||
const errors = validateLeaveForm(sickLeave);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setSickErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Submitting Sick Leave:', sickLeave);
|
||||
alert('Sick Leave request submitted successfully!');
|
||||
|
||||
setSickLeave({ fromDate: '', toDate: '', comment: '' });
|
||||
setSickErrors({});
|
||||
} catch (error) {
|
||||
alert('Failed to submit request. Please try again.');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* From Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">From Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={sickLeave.fromDate}
|
||||
onChange={(e) => handleSickChange('fromDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
sickErrors.fromDate ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
{sickErrors.fromDate && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.fromDate}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* To Date */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">To Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={sickLeave.toDate}
|
||||
onChange={(e) => handleSickChange('toDate', e.target.value)}
|
||||
className={`input input-bordered input-sm w-full focus:input-primary transition-all ${
|
||||
sickErrors.toDate ? 'input-error' : ''
|
||||
}`}
|
||||
/>
|
||||
{sickErrors.toDate && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.toDate}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description/Comment Field */}
|
||||
<div className="form-control flex flex-col gap-1 col-span-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-sm">Description / Reason</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`textarea text-xs textarea-bordered h-40 w-full focus:textarea-primary transition-all ${
|
||||
sickErrors.comment ? 'textarea-error' : ''
|
||||
}`}
|
||||
placeholder="Please describe your symptoms or reason for sick leave to help us better understand your situation."
|
||||
value={sickLeave.comment}
|
||||
onChange={(e) => handleSickChange('comment', e.target.value)}
|
||||
/>
|
||||
{sickErrors.comment && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleSickSubmit}
|
||||
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Submit Sick Leave Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SickLeaveTab;
|
||||
|
|
@ -4,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
Loading…
Reference in New Issue