Compare commits

..

117 Commits

Author SHA1 Message Date
BhagwanSingh 4b146ff5e9 Merge branch 'new-window-v2' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 22 Dec After Merge 2025-12-22 13:08:56 +05:30
BhagwanSingh cf71247e93 22 Dec 2025-12-22 12:27:56 +05:30
JaiPandey425 1fc065a59b add the times pre and next functionality in timesheet modal and add the payoneerAccount user profile update 2025-12-19 19:36:33 +05:30
Bhagwan Singh b44278e1a0 MAC VPN Update 2025-12-19 16:26:19 +05:30
Bhagwan Singh 7718e95ee5 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 18 Dec Merge 7:06 2025-12-18 19:06:55 +05:30
Bhagwan Singh e4dadea8da a 2025-12-18 19:04:35 +05:30
BhagwanSingh 9eb1a3ff1e A 2025-12-18 16:14:49 +05:30
BhagwanSingh c2ef59c621 Merge branch 'new-window-v2' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - new window v2 jai merge 18 Dec 2025-12-18 16:07:51 +05:30
BhagwanSingh 85a178e825 Added Logout Option 2025-12-18 15:57:34 +05:30
BhagwanSingh b0d296e007 Logs TIme Issue Solved 2025-12-18 15:35:24 +05:30
JaiPandey425 b4403c2cd5 fixed issue if no project assign 2025-12-18 15:32:19 +05:30
BhagwanSingh e3692467e3 Merge branch 'new-window-v2' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 17-Dec - 4:25 2025-12-17 15:25:49 +05:30
JaiPandey425 c03c89d613 handleStop tracking fixed time 2025-12-17 15:23:11 +05:30
BhagwanSingh 05e706e150 Merge branch 'new-window-v2' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 3:22 - 17Dec 2025-12-17 15:23:02 +05:30
BhagwanSingh d7128000be VPN Code UPdate 2025-12-17 15:21:43 +05:30
BhagwanSingh 2306e54515 VPN Start 2025-12-16 16:24:11 +05:30
BhagwanSingh d71ac936fa a 2025-12-12 12:07:14 +05:30
BhagwanSingh 9ab39832b5 Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-12-12 12:06:25 +05:30
JaiPandey425 68eb853987 change the tabs logs data login when stop send logs tabs data then logout 2025-12-11 19:44:58 +05:30
JaiPandey425 94840a93dd add vpn v2 2025-12-11 14:05:33 +05:30
BhagwanSingh 9172534f31 Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - Window-Build Merge 10 Dec 2025-12-10 18:53:11 +05:30
BhagwanSingh 394150a2ee Resource 10-Dec 2025-12-10 18:50:53 +05:30
JaiPandey425 882f4acad8 timesheet 2025-12-10 18:46:57 +05:30
JaiPandey425 6c223f01c9 timesheet session time update 2025-12-10 16:08:04 +05:30
JaiPandey425 d4d6ff5344 vpn check don't build 2025-12-09 13:28:45 +05:30
JaiPandey425 1cc863d3c9 isLimit end then change the color working but comment 2025-12-08 16:50:08 +05:30
Bhagwan Singh 0e5f059682 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - mac - 8-Dec - 3-38 2025-12-08 15:38:13 +05:30
Bhagwan Singh 77f91eb16f Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - MAC - 08-Dec 2025-12-08 15:29:01 +05:30
Bhagwan Singh 217781cfe8 a 2025-12-08 15:24:51 +05:30
BhagwanSingh 0421a20b4a VPN Update 2025-12-08 15:13:25 +05:30
BhagwanSingh 586531bdbc a 8-12 2025-12-08 07:42:23 +05:30
BhagwanSingh 312007655f Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 05-Dec | 5-50 2025-12-05 17:49:50 +05:30
BhagwanSingh 7020499fec a 05-Dec 5-48 2025-12-05 17:48:20 +05:30
JaiPandey425 c0a89d3f69 profile payment update 2025-12-05 17:47:58 +05:30
JaiPandey425 3efcf99151 fixed the pto , sick , adjustment quill onfocus issue 2025-12-05 16:42:50 +05:30
BhagwanSingh adfd911f7b Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-12-05 13:26:54 +05:30
BhagwanSingh 3177ac630d a - before merge 5-Dec 1:25 2025-12-05 13:25:41 +05:30
JaiPandey425 bebb4fb980 new changes 12/5/2025 2025-12-05 13:23:11 +05:30
BhagwanSingh 203a8d692c Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - Window-build Merge 04-Dec 8:39 PM 2025-12-04 20:39:15 +05:30
BhagwanSingh de44bdac72 remove Chromium Shortucut Keys, Ts Error Resolve 2025-12-04 20:37:37 +05:30
JaiPandey425 9c8499ff2f new changes 2025-12-04 20:04:23 +05:30
JaiPandey425 e4eb851bc1 solve the tracking stop issue and also timmer is run with alert window show changes in dashbpardlayout and useActiveApptracker hooks 2025-12-04 17:20:41 +05:30
JaiPandey425 f5f94f016b Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker into window-build - merge 1-14 12-Dec - B Master 2025-12-04 13:14:46 +05:30
Bhagwan Singh ec566bb14a Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 04-Dec - Mac 2025-12-04 12:55:49 +05:30
Bhagwan Singh 2c4e158d67 API Update 2025-12-04 12:55:01 +05:30
JaiPandey425 52e45f8328 Add the setTimeOut leave and adjustment api call 2025-12-04 12:06:45 +05:30
BhagwanSingh 93704a70f6 Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 04-Dec 2025-12-04 11:57:24 +05:30
JaiPandey425 9eeb14ffcd Change the Dashboard Layout when times is up show the alert window but time still run when user handleStop then only stop 2025-12-04 11:43:00 +05:30
JaiPandey425 df2b35a4e4 leave and adjut. show both the table and tabs based table 2025-12-03 16:17:10 +05:30
BhagwanSingh ed0bcb7107 Socket update 2025-12-02 20:02:55 +05:30
JaiPandey425 70193501d3 Timesheets open the modal with loader show the logs with loading 2025-12-02 19:42:19 +05:30
JaiPandey425 475776b214 payment method modal added in settings sections 2025-12-02 19:22:48 +05:30
JaiPandey425 0eea767e5e listing all the menus components and add the refresh button with the last time refresh list 2025-12-02 17:16:50 +05:30
JaiPandey425 17dcdec1c6 refresh dashboard projects 2025-12-01 16:40:24 +05:30
JaiPandey425 4fb0720e7d change the web-socket ngrok url 2025-12-01 12:05:13 +05:30
BhagwanSingh 3ef615a160 Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 30-11 Merge Window-build 2025-11-30 11:47:21 +05:30
BhagwanSingh b55f9469ad Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-30 11:44:00 +05:30
BhagwanSingh d23933c517 30-11 2025-11-30 11:39:41 +05:30
JaiPandey425 a0c95b05bc payslip and shedules 2025-11-28 19:41:06 +05:30
Bhagwan Singh 92e8c15023 Mac Docva Icon Fixed 2025-11-28 17:40:30 +05:30
Bhagwan Singh 35eb073f46 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker - 28-11 2025-11-28 13:25:02 +05:30
apple 94eedff8af Mac Input fields are not viaible fixed 2025-11-28 13:19:01 +05:30
apple ac3ddf4b2b eerge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-27 19:40:22 +05:30
apple a55e6ccba4 upd 2025-11-27 19:40:05 +05:30
JaiPandey425 f1c66c6621 Schedule ui 2025-11-27 19:32:04 +05:30
JaiPandey425 35fa90705b quill copy image create duplicate image & overlap date in PTO and SICK leave with server response show in va-tracker 2025-11-27 18:19:54 +05:30
BhagwanSingh bd4a3656e7 Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-27 16:41:51 +05:30
BhagwanSingh 345822885f a 2025-11-27 16:34:42 +05:30
JaiPandey425 d94db40a80 timmer fixed and server time set when stopping and quill setup 2025-11-27 16:32:17 +05:30
BhagwanSingh 0ae8f6185a a 2025-11-27 12:27:52 +05:30
BhagwanSingh 882e8349c9 Merge branch 'window-build' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker into window-build 2025-11-27 12:24:00 +05:30
BhagwanSingh d641e9432d 27-11 merge 2025-11-27 12:21:53 +05:30
JaiPandey425 0328183852 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker into window-build 2025-11-27 11:50:54 +05:30
apple 39014708d7 Adding Last Commit Changes 2025-11-26 20:06:22 +05:30
apple 0b7d844753 Mac Build issue, Close Button Issue and Drag Issue Fixed 2025-11-26 20:00:49 +05:30
BhagwanSingh 9fc9b964c2 Tracker Closes automatically on zoom meetings Bug Fix 2025-11-26 13:02:58 +05:30
JaiPandey425 880ef9eae5 icons setup installer app launcher 2025-11-25 12:16:22 +05:30
JaiPandey425 ff32a54aac 21/11/2025 win:build - .exe 2025-11-21 17:05:01 +05:30
JaiPandey425 6a122f3d51 window builder with latest code 21/11/2025 2025-11-21 16:05:07 +05:30
JaiPandey425 3a34520152 pto-and-leave-dark-light-mode-toggle 2025-11-21 12:22:02 +05:30
BhagwanSingh e7696925e7 s 2025-11-21 12:18:33 +05:30
BhagwanSingh 558ea81fc2 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-20 13:13:25 +05:30
BhagwanSingh 4ab4164f65 bm 2011 2025-11-20 12:57:36 +05:30
JaiPandey425 45a3040614 show timming in hh:mm:ss 2025-11-20 12:27:43 +05:30
JaiPandey425 2212359c96 change the log login and logout sync with the redux 2025-11-19 20:03:57 +05:30
BhagwanSingh deb66976e9 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-18 17:15:34 +05:30
BhagwanSingh 1d71f8f369 Live View Done 2025-11-18 17:15:03 +05:30
JaiPandey425 e324beca50 Setup installer for window .exe file to direct open the app 2025-11-18 13:35:41 +05:30
JaiPandey425 df92e68a0a timesheet adjustments 2025-11-17 20:04:42 +05:30
BhagwanSingh f0e6b704cd Multiple Click Handle, Task Manager Instance Handle, Loader, Remove Unnecessary Polling 2025-11-17 09:52:09 +05:30
BhagwanSingh c0f0986a5f Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-17 08:46:50 +05:30
BhagwanSingh c13a4ff356 17-11 08 2025-11-17 08:39:13 +05:30
BhagwanSingh 51b47beba9 test 2025-11-15 19:05:55 +05:30
JaiPandey425 8bf3343ba3 time 2025-11-14 19:12:12 +05:30
BhagwanSingh be819cd1bb a 2025-11-14 18:20:58 +05:30
JaiPandey425 d6481a0c27 sec remove show min 2025-11-14 16:49:48 +05:30
JaiPandey425 44d790abab if any user start the tracking , not login other devices and the expire token then the logout 2025-11-14 16:04:48 +05:30
BhagwanSingh f7e77f895c Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-14 13:03:27 +05:30
BhagwanSingh fc9820217b Production .exe file issue fixed 2025-11-14 12:51:25 +05:30
JaiPandey425 b56a6e11c4 chnage password 2025-11-14 12:50:18 +05:30
BhagwanSingh c2213e0969 Update DashboardLayout.tsx
merge
2025-11-13 15:56:38 +05:30
BhagwanSingh f499cbfa7b Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-13 15:56:29 +05:30
BhagwanSingh f3eb099e18 a 2025-11-13 15:48:25 +05:30
JaiPandey425 8d2961c50d type error fix 2025-11-13 15:47:02 +05:30
JaiPandey425 b01ad415f1 forget password 2025-11-12 19:50:42 +05:30
BhagwanSingh d0e8ded221 Update ipcHandler.ts
s
2025-11-12 12:22:08 +05:30
BhagwanSingh 11398c6869 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-12 12:21:46 +05:30
BhagwanSingh aa00c75edf Socket Setup 2025-11-12 12:03:23 +05:30
JaiPandey425 b448e2a253 login logout and api logs apis 2025-11-12 11:58:52 +05:30
BhagwanSingh d42a26ad8e Update package-lock.json 2025-11-10 12:41:34 +05:30
BhagwanSingh bb8b654e93 Merge branch 'master' of http://git.hicglobalsolutions.in/BhawanSingh/va-tracker 2025-11-10 12:27:22 +05:30
BhagwanSingh 70cd30af95 Get Running Apps 2025-11-10 11:56:36 +05:30
JaiPandey425 fbc5de3a59 redux toolkit setup and tracking logs 2025-11-10 11:48:19 +05:30
BhagwanSingh fbf84bb7aa Disable Close and Minimize Tracker IPC Handle 2025-11-07 19:17:18 +05:30
BhagwanSingh 3f190f333b Merge branch 'merge' 2025-11-07 15:45:39 +05:30
JaiPandey425 94a6ebdac7 timer 2025-11-07 13:25:43 +05:30
JaiPandey425 ea74c00dde timer 2025-11-07 13:09:33 +05:30
73 changed files with 12986 additions and 3604 deletions

0
.env Normal file
View File

3
.gitignore vendored
View File

@ -11,7 +11,9 @@ node_modules
dist
dist-react
dist-electron
release
*.local
release
# Editor directories and files
.vscode/*
@ -28,3 +30,4 @@ nextSteps.txt
/playwright-report/
/blob-report/
/playwright/.cache/
resources

17
.npmrc Normal file
View File

@ -0,0 +1,17 @@
# Force use of prebuilt binaries
prefer_binary=true
build_from_source=false
# Skip building from source
ignore_scripts=false
# Electron configuration
runtime=electron
target=32.1.2
disturl=https://electronjs.org/headers
# Don't fail on optional dependencies
optional=true
# Use production registry
registry=https://registry.npmjs.org/

8
banner.js Normal file
View File

@ -0,0 +1,8 @@
import figlet from "figlet";
import chalk from "chalk";
figlet("Jai Pandey DocVA", (err, data) => {
if (err) return console.log("Error generating banner");
console.log(chalk.cyan(data));
console.log(chalk.green("\n🚀 Development Mode Starting...\n"));
});

13
build-all.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# Windows
GOOS=windows GOARCH=amd64 go build -o ./bin/win/wireguard-go.exe ./wireguard-go
# Linux
GOOS=linux GOARCH=amd64 go build -o ./bin/linux/wireguard-go ./wireguard-go
# macOS Intel
GOOS=darwin GOARCH=amd64 go build -o ./bin/mac/wireguard-go ./wireguard-go
# macOS ARM
GOOS=darwin GOARCH=arm64 go build -o ./bin/mac/wireguard-go-arm ./wireguard-go

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

132
e2e/example.spec.js Normal file
View File

@ -0,0 +1,132 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var test_1 = require("@playwright/test");
var electronApp;
var mainPage;
function waitForPreloadScript() {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve) {
var interval = setInterval(function () { return __awaiter(_this, void 0, void 0, function () {
var electronBridge;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, mainPage.evaluate(function () {
return window.electron;
})];
case 1:
electronBridge = _a.sent();
if (electronBridge) {
clearInterval(interval);
resolve(true);
}
return [2 /*return*/];
}
});
}); }, 100);
})];
});
});
}
test_1.test.beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, test_1._electron.launch({
args: ['.'],
env: { NODE_ENV: 'development' },
})];
case 1:
electronApp = _a.sent();
return [4 /*yield*/, electronApp.firstWindow()];
case 2:
mainPage = _a.sent();
return [4 /*yield*/, waitForPreloadScript()];
case 3:
_a.sent();
return [2 /*return*/];
}
});
}); });
test_1.test.afterEach(function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, electronApp.close()];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });
(0, test_1.test)('custom frame should minimize the mainWindow', function () { return __awaiter(void 0, void 0, void 0, function () {
var isMinimized;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, mainPage.click('#minimize')];
case 1:
_a.sent();
return [4 /*yield*/, electronApp.evaluate(function (electron) {
return electron.BrowserWindow.getAllWindows()[0].isMinimized();
})];
case 2:
isMinimized = _a.sent();
(0, test_1.expect)(isMinimized).toBeTruthy();
return [2 /*return*/];
}
});
}); });
(0, test_1.test)('should create a custom menu', function () { return __awaiter(void 0, void 0, void 0, function () {
var menu;
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, electronApp.evaluate(function (electron) {
return electron.Menu.getApplicationMenu();
})];
case 1:
menu = _c.sent();
(0, test_1.expect)(menu).not.toBeNull();
(0, test_1.expect)(menu === null || menu === void 0 ? void 0 : menu.items).toHaveLength(2);
(0, test_1.expect)((_a = menu === null || menu === void 0 ? void 0 : menu.items[0].submenu) === null || _a === void 0 ? void 0 : _a.items).toHaveLength(2);
(0, test_1.expect)((_b = menu === null || menu === void 0 ? void 0 : menu.items[1].submenu) === null || _b === void 0 ? void 0 : _b.items).toHaveLength(3);
(0, test_1.expect)(menu === null || menu === void 0 ? void 0 : menu.items[1].label).toBe('View');
return [2 /*return*/];
}
});
}); });

View File

@ -1,16 +0,0 @@
{
"appId": "com.n-ziermann.electron-course",
"files": ["dist-electron", "dist-react"],
"extraResources": ["dist-electron/preload.cjs", "src/assets/**"],
"icon": "./desktopIcon.png",
"mac": {
"target": "dmg"
},
"linux": {
"target": "AppImage",
"category": "Utility"
},
"win": {
"target": ["portable", "msi"]
}
}

View File

@ -4,11 +4,11 @@
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VA Tracker</title>
<meta
<title>DocVa</title>
<!-- <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';"
/>
/> -->
</head>
<body>
<div id="root"></div>

6024
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,141 @@
{
"name": "va-tracker",
"private": true,
"version": "0.0.0",
"version": "1.1.1",
"type": "module",
"main": "dist-electron/main.js",
"scripts": {
"dev": "npm-run-all --parallel dev:react dev:electron",
"banner": "node banner.js",
"dev": "npm-run-all banner --parallel transpile:electron dev:react dev:electron",
"dev:react": "vite",
"dev:electron": "npm run transpile:electron && cross-env NODE_ENV=development electron --inspect=5858 .",
"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",
"transpile:electron": "tsc --project src/electron/tsconfig.json",
"dist:mac": "npm run transpile:electron && npm run build && electron-builder --mac --arm64",
"dist:mac": "npm run transpile:electron && npm run build && electron-builder --mac --universal",
"dist:win": "npm run transpile:electron && npm run build && electron-builder --win --x64",
"dist:linux": "npm run transpile:electron && npm run build && electron-builder --linux --x64",
"test:e2e": "playwright test",
"test:unit": "vitest src"
},
"dependencies": {
"@reduxjs/toolkit": "^2.10.1",
"@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.2",
"chalk": "^5.6.2",
"electron-store": "^11.0.2",
"get-windows": "^9.2.3",
"figlet": "^1.9.4",
"https": "^1.0.0",
"lucide-react": "^0.552.0",
"luxon": "^3.7.2",
"os-utils": "^0.0.14",
"quill": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-quill-new": "^3.6.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.5",
"recharts": "^2.12.7",
"sharp": "^0.34.4",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.16"
},
"optionalDependencies": {
"active-win": "^7.7.2",
"get-windows": "^9.2.3",
"sharp": "^0.34.5",
"windowmanager": "^0.14.8"
},
"build": {
"appId": "com.vatracker.app",
"productName": "VA Tracker",
"npmRebuild": false,
"buildDependenciesFromSource": false,
"nodeGypRebuild": false,
"directories": {
"output": "release",
"buildResources": "build"
},
"icon": "build/icon.ico",
"files": [
"dist-electron/**/*",
"dist-react/**/*",
"package.json"
],
"extraResources": [
{
"from": "dist-electron",
"to": "dist-electron"
},
{
"from": "src/assets",
"to": "assets"
},
{
"from": "resources/wireguard-install",
"to": "wireguard-install"
},
{
"from": "resources/wireguard",
"to": "wireguard"
}
],
"mac": {
"target": [
{
"target": "dmg",
"arch": "universal"
}
],
"icon": "build/icon.icns",
"extendInfo": {
"LSUIElement": false,
"LSBackgroundOnly": false
}
},
"win": {
"requestedExecutionLevel": "requireAdministrator",
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"signAndEditExecutable": true,
"signingHashAlgorithms": [],
"verifyUpdateCodeSignature": false
},
"nsis": {
"oneClick": true,
"perMachine": false,
"createDesktopShortcut": "always",
"createStartMenuShortcut": true,
"runAfterFinish": true,
"installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico",
"installerHeaderIcon": "build/icon.ico",
"artifactName": "${productName}-Setup-${version}.${ext}"
}
},
"devDependencies": {
"@playwright/test": "^1.47.2",
"@types/node": "^22.5.5",
"@types/luxon": "^3.7.1",
"@types/node": "^22.19.0",
"@types/os-utils": "^0.0.4",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"cross-env": "^7.0.3",
"daisyui": "^5.4.3",
"electron": "^32.1.2",
"electron": "^39.2.5",
"electron-builder": "^25.0.5",
"electron-rebuild": "^3.2.9",
"npm-run-all": "^4.1.5",
"rcedit": "^5.0.2",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"vitest": "^2.1.1"
"vite": "^7.2.6",
"vitest": "^4.0.15"
}
}

73
playwright.config.js Normal file
View File

@ -0,0 +1,73 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
var test_1 = require("@playwright/test");
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
exports.default = (0, test_1.defineConfig)({
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: __assign({}, test_1.devices['Desktop Chrome']),
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev:react',
url: 'http://localhost:5123',
reuseExistingServer: !process.env.CI,
},
});

Binary file not shown.

View File

@ -0,0 +1,10 @@
[Interface]
PrivateKey = CEzCd25ufxbOjvRhMqLfkcdP/OziG5i6OkXCfMZrI1I=
Address = 10.0.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U=
Endpoint = 35.95.209.161:51820
AllowedIPs = 10.0.0.0/32
PersistentKeepalive = 25

View File

@ -0,0 +1,13 @@
[Interface]
PrivateKey = gPqf7ScXv2xtLkZz5a8qa7RNJ7uU5TQUo/rTfcprMWE=
Address = 10.0.0.172/32
PrivateKey = wHjS7rJYnAGhlvj+XYLefAzki1uRM2xz95IxRML2q24=
Address = 10.0.0.169/32
DNS = 1.1.1.1
[Peer]
PublicKey = jG+7H5thEiNjxY1XqI1R9oyMAMTIegLwE3tZkS1Rj2U=
Endpoint = 35.95.209.161:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

325
src/assets/splash.html Normal file
View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #ffffff;
font-family: Arial, sans-serif;
}
.loader-container {
position: relative;
width: 100px;
height: 100px;
}
/* Circular spinner */
.loader-ring {
width: 100%;
height: 100%;
border: 6px solid #e0e0e0;
border-top-color: #6a5acd;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Logo in center */
.loader-logo {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
}
.loader-logo svg {
width: 70px;
height: 70px;
margin-left: 12px;
margin-top: 8px;
object-fit: contain;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="loader-container">
<div class="loader-ring"></div>
<!-- Place your logo here -->
<div class="loader-logo">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100"
zoomAndPan="magnify" viewBox="0 0 368.25 242.999999" height="100" preserveAspectRatio="xMidYMid meet"
version="1.0">
<defs>
<clipPath id="17e5ee130b">
<path d="M 137.171875 1.269531 L 231 1.269531 L 231 99 L 137.171875 99 Z M 137.171875 1.269531 "
clip-rule="nonzero" />
</clipPath>
<clipPath id="0262fc8075">
<path d="M 284 124 L 364.921875 124 L 364.921875 206 L 284 206 Z M 284 124 " clip-rule="nonzero" />
</clipPath>
<clipPath id="0914a8505c">
<path d="M 3 231 L 14 231 L 14 241.671875 L 3 241.671875 Z M 3 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="da83339c1d">
<path d="M 17 231 L 20 231 L 20 241.671875 L 17 241.671875 Z M 17 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="97dbddcd78">
<path d="M 25 231 L 34 231 L 34 241.671875 L 25 241.671875 Z M 25 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="3223358027">
<path d="M 36 231 L 46 231 L 46 241.671875 L 36 241.671875 Z M 36 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="f960afe448">
<path d="M 49 231 L 59 231 L 59 241.671875 L 49 241.671875 Z M 49 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="04b92ea6b9">
<path d="M 61 231 L 72 231 L 72 241.671875 L 61 241.671875 Z M 61 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="7b4adb9303">
<path d="M 75 231 L 84 231 L 84 241.671875 L 75 241.671875 Z M 75 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="53357fd403">
<path d="M 93 231 L 104 231 L 104 241.671875 L 93 241.671875 Z M 93 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="bcd03dbca6">
<path d="M 106 231 L 117 231 L 117 241.671875 L 106 241.671875 Z M 106 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="ff8639fe11">
<path d="M 120 231 L 130 231 L 130 241.671875 L 120 241.671875 Z M 120 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="3990de54cb">
<path d="M 134 231 L 137 231 L 137 241.671875 L 134 241.671875 Z M 134 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="045cbd7804">
<path d="M 141 231 L 151 231 L 151 241.671875 L 141 241.671875 Z M 141 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="7de9a85e20">
<path d="M 154 231 L 163 231 L 163 241.671875 L 154 241.671875 Z M 154 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="e68f5b3fba">
<path d="M 165 231 L 176 231 L 176 241.671875 L 165 241.671875 Z M 165 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="65464ab719">
<path d="M 179 231 L 189 231 L 189 241.671875 L 179 241.671875 Z M 179 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="b82da71650">
<path d="M 192 231 L 202 231 L 202 241.671875 L 192 241.671875 Z M 192 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="fe4eaf6826">
<path d="M 204 231 L 215 231 L 215 241.671875 L 204 241.671875 Z M 204 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="79d174fbbf">
<path d="M 225 231 L 234 231 L 234 241.671875 L 225 241.671875 Z M 225 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="b0be87ffda">
<path d="M 236 230 L 249 230 L 249 241.671875 L 236 241.671875 Z M 236 230 " clip-rule="nonzero" />
</clipPath>
<clipPath id="444dbef42a">
<path d="M 252 231 L 262 231 L 262 241.671875 L 252 241.671875 Z M 252 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="314903e6e8">
<path d="M 272 230 L 283 230 L 283 241.671875 L 272 241.671875 Z M 272 230 " clip-rule="nonzero" />
</clipPath>
<clipPath id="2fd70bc44d">
<path d="M 286 230 L 299 230 L 299 241.671875 L 286 241.671875 Z M 286 230 " clip-rule="nonzero" />
</clipPath>
<clipPath id="032e9be536">
<path d="M 302 230 L 312 230 L 312 241.671875 L 302 241.671875 Z M 302 230 " clip-rule="nonzero" />
</clipPath>
<clipPath id="77ab9ec736">
<path d="M 314 231 L 324 231 L 324 241.671875 L 314 241.671875 Z M 314 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="bf7e46828b">
<path d="M 326 230 L 339 230 L 339 241.671875 L 326 241.671875 Z M 326 230 " clip-rule="nonzero" />
</clipPath>
<clipPath id="b78234ca4f">
<path d="M 343 231 L 352 231 L 352 241.671875 L 343 241.671875 Z M 343 231 " clip-rule="nonzero" />
</clipPath>
<clipPath id="2b0e76334b">
<path d="M 355 231 L 364.921875 231 L 364.921875 241.671875 L 355 241.671875 Z M 355 231 "
clip-rule="nonzero" />
</clipPath>
</defs>
<g clip-path="url(#17e5ee130b)">
<path fill="#060165"
d="M 181.777344 1.269531 L 137.171875 1.269531 L 137.171875 53.847656 C 137.171875 78.601562 157.222656 98.652344 181.980469 98.652344 C 208.785156 98.652344 230.679688 76.761719 230.679688 49.960938 C 230.679688 23.160156 208.785156 1.269531 181.777344 1.269531 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<path fill="#ffffff"
d="M 173.386719 31.140625 C 173.386719 36.253906 169.089844 40.550781 163.976562 40.550781 C 158.859375 40.550781 154.5625 44.847656 154.5625 49.960938 C 154.5625 55.074219 158.859375 59.371094 163.976562 59.371094 L 182.800781 59.371094 C 187.914062 59.371094 192.210938 55.074219 192.210938 49.960938 L 192.210938 31.140625 C 192.210938 26.023438 187.914062 21.726562 182.800781 21.726562 C 177.683594 21.726562 173.386719 26.023438 173.386719 31.140625 "
fill-opacity="1" fill-rule="nonzero" />
<path fill="#05c3ec"
d="M 194.257812 68.78125 C 194.257812 63.667969 198.554688 59.371094 203.671875 59.371094 C 208.785156 59.371094 213.082031 55.074219 213.082031 49.960938 C 213.082031 44.847656 208.785156 40.550781 203.671875 40.550781 L 184.84375 40.550781 C 179.730469 40.550781 175.433594 44.847656 175.433594 49.960938 L 175.433594 68.78125 C 175.433594 73.898438 179.730469 78.191406 184.84375 78.191406 C 189.960938 78.191406 194.257812 73.898438 194.257812 68.78125 "
fill-opacity="1" fill-rule="nonzero" />
<path fill="#060165"
d="M 3.140625 205.640625 L 3.140625 124.460938 L 34.589844 124.460938 C 40.664062 124.460938 46.304688 125.542969 51.292969 127.5 C 56.28125 129.453125 60.835938 132.273438 64.304688 135.746094 C 67.992188 139.21875 70.8125 143.558594 72.765625 148.550781 C 74.71875 153.546875 75.800781 158.972656 75.800781 165.050781 C 75.800781 171.125 74.71875 176.554688 72.765625 181.546875 C 70.8125 186.539062 67.992188 190.878906 64.304688 194.351562 C 60.617188 198.042969 56.28125 200.648438 51.292969 202.816406 C 46.304688 204.769531 40.664062 205.855469 34.589844 205.855469 L 3.140625 205.855469 Z M 18.976562 190.226562 L 34.375 190.226562 C 38.0625 190.226562 41.53125 189.578125 44.570312 188.492188 C 47.605469 187.1875 50.207031 185.667969 52.378906 183.5 C 54.546875 181.328125 56.28125 178.722656 57.367188 175.46875 C 58.449219 172.429688 59.101562 168.957031 59.101562 165.050781 C 59.101562 161.140625 58.449219 157.886719 57.367188 154.628906 C 56.28125 151.589844 54.546875 148.988281 52.378906 146.816406 C 50.207031 144.644531 47.605469 142.910156 44.570312 141.824219 C 41.53125 140.738281 38.0625 140.085938 34.375 140.085938 L 18.976562 140.085938 C 18.976562 140.085938 18.976562 190.226562 18.976562 190.226562 Z M 18.976562 190.226562 "
fill-opacity="1" fill-rule="nonzero" />
<path fill="#060165"
d="M 84.695312 173.949219 C 84.695312 169.171875 85.5625 164.832031 87.296875 160.925781 C 89.03125 157.019531 91.417969 153.546875 94.238281 150.507812 C 97.273438 147.46875 100.746094 145.296875 104.648438 143.558594 C 108.769531 141.824219 113.109375 141.171875 117.664062 141.171875 C 122.21875 141.171875 126.773438 142.039062 130.675781 143.558594 C 134.796875 145.296875 138.265625 147.46875 141.304688 150.507812 C 144.339844 153.546875 146.726562 157.019531 148.246094 160.925781 C 149.980469 164.832031 150.847656 169.171875 150.847656 173.949219 C 150.847656 178.507812 149.980469 182.847656 148.246094 186.753906 C 146.507812 190.660156 144.125 194.136719 141.304688 197.171875 C 138.265625 200.210938 134.796875 202.382812 130.675781 204.121094 C 126.554688 205.855469 122.21875 206.507812 117.664062 206.507812 C 113.109375 206.507812 108.554688 205.640625 104.648438 204.121094 C 100.527344 202.382812 97.058594 200.210938 94.019531 197.171875 C 90.984375 194.136719 88.816406 190.660156 87.082031 186.753906 C 85.34375 182.847656 84.695312 178.722656 84.695312 173.949219 M 100.527344 173.949219 C 100.527344 176.335938 100.960938 178.507812 101.828125 180.675781 C 102.695312 182.847656 103.78125 184.800781 105.300781 186.320312 C 106.816406 187.839844 108.554688 189.140625 110.722656 190.226562 C 112.890625 191.097656 115.277344 191.746094 117.878906 191.746094 C 120.480469 191.746094 122.867188 191.3125 124.820312 190.226562 C 126.988281 189.359375 128.722656 188.058594 130.242188 186.320312 C 131.761719 184.800781 132.84375 182.847656 133.710938 180.675781 C 134.582031 178.507812 135.015625 176.335938 135.015625 173.949219 C 135.015625 171.5625 134.582031 169.390625 133.710938 167.21875 C 132.84375 165.050781 131.761719 163.09375 130.242188 161.578125 C 128.722656 160.058594 126.988281 158.753906 124.820312 157.667969 C 122.652344 156.800781 120.480469 156.148438 117.878906 156.148438 C 115.277344 156.148438 112.890625 156.582031 110.722656 157.667969 C 108.554688 158.539062 106.816406 159.839844 105.300781 161.578125 C 103.78125 163.09375 102.695312 165.050781 101.828125 167.21875 C 100.960938 169.390625 100.527344 171.777344 100.527344 173.949219 "
fill-opacity="1" fill-rule="nonzero" />
<path fill="#060165"
d="M 159.523438 173.949219 C 159.523438 169.171875 160.390625 164.832031 162.125 160.925781 C 163.859375 157.019531 166.246094 153.546875 169.066406 150.507812 C 172.101562 147.46875 175.574219 145.296875 179.476562 143.558594 C 183.597656 141.824219 187.9375 141.171875 192.492188 141.171875 C 196.613281 141.171875 200.515625 141.824219 203.988281 143.125 C 207.671875 144.429688 210.710938 146.164062 213.53125 148.335938 C 216.347656 150.507812 218.734375 153.328125 220.46875 156.367188 C 222.421875 159.621094 223.507812 163.09375 224.15625 166.785156 L 207.890625 166.785156 C 206.804688 163.53125 205.070312 161.140625 202.25 159.1875 C 199.648438 157.234375 196.394531 156.148438 192.492188 156.148438 C 189.886719 156.148438 187.503906 156.582031 185.332031 157.667969 C 183.164062 158.539062 181.429688 160.058594 179.910156 161.578125 C 178.394531 163.3125 177.308594 165.050781 176.441406 167.21875 C 175.574219 169.390625 175.140625 171.5625 175.140625 174.164062 C 175.140625 176.554688 175.574219 178.722656 176.441406 180.894531 C 177.308594 183.066406 178.394531 185.019531 179.910156 186.539062 C 181.429688 188.273438 183.164062 189.578125 185.332031 190.445312 C 187.503906 191.3125 189.886719 191.964844 192.492188 191.964844 C 196.179688 191.964844 199.433594 191.097656 202.25 189.140625 C 204.855469 187.1875 206.804688 184.800781 207.890625 181.546875 L 224.15625 181.546875 C 223.507812 185.234375 222.207031 188.707031 220.46875 191.746094 C 218.519531 194.785156 216.347656 197.609375 213.53125 199.777344 C 210.710938 201.949219 207.457031 203.683594 203.988281 204.988281 C 200.300781 206.289062 196.613281 206.941406 192.492188 206.941406 C 187.71875 206.941406 183.382812 206.074219 179.476562 204.554688 C 175.355469 202.816406 171.886719 200.648438 169.066406 197.609375 C 166.03125 194.570312 163.859375 191.097656 162.125 187.1875 C 160.390625 182.847656 159.523438 178.722656 159.523438 173.949219 "
fill-opacity="1" fill-rule="nonzero" />
<path fill="#060165"
d="M 238.253906 124.460938 L 261.03125 187.1875 L 284.019531 124.460938 L 301.15625 124.460938 L 271.007812 205.640625 L 251.269531 205.640625 L 221.121094 124.460938 Z M 238.253906 124.460938 "
fill-opacity="1" fill-rule="nonzero" />
<g clip-path="url(#0262fc8075)">
<path fill="#060165"
d="M 315.035156 124.460938 L 334.558594 124.460938 L 364.921875 205.640625 L 348.65625 205.640625 L 343.015625 190.226562 L 307.011719 190.226562 L 301.15625 205.640625 L 284.886719 205.640625 Z M 338.246094 177.203125 L 325.015625 141.605469 L 311.78125 177.203125 Z M 338.246094 177.203125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#0914a8505c)">
<path fill="#060165"
d="M 7.261719 241.453125 L 3.359375 231.035156 L 5.3125 231.035156 L 8.5625 239.71875 L 11.816406 231.035156 L 13.554688 231.035156 L 9.648438 241.453125 Z M 7.261719 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#da83339c1d)">
<path fill="#060165"
d="M 17.675781 231.035156 L 19.410156 231.035156 L 19.410156 241.671875 L 17.675781 241.671875 Z M 17.675781 231.035156 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#97dbddcd78)">
<path fill="#060165"
d="M 25.046875 241.453125 L 25.046875 231.035156 L 29.601562 231.035156 C 31.773438 231.035156 33.722656 231.902344 33.722656 234.289062 C 33.722656 235.808594 32.855469 236.679688 31.773438 237.113281 C 32.421875 237.546875 32.640625 238.199219 32.855469 238.847656 C 33.074219 240.152344 33.507812 241.238281 33.722656 241.453125 L 31.773438 241.453125 C 31.554688 241.019531 31.339844 240.367188 31.121094 239.28125 C 30.90625 238.199219 30.472656 237.328125 29.601562 237.328125 L 27 237.328125 L 27 241.453125 Z M 26.785156 235.808594 L 29.167969 235.808594 C 30.472656 235.808594 31.773438 235.59375 31.773438 234.074219 C 31.773438 232.554688 30.6875 232.335938 29.386719 232.335938 L 26.785156 232.335938 Z M 26.785156 235.808594 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#3223358027)">
<path fill="#060165"
d="M 40.230469 241.453125 L 40.230469 232.554688 L 36.761719 232.554688 L 36.761719 231.035156 L 45.652344 231.035156 L 45.652344 232.554688 L 41.964844 232.554688 L 41.964844 241.453125 Z M 40.230469 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#f960afe448)">
<path fill="#060165"
d="M 51.292969 231.035156 L 51.292969 236.894531 C 51.292969 238.847656 51.941406 240.152344 54.113281 240.152344 C 56.0625 240.152344 56.933594 238.847656 56.933594 236.894531 L 56.933594 231.035156 L 58.667969 231.035156 L 58.667969 237.113281 C 58.667969 239.71875 57.148438 241.886719 54.113281 241.886719 C 51.074219 241.886719 49.558594 239.933594 49.558594 237.113281 L 49.558594 231.035156 Z M 51.292969 231.035156 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#04b92ea6b9)">
<path fill="#060165"
d="M 68.859375 238.632812 L 64.523438 238.632812 L 63.4375 241.453125 L 61.703125 241.453125 L 65.390625 231.035156 L 67.992188 231.035156 L 71.679688 241.453125 L 69.730469 241.453125 Z M 68.210938 237.113281 L 66.476562 232.335938 L 64.738281 237.113281 Z M 68.210938 237.113281 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#7b4adb9303)">
<path fill="#060165"
d="M 75.800781 241.453125 L 75.800781 231.035156 L 77.753906 231.035156 L 77.753906 239.933594 L 83.394531 239.933594 L 83.394531 241.453125 Z M 75.800781 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#53357fd403)">
<path fill="#060165"
d="M 100.3125 238.632812 L 95.972656 238.632812 L 94.886719 241.453125 L 93.152344 241.453125 L 96.839844 231.035156 L 99.441406 231.035156 L 103.128906 241.453125 L 101.179688 241.453125 Z M 99.660156 237.113281 L 97.925781 232.335938 L 96.191406 237.113281 Z M 99.660156 237.113281 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#bcd03dbca6)">
<path fill="#060165"
d="M 108.554688 237.980469 C 108.769531 238.632812 109.421875 240.152344 111.804688 240.152344 C 113.757812 240.152344 114.410156 239.5 114.410156 238.632812 C 114.410156 237.980469 113.542969 237.546875 112.457031 237.328125 L 110.722656 237.113281 C 107.683594 236.460938 107.035156 235.375 107.035156 234.074219 C 107.035156 232.335938 108.554688 231.035156 111.589844 231.035156 C 115.058594 231.035156 116.144531 232.769531 116.359375 234.074219 L 114.625 234.507812 C 114.410156 233.640625 113.324219 232.769531 111.589844 232.769531 C 110.070312 232.769531 108.769531 233.421875 108.769531 234.289062 C 108.769531 235.160156 109.636719 235.59375 110.9375 235.808594 L 112.671875 236.027344 C 115.492188 236.460938 116.144531 237.761719 116.144531 238.847656 C 116.144531 240.800781 114.410156 241.886719 111.589844 241.886719 C 108.117188 241.886719 106.816406 239.933594 106.601562 238.632812 Z M 108.554688 237.980469 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#ff8639fe11)">
<path fill="#060165"
d="M 122 237.980469 C 122.21875 238.632812 122.867188 240.152344 125.253906 240.152344 C 127.207031 240.152344 127.855469 239.5 127.855469 238.632812 C 127.855469 237.980469 126.988281 237.546875 125.90625 237.328125 L 124.167969 237.113281 C 121.132812 236.460938 120.480469 235.375 120.480469 234.074219 C 120.480469 232.335938 122 231.035156 125.035156 231.035156 C 128.507812 231.035156 129.589844 232.769531 129.808594 234.074219 L 128.289062 234.074219 C 128.074219 233.207031 126.988281 232.335938 125.253906 232.335938 C 123.734375 232.335938 122.433594 232.988281 122.433594 233.855469 C 122.433594 234.726562 123.300781 235.160156 124.601562 235.375 L 126.339844 235.59375 C 129.15625 236.027344 129.808594 237.328125 129.808594 238.414062 C 129.808594 240.367188 128.074219 241.453125 125.253906 241.453125 C 121.785156 241.453125 120.480469 239.5 120.265625 238.199219 Z M 122 237.980469 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#3990de54cb)">
<path fill="#060165"
d="M 134.796875 231.035156 L 136.53125 231.035156 L 136.53125 241.671875 L 134.796875 241.671875 Z M 134.796875 231.035156 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#045cbd7804)">
<path fill="#060165"
d="M 143.039062 237.980469 C 143.257812 238.632812 143.90625 240.152344 146.292969 240.152344 C 148.246094 240.152344 148.894531 239.5 148.894531 238.632812 C 148.894531 237.980469 148.027344 237.546875 146.941406 237.328125 L 145.207031 237.113281 C 142.171875 236.460938 141.519531 235.375 141.519531 234.074219 C 141.519531 232.335938 143.039062 231.035156 146.074219 231.035156 C 149.546875 231.035156 150.628906 232.769531 150.847656 234.074219 L 149.113281 234.507812 C 148.894531 233.640625 147.8125 232.769531 146.074219 232.769531 C 144.558594 232.769531 143.257812 233.421875 143.257812 234.289062 C 143.257812 235.160156 144.125 235.59375 145.425781 235.808594 L 147.160156 236.027344 C 149.980469 236.460938 150.628906 237.761719 150.628906 238.847656 C 150.628906 240.800781 148.894531 241.886719 146.074219 241.886719 C 142.605469 241.886719 141.304688 239.933594 141.085938 238.632812 Z M 143.039062 237.980469 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#7de9a85e20)">
<path fill="#060165"
d="M 157.570312 241.453125 L 157.570312 232.554688 L 154.101562 232.554688 L 154.101562 231.035156 L 162.992188 231.035156 L 162.992188 232.554688 L 159.304688 232.554688 L 159.304688 241.453125 Z M 157.570312 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#e68f5b3fba)">
<path fill="#060165"
d="M 172.753906 238.632812 L 168.414062 238.632812 L 167.332031 241.453125 L 165.597656 241.453125 L 169.285156 231.035156 L 171.886719 231.035156 L 175.574219 241.453125 L 173.839844 241.453125 Z M 172.320312 237.113281 L 170.585938 232.335938 L 168.847656 237.113281 Z M 172.320312 237.113281 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#65464ab719)">
<path fill="#060165"
d="M 179.042969 241.453125 L 179.042969 231.035156 L 180.777344 231.035156 L 186.636719 238.414062 L 186.636719 231.035156 L 188.371094 231.035156 L 188.371094 241.453125 L 186.851562 241.453125 L 180.996094 234.074219 L 180.996094 241.453125 Z M 179.042969 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#b82da71650)">
<path fill="#060165"
d="M 196.179688 241.453125 L 196.179688 232.554688 L 192.492188 232.554688 L 192.492188 231.035156 L 201.382812 231.035156 L 201.382812 232.554688 L 197.914062 232.554688 L 197.914062 241.453125 Z M 196.179688 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#fe4eaf6826)">
<path fill="#060165"
d="M 206.371094 237.980469 C 206.589844 238.632812 207.238281 240.152344 209.625 240.152344 C 211.578125 240.152344 212.226562 239.5 212.226562 238.632812 C 212.226562 237.980469 211.359375 237.546875 210.277344 237.328125 L 208.542969 237.113281 C 205.503906 236.460938 204.855469 235.375 204.855469 234.074219 C 204.855469 232.335938 206.371094 231.035156 209.410156 231.035156 C 212.878906 231.035156 213.964844 232.769531 214.179688 234.074219 L 212.445312 234.507812 C 212.226562 233.640625 211.144531 232.769531 209.410156 232.769531 C 207.890625 232.769531 206.589844 233.421875 206.589844 234.289062 C 206.589844 235.160156 207.457031 235.59375 208.757812 235.808594 L 210.492188 236.027344 C 213.3125 236.460938 213.964844 237.761719 213.964844 238.847656 C 213.964844 240.800781 212.230469 241.886719 209.410156 241.886719 C 205.9375 241.886719 204.636719 239.933594 204.421875 238.632812 Z M 206.371094 237.980469 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#79d174fbbf)">
<path fill="#060165"
d="M 225.460938 241.453125 L 225.460938 231.035156 L 233.050781 231.035156 L 233.050781 232.554688 L 227.195312 232.554688 L 227.195312 235.375 L 233.050781 235.375 L 233.050781 236.894531 L 227.195312 236.894531 L 227.195312 241.453125 Z M 225.460938 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#b0be87ffda)">
<path fill="#060165"
d="M 242.375 230.816406 C 245.414062 230.816406 248.015625 232.988281 248.015625 236.242188 C 248.015625 239.5 245.414062 241.671875 242.375 241.671875 C 239.339844 241.671875 236.738281 239.5 236.738281 236.242188 C 236.738281 232.988281 239.339844 230.816406 242.375 230.816406 M 246.28125 236.242188 C 246.28125 234.074219 244.546875 232.335938 242.375 232.335938 C 240.207031 232.335938 238.472656 234.074219 238.472656 236.242188 C 238.472656 238.414062 240.207031 240.152344 242.375 240.152344 C 244.546875 240.152344 246.28125 238.414062 246.28125 236.242188 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#444dbef42a)">
<path fill="#060165"
d="M 252.789062 241.453125 L 252.789062 231.035156 L 257.34375 231.035156 C 259.511719 231.035156 261.464844 231.902344 261.464844 234.289062 C 261.464844 235.808594 260.597656 236.679688 259.511719 237.113281 C 260.164062 237.546875 260.378906 238.199219 260.597656 238.847656 C 260.8125 240.152344 261.246094 241.238281 261.464844 241.453125 L 259.511719 241.453125 C 259.292969 241.019531 259.078125 240.367188 258.859375 239.28125 C 258.644531 238.199219 258.210938 237.328125 257.34375 237.328125 L 254.738281 237.328125 L 254.738281 241.453125 Z M 254.523438 235.808594 L 256.910156 235.808594 C 258.210938 235.808594 259.511719 235.59375 259.511719 234.074219 C 259.511719 232.554688 258.425781 232.335938 257.125 232.335938 L 254.523438 232.335938 Z M 254.523438 235.808594 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#314903e6e8)">
<path fill="#060165"
d="M 272.523438 241.453125 L 272.523438 230.816406 L 277.078125 230.816406 C 280.550781 230.816406 282.9375 232.988281 282.9375 236.027344 C 282.9375 239.28125 280.550781 241.238281 277.078125 241.238281 Z M 274.261719 239.933594 L 276.863281 239.933594 C 279.25 239.933594 280.984375 238.632812 280.984375 236.242188 C 280.984375 233.855469 279.25 232.554688 276.863281 232.554688 L 274.261719 232.554688 Z M 274.261719 239.933594 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#2fd70bc44d)">
<path fill="#060165"
d="M 292.480469 230.816406 C 295.515625 230.816406 298.117188 232.988281 298.117188 236.242188 C 298.117188 239.5 295.515625 241.671875 292.480469 241.671875 C 289.441406 241.671875 286.839844 239.5 286.839844 236.242188 C 286.839844 232.988281 289.226562 230.816406 292.480469 230.816406 M 296.382812 236.242188 C 296.382812 234.074219 294.648438 232.335938 292.480469 232.335938 C 290.3125 232.335938 288.574219 234.074219 288.574219 236.242188 C 288.574219 238.414062 290.3125 240.152344 292.480469 240.152344 C 294.648438 240.152344 296.382812 238.414062 296.382812 236.242188 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#032e9be536)">
<path fill="#060165"
d="M 310.480469 238.847656 L 311.78125 239.933594 C 310.699219 241.019531 309.179688 241.671875 307.664062 241.671875 C 304.625 241.671875 302.023438 239.5 302.023438 236.242188 C 302.023438 232.988281 304.625 230.816406 307.664062 230.816406 C 309.398438 230.816406 310.914062 231.46875 311.78125 232.554688 L 310.480469 233.640625 C 309.832031 232.769531 308.746094 232.335938 307.445312 232.335938 C 305.277344 232.335938 303.542969 234.074219 303.542969 236.242188 C 303.542969 238.414062 305.277344 240.152344 307.445312 240.152344 C 308.964844 240.152344 309.832031 239.71875 310.480469 238.847656 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#77ab9ec736)">
<path fill="#060165"
d="M 318.289062 241.453125 L 318.289062 232.554688 L 314.820312 232.554688 L 314.820312 231.035156 L 323.710938 231.035156 L 323.710938 232.554688 L 320.242188 232.554688 L 320.242188 241.453125 Z M 318.289062 241.453125 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#bf7e46828b)">
<path fill="#060165"
d="M 332.605469 230.816406 C 335.640625 230.816406 338.246094 232.988281 338.246094 236.242188 C 338.246094 239.5 335.640625 241.671875 332.605469 241.671875 C 329.570312 241.671875 326.964844 239.5 326.964844 236.242188 C 326.964844 232.988281 329.570312 230.816406 332.605469 230.816406 M 336.507812 236.242188 C 336.507812 234.074219 334.773438 232.335938 332.605469 232.335938 C 330.4375 232.335938 328.699219 234.074219 328.699219 236.242188 C 328.699219 238.414062 330.4375 240.152344 332.605469 240.152344 C 334.773438 240.152344 336.507812 238.414062 336.507812 236.242188 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#b78234ca4f)">
<path fill="#060165"
d="M 343.015625 241.453125 L 343.015625 231.035156 L 347.570312 231.035156 C 349.738281 231.035156 351.691406 231.902344 351.691406 234.289062 C 351.691406 235.808594 350.824219 236.679688 349.738281 237.113281 C 350.390625 237.546875 350.605469 238.199219 350.824219 238.847656 C 351.042969 240.152344 351.476562 241.238281 351.691406 241.453125 L 349.523438 241.453125 C 349.304688 241.019531 349.089844 240.367188 348.871094 239.28125 C 348.65625 238.199219 348.222656 237.328125 347.355469 237.328125 L 344.75 237.328125 L 344.75 241.453125 Z M 344.75 235.808594 L 347.136719 235.808594 C 348.4375 235.808594 349.738281 235.59375 349.738281 234.074219 C 349.738281 232.554688 348.65625 232.335938 347.355469 232.335938 L 344.75 232.335938 Z M 344.75 235.808594 "
fill-opacity="1" fill-rule="nonzero" />
</g>
<g clip-path="url(#2b0e76334b)">
<path fill="#060165"
d="M 357.113281 237.980469 C 357.332031 238.632812 357.980469 240.152344 360.367188 240.152344 C 362.320312 240.152344 362.96875 239.5 362.96875 238.632812 C 362.96875 237.980469 362.101562 237.546875 361.019531 237.328125 L 359.28125 237.113281 C 356.246094 236.460938 355.597656 235.375 355.597656 234.074219 C 355.597656 232.335938 357.113281 231.035156 360.152344 231.035156 C 363.621094 231.035156 364.707031 232.769531 364.921875 234.074219 L 363.1875 234.507812 C 362.96875 233.640625 361.886719 232.769531 360.152344 232.769531 C 358.632812 232.769531 357.332031 233.421875 357.332031 234.289062 C 357.332031 235.160156 358.199219 235.59375 359.5 235.808594 L 361.234375 236.027344 C 364.054688 236.460938 364.707031 237.761719 364.707031 238.847656 C 364.707031 240.800781 362.96875 241.886719 360.152344 241.886719 C 356.679688 241.886719 355.378906 239.933594 355.160156 238.632812 Z M 357.113281 237.980469 "
fill-opacity="1" fill-rule="nonzero" />
</g>
</svg>
</div>
</div>
</body>
</html>

View File

@ -1,30 +1,30 @@
// electron/ipc/apiHandler.ts
import { ipcMain } from 'electron';
import api from '../api/axiosClient.js';
// // electron/ipc/apiHandler.ts
// import { ipcMain } from 'electron';
// import api from '../api/axiosClient.js';
interface ApiRequestArgs {
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
url: string;
data?: any;
config?: Record<string, any>;
}
// interface ApiRequestArgs {
// method: 'get' | 'post' | 'put' | 'patch' | 'delete';
// url: string;
// data?: any;
// config?: Record<string, any>;
// }
ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
try {
const response = await api.request({
method: args.method,
url: args.url,
data: args.data,
...args.config,
});
// ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
// try {
// const response = await api.request({
// method: args.method,
// url: args.url,
// data: args.data,
// ...args.config,
// });
return { success: true, data: response };
} catch (error: any) {
console.error('[API Handler Error]', error.message);
return {
success: false,
error: error.message,
status: error.response?.status,
};
}
});
// return { success: true, data: response };
// } catch (error: any) {
// console.error('[API Handler Error]', error.message);
// return {
// success: false,
// error: error.message,
// status: error.response?.status,
// };
// }
// });

View File

@ -1,14 +1,33 @@
// electron/api/axiosClient.ts
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';
import { getBaseURL } from './baseURL.js';
const TOKEN_KEY = 'ACCESS_TOKEN';
const Base_URL = await getBaseURL()
const httpsAgent = new https.Agent({
rejectUnauthorized: false
});
console.log('Base_URL$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' ,Base_URL)
const api: AxiosInstance = axios.create({
baseURL: 'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va', // 🔁 your backend
// baseURL: 'https://portal-sf.docva.com/api/v1/va', // 🔁 your backend
baseURL: `${Base_URL}/api/v1/va`,
httpsAgent
// timeout: 10000,
});
// console.log("Creating Axios Instance", axios.VERSION);
// console.log("Axios .get reference:", api.get);
// console.log("Axios .request reference:", api.request);
// --- Request Interceptor ---
api.interceptors.request.use(
async (config: AxiosRequestConfig): Promise<any> => {
@ -17,23 +36,42 @@ api.interceptors.request.use(
if (token) {
config.headers['token'] = token;
config.headers['ngrok-skip-browser-warning'] = 98098;
config.headers['ngrok-skip-browser-warning'] = 'true';
}
console.log(`[Axios] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error) => Promise.reject(error)
);
// --- Response Interceptor ---
// api.interceptors.response.use(
// (response: AxiosResponse) => response.data,
// async (error) => {
// console.error('[Axios Error]', error.status);
// if (error.status === 401 || 403) {
// console.warn('Token expired — trigger refresh logic if needed');
// // sendTokenExpired();
// }
// return Promise.reject(error);
// }
// );
api.interceptors.response.use(
(response: AxiosResponse) => response.data,
async (error) => {
console.error('[Axios Error]', error.message);
if (error.response?.status === 401) {
console.log(error, "Line 46")
const status = error?.response?.status;
console.error('[Axios Error]', status);
// Only logout when token actually expired
if (status === 401 || status === 403) {
console.warn('Token expired — trigger refresh logic if needed');
sendTokenExpired();
}
return Promise.reject(error);
}
);

View File

@ -0,0 +1,7 @@
// let CURRENT_BASE_URL = "https://portal-sf.docva.com/api/v1/va";
let CURRENT_BASE_URL = "https://portal-sf.docva.com";
export const getBaseURL = () => CURRENT_BASE_URL;
export const setBaseURL = (url: string) => {
CURRENT_BASE_URL = url;
};

View File

@ -0,0 +1,41 @@
import axios from "axios";
import https from "https";
import { getBaseURL } from "./baseURL.js";
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
export const checkRegion = async () => {
const baseURL = await getBaseURL();
console.log("Lin 11 BaseURL CHECK@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ,baseURL)
try {
const res = await axios.get(`${baseURL}/health`, {
headers: { "ngrok-skip-browser-warning": "true" },
httpsAgent,
timeout: 4000
});
console.log(res , "checkRegion")
return {
success: true,
status: res.status,
data: res.data
};
} catch (error: any) {
console.log(error , "Line 29")
const status = error?.response?.status || 500;
console.log("Region Check Error:", status);
return {
success: false,
status,
message: "Region check failed"
};
}
};

View File

@ -1,47 +1,579 @@
// electron/ipc/tokenHandler.ts
import { ipcMain } from 'electron';
import { getToken, saveToken, clearToken } from '../utils/tokenHelper.js';
import { getActiveApp, handleLogin, takeScreenshot } from '../utils/utilityFunctions.js';
import api from '../api/axiosClient.js';
import { ipcMain, BrowserWindow } from "electron";
import { getToken, saveToken, clearToken } from "../utils/tokenHelper.js";
import { getActiveApp, handleLogin, takeScreenshot, startWindowPolling, getRunningAppsList, captureSpecificApp } from "../utils/utilityFunctions.js";
import api from "../api/axiosClient.js";
import { getMainWindow } from '../main.js';
import { checkRegion } from "../api/regionCheck.js";
import { startVPN, stopVPN } from "../vpn.js";
import { getBaseURL, setBaseURL } from "../api/baseURL.js";
ipcMain.handle('get-token', async (_, TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(TOKEN_KEY);
});
ipcMain.handle('save-token', async (_, TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN", token: string) => {
await saveToken(TOKEN_KEY, token);
export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
console.log("⚙️ Registering IPC Handlers...");
ipcMain.handle("GET_BASE_URL", () => {
return getBaseURL();
});
// UPDATE Base URL
ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => {
setBaseURL(url);
api.defaults.baseURL = `${url}/api/v1/va`;
console.log("Base URL updated to:", api.defaults.baseURL);
return {
updated: true,
baseURL: url
};
});
ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(key);
});
ipcMain.handle("save-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN", token: string) => {
await saveToken(key, token);
return true;
});
});
ipcMain.handle('clear-token', async (_, TOKEN_KEY: string) => {
await clearToken(TOKEN_KEY);
ipcMain.handle("clear-token", async (_, key: string) => {
await clearToken(key);
return true;
});
ipcMain.handle("upload-screenshot", async () => {
await takeScreenshot();
return { success: true };
});
ipcMain.handle("login-request", async (_, credentials: { email: string; password: string }) => {
return handleLogin(credentials);
});
ipcMain.handle("logout-request", async () => {
try {
console.log("Handle Logout listened");
console.log("Token", await getToken("ACCESS_TOKEN"));
const res = await api.get("/logout");
if (res && "success" in res && res.success) {
return { success: true, message: "Logout Successfully" };
}
return { success: false, message: "" };
} catch {
return { success: false, message: "Logout failed" };
}
});
ipcMain.handle("get-active-app", async () => {
return await getActiveApp();
});
ipcMain.handle('toggle-close-btn', (event, shouldEnable: boolean) => {
const mainWindow = getMainWindow()
console.log("toggle-close-btn Called", mainWindow)
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.setClosable(shouldEnable);
}
});
ipcMain.handle("resize-window", (event, width: number, height: number) => {
const mainWindow = getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
console.log(`📏 Resizing window to ${width}x${height}`);
mainWindow.setResizable(true); // temporarily enable resizing
mainWindow.setSize(width, height);
mainWindow.setResizable(false); // disable again if you want to lock it
}
});
// ✅ Start background polling when main window is ready
};
// startWindowPolling(mainWindow);
ipcMain.handle("get-project-list", async () => {
console.log("📡 get-project-list called");
try {
const res = await api.get("/list/projects");
console.log(" API /list/project response:", res);
// Return only serializable data
return JSON.parse(JSON.stringify(res));
} catch (error: any) {
console.error(" get-project-list error:", error.status);
return {
success: false,
message: error?.message || String(error) || "Unknown error",
status: error?.status
};
}
});
ipcMain.handle('upload-screenshot', async () => {
takeScreenshot()
// When project tracking start
ipcMain.handle("start-project-tracking", async (event, payload) => {
try {
console.log("🚀 start-project-tracking called with:", payload);
const response = await api.post("/log/project", payload);
console.log(" Tracking start synced:", response);
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 {
console.log("stop-project-tracking called with:", payload);
const response = await api.post("/log/project", payload);
console.log(" Tracking stop synced:", response);
return response;
} catch (error) {
console.error(" Error syncing project stop:", error);
throw error;
}
});
// tabs opening and closing when and tracking start
ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
try {
console.log("🟦 IPC received payload:", payload);
const response = await api.post("/log/project/track", payload);
console.log("🟩 Log track API success:", response);
return {
success: true,
data: response.data,
};
} catch (error) {
console.error("❌ Log track API error:", error);
return {
success: false,
message: "Something went wrong",
error,
};
}
});
ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => {
try {
console.log("📥 get-project-logs called with:", { projectID, fromDate, toDate });
let url = `/report/project/${projectID}`;
// 🧠 Append query params only if dates are provided
if (fromDate && toDate) {
// const params = new URLSearchParams({
// from: fromDate,
// to: toDate,
// });
url += `/${fromDate}/${toDate}`;
}
console.log("url", url)
const response = await api.get(url);
console.log("✅ response-logs:", response);
return { success: true, data: response.data };
} catch (error) {
console.error("❌ Error fetching project logs:", error);
throw error;
}
});
ipcMain.handle("forgot-password", async (event, email: string) => {
try {
const response = await api.post("/forgot-password", { email });
console.log(response, "Line response")
if (response) {
return { success: true, message: response.data?.message };
}
} catch (error: any) {
console.error("Forgot password error:", error.response?.data || error.message);
return {
success: false,
message: error.response?.data?.message || "Something went wrong",
};
}
});
ipcMain.handle('get-running-apps', async () => {
return await getRunningAppsList()
});
ipcMain.handle('capture-specific-window', async (event, appTitle: string) => {
console.log(appTitle, "IPC HANDLE")
return await captureSpecificApp(appTitle)
});
ipcMain.handle("get-profile", async () => {
try {
const response = await api.get("/profile");
console.log(response.data)
return {
success: true,
data: response.data,
};
} catch (error: any) {
console.error(" Profile fetch error:", error.response?.data || error.message);
return {
success: false,
message: error.response?.data?.message || "Something went wrong",
};
}
});
ipcMain.handle("change-password", async (event, data) => {
console.log("data-change-password", data)
// Build final payload
const payload = {
userDetails: {
oldPassword: data.oldPassword,
password: data.password,
cPassword: data.cPassword,
},
};
console.log("Change Password payload", payload)
try {
const response = await api.patch("/profile", payload);
console.log("Chnage-passowrd-response", response)
return { success: true, data: response.data };
} catch (error: any) {
console.error("❌ Change password error:", error.message);
return {
success: false,
message: error.response?.data?.message || "Password update failed",
};
}
});
ipcMain.handle("get-project-summary", async (_, { projectId, date }) => {
try {
const url = `/timesheet/summary/${projectId}/${date}`;
console.log("📡 Fetching summary:", url);
const res = await api.get(url);
return {
success: true,
data: res.data,
};
} catch (error: any) {
console.error("❌ Error fetching summary:", error.message);
return {
success: false,
message: error.response?.data?.message || "Something went wrong",
};
}
});
ipcMain.handle("create-timesheet", async (event, data) => {
console.log("data", data)
try {
console.log("📩 Timesheet Payload:", data);
const res = await api.post("/timesheet/adjustment/request", data);
console.log(res, " Line 323")
return {
success: true,
message: "Timesheet created successfully",
data: res,
};
} catch (error: any) {
console.error("❌ Timesheet Error:", error);
return {
success: false,
message: error.response?.data?.message || "Failed to create timesheet",
};
}
})
// Handle login request from renderer
ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => {
return handleLogin(credentials);
});
ipcMain.handle('logout-request', async () => {
ipcMain.handle("create-leave-request", async (event, payload) => {
try {
console.log("Handle Logout listened")
console.log("Token", await getToken("ACCESS_TOKEN"))
const res = await api.get("/logout");
if(res && ("success" in res) && res.success) {
return {success: true, message: "Logout Successfully"}
console.log("Leave Request Received:", payload);
const response = await api.post("/leave/request", payload);
console.log("Leave Saved:", response);
return {
success: true,
data: response.data
};
} catch (error: any) {
console.error("❌ Leave Save Failed:");
let message = "Unknown error";
console.log(error.response)
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
// Extract the backend message properly
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return {success: false, message: ""}
} catch(error) {
return {success: false, message: ""}
return {
success: false,
message
};
}
});
ipcMain.handle('get-active-app', () => {
return getActiveApp();
ipcMain.handle("get-schedules", async () => {
try {
console.log(" Fetching schedules...");
const response = await api.get("/payrollSchedule"); // Backend API call
console.log("✅ Schedule Data:", response.data);
return {
success: true,
result: response.data
};
} catch (error: any) {
console.error("❌ Fetch Schedules Failed:");
let message = "Unknown error occurred";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return {
success: false,
message
};
}
});
ipcMain.handle("get-payslips", async (event, vaId: string) => {
console.log("get-payslips", vaId)
try {
const response = await api.get(`/paySlip/${vaId}`);
console.log(response.data, "Line 409")
return {
success: true,
data: response.data || [],
};
} catch (error: any) {
let message = "Unknown error occurred";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return {
success: false,
message
};
}
});
ipcMain.handle("get-leave-list", async () => {
try {
const res = await api.get("/leaves/all");
console.log("/leaves/all", res.data);
return {
success: true,
data: res.data,
};
} catch (error: any) {
let message = "Unknown error occurred";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return {
success: false,
message
};
}
});
ipcMain.handle("get-adjustments-list", async (event, vaId: string) => {
try {
const res = await api.get(`/timesheet/adjustments/list?vaId=${vaId}`);
console.log("/timesheet/adjustments/list", res.data);
return {
success: true,
data: res.data,
};
} catch (error: any) {
let message = "Unknown error occurred";
if (error.response) {
console.log("Status:", error.response.status);
console.log("Message:", error.response.statusText);
console.log("Server Response:", error.response.data);
message = error.response.data?.message || error.response.statusText;
} else {
message = error.message;
}
return {
success: false,
message
};
}
});
ipcMain.handle("update-payment", async (event, data: Record<string, any>) => {
console.log("data-update-payment", data);
// Build userDetails object
const userDetails: Record<string, any> = {
paymentMethod: data.paymentMethod,
};
// paymentMethod: 'payoneeraccount',
// payoneerAccount: 'check@gmail.com'
// Add payment-specific fields
if (data.paymentMethod === "wise" && data.wiseEmail) {
userDetails.wiseEmail = data.wiseEmail;
// Clear veemAccount when switching to wise
userDetails.veemAccount = "";
userDetails.payoneerAccount = ""
} else if (data.paymentMethod === "veem" && data.veemAccount) {
userDetails.veemAccount = data.veemAccount;
// Clear wiseEmail when switching to veem
userDetails.wiseEmail = "";
userDetails.payoneerAccount = ""
} else if (data.paymentMethod === "payoneeraccount" && data.payoneerAccount) {
userDetails.payoneerAccount = data.payoneerAccount;
userDetails.veemAccount = "";
userDetails.wiseEmail = ""
}
// Build final payload
const payload = {
userDetails: userDetails,
};
console.log("Update Payment payload", payload);
try {
const response = await api.patch("/profile", payload);
console.log("Update-payment-response", response);
return { success: true, data: response?.data?.userDetails };
} catch (error: any) {
console.error("❌ Update payment error:", error.message);
return {
success: false,
message: error.response?.data?.message || "Payment update failed",
};
}
});
ipcMain.handle("check-region", async () => {
return await checkRegion();
});
export const sendTokenExpired = () => {
console.log("⚠️ Sending token-expired event to renderer");
BrowserWindow.getAllWindows().forEach((win) => {
if (!win.isDestroyed()) {
win.webContents.send("token-expired");
}
});
};
ipcMain.handle("vpn-connect", async () => {
return startVPN();
});
ipcMain.handle("vpn-disconnect", async () => {
stopVPN();
});

View File

@ -1,80 +1,239 @@
import { app, BrowserWindow } from 'electron';
import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
import { getStaticData, pollResources } from './resourceManager.js';
import { getPreloadPath, getUIPath } from './pathResolver.js';
import { app, BrowserWindow, globalShortcut, session } from "electron";
import { isDev } from "./util.js";
// import { pollResources } from "./resourceManager.js";
import { getPreloadPath, getUIPath } from "./pathResolver.js";
import { registerIPCHandlers } from "./ipc/ipcHandler.js";
import "./ipc/ipcHandler.js"
import "./ipc/ipcHandler.js";
import path from "path";
import { fileURLToPath } from "url";
app.on('ready', async () => {
console.log("ready")
const mainWindow = new BrowserWindow({
webPreferences: {
preload: getPreloadPath(),
},
import * as fs from "fs";
import { execFile } from "child_process";
resizable: false, // disables resizing (no maximize)
minimizable: false, // disables minimize button
maximizable: false, // disables maximize button
closable: true,
alwaysOnTop: true,
// disables default system frame (dont do this if you want a proper working menu bar)
frame: process.platform === 'darwin' ? false : true,
autoHideMenuBar: true
});
app.setAppUserModelId("com.tracker.app");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
if (isDev()) {
mainWindow.loadURL('http://localhost:5123');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(getUIPath());
function resolveIcon() {
const isDev = !app.isPackaged;
if (isDev) {
// DEV mode → use project root path
return path.join(process.cwd(), "src", "assets", "icons", "appIcon.ico");
}
pollResources(mainWindow);
console.log("Line 24", path.join(__dirname, "..", "assets", "icons", "appIcon.ico"))
ipcMainHandle('getStaticData', () => {
return getStaticData();
});
const prodPath = path.join(process.resourcesPath, "assets", "icons", "appIcon.ico");
console.log("PROD Icon Path:", prodPath);
return prodPath;
}
// ipcMainOn('sendFrameAction', (payload) => {
// switch (payload) {
// case 'CLOSE':
// mainWindow.close();
// break;
// case 'MAXIMIZE':
// mainWindow.maximize();
// break;
// case 'MINIMIZE':
// mainWindow.minimize();
// break;
// }
// });
// ---------------------------
// SINGLE INSTANCE LOCK
// ---------------------------
const gotTheLock = app.requestSingleInstanceLock();
// createTray(mainWindow);
handleCloseEvents(mainWindow);
// createMenu(mainWindow);
// await takeScreenshot();
if (!gotTheLock) {
app.quit();
process.exit(0);
}
let mainWindow: BrowserWindow | null = null;
let splashWindow: BrowserWindow | null = null;
// ---------------------------
// HANDLE DOUBLE OPEN ATTEMPT
// ---------------------------
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
});
function handleCloseEvents(mainWindow: BrowserWindow) {
let willClose = false;
mainWindow.on('close', (e) => {
if (willClose) {
return;
}
e.preventDefault();
mainWindow.hide();
if (app.dock) {
app.dock.hide();
}
});
// CHECKING IF WIREGUARD EXIST - IF NOT INSTALLING IN CLIENT MACHINE
const wgPath = "C:\\Program Files\\WireGuard\\wireguard.exe";
app.on('before-quit', () => {
willClose = true;
});
if (!fs.existsSync(wgPath)) {
const installer = path.join(process.resourcesPath, "wireguard-install", "wireguard-installer.exe");
mainWindow.on('show', () => {
willClose = false;
execFile(installer, ["/quiet"], (err) => {
if (err) console.error("WireGuard install failed:", err);
else console.log("WireGuard installed successfully.");
});
}
// CHECKING IF WIREGUARD EXIST - IF NOT INSTALLING IN CLIENT MACHINE
// ---------------------------
// MAIN APP READY
// ---------------------------
app.on("ready", async () => {
splashWindow = new BrowserWindow({
width: 300,
height: 300,
frame: false,
transparent: false,
alwaysOnTop: false, // splash does NOT need to be on top || Yooo Don't make it onTop
resizable: false,
show: true,
skipTaskbar: true,
webPreferences: {
devTools: false,
}
});
splashWindow.loadFile(
path.join(__dirname, "../assets/splash.html")
);
// Create the main window
const win = new BrowserWindow({
width: 500,
height: 600,
resizable: false,
fullscreenable: false,
minimizable: false,
maximizable: false,
closable: true,
// alwaysOnTop: true,
// skipTaskbar: true,
autoHideMenuBar: true,
frame: true,
show: false,
icon: resolveIcon(),
webPreferences: {
devTools: true,
preload: getPreloadPath(),
contextIsolation: true,
nodeIntegration: false,
},
});
// win.setAlwaysOnTop(true, 'screen-saver');
// win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
// win.setFullScreenable(false);
// Cross-platform safe always-on-top
const isMac = process.platform === "darwin";
if (isMac) {
win.setAlwaysOnTop(true, "floating"); // best for macOS
win.setVisibleOnAllWorkspaces(true); // NO fullscreen override
} else {
win.setAlwaysOnTop(true, "normal"); // safest for Windows + Zoom
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
win.setFullScreenable(false);
}
mainWindow = win;
// ------------------------------------
// ONLY LOG NETWORK REQUESTS IN DEV
// ------------------------------------
if (isDev() && process.env.LOG_REQUESTS === "true") {
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
console.log("🌐 Request:", details.method, details.url);
callback({ cancel: false });
});
}
// ------------------------------------
// LOAD FRONTEND
// ------------------------------------
if (isDev()) {
win.loadURL("http://localhost:5123");
win.webContents.openDevTools(); // Optional (slows startup)
} else {
win.loadFile(getUIPath());
}
// ------------------------------------
// SHOW WINDOW WHEN READY (no white flash)
// ------------------------------------
win.once("ready-to-show", () => {
// Delay heavy work to improve startup speed
setTimeout(() => {
splashWindow?.close();
splashWindow = null;
win.show();
// pollResources(win);
registerIPCHandlers(win);
}, 500);
});
// ------------------------------------
// HANDLE CLOSE EVENTS
// ------------------------------------
// handleCloseEvents(win);
mainWindow.on("close", () => {
if(mainWindow !== null) {
mainWindow.webContents.executeJavaScript(`window.__allowClose__ = true`);
}
});
});
app.on('browser-window-focus', () => {
globalShortcut.register('CommandOrControl+R', () => {
// Do nothing; disables reload
});
globalShortcut.register('F5', () => {
// Do nothing; disables reload
});
globalShortcut.register('CommandOrControl+Shift+I', () => {
// Do nothing; disables DevTools
});
});
// app.on('browser-window-blur', () => {
// globalShortcut.unregisterAll();
// });
// ---------------------------
// CUSTOM CLOSE BEHAVIOR
// ---------------------------
// function handleCloseEvents(window: BrowserWindow) {
// let willClose = false;
// window.on("close", (e) => {
// if (willClose) return;
// e.preventDefault();
// window.hide();
// if (app.dock) {
// app.dock.hide();
// }
// });
// app.on("before-quit", () => {
// willClose = true;
// });
// window.on("show", () => {
// willClose = false;
// });
// }
// ---------------------------
// GET MAIN WINDOW EXPORT
// ---------------------------
export function getMainWindow() {
return mainWindow;
}

View File

@ -1,3 +1,28 @@
// import path from 'path';
// import { app } from 'electron';
// import { isDev } from './util.js';
// export function getPreloadPath() {
// return path.join(
// app.getAppPath(),
// isDev() ? '.' : '..',
// '/dist-electron/preload.cjs'
// );
// }
// export function getUIPath() {
// return path.join(app.getAppPath(), "dist-react", "index.html");
// // return "C:\\Users\\user\\OneDrive\\Desktop\\electron-main\\dist-react\\index.html";
// // return path.join(app.getAppPath(), '/dist-react/index.html');
// }
// export function getAssetPath() {
// return path.join(app.getAppPath(), isDev() ? '.' : '..', '/src/assets');
// }
import path from 'path';
import { app } from 'electron';
import { isDev } from './util.js';
@ -6,14 +31,22 @@ export function getPreloadPath() {
return path.join(
app.getAppPath(),
isDev() ? '.' : '..',
'/dist-electron/preload.cjs'
'/dist-electron/preload.cjs' // ✅ Changed to .js
);
}
export function getUIPath() {
return path.join(app.getAppPath(), '/dist-react/index.html');
return path.join(app.getAppPath(), "dist-react", "index.html");
}
export function getAssetPath() {
return path.join(app.getAppPath(), isDev() ? '.' : '..', '/src/assets');
return path.join(app.getAppPath(), isDev() ? '.' : '..', '/assets');
}
export function getIconPath() {
return path.join(getAssetPath(), 'icons', 'appIcon.ico');
}
export function getSplashPath() {
return path.join(getAssetPath(), 'splash.html');
}

43
src/electron/preload.cjs Normal file
View File

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var electron_1 = require("electron");
console.log("setting PreLoad");
electron_1.contextBridge.exposeInMainWorld('electron', {
login: function (credentials) { return electron_1.ipcRenderer.invoke('login-request', credentials); },
logout: function () { return electron_1.ipcRenderer.invoke('logout-request'); },
forgotPassword: function (email) { return electron_1.ipcRenderer.invoke("forgot-password", email); },
getToken: function (TOKEN_KEY) { return electron_1.ipcRenderer.invoke('get-token', TOKEN_KEY); },
uploadScreenshot: function () { return electron_1.ipcRenderer.invoke('upload-screenshot'); },
getActiveAppDetails: function () { return electron_1.ipcRenderer.invoke('get-active-app'); },
toggleCloseBtn: function (shouldEnable) { return electron_1.ipcRenderer.invoke('toggle-close-btn', shouldEnable); },
resizeWindow: function (width, height) { return electron_1.ipcRenderer.invoke('resize-window', width, height); },
getProjectList: function () { return electron_1.ipcRenderer.invoke("get-project-list"); },
onWindowUpdate: function (callback) {
return electron_1.ipcRenderer.on("window:updated", function (_, data) { return callback(data); });
},
startProjectTracking: function (data) { return electron_1.ipcRenderer.invoke("start-project-tracking", data); },
stopProjectTracking: function (data) { return electron_1.ipcRenderer.invoke("stop-project-tracking", data); },
trackingTabsOpeningClosing: function (data) {
return electron_1.ipcRenderer.invoke("tracking-tabs-opening-and-closing", data);
},
getProjectLogs: function (projectID, fromDate, toDate) { return electron_1.ipcRenderer.invoke("get-project-logs", { projectID: projectID, fromDate: fromDate, toDate: toDate }); },
});
// function ipcInvoke<Key extends keyof EventPayloadMapping>(
// key: Key
// ): Promise<EventPayloadMapping[Key]> {
// return ipcRenderer.invoke(key);
// }
// function ipcOn<Key extends keyof EventPayloadMapping>(
// key: Key,
// callback: (payload: EventPayloadMapping[Key]) => void
// ) {
// const cb = (_: Electron.IpcRendererEvent, payload: any) => callback(payload);
// ipcRenderer.on(key, cb);
// return () => ipcRenderer.off(key, cb);
// }
// function ipcSend<Key extends keyof EventPayloadMapping>(
// key: Key,
// payload: EventPayloadMapping[Key]
// ) {
// ipcRenderer.send(key, payload);
// }

View File

@ -5,9 +5,74 @@ console.log("setting PreLoad")
contextBridge.exposeInMainWorld('electron', {
login: (credentials: { email: string, password: string }) => ipcRenderer.invoke('login-request', credentials),
logout: () => ipcRenderer.invoke('logout-request'),
forgotPassword: (email: string) => ipcRenderer.invoke("forgot-password", email),
getToken: (TOKEN_KEY: "ACCESS_TOKEN" | "REFRESH_TOKEN") => ipcRenderer.invoke('get-token', TOKEN_KEY),
uploadScreenshot: () => ipcRenderer.invoke('upload-screenshot'),
getActiveAppDetails: () => ipcRenderer.invoke('get-active-app'),
toggleCloseBtn: (shouldEnable: boolean) => ipcRenderer.invoke('toggle-close-btn', shouldEnable),
resizeWindow: (width: number, height: number) => ipcRenderer.invoke('resize-window', width, height),
getProjectList: () => ipcRenderer.invoke("get-project-list"),
getRunningApps: () => ipcRenderer.invoke('get-running-apps'),
captureAppScreenShot: (appTitle: string): Promise<string> => ipcRenderer.invoke('capture-specific-window', appTitle),
onWindowUpdate: (callback) =>
ipcRenderer.on("window:updated", (_, data) => callback(data)),
startProjectTracking: (data) => ipcRenderer.invoke("start-project-tracking", data),
stopProjectTracking: (data) => ipcRenderer.invoke("stop-project-tracking", data),
trackingTabsOpeningClosing: (data) =>
ipcRenderer.invoke("tracking-tabs-opening-and-closing", data),
getProjectLogs: (
projectID: string,
fromDate?: string,
toDate?: string
) => ipcRenderer.invoke("get-project-logs", { projectID, fromDate, toDate }),
getProfile: () => ipcRenderer.invoke("get-profile"),
changePassword: (data: { oldPassword: string; password: string; cPassword: string }) =>
ipcRenderer.invoke("change-password", data),
updatePayment: (data: {
paymentMethod: string;
wiseEmail?: string;
veemAccount?: string;
}) => ipcRenderer.invoke('update-payment', data),
onTokenExpired: (callback) => {
// Remove old listeners
ipcRenderer.removeAllListeners("token-expired");
// Add single-use listener
ipcRenderer.once("token-expired", () => {
console.log("⚠️ Token expired event received in PRELOAD (ONE TIME)");
callback();
});
},
getProjectSummary: (projectId: string, date: string) =>
ipcRenderer.invoke("get-project-summary", { projectId, date }),
createTimesheet: (data: any) => ipcRenderer.invoke('create-timesheet', data),
createPTOAndSickLeaveRequest: (data) =>
ipcRenderer.invoke("create-leave-request", data),
getSchedules: () => ipcRenderer.invoke("get-schedules"),
getPayslips: (vaId: string) => ipcRenderer.invoke("get-payslips", vaId),
getLeaveList: () => ipcRenderer.invoke("get-leave-list"),
getAdjustmentsList:(vaId: string) => ipcRenderer.invoke("get-adjustments-list",vaId),
checkRegion: () => ipcRenderer.invoke("check-region"),
connectVPN: () => ipcRenderer.invoke("vpn-connect"),
disconnectVPN: () => ipcRenderer.invoke("vpn-disconnect"),
getBaseURL: () => ipcRenderer.invoke("GET_BASE_URL"),
updateBaseURL: (url: string) => ipcRenderer.invoke("UPDATE_BASE_URL", url),
} satisfies Window['electron']);
// function ipcInvoke<Key extends keyof EventPayloadMapping>(

View File

@ -35,7 +35,7 @@ test('', () => {
expect(template[0].label).toEqual('Show');
template[0]?.click?.(null as any, null as any, null as any);
expect(mainWindow.show).toHaveBeenCalled();
expect(app.dock.show).toHaveBeenCalled();
expect(app.dock!.show).toHaveBeenCalled();
template[1]?.click?.(null as any, null as any, null as any);
expect(app.quit).toHaveBeenCalled();

View File

@ -11,7 +11,7 @@ export function ipcMainHandle<Key extends keyof EventPayloadMapping>(
handler: () => EventPayloadMapping[Key]
) {
ipcMain.handle(key, (event) => {
validateEventFrame(event.senderFrame);
validateEventFrame(event.senderFrame!);
return handler();
});
}
@ -21,7 +21,7 @@ export function ipcMainOn<Key extends keyof EventPayloadMapping>(
handler: (payload: EventPayloadMapping[Key]) => void
) {
ipcMain.on(key, (event, payload) => {
validateEventFrame(event.senderFrame);
validateEventFrame(event.senderFrame!);
return handler(payload);
});
}
@ -34,11 +34,16 @@ export function ipcWebContentsSend<Key extends keyof EventPayloadMapping>(
webContents.send(key, payload);
}
export function validateEventFrame(frame: WebFrameMain) {
if (isDev() && new URL(frame.url).host === 'localhost:5123') {
export function validateEventFrame(frame: WebFrameMain | null): asserts frame is WebFrameMain {
if (!frame) throw new Error('Missing frame');
const url = frame.url;
if (isDev() && new URL(url).host === 'localhost:5123') {
return;
}
if (frame.url !== pathToFileURL(getUIPath()).toString()) {
if (url !== pathToFileURL(getUIPath()).toString()) {
throw new Error('Malicious event');
}
}

View File

@ -1,10 +1,19 @@
import { desktopCapturer } from "electron";
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";
import { getBaseURL } from "../api/baseURL.js";
const httpsAgent = new https.Agent({
rejectUnauthorized: false
});
// Blur using an offscreen canvas
async function blurAndCompressBase64Image(base64: string): Promise<Buffer> {
@ -53,12 +62,18 @@ export const handleLogin = async (credentials: { email: string, password: string
// Call your login API
console.log("🟡 login Request received", credentials);
let baseURL = getBaseURL()
console.log("app.getAppPath()", app.getAppPath());
console.log("UIPATH", getUIPath());
console.log("Doom 1" , `${baseURL}/api/v1/va/login`);
try {
console.log("➡️ Calling API...");
const response = await axios.post(
'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login',
`${baseURL}/api/v1/va/login`,
{ userDetails: { ...credentials } },
{ headers: { "ngrok-skip-browser-warning": 69420 } }
{ headers: { "ngrok-skip-browser-warning": 69420 }, httpsAgent }
);
console.log("API CAlled", response)
@ -95,7 +110,89 @@ export const handleLogin = async (credentials: { email: string, password: string
}
}
// Get active window info with timestamp
export const getActiveApp = async () => {
const result = await activeWindow();
return result;
const dataWithTime = { ...result, timestamp: new Date().toISOString() };
// console.log("🪟 Active Window with Time:", dataWithTime);
return dataWithTime;
};
// Periodically send active window updates
export const startWindowPolling = (win: BrowserWindow) => {
console.log("⏱️ Started active window polling (every 5s)");
setInterval(async () => {
try {
const app = await getActiveApp();
if (app) win.webContents.send("window:updated", app);
} catch (err) {
console.error("❌ Polling error:", err);
}
}, 5000);
};
export const getRunningAppsList = async () => {
try {
// PowerShell command: Get only apps with visible windows (non-background)
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 150, height: 150 } // Optional: set thumbnail size
});
console.log(sources, "Sources Retrieved");
const app = sources.map(source => ({
name: source.name,
pid: source.id,
// thumbnail: source.thumbnail // Can be used as screenshot
}));
return app
} catch (error) {
return []
}
}
export const captureSpecificApp = async (windowTitle: string): Promise<string> => {
try {
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
thumbnailSize: { width: 650, height: 430 }
});
const targetSource = sources.find(source => source.name === windowTitle);
if (targetSource) {
let image = targetSource.thumbnail.toDataURL();
const blurredBuffer = await blurAndCompressBase64Image(image);
// Convert buffer → base64 string
const base64String = `data:image/png;base64,${blurredBuffer.toString('base64')}`;
// ✅ Return the processed base64 instead of saving
return base64String;
// const folderPath = "C:\\Users\\Public\\vatracker\\screenshots";
// // Ensure directory exists
// if (!fs.existsSync(folderPath)) {
// fs.mkdirSync(folderPath, { recursive: true });
// }
// const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`);
// fs.writeFileSync(filePath, blurredBuffer);
// console.log(`Screenshot saved to ${filePath}`);
} else {
console.log(`Window with title "${windowTitle}" not found.`);
return "Not Found"
}
} catch (error) {
console.error('Error capturing screenshot:', error);
return 'Failed to capture screenshot.';
}
}
// timmer stop after 4-5 sec check it..

623
src/electron/vpn.ts Normal file
View File

@ -0,0 +1,623 @@
import { app } from "electron";
import { exec, execFile } from "child_process";
import fs from "fs";
import os from "os";
import path from "path";
import axios from "axios";
//
// ------------------ HELPERS ------------------
//
// Promise wrapper for exec()
function execPromise(cmd: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout, stderr) => {
if (err) return reject(stderr || err);
resolve(stdout);
});
});
}
// Detect dev vs production
function isDev() {
return !app.isPackaged;
}
//
// ------------------ PATH RESOLVERS ------------------
//
// WireGuard installer path (Windows only)
// function getWireGuardInstallerPath(): string {
// return isDev()
// ? path.join(process.cwd(), "resources", "wireguard-install", "wireguard-installer.exe")
// : path.join(process.resourcesPath, "wireguard-install", "wireguard-installer.exe");
// }
function getWireGuardInstallerPath(): string {
const devPath = path.join(process.cwd(), "resources", "wireguard-install", "wireguard-installer.exe");
// When running "npm run dev", use devPath ALWAYS
if (!app.isPackaged) {
console.log("DEV MODE Installer Path:", devPath);
return devPath;
}
// Production build path
const prodPath = path.join(process.resourcesPath, "wireguard-install", "wireguard-installer.exe");
console.log("PROD MODE Installer Path:", prodPath);
return prodPath;
}
// OS-specific wg0.conf
// function getConfigPath(): string {
// const base = isDev()
// ? path.join(process.cwd(), "resources", "wireguard")
// : path.join(process.resourcesPath, "wireguard");
// if (process.platform === "win32") {
// return path.join(base, "windows", "wg0.conf");
// }
// if (process.platform === "darwin") {
// return path.join(base, "macos", "wg0.conf");
// }
// throw new Error("Unsupported OS");
// }
// export async function getConfigPath(): Promise<string> {
// if (process.platform !== "win32") {
// throw new Error("Only Windows supported for dynamic WireGuard config");
// }
// console.log("Calling http://35.95.209.161/api/v1/vpn/connect")
// // 1️⃣ Call API to get dynamic config
// let response = null;
// try {
// response = await axios.post(
// "http://35.95.209.161/api/v1/vpn/connect",
// {},
// { timeout: 15000 }
// );
// } catch (err: any) {
// console.error("VPN API error:", {
// message: err.message,
// code: err.code,
// response: err.response?.data,
// });
// throw err;
// }
// console.log("Called http://35.95.209.161/api/v1/vpn/connect", JSON.stringify(response.data));
// if (!response.data?.success || !response.data.clientConf) {
// throw new Error("Invalid VPN config response");
// }
// const clientConf: string = response.data.clientConf;
// // 2️⃣ Decide base directory (same idea as your existing logic)
// const base = isDev()
// ? path.join(process.cwd(), "resources", "wireguard", "windows")
// : path.join(process.resourcesPath, "wireguard", "windows");
// // Ensure directory exists
// fs.mkdirSync(base, { recursive: true });
// // 3️⃣ Create temp / runtime conf file
// // IMPORTANT: name must stay stable (WireGuard service name depends on it)
// const confPath = path.join(base, "wg0.conf");
// // 4️⃣ Write config to file
// fs.writeFileSync(confPath, clientConf, {
// encoding: "utf-8",
// mode: 0o600, // secure permissions
// });
// console.log("VPN config written to:", confPath);
// // 5️⃣ Return path (same contract as before)
// return confPath;
// }
export async function getConfigPath(): Promise<string> {
console.log("Calling API: http://35.95.209.161/api/v1/vpn/connect");
// Fetch dynamic config from server (works for both Windows & macOS)
let response = null;
try {
response = await axios.post(
"http://35.95.209.161/api/v1/vpn/connect",
{},
{ timeout: 15000 }
);
} catch (err: any) {
console.error("VPN API error:", {
message: err.message,
code: err.code,
response: err.response?.data,
});
throw err;
}
if (!response.data?.success || !response.data.clientConf) {
throw new Error("Invalid VPN config response");
}
const clientConf: string = response.data.clientConf;
// ---------- PLATFORM SPECIFIC OUTPUT DIRECTORY ----------
let base = "";
if (process.platform === "win32") {
base = isDev()
? path.join(process.cwd(), "resources", "wireguard", "windows")
: path.join(process.resourcesPath, "wireguard", "windows");
}
// if (process.platform === "darwin") {
// base = isDev()
// ? path.join(process.cwd(), "resources", "wireguard", "macos")
// : path.join(process.resourcesPath, "wireguard", "macos");
// }
if (process.platform === "darwin") {
const base = "/usr/local/etc/wireguard";
fs.mkdirSync(base, { recursive: true });
const confPath = path.join(base, "wg0.conf");
fs.writeFileSync(confPath, clientConf, { encoding: "utf8", mode: 0o600 });
return confPath;
}
if (!base) throw new Error("Unsupported OS");
fs.mkdirSync(base, { recursive: true });
const confPath = path.join(base, "wg0.conf");
fs.writeFileSync(confPath, clientConf, {
encoding: "utf-8",
mode: 0o600,
});
console.log("Dynamic VPN config saved:", confPath);
return confPath;
}
// Detect installed WireGuard.exe on Windows
function findWireGuardCLI(): string | null {
const possiblePaths = [
"C:\\Program Files\\WireGuard\\wireguard.exe",
"C:\\Program Files (x86)\\WireGuard\\wireguard.exe"
];
for (const p of possiblePaths) {
if (fs.existsSync(p)) return p;
}
return null;
}
//
// ------------------ WINDOWS INSTALL LOGIC ------------------
//
// Install WireGuard silently
async function ensureWireGuardInstalled(): Promise<string> {
let cli = findWireGuardCLI();
if (cli) return cli;
// Installer path
const installer = getWireGuardInstallerPath();
if (!fs.existsSync(installer)) {
throw new Error("WireGuard installer not found in resources.");
}
console.log("WireGuard not found. Installing silently...");
await execPromise(`"${installer}" /quiet`);
cli = findWireGuardCLI();
if (!cli) throw new Error("WireGuard installation failed.");
console.log("WireGuard installed successfully:", cli);
return cli;
}
//
// ------------------ WINDOWS CONNECT/DISCONNECT ------------------
//
// async function connectWindows(): Promise<string> {
// const cli = await ensureWireGuardInstalled();
// const conf = getConfigPath();
// // Disable GUI auto popup
// await execPromise(
// `reg add "HKCU\\Software\\WireGuard" /v SuppressTray /t REG_DWORD /d 1 /f`
// );
// return new Promise((resolve, reject) => {
// execFile(cli, ["/installtunnelservice", conf], (err) => {
// if (err) return reject("Failed to connect VPN: " + err);
// resolve("Windows VPN connected without GUI");
// });
// });
// }
// let wgRuntimeProcess: any = null;
// async function connectWindows(): Promise<string> {
// const cli = await ensureWireGuardInstalled();
// const conf = getConfigPath();
// return new Promise((resolve, reject) => {
// wgRuntimeProcess = execFile(cli, ["/tunnelservice", conf], (err) => {
// if (err) return reject("VPN connection error: " + err);
// });
// // Wait a bit for interface to come up
// setTimeout(() => resolve("Windows VPN connected silently"), 800);
// });
// }
//
// ------------------ HEALTH + STATUS HELPERS ------------------
//
// 1️⃣ Check if wg0 interface exists (works for mac + windows)
async function isTunnelActive(): Promise<boolean> {
console.log("🔍 Checking if VPN tunnel is active...");
try {
// ---------------------- macOS ----------------------
if (process.platform === "darwin") {
console.log("🖥 Platform: macOS — detecting WireGuard via utun interface...");
const netOutput = await execPromise(`ifconfig`);
console.log("📡 ifconfig output length:", netOutput.length);
const utunMatch = netOutput.match(/utun\d+/);
console.log("🔎 utun interface detected:", utunMatch);
if (utunMatch) {
console.log("✅ VPN tunnel ACTIVE (utun interface exists)");
return true;
} else {
console.log("❌ VPN tunnel NOT active (no utun interface found)");
return false;
}
}
// ---------------------- Windows ----------------------
if (process.platform === "win32") {
console.log("🖥 Platform: Windows — detecting WireGuard via wg show...");
const wgOutput = await execPromise(`wg show 2>&1`);
console.log("📡 wg show output length:", wgOutput.length);
const interfaceFound = wgOutput.includes("interface: wg0");
console.log("🔎 wg0 interface detected:", interfaceFound);
if (interfaceFound) {
console.log("✅ VPN tunnel ACTIVE (wg0 found)");
return true;
} else {
console.log("❌ VPN tunnel NOT active (wg0 not found)");
return false;
}
}
// ---------------------- Unsupported OS ----------------------
console.log("⚠️ Unsupported OS platform — cannot detect VPN state");
return false;
} catch (err: any) {
console.log("❌ isTunnelActive() failed:", err);
return false;
}
}
// 2️⃣ Check endpoint health
async function isTunnelHealthy(): Promise<boolean> {
try {
const response = await axios.get("http://10.0.0.1/health", { timeout: 2000 });
return !!response.data?.success;
} catch {
return false;
}
}
// 3️⃣ Download config only if needed
async function ensureConfig(platform: string): Promise<string> {
return await getConfigPath(); // uses dynamic logic already
}
async function connectWindows(): Promise<string> {
try {
const response = await axios.get('http://35.95.209.161')
console.log("Response from test API:", response.data);
} catch (error) {
console.error("Error connecting to test API:", error);
}
const cli = await ensureWireGuardInstalled();
const conf = await getConfigPath();
// Disable GUI popup and silent mode (ignore errors)
await execPromise(`reg add "HKCU\\Software\\WireGuard" /v HideTrayIcon /t REG_DWORD /d 1 /f`).catch(() => { });
await execPromise(`reg add "HKCU\\Software\\WireGuard" /v SuppressTray /t REG_DWORD /d 1 /f`).catch(() => { });
await execPromise(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v WireGuard /f`).catch(() => { });
// Start background service
await execPromise(`sc start WireGuardManager`).catch(() => { });
return new Promise((resolve, reject) => {
execFile(cli, ["/installtunnelservice", conf], (err) => {
if (err) return reject("VPN connection failed: " + err);
resolve("Windows VPN connected silently");
});
});
}
// function disconnectWindows(): Promise<string> {
// const cli = findWireGuardCLI();
// if (!cli) return Promise.reject("WireGuard CLI not found");
// return execPromise(`"${cli}" /uninstalltunnelservice wg0`);
// }
async function disconnectWindows(): Promise<string> {
const cli = findWireGuardCLI();
if (!cli) throw new Error("WireGuard CLI not found");
// 1. Remove the installed tunnel service
await execPromise(`"${cli}" /uninstalltunnelservice wg0`);
// 2. Ensure background service is still running (optional clean-up)
// Windows sometimes marks the service as "stopped" after uninstall
await execPromise(`sc stop WireGuardTunnel$wg0`).catch(() => { });
await execPromise(`sc stop WireGuardManager`).catch(() => { });
return "Windows VPN disconnected";
}
async function ensureHomebrew(): Promise<void> {
try {
await execPromise("brew --version");
return; // brew already installed
} catch (_) {
console.log("Homebrew missing. Installing...");
await execPromise(
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
);
// Update shell env — macOS uses zprofile for system shells
await execPromise(`echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile`);
}
}
async function ensureWireGuardMac(): Promise<void> {
try {
await execPromise("which wg");
return; // already installed
} catch (_) {
console.log("Installing Homebrew + wireguard-tools...");
// Install Homebrew if missing
try {
await execPromise("brew --version");
} catch {
console.log("Homebrew missing. Installing...");
await execPromise(
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
);
// Load Homebrew env for next commands
await execPromise(`echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile`);
await execPromise(`eval "$(/opt/homebrew/bin/brew shellenv)"`);
}
// Install CLI WireGuard tools
await execPromise("brew install wireguard-tools");
}
}
//
// --------------- MAC CONNECT/DISCONNECT ---------------
//
// async function connectMac(): Promise<string> {
// const conf = getConfigPath();
// // Wrap command in login shell so Homebrew paths are available
// const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick up '${conf}'"`;
// const script = `
// do shell script "${cmd}" with administrator privileges
// `;
// const escapedScript = script.replace(/\n/g, " ");
// return execPromise(`osascript -e '${escapedScript}'`)
// .then(() => "macOS VPN connected (wg-quick)")
// .catch(err => {
// throw new Error("macOS VPN connect failed: " + err);
// });
// }
// async function connectMac(): Promise<string> {
// try {
// const conf = await getConfigPath();
// console.log("Configuration Path:", conf); // Debug the path
// const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick up '${conf}'"`;
// const script = `
// do shell script "${cmd}" with administrator privileges
// `;
// const escapedScript = script.replace(/\n/g, " ");
// await execPromise(`osascript -e '${escapedScript}'`);
// return "macOS VPN connected (wg-quick)";
// } catch (err) {
// console.error("macOS VPN connect failed:", err);
// throw new Error("macOS VPN connect failed: " + err);
// }
// }
// async function connectMac(): Promise<string> {
// await ensureWireGuardMac();
// const conf = await getConfigPath();
// const cmd = `/bin/bash -c "sudo /opt/homebrew/bin/wg-quick up '${conf}'"`;
// try {
// await execPromise(cmd);
// return "macOS VPN connected";
// } catch (err: any) {
// throw new Error("macOS VPN connect failed: " + err.message);
// }
// }
async function connectMac(): Promise<string> {
await ensureWireGuardMac();
const conf = await getConfigPath();
// Detect the correct wg-quick binary
let wgPath = (await execPromise(`which wg-quick`)).trim();
const cmd = `do shell script "${wgPath} up '${conf}'" with administrator privileges`;
try {
const result = await execPromise(`osascript -e '${cmd}'`);
console.log("macOS wg-quick result:", result);
return "macOS VPN connected";
} catch (err: any) {
console.error("macOS VPN connect failed:", err);
throw new Error("macOS VPN connect failed: " + err);
}
}
// function disconnectMac(): Promise<string> {
// const cmd = `/bin/bash -l -c "/opt/homebrew/bin/wg-quick down wg0"`;
// const script = `
// do shell script "${cmd}" with administrator privileges
// `;
// const escapedScript = script.replace(/\n/g, " ");
// return execPromise(`osascript -e '${escapedScript}'`)
// .then(() => "macOS VPN disconnected")
// .catch(err => {
// throw new Error("macOS VPN disconnect failed: " + err);
// });
// }
// function disconnectMac(): Promise<string> {
// const cmd = `/bin/bash -c "sudo /opt/homebrew/bin/wg-quick down wg0"`;
// return execPromise(cmd)
// .then(() => "macOS VPN disconnected")
// .catch(err => {
// throw new Error("macOS VPN disconnect failed: " + err.message);
// });
// }
async function disconnectMac(): Promise<string> {
let wgPath = (await execPromise(`which wg-quick`)).trim();
const cmd = `do shell script "${wgPath} down wg0" with administrator privileges`;
try {
await execPromise(`osascript -e '${cmd}'`);
return "macOS VPN disconnected";
} catch (err: any) {
throw new Error("macOS VPN disconnect failed: " + err);
}
}
//
// ------------------ PUBLIC API ------------------
//
export async function startVPN(): Promise<string> {
const platform = os.platform();
if (platform !== "win32" && platform !== "darwin") {
throw new Error("Unsupported OS");
}
// STEP 1: tunnel active?
console.log("Start VPN Called for pltf :", platform)
const active = await isTunnelActive();
console.log("VPN Active Status :", active)
if (active) {
console.log("VPN already active — checking health...");
// STEP 2: healthy?
if (await isTunnelHealthy()) {
console.log("VPN healthy — skip reconnect");
return "VPN already connected and healthy";
}
console.log("VPN active but unhealthy — reconnecting...");
await stopVPN(); // gracefully disconnect
}
// STEP 3: ensure config before connect
await ensureConfig(platform);
// STEP 4: connect
if (platform === "win32") {
return connectWindows();
}
if (platform === "darwin") {
return connectMac();
}
// STEP 5: unreachable fallback to satisfy TS
throw new Error("Unexpected platform state");
}
export async function stopVPN(): Promise<string> {
const platform = os.platform();
try {
if (platform === "win32") return await disconnectWindows();
if (platform === "darwin") return await disconnectMac();
} catch {
return "VPN not running";
}
throw new Error("Unsupported OS");
}

View File

@ -1,56 +1,131 @@
import React, { useEffect, useRef, useState } from "react";
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
// import React, { useEffect } from "react";
// import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
// import { useDispatch, useSelector } from "react-redux";
// import { AppDispatch, RootState } from "./store";
// import { loadUserFromStorage } from "./store/userSlice";
// import Login from "./pages/Login";
// import Dashboard from "./pages/Dashboard";
// 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";
// 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]);
// // ✅ Load user from localStorage into Redux when app starts
// useEffect(() => {
// dispatch(loadUserFromStorage());
// }, [dispatch]);
// console.log("App.tsx → user:", user);
// return (
// <BrowserRouter>
// <Routes>
// {/* Public Route */}
// <Route
// path="/login"
// element={
// <PublicRoute user={user}>
// <Login />
// </PublicRoute>
// }
// />
// {/* Protected Routes */}
// <Route
// path="/dashboard"
// element={
// <ProtectedRoute user={user}>
// <DashboardLayout />
// </ProtectedRoute>
// }
// >
// <Route index element={<Dashboard />} />
// <Route path="projects" element={<Projects />} />
// <Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
// <Route path="timesheet" element={<Timesheets />} />
// <Route path="settings" element={<Settings />} />
// <Route path="help-support" element={<HelpSupport />} />
// </Route>
// {/* Default Redirect */}
// <Route
// path="*"
// element={<Navigate to={user ? "/dashboard" : "/login"} replace />}
// />
// </Routes>
// </BrowserRouter>
// );
// };
// export default App;
import React, { useEffect } from "react";
import { HashRouter as Router, Route, Routes, Navigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./store";
import { loadUserFromStorage } from "./store/userSlice";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Projects from "./pages/Projects/Projects";
import DashboardLayout from "./layout/DashboardLayout";
import ProtectedRoute from "./routes/ProtectedRoute"
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";
interface User {
email: string;
password: string;
}
import Vpn from "./pages/Vpn/Vpn";
import Schedule from "./pages/Schedule/Schedule";
import PaysSlips from "./pages/PaySlips/PaysSlips";
import ApplyLeaveAndAdjustmentsTime from "./pages/LeaveAndAdjustments/ApplyLeaveAndAdjustmentsTime";
import VpnGuard from "./pages/Vpn/VpnGuard";
const App: React.FC = () => {
const theme = ThemeToggle();
const [user, setUser] = useState<User | null>(null);
const dispatch = useDispatch<AppDispatch>();
// ✅ Get user directly from Redux
const user = useSelector((state: RootState) => state.user.user);
// Apply theme
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
// Load user from localStorage
useEffect(() => {
const storedUser = localStorage.getItem("user");
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
} catch (err) {
console.error("Invalid user in localStorage", err);
}
}
}, []);
dispatch(loadUserFromStorage());
}, [dispatch]);
console.log("App.tsx → user:", user);
return (
<BrowserRouter>
<Router>
<Routes>
{/* Public Route */}
<Route
path="/login"
element={
<PublicRoute user={user}>
<Login setUser={setUser} />
<VpnGuard>
<Login />
</VpnGuard>
</PublicRoute>
}
/>
@ -60,16 +135,22 @@ const App: React.FC = () => {
path="/dashboard"
element={
<ProtectedRoute user={user}>
<DashboardLayout user={user} setUser={setUser} />
{/* <VpnGuard> */}
<DashboardLayout />
{/* </VpnGuard> */}
</ProtectedRoute>
}
>
<Route index element={<Dashboard />} />
<Route path="projects" element={<Projects />} />
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
<Route path="leave-adjustments/apply" element={<ApplyLeaveAndAdjustmentsTime />} />
<Route path="timesheet" element={<Timesheets />} />
<Route path="settings" element={<Settings />} />
<Route path="vpn" element={<Vpn />} />
<Route path="help-support" element={<HelpSupport />} />
<Route path="schedule" element={<Schedule />} />
<Route path="pays-slips" element={<PaysSlips />} />
</Route>
{/* Default Redirect */}
@ -78,7 +159,7 @@ const App: React.FC = () => {
element={<Navigate to={user ? "/dashboard" : "/login"} replace />}
/>
</Routes>
</BrowserRouter>
</Router>
);
};

View File

@ -1,24 +1,30 @@
// src/hooks/ThemeToggle.js
import { useState, useEffect } from "react";
export const ThemeToggle = () => {
export const useTheme = () => {
const [theme, setTheme] = useState("light");
useEffect(() => {
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
// Initial check
setTheme(darkQuery.matches ? "dark" : "light");
// Listen for changes
const handleChange = (e: MediaQueryListEvent) => {
const handleChange = (e:any) => {
setTheme(e.matches ? "dark" : "light");
};
darkQuery.addEventListener("change", handleChange);
darkQuery.addEventListener?.("change", handleChange);
darkQuery.addListener?.(handleChange); // For older Electron
return () => darkQuery.removeEventListener("change", handleChange);
return () => {
darkQuery.removeEventListener?.("change", handleChange);
darkQuery.removeListener?.(handleChange);
};
}, []);
return theme;
// Apply theme to <html>
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
return { theme, setTheme };
};

View File

@ -0,0 +1,99 @@
// contexts/ToastContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface Toast {
id: number;
message: string;
type: 'success' | 'error' | 'info' | 'warning';
}
interface ToastContextType {
showToast: (message: string, type?: Toast['type']) => void;
hideToast: (id: number) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = (message: string, type: Toast['type'] = 'success') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
// Auto remove after 5 seconds
setTimeout(() => {
hideToast(id);
}, 3000);
};
const hideToast = (id: number) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
return (
<ToastContext.Provider value={{ showToast, hideToast }}>
{children}
<ToastContainer toasts={toasts} hideToast={hideToast} />
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
const ToastContainer: React.FC<{
toasts: Toast[];
hideToast: (id: number) => void;
}> = ({ toasts, hideToast }) => {
const getTypeStyles = (type: Toast['type']) => {
switch (type) {
case 'success':
return 'bg-success text-success-content';
case 'error':
return 'bg-error text-error-content';
case 'warning':
return 'bg-warning text-warning-content';
case 'info':
return 'bg-info text-info-content';
default:
return 'bg-neutral text-neutral-content';
}
};
return (
<div className="toast toast-center toast-top z-2000">
{toasts.map((toast) => (
<div
key={toast.id}
className="w-full max-w-md px-2 mb-2"
>
<div
className={`alert ${getTypeStyles(toast.type)} shadow-lg flex items-start`}
onClick={() => hideToast(toast.id)}
>
<div className="flex-grow min-w-0">
<p className="text-sm break-words whitespace-pre-wrap">
{toast.message}
</p>
</div>
<button
className="btn btn-ghost btn-xs flex-shrink-0 ml-2"
onClick={(e) => {
e.stopPropagation();
hideToast(toast.id);
}}
>
✕
</button>
</div>
</div>
))}
</div>
);
};

View File

@ -0,0 +1,246 @@
// hooks/useActiveAppTracker.tsx
import { useEffect, useRef, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../store";
import { WindowInfo } from "../store/windowSlice";
import {
initializeProjectLogs,
updateWindowLog,
clearProjectLogs,
debugLogsState,
} from "../store/logsSlice";
interface DailyHours {
todayTotal: string;
dailyLimit: string;
remaining: string;
}
export const useActiveAppTracker = (
isTracking: boolean,
activeProjectLogId: string | null,
projectId: string | null,
projectName: string | null,
onLimitExceeded?: () => void
) => {
const dispatch = useDispatch();
const { logsByProject } = useSelector((state: RootState) => state.logs);
const lastActiveRef = useRef<WindowInfo | null>(null);
const lastTitleRef = useRef<string | null>(null);
const lastAppNameRef = useRef<string | null>(null);
const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
const limitCheckedRef = useRef(false);
const isStoppingRef = useRef(false);
const checkCountRef = useRef(0);
// Initialize logs when tracking starts
useEffect(() => {
if (isTracking && activeProjectLogId && projectId) {
// Initialize fresh logs for this tracking session
dispatch(
initializeProjectLogs({
projectId,
logId: activeProjectLogId,
})
);
console.log(` Logs initialized for project: ${projectName} (${projectId})`);
// Reset refs
lastActiveRef.current = null;
lastTitleRef.current = null;
lastAppNameRef.current = null;
checkCountRef.current = 0;
isStoppingRef.current = false;
}
}, [isTracking, activeProjectLogId, projectId, projectName, dispatch]);
// Stop tracking intervals
const stopIntervals = useCallback(() => {
if (intervalIdRef.current) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
}, []);
// Debug function to check logs state
const debugCurrentLogs = useCallback(() => {
if (projectId) {
dispatch(debugLogsState({ projectId }));
}
}, [projectId, dispatch]);
// Finalize and send all logs when stopping tracking
// hooks/useActiveAppTracker.tsx - finalizeAndSendLogs function
const finalizeAndSendLogs = useCallback(
async (vaId: string): Promise<boolean> => {
console.log(" FINALIZING AND SENDING ALL LOGS");
// Set stopping flag
isStoppingRef.current = true;
// Stop intervals immediately
stopIntervals();
if (!activeProjectLogId || !projectId) {
console.log("⚠️ Skipping — no active project");
return false;
}
const projectLogs = logsByProject[projectId];
if (!projectLogs || projectLogs.logs.length === 0) {
console.log("ℹ️ No logs found for project", projectId);
return true;
}
try {
// Create a DEEP copy of the logs
const allLogs = JSON.parse(JSON.stringify(projectLogs.logs));
// Filter out logs with very short duration (less than 1000ms = 1 second)
const validLogs = allLogs.filter((log:any) => {
return log.totalTime >= 1000; // Minimum 1 second (1000ms)
});
console.log(`📊 Total logs: ${allLogs.length}, Valid logs (≥1000ms): ${validLogs.length}`);
if (validLogs.length === 0) {
console.log("ℹ️ No valid logs to send");
dispatch(clearProjectLogs({ projectId }));
return true;
}
// Prepare payload with all valid logs - send directly as stored
const payload = {
vaId: vaId || "",
projectId,
logId: activeProjectLogId,
logs: validLogs, // Already formatted correctly
};
console.log("📤 Payload to send:", payload);
// console.log("📊 Sending logs summary:", {
// projectId,
// projectName,
// logCount: validLogs.length,
// totalTimeMs: validLogs.reduce((sum, log) => sum + log.totalTime, 0),
// totalTimeSec: validLogs.reduce((sum, log) => sum + Math.round(log.totalTime / 1000), 0),
// sampleLog: validLogs[0] ? {
// title: validLogs[0].title.substring(0, 40),
// from: validLogs[0].from,
// to: validLogs[0].to,
// durationMs: validLogs[0].totalTime,
// } : null,
// });
let res = await window.electron.trackingTabsOpeningClosing(payload);
console.log("Tracking response:", res);
dispatch(clearProjectLogs({ projectId }));
// Cleanup refs
lastActiveRef.current = null;
lastTitleRef.current = null;
lastAppNameRef.current = null;
limitCheckedRef.current = false;
console.log("✅ Logs sent and cleared successfully");
console.log("=".repeat(60));
return true;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("❌ Error sending logs:", error);
console.log("=".repeat(60));
return false;
}
},
[
activeProjectLogId,
projectId,
logsByProject,
stopIntervals,
dispatch,
]
);
// Main tracking logic - runs every 5 seconds
useEffect(() => {
if (!isTracking || !projectId || !activeProjectLogId) {
console.log("⏸️ Tracking inactive — stopping interval");
stopIntervals();
return;
}
console.log(`🚀 Starting tracker for: ${projectName}`);
const checkActiveWindow = async () => {
if (isStoppingRef.current) {
return;
}
try {
const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id || !details.owner?.name) {
return;
}
const currentTitle = details.title;
const currentAppName = details.owner.name;
checkCountRef.current += 1;
// Update Redux with current window activity
dispatch(
updateWindowLog({
projectId,
title: currentTitle,
appName: currentAppName,
url: null,
})
);
// Update refs
lastActiveRef.current = details;
lastTitleRef.current = currentTitle;
lastAppNameRef.current = currentAppName;
// Debug every 10 checks (every 50 seconds)
if (checkCountRef.current % 10 === 0) {
debugCurrentLogs();
}
} catch (error) {
console.error("❌ Failed to check active window:", error);
}
};
// Initial check
checkActiveWindow();
// Set up interval (every 5 seconds)
intervalIdRef.current = setInterval(checkActiveWindow, 5000);
// Cleanup
return () => {
console.log("🧹 Cleaning up tracker");
stopIntervals();
};
}, [
isTracking,
activeProjectLogId,
projectId,
projectName,
stopIntervals,
dispatch,
debugCurrentLogs,
]);
return {
finalizeAndSendLogs,
stopIntervals,
setIsStopping: (value: boolean) => {
isStoppingRef.current = value;
},
debugCurrentLogs,
};
};

View File

@ -22,6 +22,21 @@
}
}
@layer utilities {
.thin-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.thin-scrollbar::-webkit-scrollbar-thumb {
background-color: hsl(var(--bc) / 0.3); /* base-content/30 */
border-radius: 999px;
}
.thin-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
}
input[type=number] {
-moz-appearance: textfield;
}

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,44 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { Provider } from "react-redux";
import { store } from "./store";
import { ToastProvider } from './helper/ToastContext';
declare global {
interface Window {
__allowClose__: boolean;
}
}
// renderer only
window.addEventListener("online", (e) => {
console.log("Network online (ignored)");
e.preventDefault();
});
window.addEventListener("offline", (e) => {
console.log("Network offline (ignored)");
e.preventDefault();
});
// Extra safety
window.__allowClose__ = false;
window.addEventListener("beforeunload", (e) => {
if (!window.__allowClose__) {
e.returnValue = false;
}
});
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<ToastProvider>
<App />
</React.StrictMode>,
</ToastProvider>
</Provider>
)

View File

@ -0,0 +1,47 @@
import React from "react";
interface LogoutModalProps {
open: boolean;
onClose: () => void;
onConfirm: () => void | Promise<void>;
}
const LogoutModal: React.FC<LogoutModalProps> = ({ open, onClose, onConfirm }) => {
if (!open) return null;
return (
<dialog open className="modal">
<div className="modal-box max-w-[75vw]">
<h3 className="font-bold text-sm text-error">Confirm Logout</h3>
<p className="py-2 text-xs font-medium">
Are you sure you want to logout? You will need to login again to access your dashboard.
</p>
<div className="modal-action">
{/* Cancel */}
<button
type="button"
className="btn btn-ghost btn-xs"
onClick={onClose}
>
Cancel
</button>
{/* Logout */}
<button
type="button"
className="btn btn-error btn-xs"
onClick={onConfirm}
>
Logout
</button>
</div>
</div>
</dialog>
);
};
export default LogoutModal;

View File

@ -1,12 +1,22 @@
import React from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
import React, { useEffect, useRef, useState } from "react";
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause, RefreshCw } 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';
import { parseTimeStringToSeconds } from "../utils/timeParser";
// ----- Interfaces -----
// Updated Project interface to match JSON structure
interface Project {
id: number;
projectId: string;
projectName: string;
clientName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
status: string;
}
interface ProjectTrackingData {
@ -19,7 +29,7 @@ interface ProjectTrackingData {
interface TrackingState {
selectedProject: Project | null;
projectTrackingMap: Map<number, ProjectTrackingData>;
// projectTrackingMap: Map<string, ProjectTrackingData>;
isTracking: boolean;
isPaused: boolean;
elapsedTime: number;
@ -28,19 +38,23 @@ interface TrackingState {
handleStartTracking: (project: Project) => void;
handlePauseResume: () => void;
handleStopTracking: () => void;
handleProjectSelect: (id: number) => void;
handleProjectSelect: (projectId: string) => void;
projectsData: Project[];
loadingStart: boolean,
loadingStop: boolean,
isStopping: boolean
}
// ----- Helper -----
// Helper function
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 `;
};
// ----- Widget -----
// Widget Component
const Widget: React.FC<{
title: string;
value: string | number;
@ -71,11 +85,9 @@ const Widget: React.FC<{
</div>
);
// ----- Dashboard -----
// Dashboard Component
const Dashboard: React.FC = () => {
const {
selectedProject,
projectTrackingMap,
isTracking,
isPaused,
elapsedTime,
@ -84,131 +96,278 @@ const Dashboard: React.FC = () => {
handlePauseResume,
handleStopTracking,
handleProjectSelect,
projectsData,
loadingStart,
loadingStop,
isStopping
} = useOutletContext<TrackingState>();
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);
const [isRefreshing, setIsRefreshing] = useState(false);
const isLimitReached = selectedProject ? isProjectLimitReached(selectedProject) : false;
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));
}
};
const fetchProjects = async () => {
setIsRefreshing(true);
try {
console.log("📡 Fetching projects...Dashboard");
const res = await window.electron.getProjectList();
console.log("Project list response in dashboard: ", res.data?.[0]);
if (res?.success && Array.isArray(res.data)) {
// Store data in 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 || "",
})
);
// FIX: Check if we have any projects
if (res.data.length === 0) {
// No projects available, clear selected project
dispatch(setSelectedProjectSlice(null));
// fetchProfile();
return;
}
// Check if we already have a selected project that's being tracked
const currentlySelected = selectedProject;
const currentlyTracking = isTracking;
// Find if the currently selected project still exists in the fresh data
let projectToSelect = null;
if (currentlySelected && currentlyTracking) {
// We're tracking a project - try to preserve it
const existingProject = res.data.find(
(p: Project) => p.projectId === currentlySelected.projectId
);
if (existingProject) {
projectToSelect = existingProject;
}
}
// If no project is selected OR tracked project no longer exists, auto-select first project
if (!projectToSelect && res.data.length > 0) {
projectToSelect = res.data[0];
}
if (projectToSelect) {
console.log("Line 158", projectToSelect);
// Select the project
dispatch(setSelectedProjectSlice(projectToSelect));
// Only initialize tracking if it doesn't exist already
const trackingExists = projectTrackingMap[projectToSelect.projectId];
if (!trackingExists) {
const initialElapsed = parseTimeStringToSeconds(projectToSelect.totalWorkTime);
dispatch(
initializeProjectTracking({
projectId: projectToSelect.projectId,
initialTime: initialElapsed,
})
);
} else {
// Update project data but preserve tracking state
dispatch(
updateProjectTracking({
projectId: projectToSelect.projectId,
data: {
...trackingExists,
// Update elapsed time from server if needed
elapsedTime: parseTimeStringToSeconds(projectToSelect.totalWorkTime),
}
})
);
}
}
fetchProfile();
} else {
console.warn("⚠️ Unexpected response:", res);
// FIX: Also clear selected project on unexpected response
dispatch(setSelectedProjectSlice(null));
}
} catch (err) {
console.error("❌ Error fetching projects:", err);
// FIX: Clear selected project on error
dispatch(setSelectedProjectSlice(null));
} finally {
setIsRefreshing(false);
}
};
useEffect(() => {
fetchProjects();
fetchProfile()
}, []);
const handleRefresh = async () => {
// setIsRefreshing(true);
await fetchProjects();
// setTimeout(() => setIsRefreshing(false), 500); // Small delay for visual feedback
};
const selectedData = selectedProject
? projectTrackingMap.get(selectedProject.id)
? projectTrackingMap[selectedProject.projectId]
: null;
// FIX: Get the correct time to display
const getDisplayTime = () => {
if (!selectedProject) return 0;
// If tracking is active, use real-time elapsedTime
if (selectedData?.isTracking) {
return selectedData.elapsedTime || 0;
}
// If not tracking, use server time from selectedProject
return parseTimeStringToSeconds(selectedProject.totalWorkTime);
};
const displayTime = getDisplayTime();
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
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 = parseTimeStringToSeconds(project.totalWorkTime);
dispatch(initializeProjectTracking({
projectId,
initialTime: initialElapsed
}));
} else {
// Restore saved tracking state
dispatch(updateProjectTracking({
projectId,
data: existingData
}));
}
};
return (
<main className="flex-1 overflow-y-auto no-scrollbar">
<main className="flex-1 overflow-hidden no-scrollbar ">
{/* 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 */}
{/* 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">
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body">
{selectedProject ? (
{(isStopping || isRefreshing) ? (
// Skeleton loader for tracking section
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-center sm:text-left w-full">
<div className="skeleton w-32 h-6 mx-auto sm:mx-0 mb-2"></div>
<div className="skeleton w-48 h-4 mx-auto sm:mx-0"></div>
</div>
<div className="skeleton w-full sm:w-32 h-10 rounded-xl"></div>
</div>
) : selectedProject ? (
<>
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="text-center sm:text-left">
{/* FIX: Use displayTime instead of always using server time */}
<p className="text-lg font-extrabold text-success">
{formatTime(selectedData?.elapsedTime || 0)}
{formatTime(displayTime)}
</p>
<p className="text-xs text-base-content/70 mt-1">
{selectedData?.isTracking ? "Current Session" : "Ready"}
</p>
</div>
{!selectedData?.isTracking ? (
<button
onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
>
<Play className="w-3 h-3" /> Start Tracking
</button>
) : (
<button
className="btn btn-xs btn-error rounded-full"
onClick={handleStopTracking}
>
<Square className="w-3 h-3" />
Stop Tracking
</button>
)}
</div>
<div className="divider my-3" />
<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">Project:</span>{" "}
<span className="font-semibold">
{selectedProject.projectName}
</span>
</div>
<div>
<span className="text-base-content/70">Client:</span>{" "}
<span>{selectedProject.clientName}</span>
{/* Show tracking status */}
{selectedData?.isTracking && (
<div className="text-xs text-success mt-1">
● Live Tracking
</div>
<div>
<span className="text-base-content/70">Status:</span>{" "}
<span
className={`badge ${selectedData?.isPaused
? "badge-warning"
: selectedData?.isTracking
? "badge-success"
: "badge-neutral"
}`}
)}
</div>
{!selectedData?.isTracking ? (
<button
onClick={() => handleStartTracking(selectedProject)}
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
disabled={loadingStart}
>
{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>
{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}
>
{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" />
</>
) : (
<p className="text-sm text-base-content/60 text-center">
Select a project below to start tracking.
{projectsData.length === 0
? "No projects available."
: "Select a project below to start tracking."}
</p>
)}
</div>
@ -216,41 +375,89 @@ const Dashboard: React.FC = () => {
</div>
{/* Projects Table */}
<div>
<p className="text-md mb-2 font-bold">Projects</p>
<div className="overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md">
<table className="table table-zebra w-full text-sm">
<div className="border border-base-content/10 bg-base-100 shadow-md min-h-[58vh] flex flex-col">
{/* 🔹 Sticky Header */}
<div className="sticky top-0 z-20 bg-base-100 border-b border-base-content/10 p-3 flex justify-between items-center">
<p className="text-md font-bold">Projects</p>
{!selectedData?.isTracking &&
<button
onClick={handleRefresh}
disabled={isRefreshing}
className="btn btn-xs btn-ghost gap-1 text-warning"
title="Refresh projects"
>
<RefreshCw className={`w-3 h-3 text-warning ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh
</button>}
</div>
{/* 🔹 Table or Empty State or Skeleton */}
{(isStopping || isRefreshing) ? (
// Skeleton loader for projects table
<div className="p-4 space-y-3">
{[...Array(3)].map((_, index) => (
<div key={index} className="flex items-center justify-between">
<div className="skeleton w-8 h-4"></div>
<div className="skeleton w-32 h-4"></div>
<div className="skeleton w-16 h-4"></div>
<div className="skeleton w-20 h-8 rounded"></div>
</div>
))}
</div>
) : projectsData.length > 0 ? (
<div
className="overflow-y-auto no-scrollbar"
style={{
maxHeight: "50vh",
minHeight: "auto",
}}
>
<table className="table table-zebra w-full text-[12px] ">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Project</th>
<th>Client</th>
<th>Name</th>
<th>Logs Hours</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{projectsData.map((project) => {
const isCurrentlySelected = selectedProject?.id === project.id;
// ✅ FIX: Disable ALL projects when ANY project is tracking
// Only enable selection when not tracking
{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.id}>
<td>{project.id}</td>
<td>{project.projectName}</td>
<td>{project.clientName}</td>
<tr key={project.projectId} className={`${project.status === 'INACTIVE' ? 'hidden' : ""} `}>
<td className="text-xs opacity-70">{index + 1}</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>
</div>
</td>
<td>
<div className="flex flex-col gap-1">
<button
className={`btn btn-xs ${isCurrentlySelected
? "btn-success"
: "btn-primary"
className={`btn btn-xs ${isCurrentlySelected ? "btn-success" : "btn-primary"
}`}
onClick={() => handleProjectSelect(project.id)}
disabled={isDisabled}
onClick={() => handleProjectSelectLocal(project.projectId)}
disabled={isDisabled || limitReached}
>
{isCurrentlySelected ? "Selected" : "Select"}
</button>
</div>
</td>
</tr>
);
@ -258,6 +465,11 @@ const Dashboard: React.FC = () => {
</tbody>
</table>
</div>
) : (
<div className="flex items-center justify-center flex-1 text-sm text-base-content/60">
<NoData />
</div>
)}
</div>
</main>
);

View File

@ -23,7 +23,7 @@ const HelpSupport: React.FC = () => {
<Mail size={18} className="text-primary" />
<div>
<p className="text-xs text-base-content/70">Email</p>
<p className="font-medium text-sm">support@vatechcorp.com</p>
<p className="font-medium text-sm">support@docva.com</p>
</div>
</div>
@ -31,7 +31,7 @@ const HelpSupport: React.FC = () => {
<Phone size={18} className="text-primary" />
<div>
<p className="text-xs text-base-content/70">Phone</p>
<p className="font-medium text-sm">+91 98765 43210</p>
<p className="font-medium text-sm">+XXXX XXXXX</p>
</div>
</div>

View File

@ -0,0 +1,183 @@
import React from "react";
import { Loader2, RefreshCw } from "lucide-react";
import { DateTime } from "luxon";
interface AdjustmentItem {
id: string;
date: string;
loginTime: string;
logoutTime: string;
comment: string;
status: string;
requestedAt: string;
adjustedHours: string;
project: {
id: string;
name: string;
};
}
interface AdjustmentTableProps {
isTracking: boolean;
adjustments: AdjustmentItem[];
loading: boolean;
lastRefreshed: Date | null;
onRefresh: () => Promise<void>;
}
const AdjustmentsTable: React.FC<AdjustmentTableProps> = ({
isTracking,
adjustments,
loading,
lastRefreshed,
onRefresh
}) => {
const [refreshing, setRefreshing] = React.useState(false);
// 🌍 Convert UTC → Local System Time
const toLocal = (utcString: string, format: string) => {
return DateTime.fromISO(utcString, { zone: "utc" })
.setZone(DateTime.local().zoneName)
.toFormat(format);
};
const formatTime = (t: string) => toLocal(t, "HH:mm:ss");
const formatDate = (d: string) => toLocal(d, "yyyy-MM-dd");
const handleRefresh = async () => {
setRefreshing(true);
await onRefresh();
setRefreshing(false);
};
const renderSkeleton = () =>
Array.from({ length: 7 }).map((_, i) => (
<tr key={i}>
<td><div className="skeleton h-4 w-6"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
<td><div className="skeleton h-4 w-24"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
<td><div className="skeleton h-4 w-10"></div></td>
<td><div className="skeleton h-4 w-24"></div></td>
<td><div className="skeleton h-4 w-14"></div></td>
</tr>
));
return (
<>
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
style={{
height: isTracking ? "calc(100vh - 200px)" : "calc(100vh - 160px)",
}}
>
<table className="table table-zebra w-full text-[11px]">
<thead className="sticky top-0 bg-base-200 text-[12px] z-10">
<tr className="text-center">
<th>#</th>
<th>Project</th>
<th>Date</th>
<th>In → Out</th>
<th>Adjusted Hours</th>
<th>Status</th>
<th>Requested At</th>
</tr>
</thead>
<tbody className="text-center">
{loading
? renderSkeleton()
: adjustments.length === 0
? (
<tr>
<td colSpan={7} className="text-center py-4 text-base-content/50">
No adjustments found
</td>
</tr>
)
: adjustments.map((item, idx) => (
<tr key={item.id}>
<td>{idx + 1}</td>
<td>
<span className="badge badge-xs badge-info whitespace-nowrap">
{item.project?.name || "N/A"}
</span>
</td>
<td className="whitespace-nowrap">{formatDate(item.date)}</td>
<td className="whitespace-nowrap flex ">
<span className="me-1">{formatTime(item.loginTime)} </span>
→ {" "}
<span className="ms-1">{formatTime(item.logoutTime)}</span>
</td>
<td className="whitespace-nowrap">
<span className="badge badge-xs badge-primary">
{item.adjustedHours}
</span>
</td>
<td>
<span
className={`badge badge-xs ${
item.status === "PENDING"
? "badge-warning"
: item.status === "APPROVED"
? "badge-success"
: "badge-error"
}`}
>
{item.status}
</span>
</td>
<td className="whitespace-nowrap">{toLocal(item.requestedAt, "yyyy-MM-dd HH:mm:ss")}</td>
</tr>
))}
{/* {refreshing && adjustments.length > 0 && (
<tr>
<td colSpan={7} className="bg-base-200/50">
<div className="flex justify-center py-2">
{renderSkeleton()}
</div>
</td>
</tr>
)} */}
</tbody>
</table>
</div>
{/* FOOTER */}
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{adjustments.length} adjustment{adjustments.length !== 1 ? "s" : ""} found
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-base-content/60">
Last updated: {toLocal(lastRefreshed.toISOString(), "HH:mm:ss")}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
>
{refreshing ? (
<Loader2 size={14} className="animate-spin" />
) : (
<RefreshCw size={14} />
)}
</button>
</div>
</div>
</>
);
};
export default AdjustmentsTable;

View File

@ -0,0 +1,114 @@
import { useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
import PtoLeaveTab from './tabs/PtoLeaveTab';
import SickLeaveTab from './tabs/SickLeaveTab';
import AdjustmentTab from './tabs/AdjustmentTab';
import { useNavigate } from "react-router-dom";
type TabType = 'pto' | 'sick' | 'adjustment';
const ApplyLeaveAndAdjustmentsTime = () => {
const [activeTab, setActiveTab] = useState<TabType>('pto');
const [isSubmitting, setIsSubmitting] = useState(false); // NEW
const projects = useSelector((state: RootState) => state.projects.projectsData);
const navigate = useNavigate()
// ------- COMMON API CALL FOR BOTH TABS -------
const submitLeave = async (payload: {
dateFrom: string;
dateTo: string;
leaveType: "PTO" | "SICK";
comment: string;
}) => {
try {
setIsSubmitting(true);
console.log("Leave /sick Payload:", payload);
const response = await window.electron.createPTOAndSickLeaveRequest(payload)
console.log("Leave Payload:", response)
if (response.success) {
return { success: true };
}
return {
success: false,
message: response.message
};
} catch (err) {
console.error(err);
return { success: false, message: "Request failed" };
} finally {
setIsSubmitting(false);
}
};
const handleBack = () => {
navigate('/dashboard/leave-adjustments')
}
return (
<>
<div className="max-h-screen">
<div className="w-full mx-auto">
<div className='mb-3 flex justify-between items-center'>
<h1 className="text-mb font-bold ">Apply Leave & Adjust Timesheet</h1>
<button className="btn btn-xs btn-error text-base-content " onClick={handleBack}>Back</button>
</div>
{/* Tabs — Disabled while submitting */}
<div role="tablist" className="tabs tabs-border w-full mb-1 opacity-100">
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "pto" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("pto")}
>
PTO Leave
</a>
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "sick" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("sick")}
>
Sick Leave
</a>
<a
role="tab"
className={`tab text-xs font-semibold ${activeTab === "adjustment" ? "tab-active text-primary" : ""
} ${isSubmitting ? "pointer-events-none opacity-50" : ""}`}
onClick={() => setActiveTab("adjustment")}
>
Adjustment Submission
</a>
</div>
{/* Main Container */}
<div className="rounded-lg bg-base-100 overflow-hidden">
<div className="px-8 py-4 overflow-y-auto no-scrollbar max-h-[81vh]">
{activeTab === "pto" && (
<PtoLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "sick" && (
<SickLeaveTab submitLeave={submitLeave} isSubmitting={isSubmitting} />
)}
{activeTab === "adjustment" && (
<AdjustmentTab projects={projects} />
)}
</div>
</div>
</div>
</div>
</>
)
}
export default ApplyLeaveAndAdjustmentsTime

View File

@ -1,566 +1,178 @@
import React, { useState } from 'react';
import AdjustmentModal from './AdjustmentModal';
import React, { useState, useEffect } from "react";
import { useNavigate, useOutletContext } from "react-router-dom";
import { TrackingState } from "../../layout/DashboardLayout";
import LeaveTable from "./LeaveTable";
import AdjustmentsTable from "./AdjustmentsTable";
import { useSelector } from "react-redux";
interface LeaveFormData {
fromDate: string;
toDate: string;
description: string;
interface LeaveItem {
leaveId: string;
dateFrom: string;
dateTo: string;
totalDays: number;
leaveType: string;
comment: string;
status: string;
appliedOn: string;
reviewedAt: string | null;
rejectionReason: string | null;
}
interface AdjustmentEntry {
id: number;
interface AdjustmentItem {
id: string;
date: string;
loggedHours: number;
loginTime: string;
logoutTime: string;
comment: string;
status: string;
requestedAt: string;
adjustedHours: string;
project: {
id: string;
name: string;
};
}
interface LogEntry {
id: number;
date: string;
hours: number;
task: string;
status: "Completed" | "In Progress" | "Pending";
interface ApiResponse<T> {
success: boolean;
data?: T;
}
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 = 'pto' | 'sick' | 'adjustment';
const LeaveAndAdjustments: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('pto');
const { isTracking } = useOutletContext<TrackingState>();
const navigate = useNavigate();
// PTO State
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
fromDate: '',
toDate: '',
description: ''
});
const [ptoErrors, setPtoErrors] = useState<Partial<LeaveFormData>>({});
const { projectSummary } = useSelector((state: any) => state.projects);
console.log("projectSummary46", projectSummary)
// Sick Leave State
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
fromDate: '',
toDate: '',
description: ''
});
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
// Tabs state
const [activeTab, setActiveTab] = useState<"leave" | "adjustments">("leave");
// 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>('');
// Data states
const [leaves, setLeaves] = useState<LeaveItem[]>([]);
const [adjustments, setAdjustments] = useState<AdjustmentItem[]>([]);
// Validation function
const validateLeaveForm = (data: LeaveFormData): Partial<LeaveFormData> => {
const errors: Partial<LeaveFormData> = {};
// Loading states
const [loadingLeaves, setLoadingLeaves] = useState(true);
const [loadingAdjustments, setLoadingAdjustments] = useState(true);
if (!data.fromDate) {
errors.fromDate = 'From date is required';
}
// Refresh state
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
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';
}
if (!data.description.trim()) {
errors.description = 'Description/Reason is required';
}
return errors;
const handleApplyLeaveAndAdjust = () => {
navigate("/dashboard/leave-adjustments/apply");
};
// PTO Handlers
const handlePtoChange = (field: keyof LeaveFormData, value: string) => {
setPtoLeave(prev => ({ ...prev, [field]: value }));
if (ptoErrors[field]) {
setPtoErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const handlePtoSubmit = async () => {
const errors = validateLeaveForm(ptoLeave);
if (Object.keys(errors).length > 0) {
setPtoErrors(errors);
return;
}
// Fetch leaves data
const fetchLeaves = async () => {
setLoadingLeaves(true);
try {
console.log('Submitting PTO:', ptoLeave);
alert('PTO request submitted successfully!');
setPtoLeave({ fromDate: '', toDate: '', description: '' });
setPtoErrors({});
} catch (error) {
alert('Failed to submit request. Please try again.');
console.error('Error:', error);
const res: ApiResponse<{ leaves: LeaveItem[] }> = await window.electron.getLeaveList();
if (res.success) {
setLeaves(res?.data?.leaves || []);
}
} catch (e) {
console.error("Error fetching leaves:", e);
} finally {
setLoadingLeaves(false);
}
};
// 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;
}
// Fetch adjustments data
const fetchAdjustments = async () => {
setLoadingAdjustments(true);
try {
console.log('Submitting Sick Leave:', sickLeave);
alert('Sick Leave request submitted successfully!');
setSickLeave({ fromDate: '', toDate: '', description: '' });
setSickErrors({});
} catch (error) {
alert('Failed to submit request. Please try again.');
console.error('Error:', error);
const res: ApiResponse<AdjustmentItem[]> = await window.electron.getAdjustmentsList(projectSummary?.vaId);
if (res.success) {
setAdjustments(res.data || []);
}
} catch (err) {
console.error("Error fetching adjustments:", err);
} finally {
setLoadingAdjustments(false);
}
};
const [editingEntry, setEditingEntry] = useState<AdjustmentEntry | null>(null);
const [newHours, setNewHours] = useState<number>(0);
// Combined refresh function
const handleRefresh = async () => {
// if (activeTab === "leave") {
// await fetchLeaves();
// } else {
// await fetchAdjustments();
// }
const handlePeriodChange = (value: string) => setSelectedPeriod(value);
const handleEditClick = (entry: AdjustmentEntry) => {
setEditingEntry(entry);
const modal = document.getElementById("editModal") as HTMLDialogElement;
modal?.showModal();
fetchLeaves();
setTimeout(() => {
fetchAdjustments();
}, 0);
setLastRefreshed(new Date());
};
const handleCloseModal = () => {
setEditingEntry(null);
setNewHours(0)
const modal = document.getElementById("editModal") as HTMLDialogElement;
modal?.close();
};
const handleSave = () => {
console.log("Saving:", { ...editingEntry, updatedHours: newHours });
handleCloseModal();
};
// Initial data fetch
useEffect(() => {
fetchLeaves();
setTimeout(() => {
fetchAdjustments();
}, 0);
setLastRefreshed(new Date());
}, []);
return (
<div className="min-h-screen">
<div className="w-full mx-auto">
<main
className="flex flex-col gap-2 overflow-hidden"
// style={{
// height: isTracking ? "calc(100vh - 40px)" : "",
// }}
>
{/* Header */}
<div className=" ">
<h1 className="text-mb font-bold mb-2">Leave & Adjustments</h1>
<div className="flex-none mb-2 flex justify-between items-center">
<h2 className="text-md font-bold">Leave & Adjust Timesheet</h2>
<button className="btn btn-xs btn-primary" onClick={handleApplyLeaveAndAdjust}>
Apply
</button>
</div>
{/* Main Container */}
<div className=" rounded-lg bg-base-100 overflow-hidden">
{/* Tabs Navigation */}
<div className="flex ">
{/* Tabs */}
<div role="tablist" className="tabs tabs-bordered w-full">
<button
onClick={() => setActiveTab('pto')}
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'pto'
? ' text-primary border-b-2 border-primary'
: ''
}`}
role="tab"
className={`tab ${activeTab === "leave" ? "tab-active border-b-2 border-primary" : ""}`}
onClick={() => setActiveTab("leave")}
>
<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>
PTO Request
</span>
Leave
</button>
<button
onClick={() => setActiveTab('sick')}
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative cursor-pointer ${activeTab === 'sick'
? ' text-primary border-b-2 border-primary'
: ''
}`}
role="tab"
className={`tab ${activeTab === "adjustments" ? "tab-active border-primary border-b-2" : ""}`}
onClick={() => setActiveTab("adjustments")}
>
<span className="flex items-center justify-center gap-2">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Sick Leave
</span>
</button>
<button
onClick={() => setActiveTab('adjustment')}
className={`flex-1 px-6 py-3 text-xs font-semibold cursor-pointer transition-all duration-300 relative ${activeTab === 'adjustment'
? ' text-primary border-b-2 border-primary'
: ''
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Adjustments
</span>
</button>
</div>
{/* Tab Content */}
<div className="px-8 py-4">
{/* PTO Tab */}
{activeTab === 'pto' && (
<div className="animate-fade-in">
<div className="max-w-4xl ">
<div className="mb-1">
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">From Date</span>
</label>
<input
type="date"
value={ptoLeave.fromDate}
onChange={(e) => handlePtoChange('fromDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.fromDate ? 'input-error' : ''}`}
{/* TABLE AREA - Scrollable */}
<div className="flex-1 overflow-y-auto no-scrollbar">
{activeTab === "leave" && (
<LeaveTable
isTracking={isTracking}
leaves={leaves}
loading={loadingLeaves}
lastRefreshed={lastRefreshed}
onRefresh={handleRefresh}
/>
{ptoErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.fromDate}</span>
</label>
)}
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold">To Date</span>
</label>
<input
type="date"
value={ptoLeave.toDate}
onChange={(e) => handlePtoChange('toDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${ptoErrors.toDate ? 'input-error' : ''}`}
{activeTab === "adjustments" && (
<AdjustmentsTable
isTracking={isTracking}
adjustments={adjustments}
loading={loadingAdjustments}
lastRefreshed={lastRefreshed}
onRefresh={handleRefresh}
/>
{ptoErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.toDate}</span>
</label>
)}
</div>
</div>
<div className="form-control flex flex-col gap-1">
<label className="label">
<span className="label-text font-semibold ">Description / Reason</span>
</label>
<textarea
value={ptoLeave.description}
onChange={(e) => handlePtoChange('description', e.target.value)}
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
placeholder="Please provide a reason for your PTO request..."
/>
{ptoErrors.description && (
<label className="label">
<span className="label-text-alt text-error font-medium">{ptoErrors.description}</span>
</label>
)}
</div>
<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 Request
</button>
<button
onClick={() => {
setPtoLeave({ fromDate: '', toDate: '', description: '' });
setPtoErrors({});
}}
className="btn btn-secondary transition-all"
>
Clear
</button>
</div>
</div>
</div>
</div>
)}
{/* Sick Leave Tab */}
{activeTab === 'sick' && (
<div className="animate-fade-in">
<div className="max-w-4xl ">
<div className="mb-1">
<h2 className="text-sm font-bold mb-2">Sick Leave Request</h2>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">From Date</span>
</label>
<input
type="date"
value={sickLeave.fromDate}
onChange={(e) => handleSickChange('fromDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${sickErrors.fromDate ? 'input-error' : ''}`}
/>
{sickErrors.fromDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.fromDate}</span>
</label>
)}
</div>
<div className="form-control">
<label className="label">
<span className="label-text font-semibold ">To Date</span>
</label>
<input
type="date"
value={sickLeave.toDate}
onChange={(e) => handleSickChange('toDate', e.target.value)}
className={`input input-bordered w-full focus:input-primary transition-all ${sickErrors.toDate ? 'input-error' : ''}`}
/>
{sickErrors.toDate && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.toDate}</span>
</label>
)}
</div>
</div>
<div className="form-control flex flex-col gap-1">
<label className="label">
<span className="label-text font-semibold ">Description / Reason</span>
</label>
<textarea
value={sickLeave.description}
onChange={(e) => handleSickChange('description', e.target.value)}
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${sickErrors.description ? 'textarea-error' : ''}`}
placeholder="Please describe your illness or condition..."
/>
{sickErrors.description && (
<label className="label">
<span className="label-text-alt text-error font-medium">{sickErrors.description}</span>
</label>
)}
</div>
<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 Request
</button>
<button
onClick={() => {
setSickLeave({ fromDate: '', toDate: '', description: '' });
setSickErrors({});
}}
className="btn btn-secondary transition-all"
>
Clear
</button>
</div>
</div>
</div>
</div>
)}
{/* Adjustment Submissions Tab */}
{activeTab === 'adjustment' && (
<div className="animate-fade-in">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<h2 className="text-md font-bold mb-2">Time Adjustments</h2>
<div className="form-control w-48">
<select
value={selectedPeriod}
onChange={(e) => handlePeriodChange(e.target.value)}
className="select select-bordered focus:select-primary font-semibold"
>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-hidden rounded-lg border border-base-content/10">
<table className="table w-full">
<thead className="bg-base-100">
<tr>
<th className="font-bold text-base">Date</th>
<th className="font-bold text-base">Logged Hours</th>
<th className="font-bold text-base text-right">Actions</th>
</tr>
</thead>
<tbody>
{adjustmentData.map((entry) => (
<tr key={entry.id} className="transition-colors">
<td className="font-medium">
{new Date(entry.date).toLocaleDateString("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})}
</td>
<td>
<span className="badge badge-lg badge-ghost font-semibold">
{entry.loggedHours} hrs
</span>
</td>
<td className="text-right">
<button
onClick={() => handleEditClick(entry)}
className="btn btn-primary btn-sm hover:shadow-md transition-all"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* No Data */}
{adjustmentData.length === 0 && (
<div className="text-center py-12 text-slate-500">
<svg
className="h-16 w-16 mx-auto mb-4 opacity-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<p className="font-medium">No time entries found for this period</p>
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* View Status */}
<div className='mt-4'>
<h1 className="text-md font-bold mb-2">View Status</h1>
<div
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
style={{ minHeight: "150px", maxHeight: "300px" }}
>
<table className="table table-zebra w-full">
{/* Table Head */}
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Date</th>
<th>Logged Hours</th>
<th>Task</th>
<th>Status</th>
</tr>
</thead>
{/* Table Body */}
<tbody>
{data.map((log, index) => (
<tr key={log.id}>
<td>{index + 1}</td>
<td>{log.date}</td>
<td className="font-medium">{log.hours} hrs</td>
<td className="whitespace-normal break-words">{log.task}</td>
<td>
<div
className={`badge ${log.status === "Completed"
? "badge-success"
: log.status === "In Progress"
? "badge-info"
: "badge-warning"
}`}
>
{log.status}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div></div>
<style>{`
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
`}</style>
<AdjustmentModal
editingEntry={editingEntry}
newHours={newHours}
setNewHours={setNewHours}
handleCloseModal={handleCloseModal}
handleSave={handleSave}
/>
</div>
</main>
);
};

View File

@ -0,0 +1,184 @@
import React from "react";
import { Loader2, RefreshCw } from "lucide-react";
import { DateTime } from "luxon";
interface LeaveItem {
leaveId: string;
dateFrom: string;
dateTo: string;
totalDays: number;
leaveType: string;
comment: string;
status: string;
appliedOn: string;
reviewedAt: string | null;
rejectionReason: string | null;
}
interface LeaveTableProps {
isTracking: boolean;
leaves: LeaveItem[];
loading: boolean;
lastRefreshed: Date | null;
onRefresh: () => Promise<void>;
}
const LeaveTable: React.FC<LeaveTableProps> = ({
isTracking,
leaves,
loading,
lastRefreshed,
onRefresh
}) => {
const [refreshing, setRefreshing] = React.useState(false);
// 🌍 Convert UTC → Local System Time
const toLocal = (utcString: string, format: string) => {
return DateTime.fromISO(utcString, { zone: "utc" })
.setZone(DateTime.local().zoneName)
.toFormat(format);
};
const formatDate = (d: string) => toLocal(d, "yyyy-MM-dd");
const formatDateTime = (d: string) => toLocal(d, "yyyy-MM-dd HH:mm:ss");
const handleRefresh = async () => {
setRefreshing(true);
await onRefresh();
setRefreshing(false);
};
const renderSkeletonRows = () =>
Array.from({ length: 5 }).map((_, index) => (
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
<td><div className="skeleton h-4 w-6 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-20 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-28 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-16 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-10 bg-base-300"></div></td>
<td><div className="skeleton h-4 w-24 bg-base-300"></div></td>
</tr>
));
const formatLastRefreshed = () =>
lastRefreshed
? DateTime.fromJSDate(lastRefreshed)
.setZone(DateTime.local().zoneName)
.toFormat("HH:mm:ss")
: "Never";
return (
<>
{/* Table Container */}
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
style={{
height: isTracking ? "calc(100vh - 200px)" : "calc(100vh - 160px)",
}}
>
<table className="table table-zebra w-full text-[11px]">
<thead className="sticky top-0 bg-base-200 z-10 text-[12px]">
<tr className="text-center">
<th>#</th>
<th>Leave Type</th>
<th>Date From → To</th>
<th>Status</th>
<th>Total Days</th>
<th>Applied On</th>
</tr>
</thead>
<tbody className="text-center">
{loading ? (
renderSkeletonRows()
) : refreshing && leaves.length === 0 ? (
renderSkeletonRows()
) : leaves.length > 0 ? (
leaves.map((leave, index) => (
<tr key={leave.leaveId}>
<td>{index + 1}</td>
<td>
<span className="badge badge-xs badge-primary">
{leave.leaveType}
</span>
</td>
<td className="whitespace-nowrap">
<span className="">{formatDate(leave.dateFrom)} </span>
→ {" "}
<span className="">{formatDate(leave.dateTo)}</span>
</td>
<td>
<span
className={`badge badge-xs ${
leave.status === "PENDING"
? "badge-warning"
: leave.status === "APPROVED"
? "badge-success"
: "badge-error"
}`}
>
{leave.status}
</span>
</td>
<td>{leave.totalDays}</td>
<td className="whitespace-nowrap">{formatDateTime(leave.appliedOn)}</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="text-center py-4 text-base-content/50">
No leaves found
</td>
</tr>
)}
{/* {!refreshing && leaves.length > 0 && (
<tr>
<td colSpan={6} className="bg-base-200/50">
<div className="flex justify-center py-2">
{renderSkeletonRows()}
</div>
</td>
</tr>
)} */}
</tbody>
</table>
</div>
{/* Footer */}
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{leaves.length} leave{leaves.length !== 1 ? "s" : ""} found
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh Leaves"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div>
</>
);
};
export default LeaveTable;

View File

@ -0,0 +1,684 @@
import React, { useEffect, useRef, useState } from 'react';
import { DateTime } from 'luxon';
import ReactQuill from 'react-quill-new';
import { quillModules } from '../../../utils/quillConfig';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import { useNavigate } from 'react-router-dom';
import { useToast } from '../../../helper/ToastContext';
interface Project {
projectId: string;
projectName: string;
status?: string | boolean;
}
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;
comment?: 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);
const quillRef = useRef<any>(null);
const navigate = useNavigate()
const { showToast } = useToast();
// 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;
case 'comment':
if (!value.trim()) {
error = 'Description is required';
}
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 (!comment.trim()) {
errors.comment = 'Description is required';
}
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(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
// Override image handler in toolbar
editor.getModule("toolbar").handlers.image = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const base64 = await convertToBase64(file);
const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64);
}
}
};
};
// COMPLETELY disable Quill's default paste behavior
const disableQuillPaste = () => {
// Remove all existing paste event listeners from Quill
const pasteListeners = (editor.root as any).__pasteHandlers || [];
pasteListeners.forEach((handler: any) => {
editor.root.removeEventListener('paste', handler);
});
// Override the clipboard module to do nothing
const clipboard = editor.getModule('clipboard');
const originalPaste = clipboard.paste;
clipboard.paste = (e: any) => {
// Completely prevent Quill's default paste behavior
e.preventDefault();
e.stopPropagation();
return false;
};
};
disableQuillPaste();
// Our custom paste handler - this is the ONLY handler that will run
const handlePaste = async (e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const items = e.clipboardData?.items;
if (!items || items.length === 0) return;
// Check for images first
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
try {
const base64 = await convertToBase64(file);
const range = editor.getSelection(true);
if (range !== null) {
// Insert the image
editor.insertEmbed(range.index, "image", base64);
// Move cursor after the image
editor.setSelection(range.index + 1, 0);
}
} catch (error) {
console.error("Error processing pasted image:", error);
}
}
return; // Exit after handling the first image
}
}
// If no images found, handle text
const text = e.clipboardData?.getData('text/plain');
if (text) {
const range = editor.getSelection(true);
if (range !== null) {
editor.insertText(range.index, text);
editor.setSelection(range.index + text.length, 0);
}
}
};
// Add our custom paste handler with high priority
editor.root.addEventListener("paste", handlePaste, true);
return () => {
editor.root.removeEventListener("paste", handlePaste, true);
};
}, []);
const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
useEffect(() => {
if (selectedProjectId && selectedDate) {
setProjectSummary(null)
setStartTime("");
setEndTime("");
setComment("");
setFormErrors({});
setTotalAdjustment("0h 0m");
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);
validateField('comment', value);
};
const resetForm = () => {
setSelectedProjectId("");
setSelectedDate("");
setStartTime("");
setEndTime("");
setComment("");
setFormErrors({});
setTotalAdjustment("0h 0m");
// Properly reset the Quill editor
if (quillRef.current) {
const editor = quillRef.current.getEditor();
editor.setContents([]); // Clear editor contents completely
}
};
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");
resetForm();
// Show success toast instead of alert
showToast("Timesheet Request submitted successfully!", "success");
// Navigate after a small delay
setTimeout(() => {
navigate('/dashboard/leave-adjustments');
}, 300);
} 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"
}));
showToast(err.message || "Something went wrong while saving timesheet", "error");
} 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} className={`${item.status ? "" : ""}`}>
{item.projectName}
</option>
))}
</select>
{formErrors.selectedProjectId && (
<label className="label">
<span className="label-text-alt text-error text-xs 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 text-xs 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 text-xs 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 text-xs 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 with ReactQuill */}
<div className="form-control flex flex-col gap-1 col-span-2 mb-12">
<label className="label">
<span className="label-text font-semibold text-sm">Description / Reason</span>
</label>
<ReactQuill
ref={quillRef}
theme="snow"
value={comment}
modules={quillModules}
onChange={handleDescription}
className="h-40"
// placeholder="Please enter the task name or leave notes to help us better understand your request."
/>
</div>
{formErrors.comment && (
<label className="label">
<span className="label-text-alt text-error text-xs font-medium">{formErrors.comment}</span>
</label>
)}
{/* 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 ? 'Submitting...' : 'Submit Request'}
</button>
</div>
</div>
</div>
);
};
export default AdjustmentTab;

View File

@ -0,0 +1,298 @@
// PtoLeaveTab.tsx
import React, { useEffect, useRef, useState } from "react";
import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { useNavigate } from "react-router-dom";
import { useToast } from "../../../helper/ToastContext";
interface LeaveFormData {
fromDate: string;
toDate: string;
comment: string;
}
const PtoLeaveTab: React.FC<{
submitLeave: Function;
isSubmitting: boolean;
}> = ({ submitLeave, isSubmitting }) => {
const [ptoLeave, setPtoLeave] = useState<LeaveFormData>({
fromDate: "",
toDate: "",
comment: "",
});
const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null);
const navigate = useNavigate()
const { showToast } = useToast();
console.log("ptoLeave", ptoLeave)
const [errors, setErrors] = useState<Partial<LeaveFormData>>({});
useEffect(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
// Override image handler in toolbar
editor.getModule("toolbar").handlers.image = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const base64 = await convertToBase64(file);
const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64);
}
}
};
};
// COMPLETELY disable Quill's default paste behavior
const disableQuillPaste = () => {
// Remove all existing paste event listeners from Quill
const pasteListeners = (editor.root as any).__pasteHandlers || [];
pasteListeners.forEach((handler: any) => {
editor.root.removeEventListener('paste', handler);
});
// Override the clipboard module to do nothing
const clipboard = editor.getModule('clipboard');
const originalPaste = clipboard.paste;
clipboard.paste = (e: any) => {
// Completely prevent Quill's default paste behavior
e.preventDefault();
e.stopPropagation();
return false;
};
};
disableQuillPaste();
// Our custom paste handler - this is the ONLY handler that will run
const handlePaste = async (e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const items = e.clipboardData?.items;
if (!items || items.length === 0) return;
// Check for images first
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
try {
const base64 = await convertToBase64(file);
const range = editor.getSelection(true);
if (range !== null) {
// Insert the image
editor.insertEmbed(range.index, "image", base64);
// Move cursor after the image
editor.setSelection(range.index + 1, 0);
}
} catch (error) {
console.error("Error processing pasted image:", error);
}
}
return; // Exit after handling the first image
}
}
// If no images found, handle text
const text = e.clipboardData?.getData('text/plain');
if (text) {
const range = editor.getSelection(true);
if (range !== null) {
editor.insertText(range.index, text);
editor.setSelection(range.index + text.length, 0);
}
}
};
// Add our custom paste handler with high priority
editor.root.addEventListener("paste", handlePaste, true);
return () => {
editor.root.removeEventListener("paste", handlePaste, true);
};
}, []);
const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
// ... rest of your component code remains the same
const validate = () => {
const e: Partial<LeaveFormData> = {};
const today = new Date();
today.setHours(0, 0, 0, 0);
const from = ptoLeave.fromDate ? new Date(ptoLeave.fromDate) : null;
const to = ptoLeave.toDate ? new Date(ptoLeave.toDate) : null;
if (!from) {
e.fromDate = "From date is required";
} else if (from < today) {
e.fromDate = "From date cannot be in the past";
}
if (!to) {
e.toDate = "To date is required";
} else if (to < today) {
e.toDate = "To date cannot be in the past";
}
if (from && to && to < from) {
e.toDate = "To date cannot be before From date";
}
if (!ptoLeave.comment.trim()) {
e.comment = "Comment required";
}
return e;
};
const resetForm = () => {
const emptyForm: LeaveFormData = {
fromDate: "",
toDate: "",
comment: ""
};
setPtoLeave(emptyForm);
setErrors({});
if (quillRef.current) {
const editor = quillRef.current.getEditor();
editor.setContents([]);
}
};
const handleChange = (field: keyof LeaveFormData, value: string) => {
setPtoLeave(prev => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
const handleSubmit = async () => {
setServerError("")
const e = validate();
if (Object.keys(e).length) return setErrors(e);
const result = await submitLeave({
dateFrom: ptoLeave.fromDate,
dateTo: ptoLeave.toDate,
leaveType: "PTO",
comment: ptoLeave.comment,
});
if (result.success) {
resetForm();
// Show success toast instead of alert
showToast("PTO Request submitted successfully!", "success");
// Navigate after a small delay
setTimeout(() => {
navigate('/dashboard/leave-adjustments');
}, 300);
} else {
setServerError(result.message || "Something went wrong");
showToast(result.message || "Failed to submit PTO request", "error");
}
};
return (
<>
<div className="max-w-4xl 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"
disabled={isSubmitting}
value={ptoLeave.fromDate}
min={new Date().toISOString().split("T")[0]}
onChange={(e) => handleChange('fromDate', e.target.value)}
className={`input input-bordered input-sm ${errors.fromDate ? "input-error" : ""
}`}
/>
{errors.fromDate && <p className="text-error text-xs">{errors.fromDate}</p>}
</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"
disabled={isSubmitting}
value={ptoLeave.toDate}
min={new Date().toISOString().split("T")[0]}
onChange={(e) => handleChange('toDate', e.target.value)}
className={`input input-bordered input-sm ${errors.toDate ? "input-error" : ""
}`}
/>
{errors.toDate && <p className="text-error text-xs">{errors.toDate}</p>}
</div>
{/* Comment */}
<div className="form-control mb-10 col-span-2">
<label className="label">
<span className="label-text font-semibold text-sm">Description</span>
</label>
<ReactQuill
ref={quillRef}
theme="snow"
value={ptoLeave.comment}
modules={quillModules}
onChange={(val) => handleChange('comment', val)}
className="h-60"
/>
</div>
{errors.comment && <p className="text-error text-xs">{errors.comment}</p>}
</div>
{serverError && (
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
)}
<button
onClick={handleSubmit}
disabled={isSubmitting}
className="btn btn-primary flex-1 w-full"
>
{isSubmitting ? (
<span className="loading loading-spinner loading-sm"></span>
) : (
"Submit PTO Request"
)}
</button>
</div>
</>
);
};
export default PtoLeaveTab;

View File

@ -0,0 +1,324 @@
import React, { useEffect, useRef, useState } from 'react';
import { quillModules } from "../../../utils/quillConfig";
import ReactQuill from "react-quill-new";
import Quill from "quill";
import "quill/dist/quill.snow.css"; // correct CSS
import { useNavigate } from 'react-router-dom';
import { useToast } from '../../../helper/ToastContext';
interface LeaveFormData {
fromDate: string;
toDate: string;
comment: string;
}
const SickLeaveTab: React.FC<{
submitLeave: Function;
isSubmitting: boolean;
}> = ({ submitLeave, isSubmitting }) => {
const [sickLeave, setSickLeave] = useState<LeaveFormData>({
fromDate: '',
toDate: '',
comment: ''
});
const [serverError, setServerError] = useState("");
const quillRef = useRef<any>(null);
const [sickErrors, setSickErrors] = useState<Partial<LeaveFormData>>({});
const navigate = useNavigate()
const { showToast } = useToast();
useEffect(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
// Override image handler in toolbar
editor.getModule("toolbar").handlers.image = () => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
const file = input.files?.[0];
if (file) {
const base64 = await convertToBase64(file);
const range = editor.getSelection();
if (range) {
editor.insertEmbed(range.index, "image", base64);
}
}
};
};
// COMPLETELY disable Quill's default paste behavior
const disableQuillPaste = () => {
// Remove all existing paste event listeners from Quill
const pasteListeners = (editor.root as any).__pasteHandlers || [];
pasteListeners.forEach((handler: any) => {
editor.root.removeEventListener('paste', handler);
});
// Override the clipboard module to do nothing
const clipboard = editor.getModule('clipboard');
const originalPaste = clipboard.paste;
clipboard.paste = (e: any) => {
// Completely prevent Quill's default paste behavior
e.preventDefault();
e.stopPropagation();
return false;
};
};
disableQuillPaste();
// Our custom paste handler - this is the ONLY handler that will run
const handlePaste = async (e: ClipboardEvent) => {
e.preventDefault();
e.stopPropagation();
const items = e.clipboardData?.items;
if (!items || items.length === 0) return;
// Check for images first
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
try {
const base64 = await convertToBase64(file);
const range = editor.getSelection(true);
if (range !== null) {
// Insert the image
editor.insertEmbed(range.index, "image", base64);
// Move cursor after the image
editor.setSelection(range.index + 1, 0);
}
} catch (error) {
console.error("Error processing pasted image:", error);
}
}
return; // Exit after handling the first image
}
}
// If no images found, handle text
const text = e.clipboardData?.getData('text/plain');
if (text) {
const range = editor.getSelection(true);
if (range !== null) {
editor.insertText(range.index, text);
editor.setSelection(range.index + text.length, 0);
}
}
};
// Add our custom paste handler with high priority
editor.root.addEventListener("paste", handlePaste, true);
return () => {
editor.root.removeEventListener("paste", handlePaste, true);
};
}, []);
const convertToBase64 = (file: File): Promise<string> =>
new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
useEffect(() => {
const editor = quillRef.current?.getEditor();
if (!editor) return;
editor.getModule("clipboard").addMatcher(Node.ELEMENT_NODE, (node: any, delta: any) => {
return delta;
});
editor.root.addEventListener("paste", async (e: any) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.indexOf("image") === 0) {
const file = item.getAsFile();
const base64 = await convertToBase64(file);
const range = editor.getSelection();
editor.insertEmbed(range.index, "image", base64);
}
}
});
}, []);
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 resetForm = () => {
const emptyForm: LeaveFormData = {
fromDate: "",
toDate: "",
comment: ""
};
setSickLeave(emptyForm);
setSickErrors({});
// Properly reset the Quill editor
if (quillRef.current) {
const editor = quillRef.current.getEditor();
editor.setContents([]); // Clear editor contents completely
}
};
const handleSubmit = async () => {
setServerError("")
const errors = validateLeaveForm(sickLeave);
if (Object.keys(errors).length > 0) {
setSickErrors(errors);
return;
}
const result = await submitLeave({
dateFrom: sickLeave.fromDate,
dateTo: sickLeave.toDate,
leaveType: "SICK",
comment: sickLeave.comment,
});
if (result.success) {
resetForm();
// Show success toast instead of alert
showToast("SICK Request submitted successfully!", "success");
// Navigate after a small delay
setTimeout(() => {
navigate('/dashboard/leave-adjustments');
}, 300);
} else {
setServerError(result.message || "Something went wrong");
showToast(result.message || "Failed to submit SICK request", "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}
min={new Date().toISOString().split("T")[0]}
disabled={isSubmitting}
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}
min={new Date().toISOString().split("T")[0]}
disabled={isSubmitting}
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 mb-10">
<label className="label">
<span className="label-text font-semibold text-sm">Description</span>
</label>
<ReactQuill
ref={quillRef}
theme="snow"
value={sickLeave.comment}
modules={quillModules}
onChange={(val) => handleSickChange('comment', val)}
className="h-60"
/>
</div>
{sickErrors.comment && (
<label className="label">
<span className="label-text-alt text-xs text-error font-medium">{sickErrors.comment}</span>
</label>
)}
</div>
{serverError && (
<p className="text-red-500 font-medium text-center text-xs">{serverError}</p>
)}
{/* Submit Button */}
<button
onClick={handleSubmit}
disabled={isSubmitting}
className="btn btn-primary flex-1 w-full"
>
{isSubmitting ? (
<span className="loading loading-spinner loading-sm"></span>
) : (
"Submit Sick Request"
)}
</button>
</div>
</div>
</>
);
};
export default SickLeaveTab;

View File

@ -1,44 +1,87 @@
import React, { useState, useEffect, useRef, FormEvent, ChangeEvent } from 'react'
import { useDispatch } from 'react-redux';
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 { useTheme } from '../helper/ThemeToggle';
import { Eye, EyeOff, KeyRound, Mail } from 'lucide-react';
interface User {
email: string
password: string
}
const Login = () => {
const { theme, setTheme } = useTheme();
interface LoginProps {
setUser: (user: User) => void
}
const Login: React.FC<LoginProps> = ({ setUser }) => {
const [formData, setFormData] = useState<User>({ email: '', password: '' })
const inputRef = useRef<HTMLFormElement | null>(null)
const navigate = useNavigate()
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState("")
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isForgotMode, setIsForgotMode] = useState(false);
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value })
}
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
const handleSubmit = async (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
const logo = theme === "dark" ? DocVaLogoDark : DocVaLogoLight;
const handleLogin = async (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
e.preventDefault()
const { email, password } = formData
if (email === "" || password === "") return
setLoading(true);
try {
const response = await window.electron.login({ email, password });
console.log(response)
if (response.success) {
localStorage.setItem("user", JSON.stringify(formData));
setUser(response.user);
// setUser(response.user);
dispatch(setUserSlice({
id: response.user.id,
name: response.user.name,
email: response.user.email,
}));
navigate("/")
}
else if (response.success == false) {
console.log(response.error.message)
setErrorMessage(response.error.message)
}
} catch (error) {
console.log(error)
} finally {
setLoading(false);
}
}
const handleForgotPassword = async (e: FormEvent<HTMLFormElement> | KeyboardEvent) => {
e.preventDefault();
setLoading(true);
try {
const res = await window.electron.forgotPassword(email);
console.log("Forgot password response:", res);
alert("Password reset link sent to your email");
setIsForgotMode(false);
} catch (error) {
console.error("Forgot password error:", error);
} finally {
setLoading(false);
}
};
// Detect Enter key press
useEffect(() => {
const handleEnter = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
handleSubmit(e)
if (!isForgotMode) {
handleLogin(e)
} else {
handleForgotPassword(e)
}
}
}
@ -46,28 +89,18 @@ const Login: React.FC<LoginProps> = ({ setUser }) => {
inputElement?.addEventListener('keydown', handleEnter)
return () => inputElement?.removeEventListener('keydown', handleEnter)
}, [formData])
}, [email, password])
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-base-200 to-secondary/10 p-4">
<>
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-primary/10 via-base-200 to-secondary/10 p-4">
<div className="card w-full max-w-md shadow-2xl bg-base-100 backdrop-blur-sm">
<div className="card-body">
<div className="flex justify-center items-center mb-4">
<div className="avatar placeholder">
<div className="bg-primary text-primary-content rounded-full w-16 h-16 flex items-center justify-center">
<svg
fill="none"
viewBox="0 0 24 24"
className="w-8 h-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
{/* <div className="bg-primary text-primary-content rounded-full w-20 h-20 flex items-center justify-center"> */}
<img src={logo} className='w-35 h-23' />
{/* </div> */}
</div>
</div>
@ -77,70 +110,100 @@ const Login: React.FC<LoginProps> = ({ setUser }) => {
<form
ref={inputRef}
className="flex flex-col items-center justify-center w-full"
onSubmit={handleSubmit}
onSubmit={isForgotMode ? handleForgotPassword : handleLogin}
>
<div className="form-control w-90 px-5">
<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"
placeholder="you@example.com"
className="grow"
value={formData.email}
onChange={handleChange}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>
</div>
<div className="form-control w-90 px-5 mt-4">
{!isForgotMode && (
<div className="form-control w-90 px-5 mt-3">
<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"
type={showPassword ? "text" : "password"}
name="password"
placeholder="Enter your password"
className="grow"
value={formData.password}
onChange={handleChange}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{/* Toggle Icon */}
<button
type="button"
className="cursor-pointer"
onClick={() => setShowPassword((prev) => !prev)}
>
{showPassword ? (
<EyeOff size={18} className="opacity-70" />
) : (
<Eye size={18} className="opacity-70" />
)}
</button>
</label>
</div>
)}
<div className="form-control w-full px-5 mt-6">
<button type="submit" className="btn btn-primary w-full">
Sign In
<div className="form-control w-90 px-5 mt-4">
<button
type="submit"
disabled={loading}
className="btn btn-primary w-full"
>
{loading
? "Please wait..."
: isForgotMode
? "Send Reset Link"
: "Login"}
</button>
</div>
<div className="text-center mt-4">
{isForgotMode ? (
<button
type="button"
onClick={() => setIsForgotMode(false)}
className="link link-primary text-sm"
>
Back to Login
</button>
) : (
<button
type="button"
onClick={() => setIsForgotMode(true)}
className="link link-primary text-sm"
>
Forgot Password?
</button>
)}
</div>
{errorMessage && <span className='text-red-500 font-semibold mt-2'>{errorMessage}...!</span>}
</form>
</div>
</div>
</div>
</>
)
}

65
src/ui/pages/NoData.tsx Normal file
View File

@ -0,0 +1,65 @@
import React from "react";
const NoData = ({ message = "No data available", subText = "Please add new items or check back later." }) => {
return (
<div className="flex flex-col items-center justify-center min-h-[67vh] p-6 text-center">
{/* SVG Illustration */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 600 360"
className="w-64 h-40 mb-6 text-primary"
>
<rect
x="38"
y="56"
width="524"
height="208"
rx="14"
fill="currentColor"
opacity="0.05"
/>
<g transform="translate(125, 68)">
<rect
x="0"
y="24"
width="298"
height="150"
rx="8"
fill="white"
stroke="currentColor"
strokeWidth="2"
opacity="0.15"
/>
<g transform="translate(18, 43)" fill="currentColor" opacity="0.1">
<rect x="0" y="0" width="160" height="12" rx="6" />
<rect x="0" y="24" width="110" height="12" rx="6" />
<rect x="0" y="48" width="140" height="12" rx="6" />
</g>
<g
transform="translate(160, 72) scale(1.05)"
fill="none"
stroke="currentColor"
strokeWidth="10"
strokeLinecap="round"
strokeLinejoin="round"
className="animate-pulse"
>
<circle cx="24" cy="24" r="18" fill="none" />
<path d="M39 39 L58 58" />
</g>
</g>
</svg>
{/* Text section */}
<div className="card bg-base-100 w-full max-w-md">
<div className="card-body items-center">
<h2 className="card-title text-lg font-semibold text-base-content">
{message}
</h2>
</div>
</div>
</div>
);
};
export default NoData;

View File

@ -0,0 +1,135 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
const PaysSlips = () => {
const { projectSummary } =
useSelector((state: any) => state.projects);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [rows, setRows] = useState<any[]>([]);
useEffect(() => {
if (!projectSummary?.vaId) return;
fetchPayslips(projectSummary?.vaId);
}, [projectSummary?.vaId]);
const fetchPayslips = async (vaId: string) => {
setLoading(true);
setError(null);
try {
console.log("getPayslips" , vaId)
const result = await window.electron.getPayslips(vaId);
if (!result.success) {
setError(result.message || "Failed to load payslips");
return;
}
const response = result.data;
if (!response || response.length === 0) {
setRows([]);
return;
}
console.log(response , "getPayslips")
// Extract payrolls array from first index
const payrolls = response[0]?.payrolls || [];
setRows(payrolls);
} catch (err: any) {
setError("An unexpected error occurred");
} finally {
setLoading(false);
}
};
return (
<>
<div className="flex flex-col gap-2 max-h-screen">
<div className="flex items-center justify-between">
<h2 className="text-md font-bold">Payslips</h2>
</div>
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[80vh] min-h-[83vh] no-scrollbar">
{loading && (
<div className="flex justify-center py-10">
<span className="loading loading-spinner loading-md"></span>
</div>
)}
{error && (
<div className="text-center text-red-500 py-5">{error}</div>
)}
{!loading && !error && rows.length === 0 && (
<div className="text-center py-10 text-gray-500">
No payslip data available.
</div>
)}
{!loading && !error && rows.length > 0 && (
<div className="overflow-x-auto w-full no-scrollbar">
<table className="table table-zebra w-full text-[12px] min-w-[650px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Status</th>
<th>Date</th>
<th>Total Hours</th>
<th>Total Pay</th>
{/* <th>Action</th> */}
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={row.id}>
<td>{index + 1}</td>
<td>
<span
className={`badge ${
row.status === "Paid"
? "badge-success"
: "badge-warning"
}`}
>
{row.status}
</span>
</td>
<td>{row.payrollSchedule}</td>
<td>{row.totalHoursFormatted}</td>
<td>${row.totalPayAmount.toFixed(2)}</td>
{/* <td>
<button
className="btn btn-xs btn-primary"
>
View
</button>
</td> */}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</>
);
};
export default PaysSlips;

View File

@ -0,0 +1,214 @@
import React, { useEffect, useState } from "react";
import { useOutletContext } from "react-router-dom";
import { TrackingState } from "../../layout/DashboardLayout";
import { RefreshCw, Loader2 } from "lucide-react";
import { DateTime } from "luxon";
interface ScheduleItem {
id: string;
isActive: boolean;
status: string | null;
ScheduleDate: string;
cutOffTime: string | null;
periodEnd: string | null;
periodStart: string | null;
}
const Schedule = () => {
const { isTracking } = useOutletContext<TrackingState>();
const [rows, setRows] = useState<ScheduleItem[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
// const [lastUpdated, setLastUpdated] = useState<DateTime | null>(null);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
/** Format: Only Date */
const formatOnlyDate = (input: string | null) => {
if (!input) return "-";
return DateTime.fromISO(input).toLocaleString(DateTime.DATE_MED);
};
/** Format: Date + Time */
const formatDateTime = (input: string | null) => {
if (!input) return "-";
return DateTime.fromISO(input).toLocaleString(DateTime.DATETIME_MED);
};
/** Fetch Data */
const fetchSchedules = async () => {
setRefreshing(true);
setLoading(true);
try {
const res = await window.electron.getSchedules();
if (!res.success) {
setError(res.message || "Failed to load schedules");
return;
}
setRows(res.result || []);
setLastRefreshed(new Date());
} catch (err: any) {
setError(err.message || "Unexpected error");
} finally {
setLoading(false);
setRefreshing(false);
}
};
/** Refresh Button */
const handleRefresh = async () => {
await fetchSchedules();
};
useEffect(() => {
fetchSchedules();
}, []);
/** Determine Active Period */
const isActiveRow = (row: ScheduleItem) => {
if (!row.periodStart || !row.periodEnd) return false;
const now = DateTime.local();
const start = DateTime.fromISO(row.periodStart).startOf("day");
const end = DateTime.fromISO(row.periodEnd).endOf("day");
return now >= start && now <= end;
};
const activeSchedule = rows.find((r) => isActiveRow(r));
/** Skeleton Rows */
const renderSkeletonRows = () =>
Array.from({ length: 5 }).map((_, idx) => (
<tr key={`sk-${idx}`}>
<td><div className="skeleton h-4 w-6"></div></td>
<td><div className="skeleton h-4 w-32"></div></td>
<td><div className="skeleton h-4 w-28"></div></td>
<td><div className="skeleton h-4 w-20"></div></td>
</tr>
));
const formatLastRefreshed = () => {
if (!lastRefreshed) return "Never";
return DateTime.fromJSDate(lastRefreshed)
.toLocal()
.toFormat("HH:mm:ss"); // Only time
};
return (
<main
className="flex flex-col gap-2 overflow-hidden"
style={{
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
}}
>
{/* HEADER */}
<div className="flex justify-between items-center">
<h2 className="text-md font-bold">Schedules</h2>
<div className="flex items-center gap-3">
{activeSchedule && (
<div className="px-2 py-1 bg-primary text-primary-content rounded-lg text-xs">
Pay Date: <b>{formatOnlyDate(activeSchedule.ScheduleDate)}</b>
</div>
)}
</div>
</div>
{/* TABLE WRAPPER */}
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md no-scrollbar"
style={{
height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
{/* Loading */}
{loading ? (
<table className="table w-full text-[12px]">
<tbody>{renderSkeletonRows()}</tbody>
</table>
) : error ? (
<div className="text-center text-red-500 py-8">{error}</div>
) : rows.length === 0 ? (
<div className="text-center py-8 text-base-content/50">
No schedules found
</div>
) : (
<div className="overflow-x-auto no-scrollbar">
<table className="table w-full text-[12px] min-w-[600px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Period</th>
<th>Cutoff Time</th>
<th>Pay Date</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr
key={row.id}
className={
isActiveRow(row)
? "bg-primary text-primary-content font-semibold"
: ""
}
>
<td>{index + 1}</td>
<td>
{formatOnlyDate(row.periodStart)} →{" "}
{formatOnlyDate(row.periodEnd)}
</td>
<td>{formatDateTime(row.cutOffTime)}</td>
<td>{formatOnlyDate(row.ScheduleDate)}</td>
</tr>
))}
{refreshing && (
<tr>
<td colSpan={4} className="bg-base-200/40 text-center py-2">
<Loader2 className="animate-spin inline mr-2" size={16} />
Refreshing...
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
{/* FOOTER */}
<div className="flex justify-between items-center text-xs text-base-content/60 mt-2">
<div>{rows.length} schedules found</div>
<div className="">
{lastRefreshed &&
`Last updated: ${formatLastRefreshed()}`}
<button
onClick={handleRefresh}
disabled={refreshing}
className="ms-2 btn btn-xs btn-circle bg-warning"
title="Refresh schedule"
>
{refreshing ? (
<Loader2 size={14} className="animate-spin text-base-content" />
) : (
<RefreshCw size={14} className="text-base-content" />
)}
</button>
</div>
</div>
</main>
);
};
export default Schedule;

View File

@ -0,0 +1,166 @@
// src/ui/pages/Settings/ChangePasswordModal.tsx
import React, { useState } from "react";
import { Eye, EyeOff } from "lucide-react";
import { useToast } from "../../helper/ToastContext";
interface ChangePasswordModalProps {
isOpen: boolean;
onClose: () => void;
}
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({
isOpen,
onClose,
}) => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showOld, setShowOld] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const { showToast } = useToast();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
showToast("New passwords do not match!" , "error");
return;
}
setLoading(true);
try {
const payload = {
oldPassword,
password: newPassword,
cPassword: confirmPassword,
};
// 🔗 Call the Electron bridge function
const response = await window.electron.changePassword(payload);
if (response.success) {
showToast("Password changed successfully!" , "success");
setOldPassword("")
setNewPassword("")
setConfirmPassword("")
onClose();
} else {
showToast((response.message || "Failed to change password.") , "error");
}
} catch (error) {
console.error("Password change error:", error);
showToast("Error changing password." , "error");
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<dialog open className="modal modal-open">
<div className="modal-box w-96">
<h3 className="font-bold text-lg mb-3">Change Password</h3>
<form onSubmit={handleSubmit} className="space-y-3">
{/* Old Password */}
<div>
<label className="label text-xs font-medium">Current Password</label>
<div className="relative">
<input
type={showOld ? "text" : "password"}
className="input input-bordered w-full pr-10"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
required
/>
<button
type="button"
className="absolute right-2 top-3 text-gray-500"
onClick={() => setShowOld(!showOld)}
>
{showOld ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
{/* New Password */}
<div>
<label className="label text-xs font-medium">New Password</label>
<div className="relative">
<input
type={showNew ? "text" : "password"}
className="input input-bordered w-full pr-10"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
<button
type="button"
className="absolute right-2 top-3 text-gray-500"
onClick={() => setShowNew(!showNew)}
>
{showNew ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
{/* Confirm Password */}
<div>
<label className="label text-xs font-medium">Confirm New Password</label>
<div className="relative">
<input
type={showConfirm ? "text" : "password"}
className="input input-bordered w-full pr-10"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
<button
type="button"
className="absolute right-2 top-3 text-gray-500"
onClick={() => setShowConfirm(!showConfirm)}
>
{showConfirm ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<div className="modal-action">
<button
type="button"
className="btn btn-sm btn-ghost"
onClick={onClose}
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className={`btn btn-sm btn-primary ${loading ? "loading" : ""}`}
>
{loading ? "Updating..." : "Update"}
</button>
</div>
</form>
</div>
<form
method="dialog"
className="modal-backdrop"
onClick={!loading ? onClose : undefined}
>
<button disabled={loading}>close</button>
</form>
</dialog>
);
};
export default ChangePasswordModal;

View File

@ -0,0 +1,215 @@
import React, { useState, useEffect } from "react";
import { CreditCard } from "lucide-react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../../store";
import { setProfile } from "../../../store/profileSlice";
interface ChangePaymentModalProps {
isOpen: boolean;
onClose: () => void;
}
const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
isOpen,
onClose,
}) => {
const dispatch = useDispatch();
const { profile } = useSelector((state: RootState) => state.profile);
const [method, setMethod] = useState(profile?.paymentMethod || "");
const [wiseEmail, setWiseEmail] = useState(profile?.wiseEmail || "");
const [veemAccount, setVeemAccount] = useState(profile?.veemAccount || "");
const [payoneerAccount, setPayoneerAccount] = useState(profile?.payoneerAccount || "");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (isOpen) {
setMethod(profile?.paymentMethod || "");
setWiseEmail(profile?.wiseEmail || "");
setVeemAccount(profile?.veemAccount || "");
setPayoneerAccount(profile?.payoneerAccount || "")
setError("");
}
}, [isOpen]);
if (!isOpen) return null;
const handleSubmit = async () => {
setError("");
// Validation
if (!method) {
return setError("Please select a payment method.");
}
if (method === "wise" && !wiseEmail.trim()) {
return setError("Wise email is required.");
}
if (method === "veem" && !veemAccount.trim()) {
return setError("Veem email is required.");
}
if (method === "payoneeraccount" && !payoneerAccount.trim()) {
return setError("Payoneer email is required.");
}
if ((method === "wise" || "veem" || "payoneeraccount") && !/^\S+@\S+\.\S+$/.test(wiseEmail)) {
return setError("Enter a valid email.");
}
setLoading(true);
try {
// Prepare data for API
const updateData: any = {
paymentMethod: method
};
// Add appropriate field based on selection
if (method === "wise") {
updateData.wiseEmail = wiseEmail.trim();
} else if (method === "veem") {
updateData.veemAccount = veemAccount.trim();
}else if (method === "payoneeraccount") {
updateData.payoneerAccount = payoneerAccount.trim();
}
console.log("Sending to electron:", updateData);
// Call Electron API
const result = await window.electron.updatePayment(updateData);
console.log(result)
if (result.success) {
// Prepare data for Redux update (clear unused field)
const reduxUpdateData: any = {
paymentMethod: method
};
if (method === "wise") {
reduxUpdateData.wiseEmail = wiseEmail.trim();
reduxUpdateData.veemAccount = ""; // Clear veem
} else if (method === "veem") {
reduxUpdateData.veemAccount = veemAccount.trim();
reduxUpdateData.wiseEmail = ""; // Clear wise
}
// Update Redux store
dispatch(setProfile(result.data));
// Show success message if needed
// toast.success("Payment method updated successfully");
onClose();
} else {
setError(result.message || "Failed to update payment info.");
}
} catch (err: any) {
setError(err.message || "Failed to update payment info. Try again.");
} finally {
setLoading(false);
}
};
return (
<dialog open className="modal modal-open">
<div className="modal-box w-96">
<h3 className="font-bold text-lg mb-4 flex items-center gap-2">
<CreditCard size={18} /> Change Payment Method
</h3>
{/* Error */}
{error && (
<div className="alert alert-error py-2 text-xs mb-3">{error}</div>
)}
{/* Dropdown */}
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Payment Method</span>
</label>
<select
className="select select-bordered select-sm"
value={method}
onChange={(e) => setMethod(e.target.value)}
>
<option value="">Select method</option>
<option value="wise">Wise</option>
<option value="veem">Veem</option>
<option value="payoneeraccount">Payoneer account</option>
</select>
</div>
{/* Wise Email */}
{method === "wise" && (
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Wise Email</span>
</label>
<input
type="email"
className="input input-bordered input-sm"
placeholder="Enter Wise email"
value={wiseEmail}
onChange={(e) => setWiseEmail(e.target.value)}
/>
</div>
)}
{/* Veem Account */}
{method === "veem" && (
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Veem Account Number</span>
</label>
<input
type="text"
className="input input-bordered input-sm"
placeholder="Enter Veem account"
value={veemAccount}
onChange={(e) => setVeemAccount(e.target.value)}
/>
</div>
)}
{method === "payoneeraccount" && (
<div className="form-control mb-4">
<label className="label">
<span className="text-sm font-medium">Payoneer Account Number</span>
</label>
<input
type="text"
className="input input-bordered input-sm"
placeholder="Enter Payoneer account"
value={payoneerAccount}
onChange={(e) => setPayoneerAccount(e.target.value)}
/>
</div>
)}
<div className="modal-action">
<button className="btn btn-sm btn-ghost" onClick={onClose}>
Cancel
</button>
<button
className={`btn btn-sm btn-primary ${loading ? "loading" : ""}`}
onClick={handleSubmit}
disabled={loading}
>
{loading ? "Updating..." : "Update"}
</button>
</div>
</div>
</dialog>
);
};
export default ChangePaymentModal;

View File

@ -1,107 +1,161 @@
import React from "react";
import { User, Mail, Lock, Edit2, Shield, CalendarDays } from "lucide-react";
interface UserProfile {
name: string;
email: string;
role: string;
employeeId: string;
joinDate: string;
department: string;
profileImage?: string;
}
const userData: UserProfile = {
name: "Jai Pandey",
email: "jai.pandey@vatechcorp.com",
role: "Frontend Developer",
employeeId: "EMP-1023",
joinDate: "2022-07-15",
department: "Engineering",
profileImage: "https://ui-avatars.com/api/?name=Jai+Pandey&background=random",
};
import React, { useState } from "react";
import { User, Mail, Phone, Calendar, MapPin, CreditCard, Globe, Lock, SquarePen } from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
import ChangePasswordModal from "./ChangePasswordModal";
import ChangePaymentModal from "./SettingModal/ChangePaymentModal";
const Settings: React.FC = () => {
const { profile } = useSelector((state: RootState) => state.profile);
const [modalOpen, setModalOpen] = useState(false);
const [showPaymentModal,setShowPaymentModal] = useState(false)
const fullName = `${profile?.firstName || ""} ${profile?.lastName || ""}`.trim();
const maskLast5 = (value: string) => {
if (!value) return "N/A";
const str = value.toString();
const last5 = str.slice(-5);
const masked = "X".repeat(str.length - 5) + last5;
return masked;
};
return (
<div className="flex flex-col gap-2 max-h-screen">
<div className="flex flex-col gap-3">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-md font-bold flex items-center gap-2">
Profile Settings
<User size={18} /> Profile Settings
</h2>
<button className="btn btn-sm btn-primary gap-1">
<Edit2 size={16} /> Edit Profile
<button
onClick={() => setModalOpen(true)}
className="btn btn-sm btn-outline btn-primary flex items-center gap-2"
>
<Lock size={14} /> Change Password
</button>
</div>
{/* Profile Section */}
<div className="card bg-base-100 border border-base-300 shadow-sm p-4 flex flex-col md:flex-row gap-4">
{/* Profile Picture */}
<div className="flex flex-col items-center md:w-1/3">
<div className="avatar">
<div className="w-28 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
<User size={60} />
</div>
</div>
<h3 className="mt-3 text-lg font-semibold">{userData.name}</h3>
<p className="text-sm text-base-content/70">{userData.role}</p>
</div>
<div className="divider md:divider-horizontal" />
{/* Details Section */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1">
<div className="p-2 rounded-md bg-base-200/50">
<p className="text-xs text-base-content/70 flex items-center gap-2">
<Mail size={14} /> Email
</p>
<p className="text-sm font-medium">{userData.email}</p>
</div>
<div className="p-2 rounded-md bg-base-200/50">
<p className="text-xs text-base-content/70 flex items-center gap-2">
<Shield size={14} /> Role
</p>
<p className="text-sm font-medium">{userData.role}</p>
</div>
<div className="p-2 rounded-md bg-base-200/50">
<p className="text-xs text-base-content/70 flex items-center gap-2">
<Lock size={14} /> Employee ID
</p>
<p className="text-sm font-medium">{userData.employeeId}</p>
</div>
<div className="p-2 rounded-md bg-base-200/50">
<p className="text-xs text-base-content/70 flex items-center gap-2">
<CalendarDays size={14} /> Joining Date
</p>
<p className="text-sm font-medium">{userData.joinDate}</p>
</div>
<div className="p-2 rounded-md bg-base-200/50 sm:col-span-2">
<p className="text-xs text-base-content/70 flex items-center gap-2">
<User size={14} /> Department
</p>
<p className="text-sm font-medium">{userData.department}</p>
</div>
</div>
</div>
{/* Account Settings */}
{/* Profile Card */}
<div className="card bg-base-100 border border-base-300 shadow-sm p-4">
<h3 className="text-md font-semibold mb-3">Account Security</h3>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-t border-base-300 pt-3">
<p className="text-sm flex items-center gap-2">
<Lock size={16} /> Password last changed on{" "}
<span className="font-medium text-base-content">Aug 12, 2024</span>
<div className="flex flex-col md:flex-row gap-4">
{/* Compact Avatar Section */}
<div className="flex flex-col items-center md:items-start md:w-48 shrink-0">
<div className="avatar">
<div className="w-20 rounded-full ring ring-primary ring-offset-base-100 ring-offset-1 flex justify-center items-center bg-base-200">
<User size={40} className="text-base-content/50" />
</div>
</div>
<div className="mt-2 text-center md:text-left">
<h3 className="text-sm font-semibold">{fullName || "N/A"}</h3>
<p className="text-xs text-base-content/60 flex items-center gap-1 justify-center md:justify-start mt-0.5">
<Mail size={10} /> {profile?.email || "No Email"}
</p>
<button className="btn btn-sm btn-outline btn-primary">
Change Password
</button>
</div>
</div>
<div className="divider md:divider-horizontal my-0 md:mx-2" />
{/* Compact Details Grid */}
<div className="flex-1 grid grid-cols-2 sm:grid-cols-3 gap-2.5">
{/* Basic Info */}
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
<Calendar size={11} /> DOB
</p>
<p className="text-xs font-medium">{profile?.dob || "N/A"}</p>
</div>
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
🎂 Age
</p>
<p className="text-xs font-medium">{profile?.age || "N/A"}</p>
</div>
{/* Contact Info */}
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
<Phone size={11} /> Phone
</p>
<p className="text-xs font-medium truncate" title={profile?.phone || "N/A"}>
{profile?.phone || "N/A"}
</p>
</div>
{/* Payment Info */}
<div
className=" p-2 rounded bg-base-200/40 cursor-pointer hover:text-primary transition-all duration-200 ease-in-out hover:scale-[1.02] active:scale-[0.98] " onClick={() => setShowPaymentModal(true)}>
<div className="flex justify-between items-center">
<div>
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
<CreditCard size={11} /> Payment
</p>
<p className="text-xs font-medium capitalize truncate">
{profile?.paymentMethod || "N/A"}
</p>
</div>
<span>
<SquarePen size={15} />
</span>
</div>
</div>
{profile?.veemAccount && (
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
🏦 Veem Email
</p>
<p className="text-xs font-medium truncate" title={profile?.veemAccount || "N/A"}>
{profile?.veemAccount || "N/A"}
</p>
</div>
)}
{profile?.wiseEmail&& (
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5 ">
🏦 Wise Email
</p>
<p className="text-xs font-medium overflow-auto no-scrollbar" title={profile?.wiseEmail || "N/A"}>
{profile?.wiseEmail || "N/A"}
</p>
</div>
)}
{profile?.payoneerAccount && (
<div className="p-2 rounded bg-base-200/40">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5 ">
🏦 Payoneer Account
</p>
<p className="text-xs font-medium overflow-auto no-scrollbar" title={profile?.payoneerAccount || "N/A"}>
{profile?.payoneerAccount || "N/A"}
</p>
</div>
)}
{/* Location - Full Width */}
<div className="p-2 rounded bg-base-200/40 col-span-2 sm:col-span-3">
<p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
<MapPin size={11} /> Address
</p>
<p className="text-xs font-medium line-clamp-2">
{[profile?.address, profile?.city, profile?.state, profile?.country]
.filter(Boolean)
.join(", ") || "N/A"}
</p>
</div>
</div>
</div>
</div>
{/* Modal */}
<ChangePasswordModal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
<ChangePaymentModal isOpen={showPaymentModal} onClose={() => setShowPaymentModal(false)} />
</div>
);
};

View File

@ -0,0 +1,219 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DateTime } from "luxon";
import React, { useEffect, useState } from "react";
interface TabActivity {
title: string;
appName: string;
from: string;
to: string;
durationMs: number;
durationFormatted: string;
}
interface ProjectSession {
from: string;
to: string;
totalSessionTime: string;
tabs: TabActivity[];
}
interface Props {
isOpen: boolean;
onClose: () => void;
projectName: string;
logs: ProjectSession[];
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setLogs: React.Dispatch<React.SetStateAction<ProjectSession[]>>;
projectID: string | null;
}
const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs, loading, setLoading,
setLogs,
projectID, }) => {
if (!isOpen) return null;
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const isFirstRender = React.useRef(true);
useEffect(() => {
// ⛔ skip first render (parent already fetched)
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
// ✅ fetch only when date changes
fetchLogs(selectedDate);
}, [selectedDate]);
useEffect(() => {
if (!isOpen) {
isFirstRender.current = true;
}
}, [isOpen]);
const getTimeDifference = (from: string, to: string) => {
const start = DateTime.fromISO(from);
const end = DateTime.fromISO(to);
const diff = end.diff(start, ["hours", "minutes", "seconds"]);
return {
hours: diff.hours,
minutes: diff.minutes,
seconds: Math.floor(diff.seconds),
formatted: `${diff.hours}h ${diff.minutes}m ${Math.floor(diff.seconds)}s`
};
};
const getDayRange = (date: Date) => {
const d = DateTime.fromJSDate(date);
return {
fromDate: d.toFormat("yyyy-MM-dd"),
toDate: d.toFormat("yyyy-MM-dd"),
};
};
const handlePrevDay = () => {
const prev = new Date(selectedDate);
prev.setDate(prev.getDate() - 1);
setSelectedDate(prev); // triggers useEffect
};
const handleNextDay = () => {
const next = new Date(selectedDate);
next.setDate(next.getDate() + 1);
setSelectedDate(next); // triggers useEffect
};
const fetchLogs = async (date: Date) => {
if (!projectID) return;
try {
setLoading(true);
// HERE
const { fromDate, toDate } = getDayRange(date);
const res = await window.electron.getProjectLogs(
projectID,
fromDate,
toDate
);
if (res?.success && Array.isArray(res.data)) {
setLogs(res.data[0]?.sessions || []);
} else {
setLogs([]);
}
} catch (err) {
console.error("Failed to fetch logs", err);
setLogs([]);
} finally {
setLoading(false);
}
};
return (
<dialog className="modal modal-open">
<div className="modal-box max-h-[97vh] max-w-4xl bg-base-100 flex flex-col">
<div className="flex justify-between items-center mb-3">
<div className="font-bold text-xs">
🗂️ Project Logs — <span className="text-primary">{projectName}</span>
</div>
<div className="flex items-center gap-2 text-xs">
<button className="btn btn-xs btn-primary p-1" onClick={handlePrevDay}><ChevronLeft className="" size={12}/></button>
<span className="font-semibold">
{DateTime.fromJSDate(selectedDate).toFormat("dd LLL yyyy")}
</span>
<button className="btn btn-xs btn-primary p-1" onClick={handleNextDay}><ChevronRight className="" size={12}/></button>
</div>
</div>
{loading ? (
<div className="flex justify-center items-center flex-grow">
<span className="loading loading-spinner text-primary"></span>
</div>
) : logs.length === 0 ? (
<p className="text-center text-gray-500 flex-grow">No logs available.</p>
) : (
<div className="overflow-y-auto flex-grow space-y-4 no-scrollbar">
{logs.map((session, idx) => (
<div key={idx} className="border border-base-300 overflow-hidden">
<div className="bg-base-200 p-2 font-semibold flex justify-between items-center">
<span className="text-[12px]">
🕒 Session {idx + 1} — {getTimeDifference(
new Date(session.from).toISOString(),
new Date(session.to).toISOString()
).formatted}
</span>
<span className="text-xs text-gray-500">
{new Date(session.from).toLocaleTimeString()} -{" "}
{new Date(session.to).toLocaleTimeString()}
</span>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra w-full font-semibold text-[12px]">
<thead className="sticky top-0 bg-base-300 z-10">
<tr>
<th>#</th>
<th>Title</th>
<th>Time</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{session.tabs.map((tab, tabIdx) => (
<tr key={tabIdx} className="text-[10px]">
<td>{tabIdx + 1}</td>
<td className="truncate max-w-[150px]" title={tab.title}>
{tab.title} <br />
<span className="text-slate-500">{tab.appName}</span>
</td>
<td><span className="text-green-500">{new Date(tab.from).toLocaleTimeString()}</span> <br /><span className="text-red-600">{new Date(tab.to).toLocaleTimeString()}</span></td>
<td>{tab.durationFormatted}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
)}
<div className="modal-action mt-2">
<button className="btn btn-outline btn-sm" onClick={onClose}>
Close
</button>
</div>
</div>
</dialog>
);
};
export default ProjectLogsModal;

View File

@ -1,5 +1,22 @@
import React from "react";
import { CalendarDays, Clock, FileText, CheckCircle, XCircle } from "lucide-react";
import React, { useEffect, useState } from "react";
import {
CalendarDays,
Clock,
FileText,
CheckCircle,
XCircle,
Eye,
EyeOff,
RefreshCw,
Loader2
} from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "../../store";
import ProjectLogsModal from "./ProjectLogsModal";
import { TrackingState } from "../../layout/DashboardLayout";
import { useOutletContext } from "react-router-dom";
import { DateTime } from "luxon";
interface TimesheetEntry {
id: number;
@ -18,104 +35,273 @@ const timesheetData: TimesheetEntry[] = [
{ id: 5, date: "2025-11-01", project: "AI Model Trainer", hours: 8, status: "Approved", remarks: "Normal day" },
];
interface ProjectSession {
from: string;
to: string;
totalSessionTime: string;
tabs: TabActivity[];
}
interface Project {
projectId: string;
projectName: string;
activityStatus: "ACTIVE" | "AWAY";
totalWorkTime: string;
totalWorkMs: number;
totalLogs: number;
status: string
}
const Timesheets: React.FC = () => {
const { isTracking } = useOutletContext<TrackingState>();
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
const { windowLogs } = useSelector((state: RootState) => state.window);
console.log(" Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState<ProjectSession[]>([]);
const [selectedProjectLogId, setSelectedProjectLogId] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [initialLoading, setInitialLoading] = useState(true);
const [tick, setTick] = useState(0);
const { selectedProject } = useSelector((state: any) => state.projects);
const selectedProjectId = selectedProject?.projectId ?? null;
const activeProjects = projectsData.filter(p => p.status === "ACTIVE");
useEffect(() => {
const interval = setInterval(() => {
setTick((t) => t + 1); // force re-render every second
}, 1000);
return () => clearInterval(interval);
}, []);
const formatLastRefreshed = () => {
if (!lastRefreshed) return "Never";
return DateTime.fromJSDate(lastRefreshed)
.toLocal()
.toFormat("HH:mm:ss"); // Only time
};
const fetchProjects = async () => {
setInitialLoading(true);
try {
console.log("📡 Fetching projects...");
const res = await window.electron.getProjectList();
console.log("✅ Project list response:", res);
if (res?.success && Array.isArray(res.data)) {
// Store project list
setProjectsData(res.data);
setLastRefreshed(new Date());
} else {
console.warn(" Unexpected response:", res);
}
} catch (err) {
console.error("Error fetching projects:", err);
} finally {
setInitialLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
const handleRefresh = async () => {
setRefreshing(true);
await fetchProjects();
};
const handleViewLogs = async (projectID: string, projectName: string) => {
try {
setModalOpen(true);
setLoading(true);
setSelectedProjectLogId(projectID);
setSelectedProjectName(projectName);
console.log("projectID", projectID)
const res = await window.electron.getProjectLogs(projectID);
console.log("📦 Full Logs:", res);
if (res?.success && Array.isArray(res.data)) {
setLogs(res.data[0].sessions || []);
} else {
setLogs([]);
}
} catch (error) {
console.error("Error fetching logs:", error);
setModalOpen(false)
} finally {
setLoading(false);
}
};
const formatWorkTime = (time: string) => {
if (!time) return "0h 0m";
// Example input: "0h 2m 29s"
const parts = time.split(" ");
// Always return first two parts: ["0h", "2m"]
return parts.slice(0, 2).join(" ");
};
// Skeleton loader rows
const renderSkeletonRows = () => {
return Array.from({ length: 5 }).map((_, index) => (
<tr key={`skeleton-${index}`} className="hover:bg-base-200">
<td>
<div className="skeleton h-4 w-6 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-4 w-32 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-4 w-16 bg-base-300"></div>
</td>
<td>
<div className="skeleton h-8 w-8 rounded bg-base-300"></div>
</td>
</tr>
));
};
return (
<div className="flex flex-col gap-2 max-h-screen">
<main
className="flex flex-col gap-2 overflow-hidden"
style={{
height: isTracking ? "calc(100vh - 80px)" : "calc(100vh - 24px)",
}}
>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex-none mb-2 flex justify-between items-center">
<h2 className="text-md font-bold">Timesheets</h2>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Total Logged Hours</p>
<h3 className="text-lg font-semibold">{totalHours} hrs</h3>
</div>
<Clock size={20} className="text-primary" />
</div>
</div>
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Approved Days</p>
<h3 className="text-lg font-semibold">{approvedCount}</h3>
</div>
<CheckCircle size={20} className="text-success" />
</div>
</div>
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
<div>
<p className="text-xs text-base-content/70">Pending Days</p>
<h3 className="text-lg font-semibold">
{timesheetData.length - approvedCount}
</h3>
</div>
<FileText size={20} className="text-warning" />
</div>
</div>
</div>
{/* Timesheet Table */}
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[60vh]">
<table className="table table-zebra w-full text-sm">
<thead className="sticky top-0 bg-base-200 z-10 text-sm">
<div
className="flex-1 overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
style={{
height: isTracking ? "calc(100vh - 180px)" : "calc(100vh - 130px)",
}}
>
<table className="table table-zebra w-full text-[12px]">
<thead className="sticky top-0 bg-base-200 z-10">
<tr>
<th>#</th>
<th>Date</th>
<th>Project</th>
<th>Hours</th>
<th>Status</th>
<th>Remarks</th>
<th>Logs</th>
</tr>
</thead>
<tbody>
{timesheetData.map((entry) => (
<tr key={entry.id}>
<td>{entry.id}</td>
<td className="whitespace-nowrap">{entry.date}</td>
<td>{entry.project}</td>
<td>{entry.hours} hrs</td>
<td>
<div
className={`badge badge-sm ${
entry.status === "Approved"
? "badge-success"
: entry.status === "Pending"
? "badge-warning"
: "badge-error"
}`}
{initialLoading ? (
// Show skeleton while initially loading
renderSkeletonRows()
) : refreshing && projectsData.length === 0 ? (
// Show skeleton while refreshing with empty data
renderSkeletonRows()
) : (
// Show actual data
projectsData.map((entry, index) => (
<tr
key={entry.projectId}
className={`${entry.status === "INACTIVE" && "hidden"}`}
>
{entry.status}
<td>{index + 1}</td>
<td>{entry.projectName}</td>
<td>{formatWorkTime(entry.totalWorkTime)}</td>
<td>
<button
disabled={(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProjectId))}
onClick={() =>
handleViewLogs(entry.projectId, entry.projectName)
}
className="btn btn-ghost btn-xs"
>
{(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProjectId)) ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
</td>
</tr>
))
)}
{/* {refreshing && projectsData.length > 0 && (
// Show a single skeleton row at the top while refreshing with existing data
<tr>
<td colSpan={4} className="bg-base-200/50">
<div className="flex justify-center py-2">
<Loader2 size={20} className="animate-spin" />
<span className="ml-2 text-sm">Refreshing...</span>
</div>
</td>
<td>{entry.remarks || "-"}</td>
</tr>
))}
)} */}
</tbody>
</table>
</div>
{/* Footer Summary */}
<div className="flex justify-between items-center text-xs mt-2">
<span className="text-base-content/60">
Showing {timesheetData.length} entries
</span>
<span className="text-base-content/60">
Last updated: {new Date().toLocaleDateString()}
</span>
<div className="flex-none flex justify-between items-center text-xs mt-2">
<div className="text-base-content/60">
{activeProjects.length} project{activeProjects.length !== 1 ? 's' : ''} shown
</div>
<div className="flex items-center gap-4">
{lastRefreshed && (
<div className="text-xs text-base-content/60">
Last updated: {formatLastRefreshed()}
</div>
)}
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-xs btn-circle bg-warning"
title="Refresh projects"
>
{refreshing ? (
<Loader2 size={15} className="animate-spin text-base-content" />
) : (
<RefreshCw size={15} className="text-base-content" />
)}
</button>
</div>
</div>
<ProjectLogsModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
projectName={selectedProjectName}
logs={logs}
loading={loading}
setLoading={setLoading}
setLogs={setLogs}
projectID={selectedProjectLogId}
/>
</main>
);
};

50
src/ui/pages/Vpn/Vpn.tsx Normal file
View File

@ -0,0 +1,50 @@
import React from 'react'
const Vpn = () => {
return (
<>
<div className="max-h-screen">
<div className="w-full mx-auto">
{/* Header */}
<div className=" ">
<h1 className="text-mb font-bold mb-2">VPN Servers</h1>
</div>
<div className="overflow-x-auto rounded-box border border-base-content/5 bg-base-100 text-sm">
<table className="table">
{/* head */}
<thead>
<tr>
<th>#</th>
<th>Server name</th>
<th>Connect</th>
</tr>
</thead>
<tbody>
{/* row 1 */}
<tr>
<th>1</th>
<td>Los Angeles [Fixed]</td>
<td><button className="btn btn-xs btn-secondary">Connect</button></td>
</tr>
{/* row 2 */}
<tr>
<th>2</th>
<td>Dallas 1</td>
<td><button className="btn btn-xs btn-secondary">Connect</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</>
)
}
export default Vpn

View File

@ -0,0 +1,204 @@
import React, { useEffect, useState } from "react";
import { Shield, ShieldAlert, RefreshCw, Wifi, Power, X } from "lucide-react";
interface Props {
children: React.ReactNode;
}
const VpnGuard: React.FC<Props> = ({ children }) => {
const VPN_API = "http://10.0.0.1";
const DEFAULT_API = "http://dreadingly-unfighting-pandora.ngrok-free.dev"
const [checking, setChecking] = useState(true);
const [vpnConnected, setVpnConnected] = useState(true);
const [isRetrying, setIsRetrying] = useState(false);
const checkVPN = async () => {
try {
setChecking(true);
const res = await window.electron?.checkRegion();
console.log("VpnGuard", res);
if (res.status === 200) {
const baseRes = await window.electron.updateBaseURL(DEFAULT_API);
setVpnConnected(true);
} else {
setVpnConnected(false);
return;
}
// setVpnConnected(res?.success === true);
} catch (error) {
console.error("VPN check failed:", error);
setVpnConnected(false);
} finally {
setChecking(false);
setIsRetrying(false);
}
};
useEffect(() => {
checkVPN();
}, []);
const connectVPN = async () => {
setChecking(true);
try {
const res = await window.electron.connectVPN();
console.log("connectVPN result → ", res);
// Only proceed if VPN actually connected
if (res === "Windows VPN connected silently" || res === "VPN already connected and healthy") {
const vpnRes = await window.electron.updateBaseURL(VPN_API);
console.log("vpnRes", vpnRes);
if (vpnRes) {
// Run VPN check again AFTER URL changes
await checkVPN();
} else {
console.warn("Base URL updated but health check failed");
}
}
} catch (error) {
console.log(error);
const vpnRes = await window.electron.updateBaseURL(VPN_API);
console.log("vpnRes", vpnRes);
if (vpnRes) {
// Run VPN check again AFTER URL changes
await checkVPN();
} else {
console.warn("Base URL updated but health check failed");
}
} finally {
setChecking(false);
}
};
const disconnectVPN = async () => {
setChecking(true);
try {
const res = await window.electron.disconnectVPN();;
console.log("disconnectVPN →", res);
// Restore original base URL
const baseRes = await window.electron.updateBaseURL(DEFAULT_API);
console.log("baseRes disconnect →", baseRes);
setVpnConnected(true);
// await checkVPN();
} catch (error) {
console.log("Disconnect Error:", error);
} finally {
setChecking(false);
}
};
// Checking state
if (checking && !isRetrying) {
return (
<div className="min-h-screen flex flex-col justify-center items-center bg-base-200">
<div className="relative">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-24 h-24 border-4 border-primary opacity-20 rounded-full animate-ping"></div>
</div>
<div className="relative bg-base-100 rounded-full p-6 shadow-xl">
<Shield className="w-12 h-12 text-primary animate-pulse" />
</div>
</div>
<div className="mt-8 text-center">
<h2 className="text-2xl font-semibold mb-2">
Verifying Connection
</h2>
<p className="text-base-content opacity-70">Checking VPN status...</p>
<div className="flex justify-center mt-4 gap-1">
<span className="loading loading-dots loading-md text-primary"></span>
</div>
</div>
</div>
);
}
// VPN not connected state
if (!vpnConnected) {
return (
<div className="min-h-screen flex flex-col justify-center items-center bg-base-200 p-4">
<div className="max-w-md w-full">
<div className="card bg-base-100 shadow-2xl">
<X className="mt-2 mr-2 ml-auto text-slate-300 hover:text-slate-400" onClick={() => setVpnConnected(true)} />
<div className="card-body items-center text-center">
{/* Icon */}
<div className="relative mb-4">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-20 h-20 border-4 border-error opacity-20 rounded-full animate-pulse"></div>
</div>
<div className="relative bg-error bg-opacity-10 rounded-full p-4">
<ShieldAlert className="w-12 h-12" />
</div>
</div>
<h2 className="card-title text-3xl mb-3">VPN Required</h2>
<p className="mb-6 opacity-80">
Please connect to your company VPN to access this application.
</p>
{/* Connect VPN */}
<button
className="btn btn-primary btn-wide gap-2"
onClick={connectVPN}
disabled={isRetrying || vpnConnected} // Disable if retrying OR already connected
>
{isRetrying ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Checking Connection...
</>
) : (
<>
<RefreshCw className="w-5 h-5" />
Connect VPN
</>
)}
</button>
{/* Disconnect VPN (Disabled when NOT connected) */}
{/* <button
className="btn btn-error btn-wide gap-2 mt-3"
onClick={disconnectVPN}
// disabled={!vpnConnected || checking} // Disable if not connected
>
<Power className="w-4 h-4" />
Disconnect VPN
</button> */}
<div className="divider"></div>
<p className="text-xs opacity-60">
Need help? Contact IT support at{" "}
<a href="mailto:support@docva.com" className="link link-primary">
support@docva.com
</a>
</p>
</div>
</div>
</div>
</div>
);
}
return (
<>
{children}
</>
);
};
export default VpnGuard;

20
src/ui/store/index.tsx Normal file
View File

@ -0,0 +1,20 @@
// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import windowReducer from "./windowSlice";
import projectsReducer from "./projectSlice";
import userReducer from "./userSlice";
import profileReducer from "./profileSlice";
import logsReducer from './logsSlice';
export const store = configureStore({
reducer: {
window: windowReducer,
projects: projectsReducer,
user: userReducer,
profile: profileReducer,
logs: logsReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

240
src/ui/store/logsSlice.tsx Normal file
View File

@ -0,0 +1,240 @@
// store/logsSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface LogEntry {
id: string; // Unique ID for each log entry
title: string;
appName: string;
url: string | null;
from: string; // UTC ISO string with .000 milliseconds
to: string; // UTC ISO string with .000 milliseconds
totalTime: number; // milliseconds
}
export interface ProjectLogs {
projectId: string;
logId: string | null;
logs: LogEntry[]; // All collected logs during tracking
trackingStartTime: string | null;
lastWindowTitle: string | null; // Track last window title
lastWindowApp: string | null; // Track last window app
}
interface LogsState {
logsByProject: Record<string, ProjectLogs>;
}
const initialState: LogsState = {
logsByProject: {},
};
// Helper function to get timestamp with .000 milliseconds
const getTimestampWithZeroMs = () => {
const now = new Date();
// Format: YYYY-MM-DDTHH:mm:ss.000Z
const isoString = now.toISOString();
// Replace milliseconds with .000
return isoString.replace(/\.\d{3}Z$/, '.000Z');
};
// Helper function to format any date string to have .000 milliseconds
const formatToZeroMs = (dateString: string): string => {
if (dateString.endsWith('.000Z')) {
return dateString; // Already formatted
}
const date = new Date(dateString);
const isoString = date.toISOString();
return isoString.replace(/\.\d{3}Z$/, '.000Z');
};
const logsSlice = createSlice({
name: "logs",
initialState,
reducers: {
// Initialize logs for a project
initializeProjectLogs: (
state,
action: PayloadAction<{
projectId: string;
logId: string;
}>
) => {
const { projectId, logId } = action.payload;
const nowUTC = getTimestampWithZeroMs();
// Always create fresh logs for new tracking session
state.logsByProject[projectId] = {
projectId,
logId,
logs: [], // Start with empty logs
trackingStartTime: nowUTC,
lastWindowTitle: null,
lastWindowApp: null,
};
console.log(`📝 Initialized logs for project ${projectId} with logId: ${logId}`);
},
// Add or update window log
updateWindowLog: (
state,
action: PayloadAction<{
projectId: string;
title: string;
appName: string;
url?: string | null;
}>
) => {
const { projectId, title, appName, url = null } = action.payload;
const projectLogs = state.logsByProject[projectId];
if (!projectLogs) {
console.warn(`Cannot update log: Project ${projectId} not initialized`);
return;
}
const nowUTC = getTimestampWithZeroMs();
const isSameWindow =
projectLogs.lastWindowTitle === title &&
projectLogs.lastWindowApp === appName;
// If same window as before, update the last log's end time
if (isSameWindow && projectLogs.logs.length > 0) {
const lastLog = projectLogs.logs[projectLogs.logs.length - 1];
// Format the existing from time to ensure it has .000 milliseconds
lastLog.from = formatToZeroMs(lastLog.from);
lastLog.to = nowUTC;
// Calculate total time in milliseconds
const startTime = new Date(lastLog.from).getTime();
const endTime = new Date(lastLog.to).getTime();
lastLog.totalTime = endTime - startTime;
// console.log(`⏱️ Updated existing log: ${title} (${lastLog.totalTime}ms)`);
}
// If new window or first window
else {
// If there was a previous window, ensure it's properly ended
if (projectLogs.lastWindowTitle && projectLogs.logs.length > 0) {
const lastLog = projectLogs.logs[projectLogs.logs.length - 1];
// Format the existing from time to ensure it has .000 milliseconds
lastLog.from = formatToZeroMs(lastLog.from);
lastLog.to = nowUTC;
// Calculate total time in milliseconds
const startTime = new Date(lastLog.from).getTime();
const endTime = new Date(lastLog.to).getTime();
lastLog.totalTime = endTime - startTime;
console.log(
`✅ Finalized previous window: ${lastLog.title} (${lastLog.totalTime}ms)`
);
}
// Create new log entry with .000 milliseconds
const newLog: LogEntry = {
id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
title,
appName,
url,
from: nowUTC,
to: nowUTC, // Will be updated continuously
totalTime: 0, // Starts at 0 (in milliseconds)
};
projectLogs.logs.push(newLog);
console.log(`📝 Started new window: ${title} on ${appName}`);
}
// Update last window tracking
projectLogs.lastWindowTitle = title;
projectLogs.lastWindowApp = appName;
},
// Clear project logs
clearProjectLogs: (
state,
action: PayloadAction<{
projectId: string;
}>
) => {
const { projectId } = action.payload;
delete state.logsByProject[projectId];
console.log(`🧹 Cleared logs for project ${projectId}`);
},
// Clear all logs
clearAllLogs: (state) => {
state.logsByProject = {};
console.log("🧹 Cleared all logs");
},
// Debug: Get logs state
debugLogsState: (
state,
action: PayloadAction<{
projectId: string;
}>
) => {
const { projectId } = action.payload;
const projectLogs = state.logsByProject[projectId];
if (!projectLogs) {
console.log(`📋 No logs found for project ${projectId}`);
return;
}
console.log(`📋 Logs State for ${projectId}:`, {
totalLogs: projectLogs.logs.length,
lastWindow: projectLogs.lastWindowTitle,
logs: projectLogs.logs.map((log) => ({
title: log.title.substring(0, 40) + (log.title.length > 40 ? "..." : ""),
appName: log.appName,
from: log.from,
to: log.to,
durationMs: log.totalTime,
durationSec: Math.round(log.totalTime / 1000),
})),
});
},
// Optional: Add a reducer to fix all timestamps in existing logs
fixAllTimestamps: (
state,
action: PayloadAction<{
projectId: string;
}>
) => {
const { projectId } = action.payload;
const projectLogs = state.logsByProject[projectId];
if (!projectLogs) {
console.warn(`Cannot fix timestamps: Project ${projectId} not found`);
return;
}
// Fix all timestamps in existing logs
projectLogs.logs.forEach(log => {
log.from = formatToZeroMs(log.from);
log.to = formatToZeroMs(log.to);
// Recalculate totalTime based on formatted timestamps
const startTime = new Date(log.from).getTime();
const endTime = new Date(log.to).getTime();
log.totalTime = endTime - startTime;
});
console.log(`🔧 Fixed timestamps for project ${projectId}`);
},
},
});
export const {
initializeProjectLogs,
updateWindowLog,
clearProjectLogs,
clearAllLogs,
debugLogsState,
fixAllTimestamps,
} = logsSlice.actions;
export default logsSlice.reducer;

View File

@ -0,0 +1,61 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface ProfileData {
firstName: string;
lastName: string;
email: string;
dob: string;
age: string;
address: string;
bankName: string;
accountHolderName: string;
accountNumber: string;
routingNumber: string;
phone:string;
skype:string;
city:string;
state:string;
country:string;
paymentMethod:string;
payoneerAccount:string;
veemAccount:string;
wiseEmail:string
_id: string;
}
interface ProfileState {
profile: ProfileData | null;
loading: boolean;
error: string | null;
}
const initialState: ProfileState = {
profile: null,
loading: false,
error: null,
};
const profileSlice = createSlice({
name: "profile",
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setProfile: (state, action: PayloadAction<ProfileData>) => {
state.profile = action.payload;
state.error = null;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
clearProfile: (state) => {
state.profile = null;
state.error = null;
state.loading = false;
},
},
});
export const { setLoading, setProfile, setError, clearProfile } = profileSlice.actions;
export default profileSlice.reducer;

View File

@ -0,0 +1,137 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface Project {
projectId: string;
projectName: string;
[key: string]: any;
}
interface ProjectSummary {
totalProjects: number;
activeProjects: number;
awayProjects: number;
totalWorkTime: string;
totalWorkMs: number;
vaName: string;
vaId: string;
}
interface TrackingData {
elapsedTime: number;
startTime: number | null;
endTime: number | null;
isTracking: boolean;
isPaused: boolean;
pauseStartTime: number | null;
totalPausedTime: number;
accumulatedTime: number;
}
interface ProjectsState {
projectsData: Project[];
projectSummary: ProjectSummary | null;
selectedProject: Project | null;
projectTrackingMap: Record<string, TrackingData>;
}
const initialState: ProjectsState = {
projectsData: [],
projectSummary: null,
selectedProject: null,
projectTrackingMap: {},
};
const projectsSlice = createSlice({
name: "projects",
initialState,
reducers: {
setProjectsDataSlice(state, action: PayloadAction<Project[]>) {
state.projectsData = action.payload;
},
setProjectSummarySlice(state, action: PayloadAction<ProjectSummary>) {
state.projectSummary = action.payload;
},
setSelectedProjectSlice(state, action: PayloadAction<Project | null>) {
state.selectedProject = action.payload;
},
initializeProjectTracking(state, action: PayloadAction<{ projectId: string; initialTime: number }>) {
const { projectId, initialTime } = action.payload;
if (!state.projectTrackingMap[projectId]) {
state.projectTrackingMap[projectId] = {
elapsedTime: initialTime,
startTime: null,
endTime: null,
isTracking: false,
isPaused: false,
pauseStartTime: null,
totalPausedTime: 0,
accumulatedTime: initialTime,
};
}
},
updateProjectTracking(state, action: PayloadAction<{ projectId: string; data: Partial<TrackingData> }>) {
const { projectId, data } = action.payload;
if (state.projectTrackingMap[projectId]) {
state.projectTrackingMap[projectId] = {
...state.projectTrackingMap[projectId],
...data,
};
}
},
startTracking(state, action: PayloadAction<{ projectId: string; startTime: number }>) {
const { projectId, startTime } = action.payload;
const project = state.projectTrackingMap[projectId];
if (project) {
project.startTime = startTime;
project.isTracking = true;
project.isPaused = false;
project.pauseStartTime = null;
project.totalPausedTime = 0;
}
},
updateElapsedTime(state, action: PayloadAction<{ projectId: string; elapsedTime: number }>) {
const { projectId, elapsedTime } = action.payload;
const project = state.projectTrackingMap[projectId];
if (project) {
project.elapsedTime = elapsedTime;
project.accumulatedTime = elapsedTime;
}
},
stopTracking(state, action: PayloadAction<{ projectId: string }>) {
const { projectId } = action.payload;
const project = state.projectTrackingMap[projectId];
if (project) {
project.isTracking = false;
project.startTime = null;
}
},
// In your projectsSlice.ts
stopTrackingReducer(state, action: PayloadAction<{ projectId: string }>) {
const p = state.projectTrackingMap[action.payload.projectId];
if (p) {
// Only update tracking status, time will be updated from server
p.isTracking = false;
p.isPaused = false;
p.startTime = null;
p.pauseStartTime = null;
p.endTime = Date.now();
}
}
},
});
export const { setProjectsDataSlice, setProjectSummarySlice, setSelectedProjectSlice, initializeProjectTracking,
updateProjectTracking, startTracking, updateElapsedTime, stopTracking, stopTrackingReducer } =
projectsSlice.actions;
export default projectsSlice.reducer;

View File

@ -0,0 +1,50 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
// Define and export the User type so other files can use it
export interface User {
id: string;
name: string;
email: string;
}
// Define the state shape
interface UserState {
user: User | null;
}
const initialState: UserState = {
user: null,
};
// Create the slice
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
setUserSlice: (state, action: PayloadAction<User>) => {
state.user = action.payload;
// Save to localStorage
localStorage.setItem("user", JSON.stringify(action.payload));
},
clearUser: (state) => {
state.user = null;
localStorage.removeItem("user");
},
loadUserFromStorage: (state) => {
const storedUser = localStorage.getItem("user");
if (storedUser) {
try {
state.user = JSON.parse(storedUser) as User;
} catch (error) {
console.error("Invalid user in localStorage:", error);
localStorage.removeItem("user");
}
}
},
},
});
// Export actions and reducer
export const { setUserSlice, clearUser, loadUserFromStorage } = userSlice.actions;
export default userSlice.reducer;

View File

@ -0,0 +1,128 @@
// src/store/windowSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface OwnerInfo {
processId: number;
path: string;
name: string;
}
export interface WindowInfo {
platform: string;
id: number;
title: string;
owner: OwnerInfo;
bounds: Record<string, number>;
contentBounds: Record<string, number>;
memoryUsage: number;
timestamp: string;
}
export interface ProjectWindowLog {
projectId: string;
projectName: string;
appName: string;
windowTitle: string;
platform: string;
duration: number; // in seconds
startTime: string;
endTime: string;
memoryUsage: number;
}
interface WindowState {
activeWindow: WindowInfo | null;
activeSince: number | null;
activeProjectId: string | null;
activeProjectName: string | null;
windowLogs: ProjectWindowLog[];
}
const initialState: WindowState = {
activeWindow: null,
activeSince: null,
activeProjectId: null,
activeProjectName: null,
windowLogs: [],
};
interface SetActiveWindowPayload {
window: WindowInfo;
projectId: string;
projectName: string | null;
}
const windowSlice = createSlice({
name: "window",
initialState,
reducers: {
// ✅ Start tracking window for a project
setActiveWindow: (state, action: PayloadAction<SetActiveWindowPayload>) => {
const { window, projectId, projectName } = action.payload;
const now = Date.now();
// 🕒 Log previous window before switching
if (state.activeWindow && state.activeSince && state.activeProjectId) {
const duration = Math.floor((now - state.activeSince) / 1000);
const logEntry: ProjectWindowLog = {
projectId: state.activeProjectId,
projectName: state.activeProjectName || "Untitled",
appName: state.activeWindow.owner.name,
windowTitle: state.activeWindow.title,
platform: state.activeWindow.platform,
duration,
startTime: new Date(state.activeSince).toISOString(),
endTime: new Date(now).toISOString(),
memoryUsage: state.activeWindow.memoryUsage,
};
state.windowLogs.push(logEntry);
console.log("🧾 Log saved:", logEntry);
}
// 🔁 Update new active window + project context
state.activeWindow = window;
state.activeSince = now;
state.activeProjectId = projectId;
state.activeProjectName = projectName;
},
// ✅ Stop tracking (finalize)
finalizeActiveWindow: (state) => {
if (state.activeWindow && state.activeSince && state.activeProjectId) {
const now = Date.now();
const duration = Math.floor((now - state.activeSince) / 1000);
const logEntry: ProjectWindowLog = {
projectId: state.activeProjectId,
projectName: state.activeProjectName || "Untitled",
appName: state.activeWindow.owner.name,
windowTitle: state.activeWindow.title,
platform: state.activeWindow.platform,
duration,
startTime: new Date(state.activeSince).toISOString(),
endTime: new Date(now).toISOString(),
memoryUsage: state.activeWindow.memoryUsage,
};
state.windowLogs.push(logEntry);
console.log("✅ Final log saved:", logEntry);
}
state.activeWindow = null;
state.activeSince = null;
state.activeProjectId = null;
state.activeProjectName = null;
},
clearWindowLogs: (state) => {
state.windowLogs = [];
},
},
});
export const { setActiveWindow, finalizeActiveWindow, clearWindowLogs } =
windowSlice.actions;
export default windowSlice.reducer;

View File

@ -0,0 +1,138 @@
// utils/projectLimits.ts
interface Project {
projectId: string;
projectName: string;
totalWorkTime: string;
totalWorkMs: number;
dedicatedHours: string;
[key: string]: any;
}
/**
* Parse time string (e.g., "10h", "30m", "2h 15m", "0h 5m 30s") to total minutes
*/
export const parseTimeToMinutes = (timeStr: string): number => {
if (!timeStr) return 0;
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;
};
/**
* Parse time string to total seconds
*/
export const parseTimeToSeconds = (timeStr: string): number => {
if (!timeStr) return 0;
const hourMatch = timeStr.match(/(\d+)h/);
const minMatch = timeStr.match(/(\d+)m/);
const secMatch = timeStr.match(/(\d+)s/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
const seconds = secMatch ? parseInt(secMatch[1]) : 0;
return (hours * 3600) + (minutes * 60) + seconds;
};
/**
* Format minutes to readable time string
*/
export const formatMinutesToTime = (minutes: number): string => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0) {
return `${hours}h ${mins}m`;
}
return `${mins}m`;
};
/**
* Format seconds to readable time string
*/
export const formatSecondsToTime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
}
return `${secs}s`;
};
/**
* Check if project has reached its daily limit
*/
export const isProjectLimitReached = (project: Project): boolean => {
const totalWorkedSeconds = parseTimeToSeconds(project.totalWorkTime);
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
return totalWorkedSeconds >= dedicatedSeconds;
};
/**
* Get remaining time for a project in seconds
*/
export const getRemainingTimeInSeconds = (project: Project): number => {
const totalWorkedSeconds = parseTimeToSeconds(project.totalWorkTime);
const dedicatedSeconds = parseTimeToSeconds(project.dedicatedHours);
const remaining = dedicatedSeconds - totalWorkedSeconds;
return Math.max(0, remaining); // Never return negative
};
/**
* Get remaining time for a project (legacy - in minutes)
*/
export const getRemainingTime = (project: Project): number => {
const totalWorked = parseTimeToMinutes(project.totalWorkTime);
const dedicatedLimit = parseTimeToMinutes(project.dedicatedHours);
const remaining = dedicatedLimit - totalWorked;
return Math.max(0, remaining); // Never return negative
};
/**
* Get formatted remaining time string
*/
export const getFormattedRemainingTime = (project: Project): string => {
const remainingSeconds = getRemainingTimeInSeconds(project);
return formatSecondsToTime(remainingSeconds);
};
/**
* Check if project can start tracking
*/
export const canStartTracking = (project: Project): {
canStart: boolean;
reason?: string;
remaining?: string;
remainingSeconds?: number;
} => {
if (isProjectLimitReached(project)) {
return {
canStart: false,
reason: "Daily limit reached for this project",
remaining: "0m",
remainingSeconds: 0
};
}
const remainingSeconds = getRemainingTimeInSeconds(project);
return {
canStart: true,
remaining: getFormattedRemainingTime(project),
remainingSeconds
};
};

View File

@ -0,0 +1,14 @@
import ReactQuill from "react-quill-new";
import Quill from "quill";
// import ImageResize from "quill-image-resize-module-react";
// Quill.register("modules/imageResize", ImageResize);
export const quillModules = {
toolbar: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline"],
["image", "link"],
],
// imageResize: {},
};

View File

@ -0,0 +1,41 @@
// utils/timeDisplay.js
export const getDisplayTime = (project:any, trackingData:any) => {
if (!project) return "0h 0m 0s";
// If tracking is active, calculate current time based on server time + session time
if (trackingData?.isTracking && trackingData.startTime) {
const baseSeconds = parseTimeStringToSeconds(project.totalWorkTime);
const sessionSeconds = Math.floor((Date.now() - trackingData.startTime) / 1000);
const totalSeconds = baseSeconds + sessionSeconds;
return formatSecondsToTimeString(totalSeconds);
}
// If not tracking, just return the server's totalWorkTime
return project.totalWorkTime;
};
export const parseTimeStringToSeconds = (timeString:any) => {
if (!timeString || typeof timeString !== 'string') return 0;
try {
const hourMatch = timeString.match(/(\d+)h/);
const minMatch = timeString.match(/(\d+)m/);
const secMatch = timeString.match(/(\d+)s/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
const seconds = secMatch ? parseInt(secMatch[1]) : 0;
return hours * 3600 + minutes * 60 + seconds;
} catch (error) {
console.error('Error parsing time string:', timeString, error);
return 0;
}
};
export const formatSecondsToTimeString = (totalSeconds:any) => {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${hours}h ${minutes}m ${seconds}s`;
};

View File

@ -0,0 +1,20 @@
// utils/timeParser.js
export const parseTimeStringToMs = (timeString:string) => {
if (!timeString) return 0;
const hourMatch = timeString.match(/(\d+)h/);
const minMatch = timeString.match(/(\d+)m/);
const secMatch = timeString.match(/(\d+)s/);
const hours = hourMatch ? parseInt(hourMatch[1]) : 0;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
const seconds = secMatch ? parseInt(secMatch[1]) : 0;
return (hours * 3600 + minutes * 60 + seconds) * 1000;
};
export const parseTimeStringToSeconds = (timeString:string) => {
return Math.floor(parseTimeStringToMs(timeString) / 1000);
};

258
types.d.ts vendored
View File

@ -1,3 +1,13 @@
declare module "quill";
declare module "quill-image-resize-module";
declare module "quill-image-drop-module";
declare module "quill-image-resize-module-react" {
const ImageResize: any;
export default ImageResize;
}
type Statistics = {
cpuUsage: number;
ramUsage: number;
@ -23,14 +33,246 @@ type EventPayloadMapping = {
type UnsubscribeFunction = () => void;
interface Window {
electron: {
login: (credentials: { email: string; password: string }) => Promise<any>;
logout: () => Promise<any>;
getToken: (tokenKey: "ACCESS_TOKEN" | "REFRESH_TOKEN") => Promise<string | null>;
uploadScreenshot: () => {};
getActiveAppDetails: () => {};
};
interface ProjectListResponse {
success: boolean;
vaId?: string;
vaName?: string;
totalProjects?: number;
activeProjects?: number;
awayProjects?: number;
totalWorkTime?: string;
totalWorkMs?: number;
data?: any[]; // you can replace `any[]` with a typed array later
status: number
}
interface TabActivity {
title: string;
appName: string;
from: string;
to: string;
durationMs: number;
durationFormatted: string;
}
// Each session includes tabs and time span
interface ProjectSession {
from: string;
to: string;
totalSessionTime: string;
tabs: TabActivity[];
}
// Complete response for project logs
interface ProjectLogResponse {
vaId: string;
vaName: string;
vaEmail: string;
totalWorkTime: string;
totalWorkMs: number;
sessions: ProjectSession[];
}
interface ProfileData {
firstName: string;
lastName: string;
email: string;
dob: string;
age: string;
address: string;
bankName: string;
accountHolderName: string;
accountNumber: string;
routingNumber: string;
phone: string;
skype: string;
city: string;
state: string;
country: string;
paymentMethod: string;
payoneerAccount: string;
veemAccount: string;
wiseEmail: string
_id: string;
}
type LeaveRequestResponse = {
success: boolean;
data?: any;
message?: string;
};
interface ScheduleItem {
id: string;
isActive: boolean;
status: string | null;
ScheduleDate: string;
cutOffTime: string | null;
periodEnd: string | null;
periodStart: string | null;
}
interface PayslipItem {
totalHoursFormatted: string;
payrollSchedule: string;
totalPayAmount: number;
status: string;
date: string | null;
createdDate: string;
name: string;
id: string;
}
interface PayslipResponse {
payrolls: PayslipItem[];
vaPortalId: string;
vaName: string;
id: string;
}
interface RegionResponse {
success: boolean;
status: number;
data?: any;
retry?: boolean;
message?: string;
}
interface TrackingLog {
title: string;
appName: string;
url: string;
from: string;
to: string;
totalTime: number;
}
interface TrackingPayload {
vaId: string;
projectId: string;
logId: string;
logs: TrackingLog[];
}
interface Window {
electron: {
login: (credentials: { email: string; password: string }) => Promise<any>;
logout: () => Promise<any>;
forgotPassword: (email: string) => Promise<{
success: boolean;
message: string;
}>;
getToken: (tokenKey: "ACCESS_TOKEN" | "REFRESH_TOKEN") => Promise<string | null>;
uploadScreenshot: () => {};
getActiveAppDetails: () => {};
toggleCloseBtn: (shouldEnable: boolean) => {},
resizeWindow: (width: number, height: number) => {},
getRunningApps: () => {},
captureAppScreenShot: (appTitle: string) => Promise<string>,
onWindowUpdate: (callback: (data: any) => void) => void;
getProjectList: () => Promise<ProjectListResponse>;
startProjectTracking: (data: {
vaId: string;
projectId: string;
type: string;
time: string
}) => Promise<any>;
stopProjectTracking: (data: {
vaId: string;
logId: string,
projectId: string;
type: string;
time: string
}) => Promise<any>;
trackingTabsOpeningClosing: (data: TrackingPayload) => Promise<any>;
getProjectLogs: (
projectId: string,
fromDate?: string,
toDate?: string
) => Promise<{
success: boolean;
data: any;
message?: string;
}
>;
getProfile: () => Promise<{
success: boolean;
data?: ProfileData;
}>;
changePassword: (data: {
oldPassword: string;
password: string;
cPassword: string;
}) => Promise<{ success: boolean; message?: string; data?: any }>;
// In your electron API handler
updatePayment: (data: {
paymentMethod: string;
wiseEmail?: string;
veemAccount?: string;
}) => Promise<{ success: boolean; message?: string; data?: any }>;
onTokenExpired: (callback: () => void) => void;
getProjectSummary: (projectId: string, date: string) => Promise<any>;
createTimesheet: (
payload: {
projectId: string;
date: string;
loginTime: string;
logoutTime: string;
comment?: string;
}
) => Promise<{
success: boolean;
data?: any;
message?: string;
}>;
createPTOAndSickLeaveRequest: (
payload: LeaveRequestPayload
) => Promise<LeaveRequestResponse>;
getSchedules: () => Promise<any>;
getPayslips: (vaId: string) => Promise<{
success: boolean;
data?: PayslipResponse[];
message?: string;
}>;
getLeaveList: () => Promise<{
success: boolean;
data?: any;
}>;
getAdjustmentsList: (vaId: string) => Promise<{
success: boolean;
data?: any;
}>;
checkRegion: () => Promise<RegionResponse>;
connectVPN: () => Promise<string>;
disconnectVPN: () => Promise<void>;
getBaseURL: () => Promise<string>;
updateBaseURL: (url: string) => Promise<{
updated: boolean;
baseURL: string;
}>;
};
}

View File

@ -0,0 +1,19 @@
// vite.config.ts
import { defineConfig } from "file:///C:/Users/user/OneDrive/Desktop/electron-main/node_modules/vite/dist/node/index.js";
import react from "file:///C:/Users/user/OneDrive/Desktop/electron-main/node_modules/@vitejs/plugin-react/dist/index.js";
import tailwindcss from "file:///C:/Users/user/OneDrive/Desktop/electron-main/node_modules/@tailwindcss/vite/dist/index.mjs";
var vite_config_default = defineConfig({
plugins: [react(), tailwindcss()],
base: "./",
build: {
outDir: "dist-react"
},
server: {
port: 5123,
strictPort: true
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx1c2VyXFxcXE9uZURyaXZlXFxcXERlc2t0b3BcXFxcZWxlY3Ryb24tbWFpblwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcVXNlcnNcXFxcdXNlclxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXGVsZWN0cm9uLW1haW5cXFxcdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L1VzZXJzL3VzZXIvT25lRHJpdmUvRGVza3RvcC9lbGVjdHJvbi1tYWluL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XHJcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XHJcbmltcG9ydCB0YWlsd2luZGNzcyBmcm9tICdAdGFpbHdpbmRjc3Mvdml0ZSdcclxuXHJcbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXHJcbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XHJcbiAgcGx1Z2luczogW3JlYWN0KCksdGFpbHdpbmRjc3MoKV0sXHJcbiAgYmFzZTogJy4vJyxcclxuICBidWlsZDoge1xyXG4gICAgb3V0RGlyOiAnZGlzdC1yZWFjdCcsXHJcbiAgfSxcclxuICBzZXJ2ZXI6IHtcclxuICAgIHBvcnQ6IDUxMjMsXHJcbiAgICBzdHJpY3RQb3J0OiB0cnVlLFxyXG4gIH0sXHJcbn0pO1xyXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWtVLFNBQVMsb0JBQW9CO0FBQy9WLE9BQU8sV0FBVztBQUNsQixPQUFPLGlCQUFpQjtBQUd4QixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTLENBQUMsTUFBTSxHQUFFLFlBQVksQ0FBQztBQUFBLEVBQy9CLE1BQU07QUFBQSxFQUNOLE9BQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxFQUNWO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsSUFDTixZQUFZO0FBQUEsRUFDZDtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg==