Compare commits
No commits in common. "6783fba264c29dbe603d171147a6121d02489d5c" and "27c6caceb434e6a47a20efa6592597752e759cea" have entirely different histories.
6783fba264
...
27c6caceb4
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<!-- <link rel="icon" type="image/svg+xml" href="" /> -->
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>VA Tracker</title>
|
<title>VA Tracker</title>
|
||||||
<meta
|
<meta
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz",
|
||||||
"integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
|
"integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.24.2",
|
"@babel/code-frame": "^7.24.2",
|
||||||
|
|
@ -2789,7 +2788,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
|
@ -2978,6 +2976,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
||||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
|
|
@ -2996,6 +2995,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
|
||||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
|
|
@ -3017,6 +3017,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
|
|
@ -3031,13 +3032,15 @@
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3281,7 +3284,6 @@
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001587",
|
"caniuse-lite": "^1.0.30001587",
|
||||||
"electron-to-chromium": "^1.4.668",
|
"electron-to-chromium": "^1.4.668",
|
||||||
|
|
@ -3854,6 +3856,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
|
||||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
|
|
@ -4035,6 +4038,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
|
|
@ -4047,6 +4051,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
|
||||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
|
|
@ -4458,7 +4463,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.5.tgz",
|
||||||
"integrity": "sha512-ocnZV44ZqInoSFaY54fF7BlCtw+WtbrjyPrkBhaB+Ztn7GPKjmFgRbIKytifJ8h9Cib8jdFRMgjCUtkU45Y6DA==",
|
"integrity": "sha512-ocnZV44ZqInoSFaY54fF7BlCtw+WtbrjyPrkBhaB+Ztn7GPKjmFgRbIKytifJ8h9Cib8jdFRMgjCUtkU45Y6DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "25.0.5",
|
"app-builder-lib": "25.0.5",
|
||||||
"builder-util": "25.0.3",
|
"builder-util": "25.0.3",
|
||||||
|
|
@ -4682,6 +4686,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.5.tgz",
|
||||||
"integrity": "sha512-N2U7LGSdt4hmEhjEeIV2XJbjj2YIrTL6enfsGKfOhGTpL6GEejUmT3gjdKUqKBS5+NBx0GWhnEwD3MpO2P6Nfg==",
|
"integrity": "sha512-N2U7LGSdt4hmEhjEeIV2XJbjj2YIrTL6enfsGKfOhGTpL6GEejUmT3gjdKUqKBS5+NBx0GWhnEwD3MpO2P6Nfg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "25.0.5",
|
"app-builder-lib": "25.0.5",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|
@ -4694,6 +4699,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
|
|
@ -4708,6 +4714,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -4720,6 +4727,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -5432,7 +5440,8 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
|
|
@ -6360,7 +6369,8 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
|
|
@ -6621,6 +6631,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
|
|
@ -6633,6 +6644,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
|
|
@ -6647,13 +6659,15 @@
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -6931,31 +6945,36 @@
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/log-symbols": {
|
"node_modules/log-symbols": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
|
|
@ -7558,6 +7577,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -8157,7 +8177,8 @@
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
|
|
@ -8238,7 +8259,6 @@
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -8250,7 +8270,6 @@
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
|
|
@ -8402,6 +8421,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -8411,6 +8431,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.1"
|
||||||
},
|
},
|
||||||
|
|
@ -9365,6 +9386,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
|
|
@ -9789,7 +9811,6 @@
|
||||||
"version": "5.4.7",
|
"version": "5.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
|
||||||
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
|
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|
@ -10204,6 +10225,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
|
||||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
|
|
@ -10218,6 +10240,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
|
||||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
// electron/ipc/apiHandler.ts
|
|
||||||
import { ipcMain } from 'electron';
|
|
||||||
import api from '../api/axiosClient.js';
|
|
||||||
|
|
||||||
interface ApiRequestArgs {
|
|
||||||
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
||||||
url: string;
|
|
||||||
data?: any;
|
|
||||||
config?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('api:request', async (_, args: ApiRequestArgs) => {
|
|
||||||
try {
|
|
||||||
const response = await api.request({
|
|
||||||
method: args.method,
|
|
||||||
url: args.url,
|
|
||||||
data: args.data,
|
|
||||||
...args.config,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, data: response };
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[API Handler Error]', error.message);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
status: error.response?.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
// electron/api/axiosClient.ts
|
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
||||||
import { getToken } from '../utils/tokenHelper.js';
|
|
||||||
|
|
||||||
const TOKEN_KEY = 'auth_token';
|
|
||||||
|
|
||||||
const api: AxiosInstance = axios.create({
|
|
||||||
baseURL: 'https://api.example.com', // 🔁 your backend
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Request Interceptor ---
|
|
||||||
api.interceptors.request.use(
|
|
||||||
async (config: AxiosRequestConfig) : Promise<any> => {
|
|
||||||
const token = await getToken(TOKEN_KEY);
|
|
||||||
if (token && config.headers) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Axios] ${config.method?.toUpperCase()} ${config.url}`);
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error)
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Response Interceptor ---
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(response: AxiosResponse) => response.data,
|
|
||||||
async (error) => {
|
|
||||||
console.error('[Axios Error]', error.message);
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
console.warn('Token expired — trigger refresh logic if needed');
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default api;
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
// electron/ipc/tokenHandler.ts
|
|
||||||
import { ipcMain } from 'electron';
|
|
||||||
import { getToken, saveToken, clearToken } from '../utils/tokenHelper.js';
|
|
||||||
import { handleLogin, takeScreenshot } from '../utils/utilityFunctions.js';
|
|
||||||
|
|
||||||
ipcMain.handle('get-token', async (_, TOKEN_KEY: string) => {
|
|
||||||
return await getToken(TOKEN_KEY);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('save-token', async (_, TOKEN_KEY: string, token: string) => {
|
|
||||||
await saveToken(TOKEN_KEY, token);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('clear-token', async (_, TOKEN_KEY: string) => {
|
|
||||||
await clearToken(TOKEN_KEY);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('upload-screenshot', async () => {
|
|
||||||
takeScreenshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle login request from renderer
|
|
||||||
ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => {
|
|
||||||
return handleLogin(credentials);
|
|
||||||
});
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import { app, BrowserWindow } from 'electron';
|
import { app, BrowserWindow, Menu, desktopCapturer, ipcMain, safeStorage } from 'electron';
|
||||||
import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
|
import { ipcMainHandle, ipcMainOn, isDev } from './util.js';
|
||||||
import { getStaticData, pollResources } from './resourceManager.js';
|
import { getStaticData, pollResources } from './resourceManager.js';
|
||||||
import { getPreloadPath, getUIPath } from './pathResolver.js';
|
import { getPreloadPath, getUIPath } from './pathResolver.js';
|
||||||
|
import { createTray } from './tray.js';
|
||||||
|
import { createMenu } from './menu.js';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
import "./ipc/ipcHandler.js"
|
import * as fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import axios from 'axios';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
app.on('ready', async () => {
|
app.on('ready', async () => {
|
||||||
console.log("ready")
|
console.log("ready")
|
||||||
|
|
@ -50,6 +56,132 @@ app.on('ready', async () => {
|
||||||
// await takeScreenshot();
|
// await takeScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Blur using an offscreen canvas
|
||||||
|
async function blurAndCompressBase64Image(base64: string) : Promise<Buffer> {
|
||||||
|
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
||||||
|
const outputBuffer = await sharp(buffer)
|
||||||
|
.blur(8) // adjust blur radius here
|
||||||
|
.jpeg({ quality: 70 }) // compress output
|
||||||
|
.toBuffer();
|
||||||
|
return outputBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takeScreenshot() {
|
||||||
|
// const sources = await desktopCapturer.getSources({ types: ["screen"] });
|
||||||
|
const sources = await desktopCapturer.getSources({
|
||||||
|
types: ['window'], thumbnailSize: {
|
||||||
|
height: 768,
|
||||||
|
width: 1366
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Capture the first screen (you can choose others by filtering sources)
|
||||||
|
const screen = sources[0];
|
||||||
|
let image = screen.thumbnail.toDataURL();
|
||||||
|
console.log("blurAndCompressBase64Image Calling")
|
||||||
|
|
||||||
|
const blurredBuffer = await blurAndCompressBase64Image(image);
|
||||||
|
|
||||||
|
const folderPath = "C:\\Users\\Public\\vatracker\\screenshots";
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
fs.mkdirSync(folderPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`);
|
||||||
|
fs.writeFileSync(filePath, blurredBuffer);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`Screenshot saved at: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('upload-screenshot', async () => {
|
||||||
|
takeScreenshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function saveToken(accessToken: string, refreshToken: string | null = null) {
|
||||||
|
const encryptedAccessToken = safeStorage.encryptString(accessToken);
|
||||||
|
let encryptedRefreshToken = null;
|
||||||
|
|
||||||
|
if (refreshToken != null) {
|
||||||
|
encryptedRefreshToken = safeStorage.encryptString(refreshToken);
|
||||||
|
}
|
||||||
|
// Store encrypted token (example: using a file or electron-store)
|
||||||
|
// For simplicity, using Electron's built-in localStorage is not available in main, so use electron-store or filesystem.
|
||||||
|
// Here assume electron-store is already installed and set up:
|
||||||
|
const store = new Store();
|
||||||
|
store.set("access_token", encryptedAccessToken.toString('base64'));
|
||||||
|
if (encryptedRefreshToken != null) {
|
||||||
|
store.set("refresh_token", encryptedRefreshToken.toString('base64'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getToken(TOKEN_KEY: string) {
|
||||||
|
const store = new Store();
|
||||||
|
const encryptedBase64 = store.get(TOKEN_KEY) as string | undefined;
|
||||||
|
if (!encryptedBase64) return null;
|
||||||
|
const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
|
||||||
|
return safeStorage.decryptString(encryptedBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('get-token', async (event, TOKEN_KEY) => {
|
||||||
|
// return decrypted token from secure storage
|
||||||
|
return await getToken(TOKEN_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-token', async (event, token) => {
|
||||||
|
await saveToken(token);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle login request from renderer
|
||||||
|
ipcMain.handle('login-request', async (event, credentials: { email: string, password: string }) => {
|
||||||
|
try {
|
||||||
|
// Call your login API
|
||||||
|
console.log("🟡 login Request received", credentials);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("➡️ Calling API...");
|
||||||
|
const response = await axios.post(
|
||||||
|
'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login',
|
||||||
|
{ userDetails: { ...credentials } },
|
||||||
|
{ headers: { "ngrok-skip-browser-warning": 69420 } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { accessToken, refreshToken } = response.data;
|
||||||
|
await saveToken(accessToken, refreshToken);
|
||||||
|
return { success: true, user: response.data.user };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if ("response" in error && typeof (error.response) == "object" && error.response !== null && ("data" in error.response)) {
|
||||||
|
console.log("❌ login API call failed:", error?.response.data);
|
||||||
|
console.log("Server responded:", error?.response.data);
|
||||||
|
return { success: false, error: error?.response.data || "Request failed" };
|
||||||
|
} else {
|
||||||
|
return { success: false, error: error?.message || "Check you Internet connection and try again" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof err === "object" && err !== null && "response" in err) {
|
||||||
|
const axiosErr = err as any;
|
||||||
|
return { success: false, error: axiosErr.response?.data?.message || "Unknown error" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: "Unexpected error occurred" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
function handleCloseEvents(mainWindow: BrowserWindow) {
|
function handleCloseEvents(mainWindow: BrowserWindow) {
|
||||||
let willClose = false;
|
let willClose = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import { contextBridge, ipcRenderer } from "electron";
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
|
||||||
console.log("setting PreLoad")
|
console.log("setting PreLoad")
|
||||||
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
subscribeStatistics: (callback) =>
|
subscribeStatistics: (callback) =>
|
||||||
ipcOn('statistics', (stats) => {
|
ipcOn('statistics', (stats) => {
|
||||||
|
|
@ -42,10 +40,3 @@ function ipcSend<Key extends keyof EventPayloadMapping>(
|
||||||
) {
|
) {
|
||||||
electron.ipcRenderer.send(key, payload);
|
electron.ipcRenderer.send(key, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
getToken: (key: string) => ipcRenderer.invoke('get-token', key),
|
|
||||||
saveToken: (key: string, token: string) => ipcRenderer.invoke('save-token', key, token),
|
|
||||||
clearToken: (key: string) => ipcRenderer.invoke('clear-token', key),
|
|
||||||
});
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
// electron/utils/tokenHelper.ts
|
|
||||||
import { safeStorage } from 'electron';
|
|
||||||
import Store from 'electron-store';
|
|
||||||
|
|
||||||
const store = new Store();
|
|
||||||
|
|
||||||
export async function getToken(TOKEN_KEY: string): Promise<string | null> {
|
|
||||||
const encryptedBase64 = store.get(TOKEN_KEY) as string | undefined;
|
|
||||||
if (!encryptedBase64) return null;
|
|
||||||
const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
|
|
||||||
return safeStorage.decryptString(encryptedBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveToken(TOKEN_KEY: string, token: string): Promise<void> {
|
|
||||||
const encryptedBuffer = safeStorage.encryptString(token);
|
|
||||||
store.set(TOKEN_KEY, encryptedBuffer.toString('base64'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clearToken(TOKEN_KEY: string): Promise<void> {
|
|
||||||
store.delete(TOKEN_KEY);
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import { desktopCapturer } from "electron";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import axios from 'axios';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import { saveToken } from "./tokenHelper.js";
|
|
||||||
|
|
||||||
// Blur using an offscreen canvas
|
|
||||||
async function blurAndCompressBase64Image(base64: string): Promise<Buffer> {
|
|
||||||
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
|
||||||
const outputBuffer = await sharp(buffer)
|
|
||||||
.blur(8) // adjust blur radius here
|
|
||||||
.jpeg({ quality: 70 }) // compress output
|
|
||||||
.toBuffer();
|
|
||||||
return outputBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const takeScreenshot = async () => {
|
|
||||||
// const sources = await desktopCapturer.getSources({ types: ["screen"] });
|
|
||||||
const sources = await desktopCapturer.getSources({
|
|
||||||
types: ['window'], thumbnailSize: {
|
|
||||||
height: 768,
|
|
||||||
width: 1366
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Capture the first screen (you can choose others by filtering sources)
|
|
||||||
const screen = sources[0];
|
|
||||||
let image = screen.thumbnail.toDataURL();
|
|
||||||
console.log("blurAndCompressBase64Image Calling")
|
|
||||||
|
|
||||||
const blurredBuffer = await blurAndCompressBase64Image(image);
|
|
||||||
|
|
||||||
const folderPath = "C:\\Users\\Public\\vatracker\\screenshots";
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
if (!fs.existsSync(folderPath)) {
|
|
||||||
fs.mkdirSync(folderPath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(folderPath, `screenshot-${Date.now()}.png`);
|
|
||||||
fs.writeFileSync(filePath, blurredBuffer);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log(`Screenshot saved at: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handleLogin = async (credentials: { email: string, password: string }) => {
|
|
||||||
try {
|
|
||||||
// Call your login API
|
|
||||||
console.log("🟡 login Request received", credentials);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("➡️ Calling API...");
|
|
||||||
const response = await axios.post(
|
|
||||||
'https://wired-terminally-muskrat.ngrok-free.app/api/v1/va/login',
|
|
||||||
{ userDetails: { ...credentials } },
|
|
||||||
{ headers: { "ngrok-skip-browser-warning": 69420 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("API CAlled", response)
|
|
||||||
const { accessToken, refreshToken } = response.data;
|
|
||||||
await saveToken(accessToken, refreshToken);
|
|
||||||
return { success: true, user: response.data.user };
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if ("response" in error && typeof (error.response) == "object" && error.response !== null && ("data" in error.response)) {
|
|
||||||
console.log("❌ login API call failed:", error?.response.data);
|
|
||||||
console.log("Server responded:", error?.response.data);
|
|
||||||
return { success: false, error: error?.response.data || "Request failed" };
|
|
||||||
} else {
|
|
||||||
return { success: false, error: error?.message || "Check you Internet connection and try again" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
return { success: false, error: err.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof err === "object" && err !== null && "response" in err) {
|
|
||||||
const axiosErr = err as any;
|
|
||||||
return { success: false, error: axiosErr.response?.data?.message || "Unknown error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, error: "Unexpected error occurred" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
|
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
|
|
@ -8,27 +8,23 @@ import ProtectedRoute from "./routes/ProtectedRoute"
|
||||||
import PublicRoute from "./routes/PublicRoute";
|
import PublicRoute from "./routes/PublicRoute";
|
||||||
import { ThemeToggle } from "./helper/ThemeToggle"
|
import { ThemeToggle } from "./helper/ThemeToggle"
|
||||||
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
import LeaveAndAdjustments from "./pages/LeaveAndAdjustments/LeaveAndAdjustments";
|
||||||
import Timesheets from "./pages/Timesheets/Timesheets";
|
|
||||||
import Settings from "./pages/Settings/Settings";
|
|
||||||
import HelpSupport from "./pages/HelpSupport/HelpSupport";
|
|
||||||
|
|
||||||
|
// ✅ Define User type (customize based on your structure)
|
||||||
interface User {
|
interface User {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const theme = ThemeToggle();
|
const theme = ThemeToggle();
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
// ✅ Apply theme
|
||||||
// Apply theme
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
// Load user from localStorage
|
// ✅ Load user from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem("user");
|
const storedUser = localStorage.getItem("user");
|
||||||
if (storedUser) {
|
if (storedUser) {
|
||||||
|
|
@ -40,8 +36,6 @@ const App: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
@ -67,9 +61,6 @@ const App: React.FC = () => {
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="projects" element={<Projects />} />
|
<Route path="projects" element={<Projects />} />
|
||||||
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
|
<Route path="leave-adjustments" element={<LeaveAndAdjustments />} />
|
||||||
<Route path="timesheet" element={<Timesheets />} />
|
|
||||||
<Route path="settings" element={<Settings />} />
|
|
||||||
<Route path="help-support" element={<HelpSupport />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Default Redirect */}
|
{/* Default Redirect */}
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,3 @@
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
input::-webkit-outer-spin-button,
|
|
||||||
input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,324 +1,77 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect } from 'react'
|
||||||
import {
|
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'
|
||||||
Outlet,
|
import { Bell, Menu, LogOut, User, Settings, Home, FolderKanban, X, CalendarDays } from 'lucide-react'
|
||||||
NavLink,
|
|
||||||
useNavigate,
|
|
||||||
useLocation,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import {
|
|
||||||
Bell,
|
|
||||||
Menu,
|
|
||||||
LogOut,
|
|
||||||
User,
|
|
||||||
Settings,
|
|
||||||
Home,
|
|
||||||
FolderKanban,
|
|
||||||
X,
|
|
||||||
CalendarDays,
|
|
||||||
SettingsIcon,
|
|
||||||
TimerIcon,
|
|
||||||
HelpCircleIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Project {
|
|
||||||
id: number;
|
|
||||||
projectName: string;
|
|
||||||
clientName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectTrackingData {
|
|
||||||
elapsedTime: number;
|
|
||||||
startTime: Date | null;
|
|
||||||
endTime: Date | null;
|
|
||||||
isTracking: boolean;
|
|
||||||
isPaused: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrackingState {
|
|
||||||
selectedProject: Project | null;
|
|
||||||
projectTrackingMap: Map<number, ProjectTrackingData>;
|
|
||||||
isTracking: boolean;
|
|
||||||
isPaused: boolean;
|
|
||||||
elapsedTime: number;
|
|
||||||
startTime: Date | null;
|
|
||||||
endTime: Date | null;
|
|
||||||
handleStartTracking: (project: Project) => void;
|
|
||||||
handlePauseResume: () => void;
|
|
||||||
handleStopTracking: () => void;
|
|
||||||
handleProjectSelect: (id: number) => void;
|
|
||||||
projectsData: Project[];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
setUser: React.Dispatch<React.SetStateAction<User | null>>;
|
||||||
// trackingState: TrackingState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(true);
|
const [isSidebarOpen, setSidebarOpen] = useState(true)
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const location = useLocation();
|
const location = useLocation()
|
||||||
|
|
||||||
|
|
||||||
// Tracking states
|
|
||||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
|
||||||
const [isTracking, setIsTracking] = useState(false);
|
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
|
||||||
const [elapsedTime, setElapsedTime] = useState(0);
|
|
||||||
const [startTime, setStartTime] = useState<Date | null>(null);
|
|
||||||
const [endTime, setEndTime] = useState<Date | null>(null);
|
|
||||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
||||||
|
|
||||||
const [projectsData, setProjectsData] = useState<Project[]>([
|
|
||||||
{ id: 1, projectName: "CRM Redesign", clientName: "Salesforce" },
|
|
||||||
{ id: 2, projectName: "AI Assistant", clientName: "OpenAI" },
|
|
||||||
{ id: 3, projectName: "Marketing Dashboard", clientName: "Google" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [projectTrackingMap, setProjectTrackingMap] = useState<
|
|
||||||
Map<number, ProjectTrackingData>
|
|
||||||
>(new Map());
|
|
||||||
|
|
||||||
|
// ✅ Handle responsive behavior properly
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const mobile = window.innerWidth < 768;
|
const mobile = window.innerWidth < 768
|
||||||
setIsMobile(mobile);
|
setIsMobile(mobile)
|
||||||
setSidebarOpen(!mobile);
|
setSidebarOpen(!mobile) // open on desktop, closed on mobile
|
||||||
};
|
}
|
||||||
|
|
||||||
handleResize();
|
handleResize()
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener('resize', handleResize)
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem('user')
|
||||||
setUser(null);
|
setUser(null)
|
||||||
navigate("/login");
|
navigate('/login')
|
||||||
};
|
}
|
||||||
|
|
||||||
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen)
|
||||||
|
|
||||||
const closeSidebarOnMobile = () => {
|
const closeSidebarOnMobile = () => {
|
||||||
if (isMobile) setSidebarOpen(false);
|
if (isMobile) setSidebarOpen(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ label: "Dashboard", path: "/dashboard", icon: <Home size={18} />, exact: true },
|
{ label: 'Dashboard', path: '/dashboard', icon: <Home size={18} />, exact: true },
|
||||||
{ label: "Projects", path: "/dashboard/projects", icon: <FolderKanban size={18} /> },
|
{ label: 'Projects', path: '/dashboard/projects', icon: <FolderKanban size={18} /> },
|
||||||
{ label: "PTO", path: "/dashboard/leave-adjustments", icon: <CalendarDays size={18} /> },
|
{ label: 'Leave & Adjustments', path: '/dashboard/leave-adjustments', icon: <CalendarDays size={18} /> },
|
||||||
{ label: "Timesheet", path: "/dashboard/timesheet", icon: <TimerIcon size={18} /> },
|
]
|
||||||
{ label: "Settings", path: "/dashboard/settings", icon: <SettingsIcon size={18} /> },
|
|
||||||
{ label: "Help & Support", path: "/dashboard/help-support", icon: <HelpCircleIcon size={18} /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
// Sync global states with selected project tracking data
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedProject) {
|
|
||||||
const projectData = projectTrackingMap.get(selectedProject.id);
|
|
||||||
if (projectData) {
|
|
||||||
setIsTracking(projectData.isTracking);
|
|
||||||
setIsPaused(projectData.isPaused);
|
|
||||||
setElapsedTime(projectData.elapsedTime);
|
|
||||||
setStartTime(projectData.startTime);
|
|
||||||
setEndTime(projectData.endTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [selectedProject, projectTrackingMap]);
|
|
||||||
|
|
||||||
// Start tracking
|
|
||||||
const handleStartTracking = (project: Project) => {
|
|
||||||
// Clear any existing timer
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearInterval(timerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update project tracking map
|
|
||||||
setProjectTrackingMap((prev) => {
|
|
||||||
const updated = new Map(prev);
|
|
||||||
const current = updated.get(project.id) || {
|
|
||||||
elapsedTime: 0,
|
|
||||||
startTime: null,
|
|
||||||
endTime: null,
|
|
||||||
isTracking: false,
|
|
||||||
isPaused: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
current.startTime = now;
|
|
||||||
current.isTracking = true;
|
|
||||||
current.isPaused = false;
|
|
||||||
updated.set(project.id, current);
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update global tracking states
|
|
||||||
setIsTracking(true);
|
|
||||||
setIsPaused(false);
|
|
||||||
setStartTime(new Date());
|
|
||||||
|
|
||||||
// Start the interval timer
|
|
||||||
timerRef.current = setInterval(() => {
|
|
||||||
setProjectTrackingMap((prev) => {
|
|
||||||
const updated = new Map(prev);
|
|
||||||
const current = updated.get(project.id);
|
|
||||||
|
|
||||||
// Only increment when tracking AND not paused
|
|
||||||
if (current && current.isTracking && !current.isPaused) {
|
|
||||||
current.elapsedTime += 1;
|
|
||||||
updated.set(project.id, { ...current });
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pause/Resume tracking
|
|
||||||
const handlePauseResume = () => {
|
|
||||||
if (!selectedProject) return;
|
|
||||||
|
|
||||||
// Get current state before toggling to avoid double-click issues
|
|
||||||
const currentProjectData = projectTrackingMap.get(selectedProject.id);
|
|
||||||
if (!currentProjectData || !currentProjectData.isTracking) return;
|
|
||||||
|
|
||||||
const newPausedState = !currentProjectData.isPaused;
|
|
||||||
|
|
||||||
setProjectTrackingMap((prev) => {
|
|
||||||
const updated = new Map(prev);
|
|
||||||
const current = updated.get(selectedProject.id);
|
|
||||||
if (!current) return updated;
|
|
||||||
|
|
||||||
// Set the new pause state
|
|
||||||
current.isPaused = newPausedState;
|
|
||||||
updated.set(selectedProject.id, { ...current });
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update global isPaused state immediately
|
|
||||||
setIsPaused(newPausedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stop tracking
|
|
||||||
const handleStopTracking = () => {
|
|
||||||
if (!selectedProject) return;
|
|
||||||
|
|
||||||
// Clear the timer
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearInterval(timerRef.current);
|
|
||||||
timerRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update project tracking map
|
|
||||||
setProjectTrackingMap((prev) => {
|
|
||||||
const updated = new Map(prev);
|
|
||||||
const current = updated.get(selectedProject.id);
|
|
||||||
if (!current) return updated;
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
current.isTracking = false;
|
|
||||||
current.isPaused = false;
|
|
||||||
current.endTime = now;
|
|
||||||
updated.set(selectedProject.id, { ...current });
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset global tracking states to allow selecting another project
|
|
||||||
setIsTracking(false);
|
|
||||||
setIsPaused(false);
|
|
||||||
setEndTime(new Date());
|
|
||||||
|
|
||||||
console.log("projectTrackingMap",projectTrackingMap)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cleanup timer on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (timerRef.current) {
|
|
||||||
clearInterval(timerRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle project selection
|
|
||||||
const handleProjectSelect = (id: number) => {
|
|
||||||
// Only allow project selection when not tracking
|
|
||||||
if (isTracking) return;
|
|
||||||
|
|
||||||
const project = projectsData.find((p) => p.id === id);
|
|
||||||
if (project) {
|
|
||||||
setSelectedProject(project);
|
|
||||||
|
|
||||||
// Load existing tracking data if available
|
|
||||||
const projectData = projectTrackingMap.get(id);
|
|
||||||
if (projectData) {
|
|
||||||
setElapsedTime(projectData.elapsedTime);
|
|
||||||
setStartTime(projectData.startTime);
|
|
||||||
setEndTime(projectData.endTime);
|
|
||||||
setIsTracking(projectData.isTracking);
|
|
||||||
setIsPaused(projectData.isPaused);
|
|
||||||
} else {
|
|
||||||
// Reset states for new project
|
|
||||||
setElapsedTime(0);
|
|
||||||
setStartTime(null);
|
|
||||||
setEndTime(null);
|
|
||||||
setIsTracking(false);
|
|
||||||
setIsPaused(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackingState: TrackingState = {
|
|
||||||
selectedProject,
|
|
||||||
projectTrackingMap,
|
|
||||||
isTracking,
|
|
||||||
isPaused,
|
|
||||||
elapsedTime,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
handleStartTracking,
|
|
||||||
handlePauseResume,
|
|
||||||
handleStopTracking,
|
|
||||||
projectsData,
|
|
||||||
handleProjectSelect,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-base-200 overflow-hidden">
|
<div className="flex h-screen bg-base-200 overflow-hidden">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside
|
<aside
|
||||||
className={`
|
className={`
|
||||||
fixed md:relative inset-y-0 left-0 z-40 transform
|
fixed md:relative inset-y-0 left-0 z-40 transform py-1
|
||||||
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out
|
bg-base-100 border-r border-t border-base-300 transition-all duration-300 ease-in-out
|
||||||
${
|
${isMobile
|
||||||
isMobile
|
|
||||||
? isSidebarOpen
|
? isSidebarOpen
|
||||||
? "translate-x-0 w-64"
|
? 'translate-x-0 w-64'
|
||||||
: "-translate-x-full w-64"
|
: '-translate-x-full w-64'
|
||||||
: isSidebarOpen
|
: isSidebarOpen
|
||||||
? "w-50"
|
? 'w-64'
|
||||||
: "w-20"
|
: 'w-20'}
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Sidebar Header */}
|
{/* Header */}
|
||||||
<div className="h-14 text-xl font-bold flex justify-between items-center border-b border-base-300">
|
<div className="p-4 text-xl font-bold flex justify-between items-center border-b border-base-300">
|
||||||
{isSidebarOpen ? (
|
{isSidebarOpen ? (
|
||||||
<span className="whitespace-nowrap mx-auto">VA Tracker</span>
|
<span className="whitespace-nowrap">VA Tracker</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-primary font-extrabold mx-auto">VA</span>
|
<span className="text-primary font-extrabold">VA</span>
|
||||||
)}
|
)}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -341,8 +94,8 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex items-center gap-3 my-1 font-medium transition-all duration-150 ${
|
`flex items-center gap-3 my-1 font-medium transition-all duration-150 ${
|
||||||
isActive
|
isActive
|
||||||
? "bg-primary text-primary-content rounded-lg"
|
? 'bg-primary text-primary-content rounded-lg'
|
||||||
: "hover:bg-base-300 rounded-lg"
|
: 'hover:bg-base-300 rounded-lg'
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
onClick={closeSidebarOnMobile}
|
onClick={closeSidebarOnMobile}
|
||||||
|
|
@ -355,7 +108,7 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Mobile overlay */}
|
{/* Overlay for mobile */}
|
||||||
{isMobile && isSidebarOpen && (
|
{isMobile && isSidebarOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden"
|
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-30 md:hidden"
|
||||||
|
|
@ -364,11 +117,14 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex flex-col flex-1 bg-base-200 transition-all duration-300 ease-in-out">
|
<div
|
||||||
|
className={`flex flex-col flex-1 transition-all duration-300 ease-in-out bg-base-200 ${
|
||||||
|
!isMobile ? (isSidebarOpen ? 'md:ml-0' : 'md:ml-0') : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{/* Navbar */}
|
{/* Navbar */}
|
||||||
<nav className="h-14 bg-base-100 border-b border-t border-base-300 px-4 flex justify-between items-center sticky top-0 z-30">
|
<nav className="navbar bg-base-100 border-b border-t border-base-300 p-3 sticky top-0 z-30 flex justify-between items-center">
|
||||||
{/* Left */}
|
<div className="flex items-center">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="btn btn-ghost btn-square"
|
className="btn btn-ghost btn-square"
|
||||||
|
|
@ -376,25 +132,20 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
>
|
>
|
||||||
<Menu size={20} />
|
<Menu size={20} />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-lg font-semibold md:hidden">
|
<span className="text-lg font-semibold ml-2 md:hidden">
|
||||||
{location.pathname.includes("projects")
|
{location.pathname.includes('projects') ? 'Projects' : 'Dashboard'}
|
||||||
? "Projects"
|
|
||||||
: location.pathname.includes("leave-adjustments")
|
|
||||||
? "Leave & Adjustments"
|
|
||||||
: "Dashboard"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right */}
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<button className="btn btn-ghost btn-circle">
|
<button className="btn btn-ghost btn-circle">
|
||||||
<Bell size={20} />
|
<Bell size={20} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="dropdown dropdown-end">
|
<div className="dropdown dropdown-end">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
||||||
<div className="w-8 h-8 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
<div className="w-10 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
||||||
<User size={18} className="text-base-content" />
|
<User size={22} />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
|
|
@ -418,11 +169,11 @@ const DashboardLayout: React.FC<DashboardLayoutProps> = ({ setUser }) => {
|
||||||
|
|
||||||
{/* Page Content */}
|
{/* Page Content */}
|
||||||
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
<main className="flex-1 overflow-y-auto p-4 md:px-6 no-scrollbar">
|
||||||
<Outlet context={trackingState}/>
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default DashboardLayout;
|
export default DashboardLayout
|
||||||
|
|
|
||||||
|
|
@ -1,263 +1,348 @@
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Clock, TrendingUp, Play, Square, ProjectorIcon, Pause } from "lucide-react";
|
import { Clock, TrendingUp, Play, Square, ProjectorIcon } from "lucide-react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
|
||||||
|
|
||||||
// ----- Interfaces -----
|
const projectsData = [
|
||||||
interface Project {
|
{
|
||||||
id: number;
|
id: 1,
|
||||||
projectName: string;
|
projectName: "Employee Tracker",
|
||||||
clientName: string;
|
clientName: "TechCorp Pvt Ltd",
|
||||||
}
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
projectName: "Sales Dashboard",
|
||||||
|
clientName: "BlueEdge Analytics",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
projectName: "Inventory System",
|
||||||
|
clientName: "LogiFlow Inc",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
projectName: "CRM Upgrade",
|
||||||
|
clientName: "NextWave Solutions",
|
||||||
|
status: "On Hold",
|
||||||
|
action: "Resume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
projectName: "Mobile App Redesign",
|
||||||
|
clientName: "PixelFusion",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
projectName: "E-Commerce Backend",
|
||||||
|
clientName: "ShopEasy",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
projectName: "Finance Tracker",
|
||||||
|
clientName: "MoneyWise",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
projectName: "Helpdesk Portal",
|
||||||
|
clientName: "ServePro",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
projectName: "Marketing Automation",
|
||||||
|
clientName: "AdBoost",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
projectName: "Data Pipeline",
|
||||||
|
clientName: "CloudWave",
|
||||||
|
status: "On Hold",
|
||||||
|
action: "Resume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
projectName: "Support Chatbot",
|
||||||
|
clientName: "SmartServe",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
projectName: "Payroll System",
|
||||||
|
clientName: "PeopleFirst HR",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
projectName: "IoT Dashboard",
|
||||||
|
clientName: "SensorGrid",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
projectName: "Security Portal",
|
||||||
|
clientName: "SafeNet",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
projectName: "Social Media Tool",
|
||||||
|
clientName: "Buzzify",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
projectName: "AI Model Trainer",
|
||||||
|
clientName: "NeuroTech",
|
||||||
|
status: "On Hold",
|
||||||
|
action: "Resume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
projectName: "Project Tracker",
|
||||||
|
clientName: "TaskFlow",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
projectName: "Booking Portal",
|
||||||
|
clientName: "GoTravel",
|
||||||
|
status: "Completed",
|
||||||
|
action: "View Report",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
projectName: "Time Logger",
|
||||||
|
clientName: "Clockify Pro",
|
||||||
|
status: "Active",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
projectName: "Analytics Engine",
|
||||||
|
clientName: "DataStream",
|
||||||
|
status: "Pending",
|
||||||
|
action: "Start Tracking",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
interface ProjectTrackingData {
|
interface WidgetProps {
|
||||||
elapsedTime: number;
|
|
||||||
startTime: Date | null;
|
|
||||||
endTime: Date | null;
|
|
||||||
isTracking: boolean;
|
|
||||||
isPaused: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrackingState {
|
|
||||||
selectedProject: Project | null;
|
|
||||||
projectTrackingMap: Map<number, ProjectTrackingData>;
|
|
||||||
isTracking: boolean;
|
|
||||||
isPaused: boolean;
|
|
||||||
elapsedTime: number;
|
|
||||||
startTime: Date | null;
|
|
||||||
endTime: Date | null;
|
|
||||||
handleStartTracking: (project: Project) => void;
|
|
||||||
handlePauseResume: () => void;
|
|
||||||
handleStopTracking: () => void;
|
|
||||||
handleProjectSelect: (id: number) => void;
|
|
||||||
projectsData: Project[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Helper -----
|
|
||||||
const formatTime = (seconds: number) => {
|
|
||||||
const hrs = Math.floor(seconds / 3600);
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = seconds % 60;
|
|
||||||
return `${hrs}h ${mins}m ${secs}s`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----- Widget -----
|
|
||||||
const Widget: React.FC<{
|
|
||||||
title: string;
|
title: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
color?:
|
trend?: number;
|
||||||
| "primary"
|
color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info";
|
||||||
| "secondary"
|
}
|
||||||
| "accent"
|
|
||||||
| "success"
|
// Widget Component
|
||||||
| "warning"
|
const Widget: React.FC<WidgetProps> = ({
|
||||||
| "error"
|
title,
|
||||||
| "info";
|
value,
|
||||||
}> = ({ title, value, subtitle, icon, color = "primary" }) => (
|
subtitle,
|
||||||
<div className="card bg-base-100 shadow-sm hover:shadow-lg transition-shadow">
|
icon,
|
||||||
|
trend,
|
||||||
|
color = "primary",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<h3 className="font-medium text-base-content/60">{title}</h3>
|
<h3 className=" font-medium text-base-content/60">{title}</h3>
|
||||||
<p className="text-sm font-bold mt-2">{value}</p>
|
<p className="text-sm font-bold mt-2">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`p-3 bg-${color}/10 rounded-lg`}>
|
<div className={`p-3 bg-${color}/10 rounded-lg`}>
|
||||||
<div className={`text-${color}`}>{icon}</div>
|
<div className={`text-${color}`}>{icon}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{subtitle && <p className="text-xs text-base-content/60 mt-2">{subtitle}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
// ----- Dashboard -----
|
|
||||||
const Dashboard: React.FC = () => {
|
|
||||||
const {
|
|
||||||
selectedProject,
|
|
||||||
projectTrackingMap,
|
|
||||||
isTracking,
|
|
||||||
isPaused,
|
|
||||||
elapsedTime,
|
|
||||||
startTime,
|
|
||||||
handleStartTracking,
|
|
||||||
handlePauseResume,
|
|
||||||
handleStopTracking,
|
|
||||||
handleProjectSelect,
|
|
||||||
projectsData,
|
|
||||||
} = useOutletContext<TrackingState>();
|
|
||||||
|
|
||||||
const selectedData = selectedProject
|
|
||||||
? projectTrackingMap.get(selectedProject.id)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
|
// Main Dashboard Component
|
||||||
|
const Dashboard = () => {
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 overflow-y-auto no-scrollbar">
|
<main className="flex-1 overflow-y-auto p-4 md:px-2 md:py-0.5 no-scrollbar">
|
||||||
{/* Header */}
|
{/* Welcome Section */}
|
||||||
<div className="mb-2">
|
<div className="mb-4">
|
||||||
<h1 className="text-md font-bold">Dashboard</h1>
|
<h1 className="text-lg font-bold">Dashboard</h1>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await window.electron.uploadScreenshot();
|
await window.electron.uploadScreenshot();
|
||||||
}}
|
}}
|
||||||
className="btn btn-xs btn-outline mt-2"
|
|
||||||
>
|
>
|
||||||
Screenshot Test
|
ScreenShot Test
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Widgets */}
|
{/* Widgets Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<div className="mb-4">
|
||||||
|
<p className="text-md mb-3 font-bold">Overview</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4 ">
|
||||||
<Widget
|
<Widget
|
||||||
title="Logged Hours Today"
|
title="Logged Hours Today"
|
||||||
value="6h 24m"
|
value="6h 24m"
|
||||||
|
subtitle="Today's total time"
|
||||||
icon={<Clock size={24} />}
|
icon={<Clock size={24} />}
|
||||||
|
trend={12}
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
<Widget
|
<Widget
|
||||||
title="Logged Hours This Week"
|
title="Logged Hours This Week"
|
||||||
value="4h 48m"
|
value="4h 48m"
|
||||||
|
subtitle="75% productivity"
|
||||||
icon={<TrendingUp size={24} />}
|
icon={<TrendingUp size={24} />}
|
||||||
|
trend={8}
|
||||||
color="success"
|
color="success"
|
||||||
/>
|
/>
|
||||||
<Widget
|
<Widget
|
||||||
title="Active Project"
|
title="Active Project"
|
||||||
value={selectedProject ? selectedProject.projectName : "—"}
|
value="VA Tracker Project"
|
||||||
|
subtitle="Until day ends"
|
||||||
icon={<ProjectorIcon size={24} />}
|
icon={<ProjectorIcon size={24} />}
|
||||||
color="warning"
|
color="warning"
|
||||||
/>
|
/>
|
||||||
|
{/* <Widget
|
||||||
|
title="Status"
|
||||||
|
value="Online"
|
||||||
|
subtitle={`Active since ${formatTime(new Date(currentTime.getTime() - 6.4 * 60 * 60 * 1000))}`}
|
||||||
|
icon={<Activity size={24} />}
|
||||||
|
color="success"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tracking Section */}
|
|
||||||
<div className="w-full mb-4">
|
<div className="w-full mb-4">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="text-md mb-2 font-bold flex justify-between">
|
||||||
<span className="text-md font-bold">Tracking</span>
|
<span>Tracking</span>
|
||||||
</div>
|
<div>
|
||||||
|
<button className="ms-1 btn btn-xs btn-success btn-wide sm:w-auto rounded-full shadow-md hover:shadow-lg hover:scale-105 transition-all">
|
||||||
<div className="card bg-base-100 border border-base-200 shadow-sm">
|
<Play className="w-3 h-3" />
|
||||||
<div className="card-body">
|
Start Tracking
|
||||||
{selectedProject ? (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
||||||
<div className="text-center sm:text-left">
|
|
||||||
<p className="text-lg font-extrabold text-success">
|
|
||||||
{formatTime(selectedData?.elapsedTime || 0)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-base-content/70 mt-1">
|
|
||||||
{selectedData?.isTracking ? "Current Session" : "Ready"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!selectedData?.isTracking ? (
|
|
||||||
<button
|
|
||||||
onClick={() => handleStartTracking(selectedProject)}
|
|
||||||
className="btn btn-sm btn-success rounded-xl w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
<Play className="w-3 h-3" /> Start Tracking
|
|
||||||
</button>
|
</button>
|
||||||
) : (
|
<button className="ms-1 btn btn-xs btn-error btn-wide sm:w-auto rounded-full shadow-md hover:shadow-lg hover:scale-105 transition-all">
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-error rounded-full"
|
|
||||||
onClick={handleStopTracking}
|
|
||||||
>
|
|
||||||
<Square className="w-3 h-3" />
|
<Square className="w-3 h-3" />
|
||||||
Stop Tracking
|
Stop Tracking
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider my-3" />
|
<div className="card bg-base-100 shadow-md border border-base-200 transition-all duration-300 w-full">
|
||||||
|
<div className="card-body">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-sm">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div>
|
<div className="text-center sm:text-left">
|
||||||
<span className="text-base-content/70">Project:</span>{" "}
|
<p className="text-sm font-extrabold text-success tracking-tight">
|
||||||
<span className="font-semibold">
|
0h 15m
|
||||||
{selectedProject.projectName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-base-content/70">Client:</span>{" "}
|
|
||||||
<span>{selectedProject.clientName}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-base-content/70">Status:</span>{" "}
|
|
||||||
<span
|
|
||||||
className={`badge ${selectedData?.isPaused
|
|
||||||
? "badge-warning"
|
|
||||||
: selectedData?.isTracking
|
|
||||||
? "badge-success"
|
|
||||||
: "badge-neutral"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{selectedData?.isPaused
|
|
||||||
? "Paused"
|
|
||||||
: selectedData?.isTracking
|
|
||||||
? "Active"
|
|
||||||
: "Ready"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-base-content/70">Start:</span>{" "}
|
|
||||||
<span>
|
|
||||||
{selectedData?.startTime
|
|
||||||
? selectedData.startTime.toLocaleTimeString()
|
|
||||||
: "—"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-base-content/60 text-center">
|
|
||||||
Select a project below to start tracking.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
<p className="text-xs text-base-content/70 mt-1">
|
||||||
|
Current Session
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn btn-success btn-sm rounded-xl hover:scale-105 transition-transform duration-200 w-full sm:w-auto">
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider my-2"></div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-base-content/70">Project:</span>
|
||||||
|
<span className="font-semibold text-base-content">
|
||||||
|
Employee Tracker
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-base-content/70">Status:</span>
|
||||||
|
<span className="badge badge-success">Active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Projects Table */}
|
<div className="">
|
||||||
<div>
|
<p className="text-md mb-2 font-bold flex">
|
||||||
<p className="text-md mb-2 font-bold">Projects</p>
|
<span>Projects</span>
|
||||||
<div className="overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md">
|
</p>
|
||||||
|
<div
|
||||||
|
className="overflow-y-auto no-scrollbar overflow-x-auto rounded-box border border-base-content/10 bg-base-100 shadow-md"
|
||||||
|
style={{ minHeight: "100px", maxHeight: "250px" }}
|
||||||
|
>
|
||||||
<table className="table table-zebra w-full text-sm">
|
<table className="table table-zebra w-full text-sm">
|
||||||
|
{/* Table Head */}
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
<thead className="sticky top-0 bg-base-200 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Project</th>
|
<th>Project Name</th>
|
||||||
<th>Client</th>
|
<th>Client Name</th>
|
||||||
|
<th>Status</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
|
||||||
{projectsData.map((project) => {
|
|
||||||
const isCurrentlySelected = selectedProject?.id === project.id;
|
|
||||||
// ✅ FIX: Disable ALL projects when ANY project is tracking
|
|
||||||
// Only enable selection when not tracking
|
|
||||||
const isDisabled = isTracking && !isCurrentlySelected;
|
|
||||||
|
|
||||||
return (
|
{/* Table Body */}
|
||||||
<tr key={project.id}>
|
<tbody>
|
||||||
<td>{project.id}</td>
|
{projectsData.map((project) => (
|
||||||
<td>{project.projectName}</td>
|
<tr key={project.id} className="align-middle">
|
||||||
<td>{project.clientName}</td>
|
<th>{project.id}</th>
|
||||||
|
<td className="whitespace-normal break-words max-w-[150px]">{project.projectName}</td>
|
||||||
|
<td className="whitespace-normal break-words max-w-[150px]">{project.clientName}</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<div
|
||||||
className={`btn btn-xs ${isCurrentlySelected
|
className={`badge badge-sm whitespace-nowrap ${project.status === "Active"
|
||||||
? "btn-success"
|
? "badge-success"
|
||||||
: "btn-primary"
|
: project.status === "Pending"
|
||||||
|
? "badge-warning"
|
||||||
|
: project.status === "Completed"
|
||||||
|
? "badge-info"
|
||||||
|
: "badge-neutral"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleProjectSelect(project.id)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
>
|
||||||
{isCurrentlySelected ? "Selected" : "Select"}
|
{project.status}
|
||||||
</button>
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
className={`badge badge-sm whitespace-nowrap ${project.action === "View Report"
|
||||||
|
? "badge-info"
|
||||||
|
: project.action === "Resume"
|
||||||
|
? "badge-secondary"
|
||||||
|
: "badge-primary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.action}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { HelpCircle, Mail, Phone, Send, MessageSquare } from "lucide-react";
|
|
||||||
|
|
||||||
const HelpSupport: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 max-h-screen">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-md font-bold flex items-center gap-2">
|
|
||||||
Help & Support
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact Info */}
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm p-4">
|
|
||||||
<h3 className="text-md font-semibold mb-2">Need Assistance?</h3>
|
|
||||||
<p className="text-sm text-base-content/70">
|
|
||||||
We're here to help! If you’re facing any issues, please reach out to our support team.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-3">
|
|
||||||
<div className="flex items-center gap-3 border border-base-300 rounded-lg p-3 bg-base-200/40">
|
|
||||||
<Mail size={18} className="text-primary" />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-base-content/70">Email</p>
|
|
||||||
<p className="font-medium text-sm">support@vatechcorp.com</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 border border-base-300 rounded-lg p-3 bg-base-200/40">
|
|
||||||
<Phone size={18} className="text-primary" />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-base-content/70">Phone</p>
|
|
||||||
<p className="font-medium text-sm">+91 98765 43210</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 border border-base-300 rounded-lg p-3 bg-base-200/40">
|
|
||||||
<MessageSquare size={18} className="text-primary" />
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-base-content/70">Chat</p>
|
|
||||||
<p className="font-medium text-sm">Available 9 AM - 6 PM</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HelpSupport;
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface AdjustmentEntry {
|
|
||||||
id: number;
|
|
||||||
date: string;
|
|
||||||
loggedHours: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdjustmentModalProps {
|
|
||||||
editingEntry: AdjustmentEntry | null;
|
|
||||||
newHours: number;
|
|
||||||
setNewHours: React.Dispatch<React.SetStateAction<number>>;
|
|
||||||
handleCloseModal: () => void;
|
|
||||||
handleSave: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AdjustmentModal: React.FC<AdjustmentModalProps> = ({
|
|
||||||
editingEntry,
|
|
||||||
newHours,
|
|
||||||
setNewHours,
|
|
||||||
handleCloseModal,
|
|
||||||
handleSave,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<dialog id="editModal" className="modal modal-bottom sm:modal-middle">
|
|
||||||
<div className="modal-box">
|
|
||||||
<h3 className="font-bold text-md mb-4">Logged Hours</h3>
|
|
||||||
|
|
||||||
{editingEntry && (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{/* Date */}
|
|
||||||
<div className="form-control">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<label className="label-text text-sm font-medium w-32">Date</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={new Date(editingEntry.date).toLocaleDateString("en-US")}
|
|
||||||
readOnly
|
|
||||||
className="input input-bordered input-sm bg-base-200 flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Hours */}
|
|
||||||
<div className="form-control">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<label className="label-text text-sm font-medium w-32">
|
|
||||||
Current Hours
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={`${editingEntry.loggedHours} hrs`}
|
|
||||||
readOnly
|
|
||||||
className="input input-bordered input-sm bg-base-200 flex-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* New Hours */}
|
|
||||||
<div className="form-control">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<label className="label-text text-sm font-medium w-32">
|
|
||||||
New Hours
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center flex-1">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="24"
|
|
||||||
step="0.5"
|
|
||||||
placeholder="Enter hours"
|
|
||||||
value={newHours}
|
|
||||||
onChange={(e) => setNewHours(Number(e.target.value))}
|
|
||||||
className="input input-bordered input-sm flex-1 rounded-r-none"
|
|
||||||
/>
|
|
||||||
<span className="bg-base-200 border border-l-0 border-base-300 text-sm px-2 py-1.5 rounded-r-lg">
|
|
||||||
hrs
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Modal Actions */}
|
|
||||||
<div className="modal-action flex justify-end gap-2 mt-4">
|
|
||||||
<button onClick={handleCloseModal} className="btn btn-sm btn-ghost">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button onClick={handleSave} className="btn btn-sm btn-primary">
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdjustmentModal;
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import AdjustmentModal from './AdjustmentModal';
|
|
||||||
|
|
||||||
interface LeaveFormData {
|
interface LeaveFormData {
|
||||||
fromDate: string;
|
fromDate: string;
|
||||||
|
|
@ -13,22 +12,6 @@ interface AdjustmentEntry {
|
||||||
loggedHours: number;
|
loggedHours: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LogEntry {
|
|
||||||
id: number;
|
|
||||||
date: string;
|
|
||||||
hours: number;
|
|
||||||
task: string;
|
|
||||||
status: "Completed" | "In Progress" | "Pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: LogEntry[] = [
|
|
||||||
{ id: 1, date: "2025-11-01", hours: 8, task: "Frontend Development", status: "Completed" },
|
|
||||||
{ id: 2, date: "2025-11-02", hours: 6, task: "API Integration", status: "In Progress" },
|
|
||||||
{ id: 3, date: "2025-11-03", hours: 7.5, task: "UI Testing", status: "Pending" },
|
|
||||||
{ id: 4, date: "2025-11-04", hours: 9, task: "Bug Fixing", status: "Completed" },
|
|
||||||
{ id: 5, date: "2025-11-05", hours: 5.5, task: "Code Review", status: "Completed" },
|
|
||||||
];
|
|
||||||
|
|
||||||
type TabType = 'pto' | 'sick' | 'adjustment';
|
type TabType = 'pto' | 'sick' | 'adjustment';
|
||||||
|
|
||||||
const LeaveAndAdjustments: React.FC = () => {
|
const LeaveAndAdjustments: React.FC = () => {
|
||||||
|
|
@ -139,51 +122,85 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [editingEntry, setEditingEntry] = useState<AdjustmentEntry | null>(null);
|
// Adjustment Handlers
|
||||||
const [newHours, setNewHours] = useState<number>(0);
|
const handlePeriodChange = (period: string) => {
|
||||||
|
setSelectedPeriod(period);
|
||||||
const handlePeriodChange = (value: string) => setSelectedPeriod(value);
|
if (period === 'month') {
|
||||||
|
setAdjustmentData([
|
||||||
|
{ id: 1, date: '2025-11-01', loggedHours: 8 },
|
||||||
|
{ id: 2, date: '2025-11-02', loggedHours: 7.5 },
|
||||||
|
{ id: 3, date: '2025-11-03', loggedHours: 8 },
|
||||||
|
{ id: 4, date: '2025-11-04', loggedHours: 6 },
|
||||||
|
{ id: 5, date: '2025-11-05', loggedHours: 8 },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setAdjustmentData([
|
||||||
|
{ id: 1, date: '2025-11-01', loggedHours: 8 },
|
||||||
|
{ id: 2, date: '2025-11-02', loggedHours: 7.5 },
|
||||||
|
{ id: 3, date: '2025-11-03', loggedHours: 8 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleEditClick = (entry: AdjustmentEntry) => {
|
const handleEditClick = (entry: AdjustmentEntry) => {
|
||||||
setEditingEntry(entry);
|
setEditingId(entry.id);
|
||||||
const modal = document.getElementById("editModal") as HTMLDialogElement;
|
setEditHours(entry.loggedHours.toString());
|
||||||
modal?.showModal();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleSaveEdit = async (id: number) => {
|
||||||
setEditingEntry(null);
|
const hours = parseFloat(editHours);
|
||||||
setNewHours(0)
|
|
||||||
const modal = document.getElementById("editModal") as HTMLDialogElement;
|
if (isNaN(hours) || hours < 0 || hours > 24) {
|
||||||
modal?.close();
|
alert('Please enter valid hours (0-24)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Updating hours for entry:', id, 'New hours:', hours);
|
||||||
|
|
||||||
|
setAdjustmentData(prev =>
|
||||||
|
prev.map(entry =>
|
||||||
|
entry.id === id ? { ...entry, loggedHours: hours } : entry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setEditingId(null);
|
||||||
|
setEditHours('');
|
||||||
|
alert('Hours updated successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to update hours. Please try again.');
|
||||||
|
console.error('Error:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleCancelEdit = () => {
|
||||||
console.log("Saving:", { ...editingEntry, updatedHours: newHours });
|
setEditingId(null);
|
||||||
handleCloseModal();
|
setEditHours('');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<div className="w-full mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className=" ">
|
<div className=" ">
|
||||||
<h1 className="text-mb font-bold mb-2">Leave & Adjustments</h1>
|
<h1 className="text-lg font-bold mb-3">Leave & Adjustments</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<div className=" rounded-lg bg-base-100 overflow-hidden">
|
<div className=" shadow-lg rounded-lg bg-base-100 overflow-hidden">
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
<div className="flex ">
|
<div className="flex ">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('pto')}
|
onClick={() => setActiveTab('pto')}
|
||||||
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative ${activeTab === 'pto'
|
className={`flex-1 px-6 py-4 text-sm font-semibold transition-all duration-300 relative ${
|
||||||
|
activeTab === 'pto'
|
||||||
? ' text-primary border-b-2 border-primary'
|
? ' text-primary border-b-2 border-primary'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center gap-2 cursor-pointer">
|
<span className="flex items-center justify-center gap-2 cursor-pointer">
|
||||||
<svg
|
<svg
|
||||||
className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
PTO Request
|
PTO Request
|
||||||
|
|
@ -191,13 +208,14 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('sick')}
|
onClick={() => setActiveTab('sick')}
|
||||||
className={`flex-1 px-6 py-3 text-xs font-semibold transition-all duration-300 relative cursor-pointer ${activeTab === 'sick'
|
className={`flex-1 px-6 py-4 text-sm font-semibold transition-all duration-300 relative cursor-pointer ${
|
||||||
|
activeTab === 'sick'
|
||||||
? ' text-primary border-b-2 border-primary'
|
? ' text-primary border-b-2 border-primary'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center gap-2">
|
<span className="flex items-center justify-center gap-2">
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
Sick Leave
|
Sick Leave
|
||||||
|
|
@ -205,13 +223,14 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('adjustment')}
|
onClick={() => setActiveTab('adjustment')}
|
||||||
className={`flex-1 px-6 py-3 text-xs font-semibold cursor-pointer transition-all duration-300 relative ${activeTab === 'adjustment'
|
className={`flex-1 px-6 py-4 text-sm font-semibold cursor-pointer transition-all duration-300 relative ${
|
||||||
|
activeTab === 'adjustment'
|
||||||
? ' text-primary border-b-2 border-primary'
|
? ' text-primary border-b-2 border-primary'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center gap-2">
|
<span className="flex items-center justify-center gap-2">
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
Adjustments
|
Adjustments
|
||||||
|
|
@ -224,12 +243,12 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
{/* PTO Tab */}
|
{/* PTO Tab */}
|
||||||
{activeTab === 'pto' && (
|
{activeTab === 'pto' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="max-w-4xl ">
|
<div className="max-w-2xl ">
|
||||||
<div className="mb-1">
|
<div className="mb-3">
|
||||||
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
|
<h2 className="text-sm font-bold mb-2">Paid Time Off Request</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-5">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
@ -273,7 +292,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
<textarea
|
<textarea
|
||||||
value={ptoLeave.description}
|
value={ptoLeave.description}
|
||||||
onChange={(e) => handlePtoChange('description', e.target.value)}
|
onChange={(e) => handlePtoChange('description', e.target.value)}
|
||||||
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
|
className={`textarea textarea-bordered h-32 w-full focus:textarea-primary transition-all ${ptoErrors.description ? 'textarea-error' : ''}`}
|
||||||
placeholder="Please provide a reason for your PTO request..."
|
placeholder="Please provide a reason for your PTO request..."
|
||||||
/>
|
/>
|
||||||
{ptoErrors.description && (
|
{ptoErrors.description && (
|
||||||
|
|
@ -311,12 +330,12 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
{/* Sick Leave Tab */}
|
{/* Sick Leave Tab */}
|
||||||
{activeTab === 'sick' && (
|
{activeTab === 'sick' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<div className="max-w-4xl ">
|
<div className="max-w-2xl ">
|
||||||
<div className="mb-1">
|
<div className="mb-3">
|
||||||
<h2 className="text-sm font-bold mb-2">Sick Leave Request</h2>
|
<h2 className="text-sm font-bold mb-2">Sick Leave Request</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-5">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|
@ -360,7 +379,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
<textarea
|
<textarea
|
||||||
value={sickLeave.description}
|
value={sickLeave.description}
|
||||||
onChange={(e) => handleSickChange('description', e.target.value)}
|
onChange={(e) => handleSickChange('description', e.target.value)}
|
||||||
className={`textarea textarea-bordered h-20 w-full focus:textarea-primary transition-all ${sickErrors.description ? 'textarea-error' : ''}`}
|
className={`textarea textarea-bordered h-32 w-full focus:textarea-primary transition-all ${sickErrors.description ? 'textarea-error' : ''}`}
|
||||||
placeholder="Please describe your illness or condition..."
|
placeholder="Please describe your illness or condition..."
|
||||||
/>
|
/>
|
||||||
{sickErrors.description && (
|
{sickErrors.description && (
|
||||||
|
|
@ -370,7 +389,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 ">
|
<div className="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleSickSubmit}
|
onClick={handleSickSubmit}
|
||||||
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
className="btn btn-primary flex-1 hover:shadow-lg transition-all duration-300"
|
||||||
|
|
@ -385,7 +404,7 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
setSickLeave({ fromDate: '', toDate: '', description: '' });
|
setSickLeave({ fromDate: '', toDate: '', description: '' });
|
||||||
setSickErrors({});
|
setSickErrors({});
|
||||||
}}
|
}}
|
||||||
className="btn btn-secondary transition-all"
|
className="btn btn-ghost hover:bg-slate-100 transition-all"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -398,10 +417,11 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
{/* Adjustment Submissions Tab */}
|
{/* Adjustment Submissions Tab */}
|
||||||
{activeTab === 'adjustment' && (
|
{activeTab === 'adjustment' && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<h2 className="text-md font-bold mb-2">Time Adjustments</h2>
|
<h2 className="text-md font-bold mb-2">Time Adjustments</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-control w-48">
|
<div className="form-control w-48">
|
||||||
<select
|
<select
|
||||||
value={selectedPeriod}
|
value={selectedPeriod}
|
||||||
|
|
@ -414,52 +434,74 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<div className="overflow-hidden rounded-lg border border-base-content/10">
|
<div className="overflow-hidden rounded-lg border border-base-content/10">
|
||||||
<table className="table w-full">
|
<table className="table w-full">
|
||||||
<thead className="bg-base-100">
|
<thead className="bg-base-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="font-bold text-base">Date</th>
|
<th className=" font-bold text-base">Date</th>
|
||||||
<th className="font-bold text-base">Logged Hours</th>
|
<th className=" font-bold text-base">Logged Hours</th>
|
||||||
<th className="font-bold text-base text-right">Actions</th>
|
<th className=" font-bold text-base text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{adjustmentData.map((entry) => (
|
{adjustmentData.map((entry) => (
|
||||||
<tr key={entry.id} className="transition-colors">
|
<tr key={entry.id} className=" transition-colors">
|
||||||
<td className="font-medium">
|
<td className="font-medium ">
|
||||||
{new Date(entry.date).toLocaleDateString("en-US", {
|
{new Date(entry.date).toLocaleDateString('en-US', {
|
||||||
weekday: "short",
|
weekday: 'short',
|
||||||
year: "numeric",
|
year: 'numeric',
|
||||||
month: "short",
|
month: 'short',
|
||||||
day: "numeric",
|
day: 'numeric'
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{editingId === entry.id ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editHours}
|
||||||
|
onChange={(e) => setEditHours(e.target.value)}
|
||||||
|
className="input input-bordered input-sm w-28 focus:input-primary"
|
||||||
|
min="0"
|
||||||
|
max="24"
|
||||||
|
step="0.5"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<span className="badge badge-lg badge-ghost font-semibold">
|
<span className="badge badge-lg badge-ghost font-semibold">
|
||||||
{entry.loggedHours} hrs
|
{entry.loggedHours} hours
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-right">
|
<td className="text-right">
|
||||||
|
{editingId === entry.id ? (
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditClick(entry)}
|
// onClick={() => handleSaveEdit(entry.id)}
|
||||||
|
className="btn btn-success btn-sm hover:shadow-md transition-all"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className="btn btn-ghost btn-sm hover:bg-slate-100 transition-all"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
// onClick={() => handleEditClick(entry)}
|
||||||
className="btn btn-primary btn-sm hover:shadow-md transition-all"
|
className="btn btn-primary btn-sm hover:shadow-md transition-all"
|
||||||
>
|
>
|
||||||
<svg
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
className="h-4 w-4"
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -467,21 +509,10 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* No Data */}
|
|
||||||
{adjustmentData.length === 0 && (
|
{adjustmentData.length === 0 && (
|
||||||
<div className="text-center py-12 text-slate-500">
|
<div className="text-center py-12 text-slate-500">
|
||||||
<svg
|
<svg className="h-16 w-16 mx-auto mb-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
className="h-16 w-16 mx-auto mb-4 opacity-50"
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<p className="font-medium">No time entries found for this period</p>
|
<p className="font-medium">No time entries found for this period</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -492,50 +523,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Status */}
|
|
||||||
<div className='mt-4'>
|
|
||||||
<h1 className="text-md font-bold mb-2">View Status</h1>
|
|
||||||
<div
|
|
||||||
className="overflow-y-auto rounded-box border border-base-content/5 bg-base-100 shadow-md"
|
|
||||||
style={{ minHeight: "150px", maxHeight: "300px" }}
|
|
||||||
>
|
|
||||||
<table className="table table-zebra w-full">
|
|
||||||
{/* Table Head */}
|
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Logged Hours</th>
|
|
||||||
<th>Task</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
{/* Table Body */}
|
|
||||||
<tbody>
|
|
||||||
{data.map((log, index) => (
|
|
||||||
<tr key={log.id}>
|
|
||||||
<td>{index + 1}</td>
|
|
||||||
<td>{log.date}</td>
|
|
||||||
<td className="font-medium">{log.hours} hrs</td>
|
|
||||||
<td className="whitespace-normal break-words">{log.task}</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className={`badge ${log.status === "Completed"
|
|
||||||
? "badge-success"
|
|
||||||
: log.status === "In Progress"
|
|
||||||
? "badge-info"
|
|
||||||
: "badge-warning"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{log.status}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div></div>
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
from {
|
from {
|
||||||
|
|
@ -552,14 +539,6 @@ const LeaveAndAdjustments: React.FC = () => {
|
||||||
animation: fade-in 0.3s ease-out;
|
animation: fade-in 0.3s ease-out;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<AdjustmentModal
|
|
||||||
editingEntry={editingEntry}
|
|
||||||
newHours={newHours}
|
|
||||||
setNewHours={setNewHours}
|
|
||||||
handleCloseModal={handleCloseModal}
|
|
||||||
handleSave={handleSave}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ interface LoginProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Login: React.FC<LoginProps> = ({ setUser }) => {
|
const Login: React.FC<LoginProps> = ({ setUser }) => {
|
||||||
const [formData, setFormData] = useState<User>({ email: '', password: '' })
|
const [formData, setFormData] = useState<User>({ email: 'jai@gmail.com', password: '12345' })
|
||||||
const inputRef = useRef<HTMLFormElement | null>(null)
|
const inputRef = useRef<HTMLFormElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,9 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
const projectsData = [
|
const Projects = () => {
|
||||||
{ id: 1, projectName: "Employee Tracker", clientName: "TechCorp Pvt Ltd", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 2, projectName: "Sales Dashboard", clientName: "BlueEdge Analytics", status: "Pending", action: "Start Tracking" },
|
|
||||||
{ id: 3, projectName: "Inventory System", clientName: "LogiFlow Inc", status: "Completed", action: "View Report" },
|
|
||||||
{ id: 4, projectName: "CRM Upgrade", clientName: "NextWave Solutions", status: "On Hold", action: "Resume" },
|
|
||||||
{ id: 5, projectName: "Mobile App Redesign", clientName: "PixelFusion", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 6, projectName: "E-Commerce Backend", clientName: "ShopEasy", status: "Pending", action: "Start Tracking" },
|
|
||||||
{ id: 7, projectName: "Finance Tracker", clientName: "MoneyWise", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 8, projectName: "Helpdesk Portal", clientName: "ServePro", status: "Completed", action: "View Report" },
|
|
||||||
{ id: 9, projectName: "Marketing Automation", clientName: "AdBoost", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 10, projectName: "Data Pipeline", clientName: "CloudWave", status: "On Hold", action: "Resume" },
|
|
||||||
{ id: 11, projectName: "Support Chatbot", clientName: "SmartServe", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 12, projectName: "Payroll System", clientName: "PeopleFirst HR", status: "Pending", action: "Start Tracking" },
|
|
||||||
{ id: 13, projectName: "IoT Dashboard", clientName: "SensorGrid", status: "Completed", action: "View Report" },
|
|
||||||
{ id: 14, projectName: "Security Portal", clientName: "SafeNet", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 15, projectName: "Social Media Tool", clientName: "Buzzify", status: "Pending", action: "Start Tracking" },
|
|
||||||
{ id: 16, projectName: "AI Model Trainer", clientName: "NeuroTech", status: "On Hold", action: "Resume" },
|
|
||||||
{ id: 17, projectName: "Project Tracker", clientName: "TaskFlow", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 18, projectName: "Booking Portal", clientName: "GoTravel", status: "Completed", action: "View Report" },
|
|
||||||
{ id: 19, projectName: "Time Logger", clientName: "Clockify Pro", status: "Active", action: "Start Tracking" },
|
|
||||||
{ id: 20, projectName: "Analytics Engine", clientName: "DataStream", status: "Pending", action: "Start Tracking" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const Projects: React.FC = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div>Projects</div>
|
||||||
<p className="text-md mb-2 font-bold flex">
|
)
|
||||||
<span>Projects</span>
|
}
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
export default Projects
|
||||||
className="overflow-y-auto overflow-x-auto no-scrollbar rounded-box border border-base-content/10 bg-base-100 shadow-md"
|
|
||||||
style={{ maxHeight: "80vh", minHeight: "200px" }} // 👈 auto scroll region
|
|
||||||
>
|
|
||||||
<table className="table table-zebra w-full text-sm">
|
|
||||||
{/* Table Head */}
|
|
||||||
<thead className="sticky top-0 bg-base-200 z-10">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Project Name</th>
|
|
||||||
<th>Client Name</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
{/* Table Body */}
|
|
||||||
<tbody>
|
|
||||||
{projectsData.map((project) => (
|
|
||||||
<tr key={project.id} className="align-middle">
|
|
||||||
<th>{project.id}</th>
|
|
||||||
<td className="whitespace-normal break-words max-w-[160px]">{project.projectName}</td>
|
|
||||||
<td className="whitespace-normal break-words max-w-[160px]">{project.clientName}</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className={`badge badge-sm ${
|
|
||||||
project.status === "Active"
|
|
||||||
? "badge-success"
|
|
||||||
: project.status === "Pending"
|
|
||||||
? "badge-warning"
|
|
||||||
: project.status === "Completed"
|
|
||||||
? "badge-info"
|
|
||||||
: "badge-neutral"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{project.status}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
className={`btn btn-xs btn-primary`}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Projects;
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { User, Mail, Lock, Edit2, Shield, CalendarDays } from "lucide-react";
|
|
||||||
|
|
||||||
interface UserProfile {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
employeeId: string;
|
|
||||||
joinDate: string;
|
|
||||||
department: string;
|
|
||||||
profileImage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData: UserProfile = {
|
|
||||||
name: "Jai Pandey",
|
|
||||||
email: "jai.pandey@vatechcorp.com",
|
|
||||||
role: "Frontend Developer",
|
|
||||||
employeeId: "EMP-1023",
|
|
||||||
joinDate: "2022-07-15",
|
|
||||||
department: "Engineering",
|
|
||||||
profileImage: "https://ui-avatars.com/api/?name=Jai+Pandey&background=random",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 max-h-screen">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-md font-bold flex items-center gap-2">
|
|
||||||
Profile Settings
|
|
||||||
</h2>
|
|
||||||
<button className="btn btn-sm btn-primary gap-1">
|
|
||||||
<Edit2 size={16} /> Edit Profile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Profile Section */}
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm p-4 flex flex-col md:flex-row gap-4">
|
|
||||||
{/* Profile Picture */}
|
|
||||||
<div className="flex flex-col items-center md:w-1/3">
|
|
||||||
<div className="avatar">
|
|
||||||
<div className="w-28 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 flex justify-center items-center">
|
|
||||||
<User size={60} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 className="mt-3 text-lg font-semibold">{userData.name}</h3>
|
|
||||||
<p className="text-sm text-base-content/70">{userData.role}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divider md:divider-horizontal" />
|
|
||||||
|
|
||||||
{/* Details Section */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1">
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<Mail size={14} /> Email
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.email}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<Shield size={14} /> Role
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.role}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<Lock size={14} /> Employee ID
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.employeeId}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<CalendarDays size={14} /> Joining Date
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.joinDate}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-2 rounded-md bg-base-200/50 sm:col-span-2">
|
|
||||||
<p className="text-xs text-base-content/70 flex items-center gap-2">
|
|
||||||
<User size={14} /> Department
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium">{userData.department}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Account Settings */}
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm p-4">
|
|
||||||
<h3 className="text-md font-semibold mb-3">Account Security</h3>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between border-t border-base-300 pt-3">
|
|
||||||
<p className="text-sm flex items-center gap-2">
|
|
||||||
<Lock size={16} /> Password last changed on{" "}
|
|
||||||
<span className="font-medium text-base-content">Aug 12, 2024</span>
|
|
||||||
</p>
|
|
||||||
<button className="btn btn-sm btn-outline btn-primary">
|
|
||||||
Change Password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Settings;
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { CalendarDays, Clock, FileText, CheckCircle, XCircle } from "lucide-react";
|
|
||||||
|
|
||||||
interface TimesheetEntry {
|
|
||||||
id: number;
|
|
||||||
date: string;
|
|
||||||
project: string;
|
|
||||||
hours: number;
|
|
||||||
status: "Approved" | "Pending" | "Rejected";
|
|
||||||
remarks?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timesheetData: TimesheetEntry[] = [
|
|
||||||
{ id: 1, date: "2025-10-28", project: "Employee Tracker", hours: 8, status: "Approved", remarks: "Full day" },
|
|
||||||
{ id: 2, date: "2025-10-29", project: "CRM Upgrade", hours: 6, status: "Pending", remarks: "Client call delays" },
|
|
||||||
{ id: 3, date: "2025-10-30", project: "Sales Dashboard", hours: 7, status: "Rejected", remarks: "Missing details" },
|
|
||||||
{ id: 4, date: "2025-10-31", project: "Finance Tracker", hours: 9, status: "Approved", remarks: "Overtime" },
|
|
||||||
{ id: 5, date: "2025-11-01", project: "AI Model Trainer", hours: 8, status: "Approved", remarks: "Normal day" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const Timesheets: React.FC = () => {
|
|
||||||
const totalHours = timesheetData.reduce((acc, t) => acc + t.hours, 0);
|
|
||||||
const approvedCount = timesheetData.filter((t) => t.status === "Approved").length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 max-h-screen">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="text-md font-bold">Timesheets</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary Cards */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm">
|
|
||||||
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-base-content/70">Total Logged Hours</p>
|
|
||||||
<h3 className="text-lg font-semibold">{totalHours} hrs</h3>
|
|
||||||
</div>
|
|
||||||
<Clock size={20} className="text-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm">
|
|
||||||
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-base-content/70">Approved Days</p>
|
|
||||||
<h3 className="text-lg font-semibold">{approvedCount}</h3>
|
|
||||||
</div>
|
|
||||||
<CheckCircle size={20} className="text-success" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card bg-base-100 border border-base-300 shadow-sm">
|
|
||||||
<div className="card-body py-3 px-4 flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-base-content/70">Pending Days</p>
|
|
||||||
<h3 className="text-lg font-semibold">
|
|
||||||
{timesheetData.length - approvedCount}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<FileText size={20} className="text-warning" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Timesheet Table */}
|
|
||||||
<div className="overflow-y-auto rounded-box border border-base-content/10 bg-base-100 shadow-md max-h-[60vh]">
|
|
||||||
<table className="table table-zebra w-full text-sm">
|
|
||||||
<thead className="sticky top-0 bg-base-200 z-10 text-sm">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Project</th>
|
|
||||||
<th>Hours</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Remarks</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{timesheetData.map((entry) => (
|
|
||||||
<tr key={entry.id}>
|
|
||||||
<td>{entry.id}</td>
|
|
||||||
<td className="whitespace-nowrap">{entry.date}</td>
|
|
||||||
<td>{entry.project}</td>
|
|
||||||
<td>{entry.hours} hrs</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
className={`badge badge-sm ${
|
|
||||||
entry.status === "Approved"
|
|
||||||
? "badge-success"
|
|
||||||
: entry.status === "Pending"
|
|
||||||
? "badge-warning"
|
|
||||||
: "badge-error"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{entry.status}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{entry.remarks || "-"}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Summary */}
|
|
||||||
<div className="flex justify-between items-center text-xs mt-2">
|
|
||||||
<span className="text-base-content/60">
|
|
||||||
Showing {timesheetData.length} entries
|
|
||||||
</span>
|
|
||||||
<span className="text-base-content/60">
|
|
||||||
Last updated: {new Date().toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Timesheets;
|
|
||||||
Loading…
Reference in New Issue