Compare commits

...

26 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
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
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
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
28 changed files with 1941 additions and 789 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ nextSteps.txt
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
resources

774
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"transpile:electron": "tsc --project src/electron/tsconfig.json", "transpile:electron": "tsc --project src/electron/tsconfig.json",
"dist:mac": "npm run transpile:electron && npm run build && electron-builder --mac --x64 --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: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", "dist:linux": "npm run transpile:electron && npm run build && electron-builder --linux --x64",
"test:e2e": "playwright test", "test:e2e": "playwright test",
@ -82,7 +82,10 @@
], ],
"mac": { "mac": {
"target": [ "target": [
"dmg" {
"target": "dmg",
"arch": "universal"
}
], ],
"icon": "build/icon.icns", "icon": "build/icon.icns",
"extendInfo": { "extendInfo": {
@ -126,13 +129,13 @@
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"daisyui": "^5.4.3", "daisyui": "^5.4.3",
"electron": "^32.1.2", "electron": "^39.2.5",
"electron-builder": "^25.0.5", "electron-builder": "^25.0.5",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"rcedit": "^5.0.2", "rcedit": "^5.0.2",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.7", "vite": "^7.2.6",
"vitest": "^2.1.1" "vitest": "^4.0.15"
} }
} }

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<style> <style>
body { body {
@ -37,14 +38,18 @@
align-items: center; align-items: center;
} }
.loader-logo img { .loader-logo svg {
width: 45px; width: 70px;
height: 45px; height: 70px;
margin-left: 12px;
margin-top: 8px;
object-fit: contain; object-fit: contain;
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
</style> </style>
</head> </head>
@ -55,8 +60,266 @@
<!-- Place your logo here --> <!-- Place your logo here -->
<div class="loader-logo"> <div class="loader-logo">
<img src="your-logo.png" alt="App 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>
</div> </div>
</body> </body>
</html>
</html>

View File

@ -18,7 +18,7 @@ const httpsAgent = new https.Agent({
console.log('Base_URL$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' ,Base_URL) console.log('Base_URL$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$' ,Base_URL)
const api: AxiosInstance = axios.create({ const api: AxiosInstance = axios.create({
// baseURL: 'https://quality-friendly-goldfish.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`, baseURL: `${Base_URL}/api/v1/va`,
httpsAgent httpsAgent
// timeout: 10000, // timeout: 10000,

View File

@ -1,5 +1,5 @@
// let CURRENT_BASE_URL = "https://quality-friendly-goldfish.ngrok-free.app/api/v1/va"; // let CURRENT_BASE_URL = "https://portal-sf.docva.com/api/v1/va";
let CURRENT_BASE_URL = "https://quality-friendly-goldfish.ngrok-free.app"; let CURRENT_BASE_URL = "https://portal-sf.docva.com";
export const getBaseURL = () => CURRENT_BASE_URL; export const getBaseURL = () => CURRENT_BASE_URL;
export const setBaseURL = (url: string) => { export const setBaseURL = (url: string) => {

View File

@ -8,13 +8,14 @@ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
export const checkRegion = async () => { export const checkRegion = async () => {
const baseURL = await getBaseURL(); const baseURL = await getBaseURL();
console.log("checkRegion@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ,baseURL) console.log("Lin 11 BaseURL CHECK@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ,baseURL)
try { try {
const res = await axios.get(`${baseURL}/health`, { const res = await axios.get(`${baseURL}/health`, {
headers: { "ngrok-skip-browser-warning": "true" }, headers: { "ngrok-skip-browser-warning": "true" },
httpsAgent httpsAgent,
timeout: 4000
}); });
console.log(res , "checkRegion") console.log(res , "checkRegion")

View File

@ -15,17 +15,19 @@ export const registerIPCHandlers = (mainWindow: BrowserWindow) => {
ipcMain.handle("GET_BASE_URL", () => { ipcMain.handle("GET_BASE_URL", () => {
return getBaseURL(); return getBaseURL();
}); });
// UPDATE Base URL // UPDATE Base URL
ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => { ipcMain.handle("UPDATE_BASE_URL", (event, url: string) => {
setBaseURL(url); setBaseURL(url);
return { api.defaults.baseURL = `${url}/api/v1/va`;
updated: true, console.log("Base URL updated to:", api.defaults.baseURL);
baseURL: url return {
}; updated: true,
}); baseURL: url
};
});
ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => { ipcMain.handle("get-token", async (_, key: "ACCESS_TOKEN" | "REFRESH_TOKEN") => {
return await getToken(key); return await getToken(key);
@ -168,39 +170,30 @@ ipcMain.handle("stop-project-tracking", async (event, payload) => {
// tabs opening and closing when and tracking start // tabs opening and closing when and tracking start
ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => { ipcMain.handle("tracking-tabs-opening-and-closing", async (event, payload) => {
try { try {
console.log("🟦 IPC tracking-tabs-opening-and-closing:", payload); console.log("🟦 IPC received payload:", payload);
let response; const response = await api.post("/log/project/track", payload);
if (!payload.id) {
// POST new log (new tab opened)
response = await api.post("/log/project/track", {
logId: payload.logId,
projectId: payload.projectId,
title: payload.title,
appName: payload.appName,
});
console.log(" New log created:", response);
} else {
// PATCH existing log (same tab, update)
response = await api.patch("/log/project/track", {
id: payload.id,
logId: payload.logId,
projectId: payload.projectId,
title: payload.title,
appName: payload.appName,
});
console.log("Log updated:", response);
}
return response; console.log("🟩 Log track API success:", response);
return {
success: true,
data: response.data,
};
} catch (error) { } catch (error) {
console.error("Error syncing project tab tracking:", error); console.error("❌ Log track API error:", error);
throw error;
return {
success: false,
message: "Something went wrong",
error,
};
} }
}); });
ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => { ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }) => {
try { try {
console.log("📥 get-project-logs called with:", { projectID, fromDate, toDate }); console.log("📥 get-project-logs called with:", { projectID, fromDate, toDate });
@ -211,11 +204,11 @@ ipcMain.handle("get-project-logs", async (event, { projectID, fromDate, toDate }
// 🧠 Append query params only if dates are provided // 🧠 Append query params only if dates are provided
if (fromDate && toDate) { if (fromDate && toDate) {
const params = new URLSearchParams({ // const params = new URLSearchParams({
from: fromDate, // from: fromDate,
to: toDate, // to: toDate,
}); // });
url += `?${params.toString()}`; url += `/${fromDate}/${toDate}`;
} }
console.log("url", url) console.log("url", url)
@ -518,15 +511,24 @@ ipcMain.handle("update-payment", async (event, data: Record<string, any>) => {
paymentMethod: data.paymentMethod, paymentMethod: data.paymentMethod,
}; };
// paymentMethod: 'payoneeraccount',
// payoneerAccount: 'check@gmail.com'
// Add payment-specific fields // Add payment-specific fields
if (data.paymentMethod === "wise" && data.wiseEmail) { if (data.paymentMethod === "wise" && data.wiseEmail) {
userDetails.wiseEmail = data.wiseEmail; userDetails.wiseEmail = data.wiseEmail;
// Clear veemAccount when switching to wise // Clear veemAccount when switching to wise
userDetails.veemAccount = ""; userDetails.veemAccount = "";
userDetails.payoneerAccount = ""
} else if (data.paymentMethod === "veem" && data.veemAccount) { } else if (data.paymentMethod === "veem" && data.veemAccount) {
userDetails.veemAccount = data.veemAccount; userDetails.veemAccount = data.veemAccount;
// Clear wiseEmail when switching to veem // Clear wiseEmail when switching to veem
userDetails.wiseEmail = ""; userDetails.wiseEmail = "";
userDetails.payoneerAccount = ""
} else if (data.paymentMethod === "payoneeraccount" && data.payoneerAccount) {
userDetails.payoneerAccount = data.payoneerAccount;
userDetails.veemAccount = "";
userDetails.wiseEmail = ""
} }
// Build final payload // Build final payload

View File

@ -178,6 +178,12 @@ app.on("ready", async () => {
// HANDLE CLOSE EVENTS // HANDLE CLOSE EVENTS
// ------------------------------------ // ------------------------------------
// handleCloseEvents(win); // handleCloseEvents(win);
mainWindow.on("close", () => {
if(mainWindow !== null) {
mainWindow.webContents.executeJavaScript(`window.__allowClose__ = true`);
}
});
}); });
@ -198,6 +204,7 @@ app.on('browser-window-focus', () => {
// }); // });
// --------------------------- // ---------------------------
// CUSTOM CLOSE BEHAVIOR // CUSTOM CLOSE BEHAVIOR
// --------------------------- // ---------------------------

View File

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

View File

@ -3,6 +3,7 @@ import { exec, execFile } from "child_process";
import fs from "fs"; import fs from "fs";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import axios from "axios";
// //
// ------------------ HELPERS ------------------ // ------------------ HELPERS ------------------
@ -51,22 +52,142 @@ function getWireGuardInstallerPath(): string {
// OS-specific wg0.conf // OS-specific wg0.conf
function getConfigPath(): string { // function getConfigPath(): string {
const base = isDev() // const base = isDev()
? path.join(process.cwd(), "resources", "wireguard") // ? path.join(process.cwd(), "resources", "wireguard")
: path.join(process.resourcesPath, "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") { if (process.platform === "win32") {
return path.join(base, "windows", "wg0.conf"); 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") { if (process.platform === "darwin") {
return path.join(base, "macos", "wg0.conf"); 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;
} }
throw new Error("Unsupported OS");
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 // Detect installed WireGuard.exe on Windows
function findWireGuardCLI(): string | null { function findWireGuardCLI(): string | null {
const possiblePaths = [ const possiblePaths = [
@ -143,18 +264,100 @@ async function ensureWireGuardInstalled(): Promise<string> {
// }); // });
// } // }
//
// ------------------ 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> { 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 cli = await ensureWireGuardInstalled();
const conf = getConfigPath(); const conf = await getConfigPath();
// Disable GUI popup and silent mode (ignore errors) // 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 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 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(() => {}); await execPromise(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v WireGuard /f`).catch(() => { });
// Start background service // Start background service
await execPromise(`sc start WireGuardManager`).catch(() => {}); await execPromise(`sc start WireGuardManager`).catch(() => { });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile(cli, ["/installtunnelservice", conf], (err) => { execFile(cli, ["/installtunnelservice", conf], (err) => {
@ -187,37 +390,234 @@ async function disconnectWindows(): Promise<string> {
} }
// async function ensureHomebrew(): Promise<void> {
// ------------------ MACOS CONNECT/DISCONNECT ------------------ try {
// await execPromise("brew --version");
return; // brew already installed
} catch (_) {
console.log("Homebrew missing. Installing...");
function connectMac(): Promise<string> { await execPromise(
const conf = getConfigPath(); '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
return execPromise(`sudo wg-quick up "${conf}"`).then(() => "macOS VPN connected"); );
// Update shell env — macOS uses zprofile for system shells
await execPromise(`echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile`);
}
} }
function disconnectMac(): Promise<string> { async function ensureWireGuardMac(): Promise<void> {
return execPromise(`sudo wg-quick down wg0`).then(() => "macOS VPN disconnected"); 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 ------------------ // ------------------ PUBLIC API ------------------
// //
export function startVPN(): Promise<string> { export async function startVPN(): Promise<string> {
const platform = os.platform(); const platform = os.platform();
if (platform === "win32") return connectWindows(); if (platform !== "win32" && platform !== "darwin") {
if (platform === "darwin") return connectMac(); throw new Error("Unsupported OS");
}
return Promise.reject("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 function stopVPN(): Promise<string> {
export async function stopVPN(): Promise<string> {
const platform = os.platform(); const platform = os.platform();
if (platform === "win32") return disconnectWindows(); try {
if (platform === "darwin") return disconnectMac(); if (platform === "win32") return await disconnectWindows();
if (platform === "darwin") return await disconnectMac();
} catch {
return "VPN not running";
}
return Promise.reject("Unsupported OS"); throw new Error("Unsupported OS");
} }

View File

@ -1,6 +1,14 @@
// hooks/useActiveAppTracker.tsx
import { useEffect, useRef, useCallback } from "react"; import { useEffect, useRef, useCallback } from "react";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setActiveWindow, WindowInfo } from "../store/windowSlice"; import { RootState } from "../store";
import { WindowInfo } from "../store/windowSlice";
import {
initializeProjectLogs,
updateWindowLog,
clearProjectLogs,
debugLogsState,
} from "../store/logsSlice";
interface DailyHours { interface DailyHours {
todayTotal: string; todayTotal: string;
@ -16,273 +24,223 @@ export const useActiveAppTracker = (
onLimitExceeded?: () => void onLimitExceeded?: () => void
) => { ) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { logsByProject } = useSelector((state: RootState) => state.logs);
const lastActiveRef = useRef<WindowInfo | null>(null); const lastActiveRef = useRef<WindowInfo | null>(null);
const lastLogIdRef = useRef<string | null>(null);
const lastTitleRef = useRef<string | null>(null); const lastTitleRef = useRef<string | null>(null);
const intervalIdRef = useRef<number | null>(null); const lastAppNameRef = useRef<string | null>(null);
const finalizeTrackingRef = useRef<(() => Promise<void>) | null>(null); const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
const limitCheckedRef = useRef<boolean>(false); const limitCheckedRef = useRef(false);
const isStoppingRef = useRef<boolean>(false); // ✅ Flag to prevent new requests const isStoppingRef = useRef(false);
const checkCountRef = useRef(0);
const canSend = !!(activeProjectLogId && projectId && !isStoppingRef.current); // 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})`);
// ✅ Parse time string to minutes // Reset refs
const parseTimeToMinutes = (timeStr: string): number => { lastActiveRef.current = null;
const hourMatch = timeStr.match(/(\d+)h/); lastTitleRef.current = null;
const minMatch = timeStr.match(/(\d+)m/); lastAppNameRef.current = null;
checkCountRef.current = 0;
const hours = hourMatch ? parseInt(hourMatch[1]) : 0; isStoppingRef.current = false;
const minutes = minMatch ? parseInt(minMatch[1]) : 0;
return hours * 60 + minutes;
};
// ✅ Check if daily limit is exceeded
const checkDailyLimit = (dailyHours?: DailyHours): boolean => {
if (!dailyHours) return false;
const todayTotalMins = parseTimeToMinutes(dailyHours.todayTotal);
const dailyLimitMins = parseTimeToMinutes(dailyHours.dailyLimit);
const remainingMins = parseTimeToMinutes(dailyHours.remaining);
console.log("⏰ Daily Limit Check:", {
todayTotal: `${todayTotalMins} mins`,
dailyLimit: `${dailyLimitMins} mins`,
remaining: `${remainingMins} mins`,
exceeded: remainingMins <= 0
});
if (remainingMins <= 0) {
return true;
} }
}, [isTracking, activeProjectLogId, projectId, projectName, dispatch]);
return false; // Stop tracking intervals
}; const stopIntervals = useCallback(() => {
// ✅ Send activity log (POST for new, PATCH for update)
const sendActivityLog = async (
projectLogId: string,
projectId: string,
title: string,
appName: string,
isNew: boolean
) => {
// ✅ CRITICAL: Block all requests when stopping
if (isStoppingRef.current) {
console.log("🚫 Stopping in progress — blocking activity log");
return null;
}
if (!canSend) {
console.log("🚫 Skipping API — missing required IDs");
return null;
}
try {
const payload: {
id?: string;
logId: string;
projectId: string;
title: string;
appName: string;
} = {
logId: projectLogId,
projectId,
title,
appName,
};
if (!isNew && lastLogIdRef.current) {
payload.id = lastLogIdRef.current;
}
console.log(isNew ? "➡️ POST new log" : "✏️ PATCH update log", payload);
const res = await window.electron.trackingTabsOpeningClosing(payload);
console.log("📊 Activity log response:", res);
// Store the log ID for future updates
if (isNew && res?.data?._id) {
lastLogIdRef.current = res.data._id;
console.log("💾 Stored log ID:", res.data._id);
}
// ✅ Check daily limit after every API call
if (res?.dailyHours) {
const limitExceeded = checkDailyLimit(res.dailyHours);
if (limitExceeded && !limitCheckedRef.current) {
limitCheckedRef.current = true;
console.warn("🚨 Daily limit exceeded!");
if (onLimitExceeded) {
setTimeout(() => {
onLimitExceeded();
}, 500);
}
}
}
return res;
} catch (error) {
console.error("❌ Activity log error:", error);
throw error;
}
};
// ✅ Stop the interval immediately
const stopInterval = useCallback(() => {
console.log("🛑 Stopping activity tracking interval...");
if (intervalIdRef.current) { if (intervalIdRef.current) {
clearInterval(intervalIdRef.current); clearInterval(intervalIdRef.current);
intervalIdRef.current = null; intervalIdRef.current = null;
console.log("✅ Interval cleared");
} }
}, []); }, []);
// ✅ Send final PATCH before stopping (called by handleStopTracking) // Debug function to check logs state
const sendFinalPatch = useCallback(async () => { const debugCurrentLogs = useCallback(() => {
console.log("=".repeat(60)); if (projectId) {
console.log("🛑 FINAL PATCH INITIATED"); dispatch(debugLogsState({ projectId }));
console.log("=".repeat(60));
// ✅ STEP 1: Set stopping flag IMMEDIATELY
isStoppingRef.current = true;
console.log("🔒 Stopping flag set — no new requests allowed");
// ✅ STEP 2: Stop the interval IMMEDIATELY
stopInterval();
if (!activeProjectLogId || !projectId) {
console.log("⚠️ Skipping final PATCH — no active project log ID");
return;
} }
}, [projectId, dispatch]);
// ✅ STEP 3: Send ONE final PATCH for the last active window // Finalize and send all logs when stopping tracking
if (lastLogIdRef.current && lastTitleRef.current && lastActiveRef.current) { // hooks/useActiveAppTracker.tsx - finalizeAndSendLogs function
try { const finalizeAndSendLogs = useCallback(
console.log("📊 Final Activity Details:"); async (vaId: string): Promise<boolean> => {
console.log(` • Window: ${lastTitleRef.current}`); console.log(" FINALIZING AND SENDING ALL LOGS");
console.log(` • App: ${lastActiveRef.current.owner.name}`);
console.log(` • Log ID: ${lastLogIdRef.current}`);
console.log(` • Project: ${projectId}`);
const result = await sendActivityLog( // Set stopping flag
activeProjectLogId, isStoppingRef.current = true;
projectId,
lastTitleRef.current,
lastActiveRef.current.owner.name || "Unknown",
false // PATCH (update existing log)
);
console.log("✅ Final PATCH sent successfully!"); // Stop intervals immediately
stopIntervals();
if (result?.dailyHours) { if (!activeProjectLogId || !projectId) {
console.log("📈 Final Daily Hours:"); console.log("⚠️ Skipping — no active project");
console.log(` • Today: ${result.dailyHours.todayTotal}`); return false;
console.log(` • Limit: ${result.dailyHours.dailyLimit}`);
console.log(` • Remaining: ${result.dailyHours.remaining}`);
} }
} catch (error) { const projectLogs = logsByProject[projectId];
console.error("❌ Final PATCH failed:", error); if (!projectLogs || projectLogs.logs.length === 0) {
throw error; console.log("ℹ️ No logs found for project", projectId);
} return true;
} else { }
console.log("ℹ️ No active window to finalize");
}
// ✅ STEP 4: Clean up all refs try {
console.log("🧹 Cleaning up tracking references..."); // Create a DEEP copy of the logs
lastActiveRef.current = null; const allLogs = JSON.parse(JSON.stringify(projectLogs.logs));
lastTitleRef.current = null;
lastLogIdRef.current = null;
limitCheckedRef.current = false;
console.log("✅ Cleanup complete");
console.log("=".repeat(60));
}, [activeProjectLogId, projectId, stopInterval]);
// ✅ Update ref whenever sendFinalPatch changes // 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(() => { useEffect(() => {
finalizeTrackingRef.current = sendFinalPatch; if (!isTracking || !projectId || !activeProjectLogId) {
}, [sendFinalPatch]); console.log("⏸️ Tracking inactive — stopping interval");
stopIntervals();
// ✅ Main tracking interval effect
useEffect(() => {
// Reset flags when tracking starts
if (isTracking) {
limitCheckedRef.current = false;
isStoppingRef.current = false;
console.log("🟢 Tracking started — flags reset");
}
if (!isTracking || !canSend) {
console.log("🟡 Tracking inactive — cleaning up");
stopInterval();
return; return;
} }
console.log(`🟢 Starting active tracker for: ${projectName}`); console.log(`🚀 Starting tracker for: ${projectName}`);
const fetchActiveApp = async () => { const checkActiveWindow = async () => {
// ✅ Double-check stopping flag before each fetch
if (isStoppingRef.current) { if (isStoppingRef.current) {
console.log("🚫 Stopping in progress — skipping fetch");
return; return;
} }
try { try {
const details = (await window.electron.getActiveAppDetails()) as WindowInfo; const details = (await window.electron.getActiveAppDetails()) as WindowInfo;
if (!details || !details.id) return;
const title = details.title; if (!details || !details.id || !details.owner?.name) {
const appName = details.owner.name; return;
const prevTitle = lastTitleRef.current; }
const hasChanged = !prevTitle || prevTitle !== title;
if (hasChanged) { const currentTitle = details.title;
// PATCH previous window before creating new log const currentAppName = details.owner.name;
if (lastLogIdRef.current && prevTitle) {
console.log("🔄 Updating previous window:", prevTitle);
await sendActivityLog(activeProjectLogId!, projectId!, prevTitle, appName, false);
}
// POST new window log checkCountRef.current += 1;
console.log("🆕 New window detected:", title);
await sendActivityLog(activeProjectLogId!, projectId!, title, appName, true);
// Update refs and Redux // Update Redux with current window activity
lastActiveRef.current = details; dispatch(
lastTitleRef.current = title; updateWindowLog({
dispatch(setActiveWindow({ window: details, projectId, projectName })); projectId,
} else { title: currentTitle,
console.log("⏸️ Same window, skipping"); 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) { } catch (error) {
console.error("❌ Failed to fetch active app:", error); console.error("❌ Failed to check active window:", error);
} }
}; };
// Initial fetch // Initial check
fetchActiveApp(); checkActiveWindow();
// Set up interval (every 5 seconds) // Set up interval (every 5 seconds)
intervalIdRef.current = setInterval(fetchActiveApp, 5000); intervalIdRef.current = setInterval(checkActiveWindow, 5000);
// Cleanup on unmount // Cleanup
return () => { return () => {
console.log("🧹 Cleaning up tracker interval"); console.log("🧹 Cleaning up tracker");
stopInterval(); stopIntervals();
}; };
}, [isTracking, activeProjectLogId, projectId, projectName, dispatch, stopInterval]); }, [
isTracking,
activeProjectLogId,
projectId,
projectName,
stopIntervals,
dispatch,
debugCurrentLogs,
]);
return { return {
finalizeTracking: finalizeTrackingRef.current, finalizeAndSendLogs,
stopInterval, stopIntervals,
setIsStopping: (value: boolean) => { setIsStopping: (value: boolean) => {
isStoppingRef.current = value; isStoppingRef.current = value;
console.log(`🔒 Stopping flag manually set to: ${value}`); },
} debugCurrentLogs,
}; };
}; };

View File

@ -39,6 +39,7 @@ import { canStartTracking } from "../utils/projectLimits";
import { useTheme } from "../helper/ThemeToggle"; import { useTheme } from "../helper/ThemeToggle";
import LogoutModal from "../modal/LogoutModal"; import LogoutModal from "../modal/LogoutModal";
import { parseTimeStringToSeconds } from "../utils/timeParser"; import { parseTimeStringToSeconds } from "../utils/timeParser";
import { clearAllLogs, clearProjectLogs } from "../store/logsSlice";
interface User { interface User {
id: string; id: string;
@ -100,6 +101,8 @@ const DashboardLayout = () => {
const { projectsData, projectSummary, selectedProject, projectTrackingMap } = const { projectsData, projectSummary, selectedProject, projectTrackingMap } =
useSelector((state: any) => state.projects); useSelector((state: any) => state.projects);
const { logsByProject } = useSelector((state: RootState) => state.logs);
const tracking = selectedProject const tracking = selectedProject
? projectTrackingMap[selectedProject.projectId] ? projectTrackingMap[selectedProject.projectId]
: null; : null;
@ -116,7 +119,7 @@ const DashboardLayout = () => {
{ label: "Schedule", path: "/dashboard/schedule", icon: <ClipboardClock size={18} /> }, { label: "Schedule", path: "/dashboard/schedule", icon: <ClipboardClock size={18} /> },
{ label: "Pays Slips", path: "/dashboard/pays-slips", icon: <CircleDollarSign size={18} /> }, { label: "Pays Slips", path: "/dashboard/pays-slips", icon: <CircleDollarSign size={18} /> },
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> }, { label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
{ label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> }, // { label: "VPN", path: "/dashboard/vpn", icon: <GlobeLock size={18} /> },
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> }, { label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
]; ];
@ -173,7 +176,7 @@ const DashboardLayout = () => {
}, [selectedProject]); }, [selectedProject]);
// Initialize useActiveAppTracker and store finalizeTracking in ref // Initialize useActiveAppTracker and store finalizeTracking in ref
const { finalizeTracking, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker( const { finalizeAndSendLogs, setIsStopping: setIsStoppingInTracker } = useActiveAppTracker(
isTracking, isTracking,
activeProjectLogId, activeProjectLogId,
selectedProject?.projectId ?? null, selectedProject?.projectId ?? null,
@ -184,95 +187,81 @@ const DashboardLayout = () => {
// Define handleStopTracking FIRST // Define handleStopTracking FIRST
const handleStopTracking = useCallback(async () => { const handleStopTracking = useCallback(async () => {
if (loadingStop || loadingStart) { if (loadingStop || loadingStart) {
console.log("⏸️ Already processing, skipping...");
return; return;
} }
if (!selectedProject) { if (!selectedProject) {
console.warn("⚠️ No selected project to stop");
return; return;
} }
console.log("=".repeat(70));
console.log("🛑 STOP TRACKING INITIATED"); console.log("🛑 STOP TRACKING INITIATED");
console.log("=".repeat(70));
console.log(`📦 Project: ${selectedProject.projectName}`);
console.log(`⏱️ Elapsed Time: ${elapsedTime}s`);
// Reset alert flag when stopping // Reset alert flag when stopping
limitAlertShownRef.current = false; limitAlertShownRef.current = false;
const currentStopTime = new Date().toISOString(); const currentStopTime = new Date().toISOString();
const projectId = selectedProject.projectId; const projectId = selectedProject.projectId;
const vaId = projectSummary.vaId || "";
// ✅ CRITICAL: Set stopping flag IMMEDIATELY to block all new tab tracking // STEP 1: Set stopping flag in tracker
console.log("🔒 Setting stopping flag...");
if (setIsStoppingInTracker) { if (setIsStoppingInTracker) {
setIsStoppingInTracker(true); setIsStoppingInTracker(true);
} }
// Set loading state
setIsStopping(true); setIsStopping(true);
setLoadingStop(true); setLoadingStop(true);
// STEP 2: Disconnect socket
handleSocketDisconnect(); handleSocketDisconnect();
// STEP 1: Clear local timer to prevent further client-side updates // STEP 3: Clear local timer
console.log("⏹️ STEP 1: Clearing local timer...");
if (timerRef.current) { if (timerRef.current) {
clearInterval(timerRef.current); clearInterval(timerRef.current);
timerRef.current = null; timerRef.current = null;
console.log("✅ Timer cleared");
} }
// STEP 2: Send final PATCH with last active window data // STEP 4: Finalize and send logs
console.log("📤 STEP 2: Sending final activity tracking..."); console.log("📤 STEP 4: Finalizing and sending logs...");
if (stopTrackingRef.current) { let logsSent = false;
if (finalizeAndSendLogs) {
try { try {
await stopTrackingRef.current(); // This calls sendFinalPatch() in the hook logsSent = await finalizeAndSendLogs(vaId); // Pass vaId here
console.log("✅ Final activity tracking completed"); console.log(logsSent ? "✅ Logs sent successfully" : "⚠️ Logs not sent");
} catch (error) { } catch (error) {
console.error("❌ Error during final tracking:", error); console.error("❌ Error sending logs:", error);
} }
} else {
console.log("⚠️ No tracking ref available");
} }
// STEP 3: Send LOGOUT to backend // STEP 5: Send LOGOUT to backend
console.log("🚪 STEP 3: Sending LOGOUT request..."); console.log("🚪 STEP 5: Sending LOGOUT request...");
try { try {
const payload = { const logoutPayload = {
vaId: projectSummary.vaId, vaId: vaId,
logId: activeProjectLogId,
projectId: projectId, projectId: projectId,
type: "LOGOUT", type: "LOGOUT",
time: currentStopTime time: currentStopTime
}; };
console.log("📡 Logout payload:", payload); await window.electron.stopProjectTracking(logoutPayload);
const res = await window.electron.stopProjectTracking(payload); console.log("✅ LOGOUT successful");
console.log("✅ LOGOUT successful:", res);
} catch (error) { } catch (error) {
console.error("❌ LOGOUT failed:", error); console.error("❌ LOGOUT failed:", error);
} }
// STEP 4: Update Redux tracking state // STEP 6: Update Redux tracking state
console.log("🔄 STEP 4: Updating Redux state..."); dispatch(stopTrackingReducer({ projectId }));
dispatch(
stopTrackingReducer({
projectId,
})
);
// STEP 6: Update UI (restore window)
console.log("🪟 STEP 6: Restoring window...");
await window.electron.toggleCloseBtn(true); await window.electron.toggleCloseBtn(true);
await window.electron.resizeWindow(500, 600); await window.electron.resizeWindow(500, 600);
toggleWindowSize.current = true; toggleWindowSize.current = true;
console.log("✅ Window restored");
// Allow React to process the state update // Allow React to process the state update
await new Promise(resolve => setTimeout(resolve, 50)); await new Promise(resolve => setTimeout(resolve, 50));
// STEP 5: Refresh project list to get updated work hours from server // STEP 7: Refresh project list
console.log("📊 STEP 5: Refreshing project list...");
try { try {
const projectListRes = await window.electron.getProjectList(); const projectListRes = await window.electron.getProjectList();
console.log("✅ Project list fetched:", projectListRes); console.log("✅ Project list fetched:", projectListRes);
@ -351,20 +340,27 @@ const DashboardLayout = () => {
console.log("🎉 TRACKING STOPPED SUCCESSFULLY"); console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
console.log("=".repeat(70)); console.log("=".repeat(70));
} }
// STEP 8: Reset logs for this project
if (logsSent && projectId) {
dispatch(clearProjectLogs({ projectId }));
}
console.log("🎉 TRACKING STOPPED SUCCESSFULLY");
}, [ }, [
selectedProject, selectedProject,
projectSummary, projectSummary,
elapsedTime,
dispatch, dispatch,
loadingStop, loadingStop,
loadingStart, loadingStart,
setIsStoppingInTracker setIsStoppingInTracker,
finalizeAndSendLogs,
]); ]);
// Update the ref whenever finalizeTracking changes // Update the ref whenever finalizeTracking changes
useEffect(() => { // useEffect(() => {
stopTrackingRef.current = finalizeTracking; // stopTrackingRef.current = finalizeTracking;
}, [finalizeTracking]); // }, [finalizeTracking]);
const socketRef = useRef<Socket | null>(null); const socketRef = useRef<Socket | null>(null);
const intervalRef = useRef<number | null>(null); const intervalRef = useRef<number | null>(null);
@ -404,6 +400,7 @@ const DashboardLayout = () => {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
dispatch(clearAllLogs());
const res = await window.electron.logout(); const res = await window.electron.logout();
if (res) { if (res) {
dispatch(clearUser()); dispatch(clearUser());
@ -459,6 +456,11 @@ const DashboardLayout = () => {
return; return;
} }
// Reset logs for this project if they exist
if (logsByProject[project.projectId]) {
dispatch(clearProjectLogs({ projectId: project.projectId }));
}
// CRITICAL FIX: Get the current accumulated time BEFORE starting timer // CRITICAL FIX: Get the current accumulated time BEFORE starting timer
const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime); const currentAccumulatedTime = selectedData?.accumulatedTime || parseTimeStringToSeconds(project.totalWorkTime);
@ -590,7 +592,7 @@ const DashboardLayout = () => {
if (!selectedProject?.projectId) return if (!selectedProject?.projectId) return
console.log(selectedProject?.projectId, "SelectedProject ID") console.log(selectedProject?.projectId, "SelectedProject ID")
socketRef.current = io("https://quality-friendly-goldfish.ngrok-free.app", { socketRef.current = io("http://dreadingly-unfighting-pandora.ngrok-free.dev", {
transports: ["websocket"], transports: ["websocket"],
secure: true, secure: true,
rejectUnauthorized: false, rejectUnauthorized: false,
@ -927,4 +929,6 @@ const DashboardLayout = () => {
); );
}; };
export default DashboardLayout; export default DashboardLayout;

View File

@ -6,6 +6,36 @@ import { Provider } from "react-redux";
import { store } from "./store"; import { store } from "./store";
import { ToastProvider } from './helper/ToastContext'; 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( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Provider store={store}> <Provider store={store}>
<ToastProvider> <ToastProvider>

View File

@ -152,6 +152,14 @@ const Dashboard: React.FC = () => {
}) })
); );
// 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 // Check if we already have a selected project that's being tracked
const currentlySelected = selectedProject; const currentlySelected = selectedProject;
const currentlyTracking = isTracking; const currentlyTracking = isTracking;
@ -208,9 +216,13 @@ const Dashboard: React.FC = () => {
fetchProfile(); fetchProfile();
} else { } else {
console.warn("⚠️ Unexpected response:", res); console.warn("⚠️ Unexpected response:", res);
// FIX: Also clear selected project on unexpected response
dispatch(setSelectedProjectSlice(null));
} }
} catch (err) { } catch (err) {
console.error("❌ Error fetching projects:", err); console.error("❌ Error fetching projects:", err);
// FIX: Clear selected project on error
dispatch(setSelectedProjectSlice(null));
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
@ -218,6 +230,7 @@ const Dashboard: React.FC = () => {
useEffect(() => { useEffect(() => {
fetchProjects(); fetchProjects();
fetchProfile()
}, []); }, []);
const handleRefresh = async () => { const handleRefresh = async () => {
@ -285,6 +298,7 @@ const Dashboard: React.FC = () => {
<h1 className="text-md font-bold">Dashboard</h1> <h1 className="text-md font-bold">Dashboard</h1>
</div> </div>
{/* Tracking Section */} {/* Tracking Section */}
{/* Tracking Section */}
<div className="w-full mb-4"> <div className="w-full mb-4">
<div className="card bg-base-100 border border-base-200 shadow-sm card-sm"> <div className="card bg-base-100 border border-base-200 shadow-sm card-sm">
<div className="card-body"> <div className="card-body">
@ -305,11 +319,6 @@ const Dashboard: React.FC = () => {
<p className="text-lg font-extrabold text-success"> <p className="text-lg font-extrabold text-success">
{formatTime(displayTime)} {formatTime(displayTime)}
</p> </p>
{/* <p className={`text-lg font-extrabold ${isLimitReached ? 'text-error' : 'text-success'}`}>
{formatTime(displayTime)}
{isLimitReached && " ⚠️"}
</p> */}
<div> <div>
<span className="text-base-content/70">Project:</span>{" "} <span className="text-base-content/70">Project:</span>{" "}
<span className="font-semibold"> <span className="font-semibold">
@ -356,7 +365,9 @@ const Dashboard: React.FC = () => {
</> </>
) : ( ) : (
<p className="text-sm text-base-content/60 text-center"> <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> </p>
)} )}
</div> </div>
@ -368,16 +379,16 @@ const Dashboard: React.FC = () => {
{/* 🔹 Sticky Header */} {/* 🔹 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"> <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> <p className="text-md font-bold">Projects</p>
{!selectedData?.isTracking && {!selectedData?.isTracking &&
<button <button
onClick={handleRefresh} onClick={handleRefresh}
disabled={isRefreshing } disabled={isRefreshing}
className="btn btn-xs btn-ghost gap-1 text-warning" className="btn btn-xs btn-ghost gap-1 text-warning"
title="Refresh projects" title="Refresh projects"
> >
<RefreshCw className={`w-3 h-3 text-warning ${isRefreshing ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-3 h-3 text-warning ${isRefreshing ? 'animate-spin' : ''}`} />
Refresh Refresh
</button>} </button>}
</div> </div>
{/* 🔹 Table or Empty State or Skeleton */} {/* 🔹 Table or Empty State or Skeleton */}

View File

@ -93,7 +93,7 @@ const Login = () => {
return ( 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 w-full max-w-md shadow-2xl bg-base-100 backdrop-blur-sm">
<div className="card-body"> <div className="card-body">
<div className="flex justify-center items-center mb-4"> <div className="flex justify-center items-center mb-4">

View File

@ -9,6 +9,8 @@ const PaysSlips = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [rows, setRows] = useState<any[]>([]); const [rows, setRows] = useState<any[]>([]);
useEffect(() => { useEffect(() => {
if (!projectSummary?.vaId) return; if (!projectSummary?.vaId) return;

View File

@ -15,10 +15,11 @@ const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { profile } = useSelector((state: RootState) => state.profile); const { profile } = useSelector((state: RootState) => state.profile);
const [method, setMethod] = useState(profile?.paymentMethod || ""); const [method, setMethod] = useState(profile?.paymentMethod || "");
const [wiseEmail, setWiseEmail] = useState(profile?.wiseEmail || ""); const [wiseEmail, setWiseEmail] = useState(profile?.wiseEmail || "");
const [veemAccount, setVeemAccount] = useState(profile?.veemAccount || ""); const [veemAccount, setVeemAccount] = useState(profile?.veemAccount || "");
const [payoneerAccount, setPayoneerAccount] = useState(profile?.payoneerAccount || "");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
@ -28,6 +29,7 @@ const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
setMethod(profile?.paymentMethod || ""); setMethod(profile?.paymentMethod || "");
setWiseEmail(profile?.wiseEmail || ""); setWiseEmail(profile?.wiseEmail || "");
setVeemAccount(profile?.veemAccount || ""); setVeemAccount(profile?.veemAccount || "");
setPayoneerAccount(profile?.payoneerAccount || "")
setError(""); setError("");
} }
}, [isOpen]); }, [isOpen]);
@ -46,12 +48,16 @@ const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
return setError("Wise email is required."); return setError("Wise email is required.");
} }
if (method === "wise" && !/^\S+@\S+\.\S+$/.test(wiseEmail)) { if (method === "veem" && !veemAccount.trim()) {
return setError("Enter a valid Wise email."); return setError("Veem email is required.");
} }
if (method === "veem" && !veemAccount.trim()) { if (method === "payoneeraccount" && !payoneerAccount.trim()) {
return setError("Veem account number is required."); return setError("Payoneer email is required.");
}
if ((method === "wise" || "veem" || "payoneeraccount") && !/^\S+@\S+\.\S+$/.test(wiseEmail)) {
return setError("Enter a valid email.");
} }
setLoading(true); setLoading(true);
@ -67,6 +73,8 @@ const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
updateData.wiseEmail = wiseEmail.trim(); updateData.wiseEmail = wiseEmail.trim();
} else if (method === "veem") { } else if (method === "veem") {
updateData.veemAccount = veemAccount.trim(); updateData.veemAccount = veemAccount.trim();
}else if (method === "payoneeraccount") {
updateData.payoneerAccount = payoneerAccount.trim();
} }
console.log("Sending to electron:", updateData); console.log("Sending to electron:", updateData);
@ -134,6 +142,7 @@ const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
<option value="">Select method</option> <option value="">Select method</option>
<option value="wise">Wise</option> <option value="wise">Wise</option>
<option value="veem">Veem</option> <option value="veem">Veem</option>
<option value="payoneeraccount">Payoneer account</option>
</select> </select>
</div> </div>
@ -169,6 +178,22 @@ const ChangePaymentModal: React.FC<ChangePaymentModalProps> = ({
</div> </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"> <div className="modal-action">
<button className="btn btn-sm btn-ghost" onClick={onClose}> <button className="btn btn-sm btn-ghost" onClick={onClose}>
Cancel Cancel

View File

@ -107,7 +107,7 @@ const Settings: React.FC = () => {
{profile?.veemAccount && ( {profile?.veemAccount && (
<div className="p-2 rounded bg-base-200/40"> <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"> <p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">
🏦 Veem Account 🏦 Veem Email
</p> </p>
<p className="text-xs font-medium truncate" title={profile?.veemAccount || "N/A"}> <p className="text-xs font-medium truncate" title={profile?.veemAccount || "N/A"}>
{profile?.veemAccount || "N/A"} {profile?.veemAccount || "N/A"}
@ -126,6 +126,19 @@ const Settings: React.FC = () => {
</div> </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 */} {/* Location - Full Width */}
<div className="p-2 rounded bg-base-200/40 col-span-2 sm:col-span-3"> <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"> <p className="text-xs text-base-content/60 flex items-center gap-1 mb-0.5">

View File

@ -1,5 +1,6 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import React from "react"; import React, { useEffect, useState } from "react";
interface TabActivity { interface TabActivity {
title: string; title: string;
@ -21,13 +22,44 @@ interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
projectName: string; projectName: string;
logs: ProjectSession[]; logs: ProjectSession[];
loading: boolean; 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 }) => {
const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs, loading, setLoading,
setLogs,
projectID, }) => {
if (!isOpen) return null; 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 getTimeDifference = (from: string, to: string) => {
const start = DateTime.fromISO(from); const start = DateTime.fromISO(from);
@ -43,12 +75,81 @@ const ProjectLogsModal: React.FC<Props> = ({ isOpen, onClose, projectName, logs,
}; };
}; };
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 ( return (
<dialog className="modal modal-open"> <dialog className="modal modal-open">
<div className="modal-box max-h-[97vh] max-w-4xl bg-base-100 flex flex-col"> <div className="modal-box max-h-[97vh] max-w-4xl bg-base-100 flex flex-col">
<h3 className="font-bold text-xs mb-3"> <div className="flex justify-between items-center mb-3">
🗂️ Project Logs — <span className="text-primary">{projectName}</span> <div className="font-bold text-xs">
</h3> 🗂️ 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 ? ( {loading ? (
<div className="flex justify-center items-center flex-grow"> <div className="flex justify-center items-center flex-grow">

View File

@ -35,6 +35,13 @@ const timesheetData: TimesheetEntry[] = [
{ id: 5, date: "2025-11-01", project: "AI Model Trainer", hours: 8, status: "Approved", remarks: "Normal day" }, { 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 { interface Project {
projectId: string; projectId: string;
projectName: string; projectName: string;
@ -55,8 +62,8 @@ const Timesheets: React.FC = () => {
console.log(" Timesheet in component:", windowLogs); console.log(" Timesheet in component:", windowLogs);
const [projectsData, setProjectsData] = useState<Project[]>([]); const [projectsData, setProjectsData] = useState<Project[]>([]);
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState<ProjectSession[]>([]);
// const [selectedProject, setSelectedProject] = useState<string | null>(null); const [selectedProjectLogId, setSelectedProjectLogId] = useState<string | null>(null);
const [selectedProjectName, setSelectedProjectName] = useState<string>(""); const [selectedProjectName, setSelectedProjectName] = useState<string>("");
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -64,13 +71,11 @@ const Timesheets: React.FC = () => {
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null); const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
const { selectedProject } = const { selectedProject } = useSelector((state: any) => state.projects);
useSelector((state: any) => state.projects); const selectedProjectId = selectedProject?.projectId ?? null;
const activeProjects = projectsData.filter(p => p.status === "ACTIVE"); const activeProjects = projectsData.filter(p => p.status === "ACTIVE");
console.log("selectedProject", selectedProject.projectId)
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
@ -124,7 +129,7 @@ const Timesheets: React.FC = () => {
try { try {
setModalOpen(true); setModalOpen(true);
setLoading(true); setLoading(true);
// setSelectedProject(projectID); setSelectedProjectLogId(projectID);
setSelectedProjectName(projectName); setSelectedProjectName(projectName);
console.log("projectID", projectID) console.log("projectID", projectID)
@ -225,13 +230,13 @@ const Timesheets: React.FC = () => {
<td>{formatWorkTime(entry.totalWorkTime)}</td> <td>{formatWorkTime(entry.totalWorkTime)}</td>
<td> <td>
<button <button
disabled={(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProject.projectId))} disabled={(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProjectId))}
onClick={() => onClick={() =>
handleViewLogs(entry.projectId, entry.projectName) handleViewLogs(entry.projectId, entry.projectName)
} }
className="btn btn-ghost btn-xs" className="btn btn-ghost btn-xs"
> >
{(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProject.projectId)) ? ( {(formatWorkTime(entry.totalWorkTime) === "0h 0m" || (isTracking && entry.projectId === selectedProjectId)) ? (
<EyeOff size={16} /> <EyeOff size={16} />
) : ( ) : (
<Eye size={16} /> <Eye size={16} />
@ -292,6 +297,9 @@ const Timesheets: React.FC = () => {
projectName={selectedProjectName} projectName={selectedProjectName}
logs={logs} logs={logs}
loading={loading} loading={loading}
setLoading={setLoading}
setLogs={setLogs}
projectID={selectedProjectLogId}
/> />
</main> </main>
); );

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Shield, ShieldAlert, RefreshCw, Wifi, Power } from "lucide-react"; import { Shield, ShieldAlert, RefreshCw, Wifi, Power, X } from "lucide-react";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@ -7,8 +7,8 @@ interface Props {
const VpnGuard: React.FC<Props> = ({ children }) => { const VpnGuard: React.FC<Props> = ({ children }) => {
const VPN_API = "http://10.0.0.2:3000"; const VPN_API = "http://10.0.0.1";
const DEFAULT_API = "https://quality-friendly-goldfish.ngrok-free.app" const DEFAULT_API = "http://dreadingly-unfighting-pandora.ngrok-free.dev"
const [checking, setChecking] = useState(true); const [checking, setChecking] = useState(true);
const [vpnConnected, setVpnConnected] = useState(true); const [vpnConnected, setVpnConnected] = useState(true);
@ -20,9 +20,13 @@ const VpnGuard: React.FC<Props> = ({ children }) => {
const res = await window.electron?.checkRegion(); const res = await window.electron?.checkRegion();
console.log("VpnGuard", res); console.log("VpnGuard", res);
setVpnConnected(false); if (res.status === 200) {
return const baseRes = await window.electron.updateBaseURL(DEFAULT_API);
setVpnConnected(true);
} else {
setVpnConnected(false);
return;
}
// setVpnConnected(res?.success === true); // setVpnConnected(res?.success === true);
} catch (error) { } catch (error) {
console.error("VPN check failed:", error); console.error("VPN check failed:", error);
@ -44,7 +48,7 @@ const VpnGuard: React.FC<Props> = ({ children }) => {
console.log("connectVPN result → ", res); console.log("connectVPN result → ", res);
// Only proceed if VPN actually connected // Only proceed if VPN actually connected
if (res === "Windows VPN connected silently") { if (res === "Windows VPN connected silently" || res === "VPN already connected and healthy") {
const vpnRes = await window.electron.updateBaseURL(VPN_API); const vpnRes = await window.electron.updateBaseURL(VPN_API);
console.log("vpnRes", vpnRes); console.log("vpnRes", vpnRes);
@ -58,6 +62,17 @@ const VpnGuard: React.FC<Props> = ({ children }) => {
} }
} catch (error) { } catch (error) {
console.log(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 { } finally {
setChecking(false); setChecking(false);
} }
@ -116,6 +131,7 @@ const VpnGuard: React.FC<Props> = ({ children }) => {
<div className="min-h-screen flex flex-col justify-center items-center bg-base-200 p-4"> <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="max-w-md w-full">
<div className="card bg-base-100 shadow-2xl"> <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"> <div className="card-body items-center text-center">
{/* Icon */} {/* Icon */}
@ -153,14 +169,14 @@ const VpnGuard: React.FC<Props> = ({ children }) => {
</button> </button>
{/* Disconnect VPN (Disabled when NOT connected) */} {/* Disconnect VPN (Disabled when NOT connected) */}
<button {/* <button
className="btn btn-error btn-wide gap-2 mt-3" className="btn btn-error btn-wide gap-2 mt-3"
onClick={disconnectVPN} onClick={disconnectVPN}
// disabled={!vpnConnected || checking} // Disable if not connected // disabled={!vpnConnected || checking} // Disable if not connected
> >
<Power className="w-4 h-4" /> <Power className="w-4 h-4" />
Disconnect VPN Disconnect VPN
</button> </button> */}
<div className="divider"></div> <div className="divider"></div>
<p className="text-xs opacity-60"> <p className="text-xs opacity-60">

View File

@ -4,6 +4,7 @@ import windowReducer from "./windowSlice";
import projectsReducer from "./projectSlice"; import projectsReducer from "./projectSlice";
import userReducer from "./userSlice"; import userReducer from "./userSlice";
import profileReducer from "./profileSlice"; import profileReducer from "./profileSlice";
import logsReducer from './logsSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -11,6 +12,7 @@ export const store = configureStore({
projects: projectsReducer, projects: projectsReducer,
user: userReducer, user: userReducer,
profile: profileReducer, profile: profileReducer,
logs: logsReducer,
}, },
}); });

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;

28
types.d.ts vendored
View File

@ -142,6 +142,24 @@ interface RegionResponse {
} }
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 { interface Window {
electron: { electron: {
login: (credentials: { email: string; password: string }) => Promise<any>; login: (credentials: { email: string; password: string }) => Promise<any>;
@ -167,18 +185,12 @@ interface Window {
}) => Promise<any>; }) => Promise<any>;
stopProjectTracking: (data: { stopProjectTracking: (data: {
vaId: string; vaId: string;
logId: string,
projectId: string; projectId: string;
type: string; type: string;
time: string time: string
}) => Promise<any>; }) => Promise<any>;
trackingTabsOpeningClosing: (data: { trackingTabsOpeningClosing: (data: TrackingPayload) => Promise<any>;
id?: string;
logId: string;
projectId: string;
title: string;
appName: string;
}) => Promise<any>;
getProjectLogs: ( getProjectLogs: (
projectId: string, projectId: string,
fromDate?: string, fromDate?: 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==