Compare commits
2 Commits
4174687024
...
99412992fa
| Author | SHA1 | Date | |
|---|---|---|---|
| 99412992fa | |||
| 527d762538 |
27
.github/workflows/playwright.yml
vendored
Normal file
27
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: npx playwright install --with-deps
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: npx playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -87,4 +87,10 @@ test-results/
|
|||||||
# =====================
|
# =====================
|
||||||
# Misc
|
# Misc
|
||||||
# =====================
|
# =====================
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/playwright/.auth/
|
||||||
|
|||||||
343
package-lock.json
generated
343
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"protobufjs": "^8.6.5",
|
"protobufjs": "^8.6.5",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@ -1074,6 +1076,22 @@
|
|||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
@ -2124,6 +2142,18 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
@ -2141,6 +2171,12 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.5.2",
|
"version": "10.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz",
|
||||||
@ -2178,6 +2214,18 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.16.0",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-plugin-macros": {
|
"node_modules/babel-plugin-macros": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||||
@ -2269,6 +2317,19 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@ -2308,6 +2369,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@ -2547,6 +2620,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@ -2567,6 +2649,20 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.385",
|
"version": "1.5.385",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
|
||||||
@ -2596,6 +2692,15 @@
|
|||||||
"is-arrayish": "^0.2.1"
|
"is-arrayish": "^0.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-errors": {
|
"node_modules/es-errors": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
@ -2605,6 +2710,33 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-toolkit": {
|
"node_modules/es-toolkit": {
|
||||||
"version": "1.49.0",
|
"version": "1.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz",
|
||||||
@ -2929,6 +3061,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.4",
|
||||||
|
"mime-types": "^2.1.35"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@ -2976,6 +3144,43 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@ -3002,12 +3207,51 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
@ -3052,6 +3296,19 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@ -3566,6 +3823,36 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.2.5",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
@ -3765,6 +4052,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.16",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
@ -3839,6 +4173,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
|
"axios": "^1.18.1",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"protobufjs": "^8.6.5",
|
"protobufjs": "^8.6.5",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/node": "^24.13.2",
|
"@types/node": "^24.13.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
79
playwright.config.ts
Normal file
79
playwright.config.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('')`. */
|
||||||
|
// baseURL: 'http://localhost:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'npm run start',
|
||||||
|
// url: 'http://localhost:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
||||||
@ -1,30 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"date": "2026-07-01",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-02",
|
|
||||||
"timeIn": "09:00 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 00m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-07",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Holiday"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-15",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Absent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "REG001",
|
|
||||||
"empId": "EMP001",
|
|
||||||
"empName": "Alice",
|
|
||||||
"reqCheckIn": "09:30 AM",
|
|
||||||
"reqCheckOut": "06:30 PM",
|
|
||||||
"date": "2024-07-15",
|
|
||||||
"reason": "Forgot to punch in due to system issue.",
|
|
||||||
"status": "Pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "REG002",
|
|
||||||
"empId": "EMP003",
|
|
||||||
"empName": "Charlie",
|
|
||||||
"reqCheckIn": "10:00 AM",
|
|
||||||
"reqCheckOut": "07:00 PM",
|
|
||||||
"date": "2024-07-16",
|
|
||||||
"reason": "Late arrival due to traffic.",
|
|
||||||
"status": "Pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "REG003",
|
|
||||||
"empId": "EMP005",
|
|
||||||
"empName": "Eve",
|
|
||||||
"reqCheckIn": "08:45 AM",
|
|
||||||
"reqCheckOut": "05:15 PM",
|
|
||||||
"date": "2024-07-14",
|
|
||||||
"reason": "Mispunch while swiping card.",
|
|
||||||
"status": "Pending"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,722 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-01"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-02"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-04"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-05"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-06"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-07"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-08"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-09"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"employeeName": "Alice",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"employeeName": "Bob",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"employeeName": "Charlie",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"employeeName": "David",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Placement",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"employeeName": "Eve",
|
|
||||||
"branch": "Pune",
|
|
||||||
"department": "Digital Marketing",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"employeeName": "Frank",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "IT",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"employeeName": "Grace",
|
|
||||||
"branch": "Hyderabad",
|
|
||||||
"department": "Training",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": false,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"employeeName": "Heidi",
|
|
||||||
"branch": "Bengaluru",
|
|
||||||
"department": "Analytics",
|
|
||||||
"company": "CLRI",
|
|
||||||
"isCheckedIn": true,
|
|
||||||
"date": "2026-07-10"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"date": "2026-07-01",
|
|
||||||
"timeIn": "09:05 AM",
|
|
||||||
"timeOut": "06:15 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-02",
|
|
||||||
"timeIn": "10:30 AM",
|
|
||||||
"timeOut": "06:30 PM",
|
|
||||||
"loggedHours": "8h 00m",
|
|
||||||
"status": "Late"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-03",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Leave"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-04",
|
|
||||||
"timeIn": "10:12 AM",
|
|
||||||
"timeOut": "06:30 PM",
|
|
||||||
"loggedHours": "8h 18m",
|
|
||||||
"status": "Late"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-06",
|
|
||||||
"timeIn": "09:00 AM",
|
|
||||||
"timeOut": "05:05 PM",
|
|
||||||
"loggedHours": "8h 05m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-07",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Holiday"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-08",
|
|
||||||
"timeIn": "09:15 AM",
|
|
||||||
"timeOut": "02:00 PM",
|
|
||||||
"loggedHours": "4h 45m",
|
|
||||||
"status": "Half Day"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-10",
|
|
||||||
"timeIn": "08:45 AM",
|
|
||||||
"timeOut": "05:30 PM",
|
|
||||||
"loggedHours": "8h 45m",
|
|
||||||
"status": "WFH"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-13",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-15",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Absent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-17",
|
|
||||||
"timeIn": "09:15 AM",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Mispunch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-20",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Holiday"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-21",
|
|
||||||
"timeIn": "09:00 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 00m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-26",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Holiday"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-27",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "WFH"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-28",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-29",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-30",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-31",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-08-01",
|
|
||||||
"timeIn": "08:50 AM",
|
|
||||||
"timeOut": "06:00 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"date": "2026-07-01",
|
|
||||||
"timeIn": "09:05 AM",
|
|
||||||
"timeOut": "06:15 PM",
|
|
||||||
"loggedHours": "9h 10m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-02",
|
|
||||||
"timeIn": "10:30 AM",
|
|
||||||
"timeOut": "06:30 PM",
|
|
||||||
"loggedHours": "8h 00m",
|
|
||||||
"status": "Late"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-03",
|
|
||||||
"timeIn": "-",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Leave"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-04",
|
|
||||||
"timeIn": "09:00 AM",
|
|
||||||
"timeOut": "05:05 PM",
|
|
||||||
"loggedHours": "8h 05m",
|
|
||||||
"status": "Present"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-08",
|
|
||||||
"timeIn": "09:15 AM",
|
|
||||||
"timeOut": "02:00 PM",
|
|
||||||
"loggedHours": "4h 45m",
|
|
||||||
"status": "Half Day"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-17",
|
|
||||||
"timeIn": "09:15 AM",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Mispunch"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"date": "2026-07-01",
|
|
||||||
"timeIn": "09:30 AM",
|
|
||||||
"timeOut": "06:30 PM",
|
|
||||||
"loggedHours": "9h 00m",
|
|
||||||
"status": "Late"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-03",
|
|
||||||
"timeIn": "08:45 AM",
|
|
||||||
"timeOut": "05:30 PM",
|
|
||||||
"loggedHours": "8h 45m",
|
|
||||||
"status": "WFH"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-07-10",
|
|
||||||
"timeIn": "09:00 AM",
|
|
||||||
"timeOut": "-",
|
|
||||||
"loggedHours": "-",
|
|
||||||
"status": "Mispunch"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "BR001",
|
|
||||||
"name": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR002",
|
|
||||||
"name": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR003",
|
|
||||||
"name": "Hyderabad",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR004",
|
|
||||||
"name": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR005",
|
|
||||||
"name": "Delhi",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR006",
|
|
||||||
"name": "Chennai",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR007",
|
|
||||||
"name": "Kolkata",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR008",
|
|
||||||
"name": "Ahmedabad",
|
|
||||||
"companyId": "CMP004",
|
|
||||||
"companyName": "Global Systems",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR009",
|
|
||||||
"name": "Jaipur",
|
|
||||||
"companyId": "CMP004",
|
|
||||||
"companyName": "Global Systems",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR010",
|
|
||||||
"name": "Surat",
|
|
||||||
"companyId": "CMP005",
|
|
||||||
"companyName": "Alpha Corp",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR011",
|
|
||||||
"name": "Lucknow",
|
|
||||||
"companyId": "CMP006",
|
|
||||||
"companyName": "Beta Industries",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "BR012",
|
|
||||||
"name": "Chandigarh",
|
|
||||||
"companyId": "CMP006",
|
|
||||||
"companyName": "Beta Industries",
|
|
||||||
"status": "Active"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "CMP001",
|
|
||||||
"name": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP002",
|
|
||||||
"name": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP003",
|
|
||||||
"name": "Infotech Ltd",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP004",
|
|
||||||
"name": "Global Systems",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP005",
|
|
||||||
"name": "Alpha Corp",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP006",
|
|
||||||
"name": "Beta Industries",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP007",
|
|
||||||
"name": "Gamma Tech",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP008",
|
|
||||||
"name": "Delta Services",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP009",
|
|
||||||
"name": "Epsilon Enterprises",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP010",
|
|
||||||
"name": "Zeta Labs",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP011",
|
|
||||||
"name": "Eta Group",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "CMP012",
|
|
||||||
"name": "Theta Digital",
|
|
||||||
"status": "Active"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,110 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "DEP001",
|
|
||||||
"name": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP002",
|
|
||||||
"name": "Training",
|
|
||||||
"branchId": "BR002",
|
|
||||||
"branchName": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP003",
|
|
||||||
"name": "Analytics",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP004",
|
|
||||||
"name": "Placement",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP005",
|
|
||||||
"name": "Digital Marketing",
|
|
||||||
"branchId": "BR002",
|
|
||||||
"branchName": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP006",
|
|
||||||
"name": "Software Dev",
|
|
||||||
"branchId": "BR004",
|
|
||||||
"branchName": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP007",
|
|
||||||
"name": "QA",
|
|
||||||
"branchId": "BR004",
|
|
||||||
"branchName": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP008",
|
|
||||||
"name": "HR",
|
|
||||||
"branchId": "BR006",
|
|
||||||
"branchName": "Chennai",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP009",
|
|
||||||
"name": "Sales",
|
|
||||||
"branchId": "BR006",
|
|
||||||
"branchName": "Chennai",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP010",
|
|
||||||
"name": "Support",
|
|
||||||
"branchId": "BR008",
|
|
||||||
"branchName": "Ahmedabad",
|
|
||||||
"companyId": "CMP004",
|
|
||||||
"companyName": "Global Systems",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP011",
|
|
||||||
"name": "Admin",
|
|
||||||
"branchId": "BR010",
|
|
||||||
"branchName": "Surat",
|
|
||||||
"companyId": "CMP005",
|
|
||||||
"companyName": "Alpha Corp",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DEP012",
|
|
||||||
"name": "Operations",
|
|
||||||
"branchId": "BR011",
|
|
||||||
"branchName": "Lucknow",
|
|
||||||
"companyId": "CMP006",
|
|
||||||
"companyName": "Beta Industries",
|
|
||||||
"status": "Active"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "DSG001",
|
|
||||||
"name": "Software Engineer",
|
|
||||||
"deptId": "DEP001",
|
|
||||||
"deptName": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG002",
|
|
||||||
"name": "Senior Software Engineer",
|
|
||||||
"deptId": "DEP001",
|
|
||||||
"deptName": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG003",
|
|
||||||
"name": "IT Manager",
|
|
||||||
"deptId": "DEP001",
|
|
||||||
"deptName": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG004",
|
|
||||||
"name": "Trainer",
|
|
||||||
"deptId": "DEP002",
|
|
||||||
"deptName": "Training",
|
|
||||||
"branchId": "BR002",
|
|
||||||
"branchName": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG005",
|
|
||||||
"name": "Lead Trainer",
|
|
||||||
"deptId": "DEP002",
|
|
||||||
"deptName": "Training",
|
|
||||||
"branchId": "BR002",
|
|
||||||
"branchName": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG006",
|
|
||||||
"name": "Data Analyst",
|
|
||||||
"deptId": "DEP003",
|
|
||||||
"deptName": "Analytics",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG007",
|
|
||||||
"name": "Placement Officer",
|
|
||||||
"deptId": "DEP004",
|
|
||||||
"deptName": "Placement",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG008",
|
|
||||||
"name": "QA Tester",
|
|
||||||
"deptId": "DEP006",
|
|
||||||
"deptName": "Software Dev",
|
|
||||||
"branchId": "BR004",
|
|
||||||
"branchName": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG009",
|
|
||||||
"name": "Project Manager",
|
|
||||||
"deptId": "DEP006",
|
|
||||||
"deptName": "Software Dev",
|
|
||||||
"branchId": "BR004",
|
|
||||||
"branchName": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG010",
|
|
||||||
"name": "HR Executive",
|
|
||||||
"deptId": "DEP008",
|
|
||||||
"deptName": "HR",
|
|
||||||
"branchId": "BR006",
|
|
||||||
"branchName": "Chennai",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG011",
|
|
||||||
"name": "Sales Head",
|
|
||||||
"deptId": "DEP009",
|
|
||||||
"deptName": "Sales",
|
|
||||||
"branchId": "BR006",
|
|
||||||
"branchName": "Chennai",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "DSG012",
|
|
||||||
"name": "Support Engineer",
|
|
||||||
"deptId": "DEP010",
|
|
||||||
"deptName": "Support",
|
|
||||||
"branchId": "BR008",
|
|
||||||
"branchName": "Ahmedabad",
|
|
||||||
"companyId": "CMP004",
|
|
||||||
"companyName": "Global Systems",
|
|
||||||
"status": "Active"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "EMP001",
|
|
||||||
"name": "Alice Williams",
|
|
||||||
"email": "AliceWilliams@gmail.com",
|
|
||||||
"designationId": "DSG001",
|
|
||||||
"designationName": "Software Engineer",
|
|
||||||
"deptId": "DEP001",
|
|
||||||
"deptName": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP002",
|
|
||||||
"name": "Bob Smith",
|
|
||||||
"email": "BobSmith@gmail.com",
|
|
||||||
"designationId": "DSG002",
|
|
||||||
"designationName": "Senior Software Engineer",
|
|
||||||
"deptId": "DEP001",
|
|
||||||
"deptName": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP003",
|
|
||||||
"name": "Charlie Brown",
|
|
||||||
"email": "CharlieBrown@gmail.com",
|
|
||||||
"designationId": "DSG004",
|
|
||||||
"designationName": "Trainer",
|
|
||||||
"deptId": "DEP002",
|
|
||||||
"deptName": "Training",
|
|
||||||
"branchId": "BR002",
|
|
||||||
"branchName": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP004",
|
|
||||||
"name": "Diana Prince",
|
|
||||||
"email": "DianaPrince@gmail.com",
|
|
||||||
"designationId": "DSG006",
|
|
||||||
"designationName": "Data Analyst",
|
|
||||||
"deptId": "DEP003",
|
|
||||||
"deptName": "Analytics",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP005",
|
|
||||||
"name": "Evan Wright",
|
|
||||||
"email": "EvanWright@gmail.com",
|
|
||||||
"designationId": "DSG008",
|
|
||||||
"designationName": "QA Tester",
|
|
||||||
"deptId": "DEP006",
|
|
||||||
"deptName": "Software Dev",
|
|
||||||
"branchId": "BR004",
|
|
||||||
"branchName": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP006",
|
|
||||||
"name": "Fiona Gallagher",
|
|
||||||
"email": "FionaGallagher@gmail.com",
|
|
||||||
"designationId": "DSG010",
|
|
||||||
"designationName": "HR Executive",
|
|
||||||
"deptId": "DEP008",
|
|
||||||
"deptName": "HR",
|
|
||||||
"branchId": "BR006",
|
|
||||||
"branchName": "Chennai",
|
|
||||||
"companyId": "CMP003",
|
|
||||||
"companyName": "Infotech Ltd",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP007",
|
|
||||||
"name": "George Costanza",
|
|
||||||
"email": "GeorgeCostanza@gmail.com",
|
|
||||||
"designationId": "DSG007",
|
|
||||||
"designationName": "Placement Officer",
|
|
||||||
"deptId": "DEP004",
|
|
||||||
"deptName": "Placement",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP008",
|
|
||||||
"name": "Hannah Abbott",
|
|
||||||
"email": "HannahAbbott@gmail.com",
|
|
||||||
"designationId": "DSG012",
|
|
||||||
"designationName": "Support Engineer",
|
|
||||||
"deptId": "DEP010",
|
|
||||||
"deptName": "Support",
|
|
||||||
"branchId": "BR008",
|
|
||||||
"branchName": "Ahmedabad",
|
|
||||||
"companyId": "CMP004",
|
|
||||||
"companyName": "Global Systems",
|
|
||||||
"status": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP009",
|
|
||||||
"name": "Ian Malcolm",
|
|
||||||
"email": "IanMalcolm@gmail.com",
|
|
||||||
"designationId": "DSG005",
|
|
||||||
"designationName": "Lead Trainer",
|
|
||||||
"deptId": "DEP002",
|
|
||||||
"deptName": "Training",
|
|
||||||
"branchId": "BR002",
|
|
||||||
"branchName": "Pune",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP010",
|
|
||||||
"name": "Jane Porter",
|
|
||||||
"email": "JanePorter@gmail.com",
|
|
||||||
"designationId": "DSG009",
|
|
||||||
"designationName": "Project Manager",
|
|
||||||
"deptId": "DEP006",
|
|
||||||
"deptName": "Software Dev",
|
|
||||||
"branchId": "BR004",
|
|
||||||
"branchName": "Mumbai",
|
|
||||||
"companyId": "CMP002",
|
|
||||||
"companyName": "TechNova Solutions",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP011",
|
|
||||||
"name": "Kevin Hart",
|
|
||||||
"email": "KevinHart@gmail.com",
|
|
||||||
"designationId": "DSG001",
|
|
||||||
"designationName": "Software Engineer",
|
|
||||||
"deptId": "DEP001",
|
|
||||||
"deptName": "IT",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "EMP012",
|
|
||||||
"name": "Laura Croft",
|
|
||||||
"email": "LauraCroft@gmail.com",
|
|
||||||
"designationId": "DSG006",
|
|
||||||
"designationName": "Data Analyst",
|
|
||||||
"deptId": "DEP003",
|
|
||||||
"deptName": "Analytics",
|
|
||||||
"branchId": "BR001",
|
|
||||||
"branchName": "Bengaluru",
|
|
||||||
"companyId": "CMP001",
|
|
||||||
"companyName": "CLRI",
|
|
||||||
"status": "Active"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"branchName": "Bengaluru",
|
"branchName": "Bengaluru",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": true
|
"isHRManager": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP002",
|
"id": "EMP002",
|
||||||
@ -19,7 +19,7 @@
|
|||||||
"branchName": "Bengaluru",
|
"branchName": "Bengaluru",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP003",
|
"id": "EMP003",
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"branchName": "Pune",
|
"branchName": "Pune",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Disabled",
|
"status": "Disabled",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP004",
|
"id": "EMP004",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"branchName": "Bengaluru",
|
"branchName": "Bengaluru",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": true
|
"isHRManager": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP005",
|
"id": "EMP005",
|
||||||
@ -52,7 +52,7 @@
|
|||||||
"branchName": "Mumbai",
|
"branchName": "Mumbai",
|
||||||
"companyName": "TechNova Solutions",
|
"companyName": "TechNova Solutions",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP006",
|
"id": "EMP006",
|
||||||
@ -63,7 +63,7 @@
|
|||||||
"branchName": "Chennai",
|
"branchName": "Chennai",
|
||||||
"companyName": "Infotech Ltd",
|
"companyName": "Infotech Ltd",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP007",
|
"id": "EMP007",
|
||||||
@ -74,7 +74,7 @@
|
|||||||
"branchName": "Bengaluru",
|
"branchName": "Bengaluru",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": true
|
"isHRManager": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP008",
|
"id": "EMP008",
|
||||||
@ -85,7 +85,7 @@
|
|||||||
"branchName": "Ahmedabad",
|
"branchName": "Ahmedabad",
|
||||||
"companyName": "Global Systems",
|
"companyName": "Global Systems",
|
||||||
"status": "Disabled",
|
"status": "Disabled",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP009",
|
"id": "EMP009",
|
||||||
@ -96,7 +96,7 @@
|
|||||||
"branchName": "Pune",
|
"branchName": "Pune",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP010",
|
"id": "EMP010",
|
||||||
@ -107,7 +107,7 @@
|
|||||||
"branchName": "Mumbai",
|
"branchName": "Mumbai",
|
||||||
"companyName": "TechNova Solutions",
|
"companyName": "TechNova Solutions",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": true
|
"isHRManager": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP011",
|
"id": "EMP011",
|
||||||
@ -118,7 +118,7 @@
|
|||||||
"branchName": "Bengaluru",
|
"branchName": "Bengaluru",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "EMP012",
|
"id": "EMP012",
|
||||||
@ -129,6 +129,6 @@
|
|||||||
"branchName": "Bengaluru",
|
"branchName": "Bengaluru",
|
||||||
"companyName": "CLRI",
|
"companyName": "CLRI",
|
||||||
"status": "Active",
|
"status": "Active",
|
||||||
"isAdmin": false
|
"isHRManager": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1,49 +1,73 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"companyId": "CMP001",
|
"companyId": "CMP001",
|
||||||
"companyName": "CLRI",
|
"companyName": "CliniLaunch Research Institute - CLRI",
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"id": "POL001",
|
"id": "POL001",
|
||||||
"title": "Casual Leave Policy",
|
|
||||||
"category": "Leave",
|
|
||||||
"description": "Employees are entitled to 12 casual leaves per calendar year. A maximum of 3 casual leaves can be carried forward to the next year. Leaves must be approved by the reporting manager at least 2 days in advance."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "POL002",
|
|
||||||
"title": "Late Arrival Rule",
|
|
||||||
"category": "Attendance",
|
|
||||||
"description": "Employees are expected to check in by 09:30 AM. A grace period of 15 minutes is allowed. More than 3 late arrivals in a month will result in a half-day leave deduction."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "POL003",
|
|
||||||
"title": "Loss of Pay (LOP)",
|
"title": "Loss of Pay (LOP)",
|
||||||
"category": "Leave",
|
"category": "Leave",
|
||||||
"description": "If an employee exhausts all available leave balances and cannot attend work, the days will be marked as LOP. LOP is deducted from the monthly salary on a pro-rata basis."
|
"description": "If an employee exhausts all available leave balances and cannot attend work, the days will be marked as LOP. LOP is deducted from the monthly salary on a pro-rata basis."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "POL002",
|
||||||
|
"title": "Casual Leave Policy",
|
||||||
|
"category": "Leave",
|
||||||
|
"description": "Employees are entitled to 10 casual leaves per calendar year. A maximum of 2 casual leaves can be taken in a month. Leaves must be approved by the reporting manager at least 2 days in advance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "POL003",
|
||||||
|
"title": "Optional Holidays",
|
||||||
|
"category": "Leave",
|
||||||
|
"description": "Employees can choose up to 4 optional holidays from the list provided by the HR department. These are not encashable and cannot be carried forward."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "POL004",
|
"id": "POL004",
|
||||||
"title": "Work From Home (WFH)",
|
"title": "Late Arrival Rule",
|
||||||
"category": "General",
|
"category": "Attendance",
|
||||||
"description": "Employees are allowed to avail WFH for up to 4 days a month, subject to prior approval from their manager. WFH cannot be combined with weekends or holidays."
|
"description": "Employees are expected to check in by 10:00 AM. A grace period of 10 minutes is allowed. More than 2 late arrivals in a month will result in a half-day leave deduction."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "POL005",
|
||||||
|
"title": "Shift Timings",
|
||||||
|
"category": "Attendance",
|
||||||
|
"description": "General shift is from 10:00 AM to 06:00 PM. Employees must complete a minimum of 8 hours per day, excluding the lunch break."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"companyId": "CMP002",
|
"companyId": "CMP002",
|
||||||
"companyName": "TechNova Solutions",
|
"companyName": "Win in Life Academy - WILA",
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"id": "POL005",
|
"id": "POL001",
|
||||||
"title": "Optional Holidays",
|
"title": "Loss of Pay (LOP)",
|
||||||
"category": "Leave",
|
"category": "Leave",
|
||||||
"description": "Employees can choose up to 2 optional holidays from the list provided by the HR department. These are not encashable and cannot be carried forward."
|
"description": "If an employee exhausts all available leave balances and cannot attend work, the days will be marked as LOP. LOP is deducted from the monthly salary on a pro-rata basis."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "POL006",
|
"id": "POL002",
|
||||||
|
"title": "Casual Leave Policy",
|
||||||
|
"category": "Leave",
|
||||||
|
"description": "Employees are entitled to 10 casual leaves per calendar year. A maximum of 2 casual leaves can be taken in a month. Leaves must be approved by the reporting manager at least 2 days in advance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "POL003",
|
||||||
|
"title": "Optional Holidays",
|
||||||
|
"category": "Leave",
|
||||||
|
"description": "Employees can choose up to 4 optional holidays from the list provided by the HR department. These are not encashable and cannot be carried forward."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "POL004",
|
||||||
|
"title": "Late Arrival Rule",
|
||||||
|
"category": "Attendance",
|
||||||
|
"description": "Employees are expected to check in by 10:00 AM. A grace period of 10 minutes is allowed. More than 2 late arrivals in a month will result in a half-day leave deduction."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "POL005",
|
||||||
"title": "Shift Timings",
|
"title": "Shift Timings",
|
||||||
"category": "Attendance",
|
"category": "Attendance",
|
||||||
"description": "General shift is from 09:00 AM to 06:00 PM. Employees must complete a minimum of 8.5 hours per day, excluding the lunch break."
|
"description": "General shift is from 10:00 AM to 06:00 PM. Employees must complete a minimum of 8 hours per day, excluding the lunch break."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"totalCompanies": 1,
|
|
||||||
"totalBranches": 3,
|
|
||||||
"totalDepartments": 5,
|
|
||||||
"totalDesignations": 4,
|
|
||||||
"totalEmployees": 245
|
|
||||||
}
|
|
||||||
@ -1,4 +1,34 @@
|
|||||||
// Generic GET request for any module
|
import axios from 'axios';
|
||||||
|
import { store } from '../store/store';
|
||||||
|
|
||||||
|
// Use the Vite Proxy! Do not put the full URL here.
|
||||||
|
// By using an empty string, requests go to localhost:5173/api/...
|
||||||
|
// and Vite forwards them to 192.168.60.237:8000/api/...
|
||||||
|
const API_BASE_URL = '';
|
||||||
|
|
||||||
|
export const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const state = store.getState();
|
||||||
|
const { mockUserId, mockUserRole, mockBranches } = state.role;
|
||||||
|
|
||||||
|
if (mockUserId) {
|
||||||
|
config.headers['X-Mock-User-Id'] = mockUserId;
|
||||||
|
config.headers['X-Mock-User-Role'] = mockUserRole;
|
||||||
|
config.headers['X-Mock-Branches'] = mockBranches;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T> => {
|
export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T> => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
const response = await fetch(`/mockApi/${endpoint}`);
|
const response = await fetch(`/mockApi/${endpoint}`);
|
||||||
@ -6,7 +36,6 @@ export const fetchMockData = async <T>(endpoint: string, delay = 500): Promise<T
|
|||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generic POST request for any module
|
|
||||||
export const postMockData = async <T>(endpoint: string, payload: T, delay = 800): Promise<T> => {
|
export const postMockData = async <T>(endpoint: string, payload: T, delay = 800): Promise<T> => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
console.log(`[Mock POST] /mockApi/${endpoint}`, payload);
|
console.log(`[Mock POST] /mockApi/${endpoint}`, payload);
|
||||||
|
|||||||
@ -1,34 +1,59 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import type { RootState } from '../../store/store';
|
||||||
|
import { getSidebarItems, type NavItem } from './types/sidebarConfig';
|
||||||
import { ChevronRight, Home } from 'lucide-react';
|
import { ChevronRight, Home } from 'lucide-react';
|
||||||
|
|
||||||
// Map URL segments to readable names
|
// Fallback map for URL segments if dynamic lookup fails (e.g., for IDs in URL)
|
||||||
const routeNameMap: Record<string, string> = {
|
const routeNameMap: Record<string, string> = {
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
attendance: 'Attendance',
|
attendance: 'Attendance Summary',
|
||||||
leave: 'Leave',
|
leave: 'Leave Summary',
|
||||||
approval: 'Approval',
|
approval: 'Approve Leaves',
|
||||||
'attendance-summary': 'Attendance Summary',
|
'attendance-summary': 'Attendance Summary',
|
||||||
monthly: 'Monthly Report',
|
monthly: 'Monthly Report',
|
||||||
daily: 'Daily Report',
|
daily: 'Daily Report',
|
||||||
regularization: 'Regularization',
|
regularization: 'Regularization',
|
||||||
'leave-summary': 'Leave Summary',
|
'leave-summary': 'Leave Summary',
|
||||||
'master-data': 'Master Data',
|
'master-data': 'Master Data Service',
|
||||||
company: 'Manage Company',
|
company: 'Manage Company',
|
||||||
branches: 'Manage Branches',
|
branches: 'Manage Branches',
|
||||||
departments: 'Manage Departments',
|
departments: 'Manage Departments',
|
||||||
designations: 'Manage Designations',
|
'job-roles': 'Manage Job Roles',
|
||||||
employees: 'Manage Employees',
|
employees: 'Manage Employees',
|
||||||
'manage-admins': 'Manage Admins',
|
'manage-hrmanagers': 'Manage HR Managers',
|
||||||
policies: 'Policy and Rules',
|
policies: 'Policy and Rules',
|
||||||
applications: 'Leave Applications',
|
applications: 'Leave Applications',
|
||||||
ledger: 'Ledger Report'
|
balance: 'Leave Balance Report'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Breadcrumbs() {
|
export default function Breadcrumbs() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const currentRole = useSelector((state: RootState) => state.role.currentRole);
|
||||||
const pathnames = location.pathname.split('/').filter(x => x);
|
const pathnames = location.pathname.split('/').filter(x => x);
|
||||||
|
|
||||||
// If we are exactly at /dashboard, just show a static breadcrumb
|
// Generate a map of paths to breadcrumb trails based on the user's role sidebar config
|
||||||
|
const dynamicBreadcrumbs = useMemo(() => {
|
||||||
|
const items = getSidebarItems(currentRole);
|
||||||
|
const map: Record<string, { name: string; path?: string }[]> = {};
|
||||||
|
|
||||||
|
const traverse = (nodes: NavItem[], trail: { name: string; path?: string }[] = []) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
const newTrail = [...trail, { name: node.name, path: node.path }];
|
||||||
|
if (node.path) {
|
||||||
|
map[node.path] = newTrail;
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
traverse(node.children, newTrail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
traverse(items);
|
||||||
|
return map;
|
||||||
|
}, [currentRole]);
|
||||||
|
|
||||||
|
// Special case for the main dashboard route
|
||||||
if (pathnames.length === 0 || (pathnames.length === 1 && pathnames[0] === 'dashboard')) {
|
if (pathnames.length === 0 || (pathnames.length === 1 && pathnames[0] === 'dashboard')) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center text-sm text-text-muted mb-4">
|
<div className="flex items-center text-sm text-text-muted mb-4">
|
||||||
@ -38,6 +63,36 @@ export default function Breadcrumbs() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look up the current URL in our dynamically generated breadcrumb map
|
||||||
|
const breadcrumbTrail = dynamicBreadcrumbs[location.pathname];
|
||||||
|
|
||||||
|
// If found, render the exact hierarchy from the sidebar config
|
||||||
|
if (breadcrumbTrail) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center text-sm text-text-muted mb-4">
|
||||||
|
<Link to="/dashboard" className="hover:text-primary flex items-center">
|
||||||
|
<Home size={16} className="mr-1 text-text-light" />
|
||||||
|
</Link>
|
||||||
|
{breadcrumbTrail.map((crumb, index) => {
|
||||||
|
const isLast = index === breadcrumbTrail.length - 1;
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex items-center">
|
||||||
|
<ChevronRight size={16} className="mx-1 text-text-light" />
|
||||||
|
{isLast || !crumb.path ? (
|
||||||
|
<span className={`font-medium ${isLast ? 'text-text-primary' : 'text-text-muted'}`}>
|
||||||
|
{crumb.name}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Link to={crumb.path} className="hover:text-primary">{crumb.name}</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to URL segment mapping for unknown routes (like viewing a specific employee by ID)
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center text-sm text-text-muted mb-4">
|
<div className="flex items-center text-sm text-text-muted mb-4">
|
||||||
<Link to="/dashboard" className="hover:text-primary flex items-center">
|
<Link to="/dashboard" className="hover:text-primary flex items-center">
|
||||||
@ -45,11 +100,8 @@ export default function Breadcrumbs() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{pathnames.map((name, index) => {
|
{pathnames.map((name, index) => {
|
||||||
// Reconstruct the route path up to this segment
|
|
||||||
const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
|
const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
|
||||||
const isLast = index === pathnames.length - 1;
|
const isLast = index === pathnames.length - 1;
|
||||||
|
|
||||||
// Use the map for pretty names, fallback to capitalizing the string
|
|
||||||
const displayName = routeNameMap[name] || name.charAt(0).toUpperCase() + name.slice(1);
|
const displayName = routeNameMap[name] || name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -10,9 +10,9 @@ import LeaveRouter from '../../features/leave/routes/LeaveRouter';
|
|||||||
import ApprovalRouter from '../../features/leave/routes/ApprovalRouter';
|
import ApprovalRouter from '../../features/leave/routes/ApprovalRouter';
|
||||||
import AttendanceSummaryRouter from '../../features/attendance/routes/AttendanceSummaryRouter';
|
import AttendanceSummaryRouter from '../../features/attendance/routes/AttendanceSummaryRouter';
|
||||||
import LeaveSummaryRouter from '../../features/leave/routes/LeaveSummaryRouter';
|
import LeaveSummaryRouter from '../../features/leave/routes/LeaveSummaryRouter';
|
||||||
import MasterDataRouter from '../../features/employee-management/routes/MasterDataRouter';
|
import MasterDataRouter from '../../features/masterData/routes/MasterDataRouter';
|
||||||
import PolicyRouter from '../../features/employee-management/routes/PolicyRouter';
|
import PolicyRouter from '../../features/masterData/routes/PolicyRouter';
|
||||||
import ManageAdminsRouter from '../../features/employee-management/routes/ManageAdminsRouter';
|
import ManageHRManagersRouter from '../../features/masterData/routes/ManageHRManagersRouter';
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
@ -40,7 +40,7 @@ export default function MainLayout() {
|
|||||||
<Route path="/attendance-summary/*" element={<AttendanceSummaryRouter />} />
|
<Route path="/attendance-summary/*" element={<AttendanceSummaryRouter />} />
|
||||||
<Route path="/leave-summary/*" element={<LeaveSummaryRouter />} />
|
<Route path="/leave-summary/*" element={<LeaveSummaryRouter />} />
|
||||||
<Route path="/master-data/*" element={<MasterDataRouter />} />
|
<Route path="/master-data/*" element={<MasterDataRouter />} />
|
||||||
<Route path="/manage-admins" element={<ManageAdminsRouter />} />
|
<Route path="/manage-hrmanagers" element={<ManageHRManagersRouter />} />
|
||||||
<Route path="/policies" element={<PolicyRouter />} />
|
<Route path="/policies" element={<PolicyRouter />} />
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -2,59 +2,22 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import type { RootState } from '../../store/store';
|
import type { RootState } from '../../store/store';
|
||||||
import { setRole, type UserRole } from '../../store/roleSlice';
|
import { setRole, type UserRole } from '../../store/roleSlice';
|
||||||
import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
|
import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { getSidebarItems, type NavItem } from './types/sidebarConfig';
|
||||||
|
|
||||||
// Role-specific search data
|
// Helper function to flatten the nested sidebar items into a simple list for search
|
||||||
const roleSearchData: Record<UserRole, { name: string; path: string }[]> = {
|
const flattenNavItems = (items: NavItem[]): { name: string; path: string }[] => {
|
||||||
employee: [
|
let result: { name: string; path: string }[] = [];
|
||||||
{ name: 'My Dashboard', path: '/dashboard' },
|
for (const item of items) {
|
||||||
{ name: 'My Attendance', path: '/attendance' },
|
if (item.path) {
|
||||||
{ name: 'Apply for Leave', path: '/leave' },
|
result.push({ name: item.name, path: item.path });
|
||||||
],
|
}
|
||||||
manager: [
|
if (item.children && item.children.length > 0) {
|
||||||
{ name: 'Manager Dashboard', path: '/dashboard' },
|
result = result.concat(flattenNavItems(item.children));
|
||||||
{ name: 'My Attendance', path: '/attendance' },
|
}
|
||||||
{ name: 'My Leaves', path: '/leave' },
|
}
|
||||||
{ name: 'Pending Approvals', path: '/approval' },
|
return result;
|
||||||
],
|
|
||||||
admin: [
|
|
||||||
{ name: 'Admin Dashboard', path: '/dashboard' },
|
|
||||||
{ name: 'All Attendance Logs', path: '/attendance' },
|
|
||||||
{ name: 'All Leave Requests', path: '/leave' },
|
|
||||||
{ name: 'Pending Approvals', path: '/approval' },
|
|
||||||
{ name: 'Attendance Summary Dashboard', path: '/attendance-summary/dashboard' },
|
|
||||||
{ name: 'Monthly Attendance Report', path: '/attendance-summary/monthly' },
|
|
||||||
{ name: 'Daily Attendance Report', path: '/attendance-summary/daily' },
|
|
||||||
{ name: 'Attendance Regularization', path: '/attendance-summary/regularization' },
|
|
||||||
{ name: 'Leave Applications', path: '/leave-summary/applications' },
|
|
||||||
{ name: 'Leave Ledger Report', path: '/leave-summary/ledger' },
|
|
||||||
{ name: 'Master Data Dashboard', path: '/master-data/dashboard' },
|
|
||||||
{ name: 'Manage Company', path: '/master-data/company' },
|
|
||||||
{ name: 'Manage Branches', path: '/master-data/branches' },
|
|
||||||
{ name: 'Manage Departments', path: '/master-data/departments' },
|
|
||||||
{ name: 'Manage Designations', path: '/master-data/designations' },
|
|
||||||
{ name: 'Manage Employees', path: '/master-data/employees' },
|
|
||||||
],
|
|
||||||
superadmin: [
|
|
||||||
{ name: 'SuperAdmin Dashboard', path: '/dashboard' },
|
|
||||||
{ name: 'Global Attendance Logs', path: '/attendance' },
|
|
||||||
{ name: 'Global Leave Requests', path: '/leave' },
|
|
||||||
{ name: 'Pending Approvals', path: '/approval' },
|
|
||||||
{ name: 'Attendance Summary Dashboard', path: '/attendance-summary/dashboard' },
|
|
||||||
{ name: 'Monthly Attendance Report', path: '/attendance-summary/monthly' },
|
|
||||||
{ name: 'Daily Attendance Report', path: '/attendance-summary/daily' },
|
|
||||||
{ name: 'Leave Applications', path: '/leave-summary/applications' },
|
|
||||||
{ name: 'Leave Ledger Report', path: '/leave-summary/ledger' },
|
|
||||||
{ name: 'Master Data Dashboard', path: '/master-data/dashboard' },
|
|
||||||
{ name: 'Manage Company', path: '/master-data/company' },
|
|
||||||
{ name: 'Manage Branches', path: '/master-data/branches' },
|
|
||||||
{ name: 'Manage Departments', path: '/master-data/departments' },
|
|
||||||
{ name: 'Manage Designations', path: '/master-data/designations' },
|
|
||||||
{ name: 'Manage Employees', path: '/master-data/employees' },
|
|
||||||
{ name: 'Manage Admins', path: '/manage-admins' },
|
|
||||||
{ name: 'Policy and Rules', path: '/policies' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
@ -67,7 +30,7 @@ export default function Navbar() {
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const roles: UserRole[] = ['employee', 'manager', 'admin', 'superadmin'];
|
const roles: UserRole[] = ['employee', 'manager', 'hrmanager', 'director'];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSearchOpen && searchInputRef.current) {
|
if (isSearchOpen && searchInputRef.current) {
|
||||||
@ -86,11 +49,18 @@ export default function Navbar() {
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Get search items based on the current role
|
// Dynamically generate search items based on the current role's sidebar configuration
|
||||||
const availableItems = roleSearchData[currentRole] || [];
|
const availableItems = useMemo(() => {
|
||||||
const searchResults = availableItems.filter(item =>
|
const sidebarItems = getSidebarItems(currentRole);
|
||||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
return flattenNavItems(sidebarItems);
|
||||||
);
|
}, [currentRole]);
|
||||||
|
|
||||||
|
const searchResults = useMemo(() => {
|
||||||
|
if (!searchQuery) return [];
|
||||||
|
return availableItems.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [availableItems, searchQuery]);
|
||||||
|
|
||||||
const handleSearchClick = (path: string) => {
|
const handleSearchClick = (path: string) => {
|
||||||
navigate(path);
|
navigate(path);
|
||||||
@ -102,7 +72,7 @@ export default function Navbar() {
|
|||||||
<div className="bg-app-card shadow-sm h-16 flex items-center justify-between px-4 md:px-6 border-b border-app-border relative z-30">
|
<div className="bg-app-card shadow-sm h-16 flex items-center justify-between px-4 md:px-6 border-b border-app-border relative z-30">
|
||||||
|
|
||||||
<div className="flex items-center shrink-0">
|
<div className="flex items-center shrink-0">
|
||||||
<h2 className="text-lg font-bold text-primary tracking-wide">CLRI HRMS</h2>
|
<h2 className="text-lg font-bold text-primary tracking-wide">CliniLaunch HRMS</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 md:space-x-4 ml-auto">
|
<div className="flex items-center space-x-2 md:space-x-4 ml-auto">
|
||||||
@ -114,7 +84,7 @@ export default function Navbar() {
|
|||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search ....."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-32 sm:w-44 px-3 py-2 text-sm focus:outline-none rounded-lg text-text-primary min-w-0"
|
className="w-32 sm:w-44 px-3 py-2 text-sm focus:outline-none rounded-lg text-text-primary min-w-0"
|
||||||
@ -175,7 +145,7 @@ export default function Navbar() {
|
|||||||
|
|
||||||
{dropdownOpen && (
|
{dropdownOpen && (
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-app-card border border-app-border rounded-lg shadow-lg z-50">
|
<div className="absolute right-0 mt-2 w-48 bg-app-card border border-app-border rounded-lg shadow-lg z-50">
|
||||||
<p className="px-4 py-2 text-xs font-semibold text-text-light uppercase">Switch Role (Test)</p>
|
<p className="px-4 py-2 text-xs font-semibold text-text-light uppercase">Switch Role</p>
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<button
|
<button
|
||||||
key={role}
|
key={role}
|
||||||
|
|||||||
@ -6,14 +6,14 @@ import { Link, useLocation } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||||
Menu, Users, ShieldUser, ScrollText, ChevronDown, FileText, CalendarDays, ClipboardCheck,
|
Menu, Users, ShieldUser, ScrollText, ChevronDown, FileText, CalendarDays, ClipboardCheck,
|
||||||
Database, Building, Network, Briefcase, Tag, Receipt
|
Database, Building, Network, Briefcase, Tag, ClipboardType, ClipboardMinus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const iconMap: Record<string, any> = {
|
const iconMap: Record<string, any> = {
|
||||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||||
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
|
Users, ShieldUser, ScrollText, FileText, CalendarDays, ClipboardCheck,
|
||||||
Database, Building, Network, Briefcase, Tag, Receipt
|
Database, Building, Network, Briefcase, Tag, ClipboardType,ClipboardMinus
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
|
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
|
||||||
@ -137,12 +137,12 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
|
|||||||
<div className={`bg-sidebar text-white h-screen transition-all duration-300 flex flex-col ${isCollapsed ? 'w-20' : 'w-64'}`}>
|
<div className={`bg-sidebar text-white h-screen transition-all duration-300 flex flex-col ${isCollapsed ? 'w-20' : 'w-64'}`}>
|
||||||
<div className={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
|
<div className={`flex items-center h-16 border-b border-primary/30 px-4 flex-shrink-0 ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<Link to="/dashboard" className="flex items-center">
|
<Link to="/dashboard" className="flex items-center flex-shrink-0">
|
||||||
<img src="logo.gif" alt="Company Logo" className="h-13.5 w-71 cursor-pointer" />
|
<img src="logo.gif" alt="Company Logo" className="h-10 w-auto max-w-[140px] object-contain cursor-pointer" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors">
|
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors flex-shrink-0">
|
||||||
<Menu size={24} />
|
<Menu size={24} className="text-white" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -8,62 +8,94 @@ export interface NavItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getSidebarItems = (role: UserRole): NavItem[] => {
|
export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||||
const baseItems: NavItem[] = [
|
if (role === 'employee') {
|
||||||
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
|
||||||
{ name: 'Attendance', icon: 'CalendarCheck', path: '/attendance' },
|
|
||||||
{ name: 'Leave', icon: 'Plane', path: '/leave' },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (role === 'manager') {
|
|
||||||
return [...baseItems, { name: 'Approval', icon: 'CheckCircle', path: '/approval' }];
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminItems: NavItem[] = [
|
|
||||||
{
|
|
||||||
name: 'Attendance Summary',
|
|
||||||
icon: 'BarChart3',
|
|
||||||
children: [
|
|
||||||
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/attendance-summary/dashboard' },
|
|
||||||
{ name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' },
|
|
||||||
{ name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' },
|
|
||||||
...(role === 'admin' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : [])
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Leave Summary',
|
|
||||||
icon: 'ClipboardList',
|
|
||||||
children: [
|
|
||||||
{ name: 'Leave Applications', icon: 'FileText', path: '/leave-summary/applications' },
|
|
||||||
{ name: 'Ledger Report', icon: 'Receipt', path: '/leave-summary/ledger' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Master Data',
|
|
||||||
icon: 'Database',
|
|
||||||
children: [
|
|
||||||
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/master-data/dashboard' },
|
|
||||||
{ name: 'Manage Company', icon: 'Building', path: '/master-data/company' },
|
|
||||||
{ name: 'Manage Branches', icon: 'Network', path: '/master-data/branches' },
|
|
||||||
{ name: 'Manage Departments', icon: 'Briefcase', path: '/master-data/departments' },
|
|
||||||
{ name: 'Manage Designations', icon: 'Tag', path: '/master-data/designations' },
|
|
||||||
{ name: 'Manage Employees', icon: 'Users', path: '/master-data/employees' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (role === 'admin') {
|
|
||||||
return [...baseItems, { name: 'Approval', icon: 'CheckCircle', path: '/approval' }, ...adminItems];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role === 'superadmin') {
|
|
||||||
return [
|
return [
|
||||||
...baseItems,
|
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||||
{ name: 'Approval', icon: 'CheckCircle', path: '/approval' },
|
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' },
|
||||||
...adminItems,
|
{ name: 'Leave Summary', icon: 'ClipboardMinus', path: '/leave' },
|
||||||
{ name: 'Manage Admins', icon: 'ShieldUser', path: '/manage-admins' },
|
|
||||||
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseItems; // employee
|
if (role === 'manager') {
|
||||||
|
return [
|
||||||
|
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||||
|
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' },
|
||||||
|
{
|
||||||
|
name: 'Leave Service',
|
||||||
|
icon: 'ClipboardMinus',
|
||||||
|
children: [
|
||||||
|
{ name: 'Leave Summary', icon: 'ClipboardMinus', path: '/leave' },
|
||||||
|
{ name: 'Approve Leaves', icon: 'CheckCircle', path: '/approval' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared children for HRMANAGER and DIRECTOR
|
||||||
|
const attendanceSummaryItems: NavItem = {
|
||||||
|
name: 'Attendance Service',
|
||||||
|
icon: 'BarChart3',
|
||||||
|
children: [
|
||||||
|
{ name: 'Attendance Dashboard', icon: 'LayoutDashboard', path: '/attendance-summary/dashboard' },
|
||||||
|
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' },
|
||||||
|
{ name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' },
|
||||||
|
{ name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' },
|
||||||
|
...(role === 'hrmanager' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : [])
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaveSummaryItems: NavItem = {
|
||||||
|
name: 'Leave Service',
|
||||||
|
icon: 'ClipboardList',
|
||||||
|
children: [
|
||||||
|
{ name: 'Leave Summary', icon: 'ClipboardMinus', path: '/leave' },
|
||||||
|
{ name: 'Leave Applications', icon: 'FileText', path: '/leave-summary/applications' },
|
||||||
|
{ name: 'Approve Leaves', icon: 'CheckCircle', path: '/approval' },
|
||||||
|
{ name: 'Leave Balance Report', icon: 'ClipboardType', path: '/leave-summary/balance' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const masterDataItems: NavItem = {
|
||||||
|
name: 'Master Data Service',
|
||||||
|
icon: 'Database',
|
||||||
|
children: [
|
||||||
|
{ name: 'Master Data Dashboard', icon: 'LayoutDashboard', path: '/master-data/dashboard' },
|
||||||
|
{ name: 'Manage Company', icon: 'Building', path: '/master-data/company' },
|
||||||
|
{ name: 'Manage Branches', icon: 'Network', path: '/master-data/branches' },
|
||||||
|
{ name: 'Manage Departments', icon: 'Briefcase', path: '/master-data/departments' },
|
||||||
|
{ name: 'Manage Job Roles', icon: 'Tag', path: '/master-data/job-roles' },
|
||||||
|
{ name: 'Manage Employees', icon: 'Users', path: '/master-data/employees' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. HRMANAGER
|
||||||
|
if (role === 'hrmanager') {
|
||||||
|
return [
|
||||||
|
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||||
|
attendanceSummaryItems,
|
||||||
|
leaveSummaryItems,
|
||||||
|
masterDataItems,
|
||||||
|
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. DIRECTOR
|
||||||
|
if (role === 'director') {
|
||||||
|
return [
|
||||||
|
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||||
|
attendanceSummaryItems,
|
||||||
|
leaveSummaryItems,
|
||||||
|
masterDataItems,
|
||||||
|
{ name: 'Manage HR Managers', icon: 'ShieldUser', path: '/manage-hrmanagers' },
|
||||||
|
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (should ideally not be reached if role is always valid)
|
||||||
|
return [
|
||||||
|
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||||
|
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
|
||||||
|
];
|
||||||
}
|
}
|
||||||
@ -5,35 +5,36 @@ interface ConfirmationModalProps {
|
|||||||
message: string;
|
message: string;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
confirmText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfirmationModal({ title, message, onConfirm, onClose }: ConfirmationModalProps) {
|
export default function ConfirmationModal({ title, message, onConfirm, onClose, confirmText = "Yes, Delete" }: ConfirmationModalProps) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[60] p-4">
|
||||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-sm p-6 relative text-center">
|
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-sm p-6 relative text-center">
|
||||||
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
||||||
<X size={24} />
|
<X size={24} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-absent-100 mb-4">
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-absent-100 mb-4">
|
||||||
<AlertTriangle className="h-6 w-6 text-absent-500" />
|
<AlertTriangle className="h-6 w-6 text-absent-500" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-lg font-medium text-text-primary mb-2">{title}</h3>
|
<h3 className="text-lg font-medium text-text-primary mb-2">{title}</h3>
|
||||||
<p className="text-sm text-text-muted mb-6">{message}</p>
|
<p className="text-sm text-text-muted mb-6">{message}</p>
|
||||||
|
|
||||||
<div className="flex justify-center space-x-3">
|
<div className="flex justify-center space-x-3">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium"
|
className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
className="px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium"
|
className="px-4 py-2 bg-absent-500 text-white rounded-lg hover:bg-absent-600 transition-colors text-sm font-medium"
|
||||||
>
|
>
|
||||||
Yes, Delete
|
{confirmText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ interface LoaderProps {
|
|||||||
|
|
||||||
export default function Loader({
|
export default function Loader({
|
||||||
message = 'Loading...',
|
message = 'Loading...',
|
||||||
color = '#0A66C2',
|
color = '#001045',
|
||||||
fullScreen = false
|
fullScreen = false
|
||||||
}: LoaderProps) {
|
}: LoaderProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,43 +1,203 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { fetchMockData } from '../../../api/client';
|
import { apiClient, fetchMockData } from '../../../api/client';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import type { UserRole } from '../../../store/roleSlice';
|
import type { UserRole } from '../../../store/roleSlice';
|
||||||
import type { AttendanceRecord } from '../types/attendance';
|
import type { AttendanceRecord, AttendanceStatus, AttendanceSummary, DashboardMetrics, DailyReportRecord, AdminReportRecord } from '../types/attendance';
|
||||||
|
|
||||||
const ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
|
// Helper to map backend statuses to frontend statuses
|
||||||
|
const mapStatus = (backendStatus: string): AttendanceStatus => {
|
||||||
|
switch (backendStatus) {
|
||||||
|
case 'FULL_DAY': return 'Present';
|
||||||
|
case 'MISPUNCH': return 'Mispunch';
|
||||||
|
case 'LATE': return 'Late';
|
||||||
|
case 'HALF_DAY': return 'Half Day';
|
||||||
|
case 'WFH': return 'WFH';
|
||||||
|
case 'ABSENT': return 'Absent';
|
||||||
|
case 'LEAVE': return 'Leave';
|
||||||
|
case 'HOLIDAY': return 'Holiday';
|
||||||
|
default: return 'Absent';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Fetch Employee Range Report
|
||||||
|
export const useEmployeeRangeReport = (employeeId: number | null, fromDate: string, toDate: string) => {
|
||||||
|
return useQuery<AttendanceRecord[]>({
|
||||||
|
queryKey: ['employeeRangeReport', employeeId, fromDate, toDate],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ams/attendance/employee-range-report', {
|
||||||
|
params: { employee_id: employeeId, from_date: fromDate, to_date: toDate }
|
||||||
|
});
|
||||||
|
const history = response.data.history || [];
|
||||||
|
return history.map((h: any) => ({
|
||||||
|
date: h.workDate,
|
||||||
|
timeIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
|
||||||
|
timeOut: h.checkOut ? h.checkOut.substring(0, 5) : '-',
|
||||||
|
loggedHours: h.workedHours ? `${Math.floor(h.workedHours)}h ${Math.round((h.workedHours % 1) * 60)}m` : '-',
|
||||||
|
status: mapStatus(h.finalStatus),
|
||||||
|
attendanceId: h.attendanceId || 0
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
enabled: !!employeeId && !!fromDate && !!toDate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2.Fetch Employee Summary
|
||||||
|
export const useAttendanceSummary = (employeeId: number, startDate: string, endDate: string) => {
|
||||||
|
return useQuery<AttendanceSummary>({
|
||||||
|
queryKey: ['attendanceSummary', employeeId, startDate, endDate],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ams/attendance/my-summary', {
|
||||||
|
params: { employee_id: employeeId, start_date: startDate, end_date: endDate }
|
||||||
|
});
|
||||||
|
const data = response.data;
|
||||||
|
const history: AttendanceRecord[] = data.history.map((h: any) => ({
|
||||||
|
date: h.workDate,
|
||||||
|
timeIn: h.checkIn ? h.checkIn.substring(0, 5) : '-',
|
||||||
|
timeOut: h.checkOut ? h.checkOut.substring(0, 5) : '-',
|
||||||
|
loggedHours: h.workedHours ? `${Math.floor(h.workedHours)}h ${Math.round((h.workedHours % 1) * 60)}m` : '-',
|
||||||
|
status: mapStatus(h.finalStatus),
|
||||||
|
attendanceId: h.attendanceId || 0
|
||||||
|
}));
|
||||||
|
return { metrics: data.metrics, history };
|
||||||
|
},
|
||||||
|
enabled: !!employeeId && !!startDate && !!endDate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Fetch Dashboard Metrics
|
||||||
|
export const useDashboardMetrics = (date: string, companyId: string, branchId: string, departmentId: string) => {
|
||||||
|
return useQuery<DashboardMetrics>({
|
||||||
|
queryKey: ['dashboardMetrics', date, companyId, branchId, departmentId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ams/attendance/dashboard/metrics', {
|
||||||
|
params: {
|
||||||
|
date,
|
||||||
|
company_id: companyId || undefined,
|
||||||
|
branch_id: branchId || undefined,
|
||||||
|
department_id: departmentId || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const rawData = response.data?.data || response.data;
|
||||||
|
return {
|
||||||
|
total_employees: rawData?.total_employees ?? 0,
|
||||||
|
total_checked_in: rawData?.total_checked_in ?? 0,
|
||||||
|
total_not_checked_in: rawData?.total_not_checked_in ?? 0,
|
||||||
|
} as DashboardMetrics;
|
||||||
|
},
|
||||||
|
enabled: !!date,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Fetch Daily Report
|
||||||
|
export interface DailyReportFilters {
|
||||||
|
date: string;
|
||||||
|
companyId: string;
|
||||||
|
branchId: string;
|
||||||
|
departmentId: string;
|
||||||
|
designationId: string;
|
||||||
|
employeeName: string;
|
||||||
|
employeeCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDailyReport = (filters: DailyReportFilters) => {
|
||||||
|
return useQuery<DailyReportRecord[]>({
|
||||||
|
queryKey: ['dailyReport', filters],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ams/attendance/daily-report', {
|
||||||
|
params: {
|
||||||
|
date: filters.date,
|
||||||
|
company_id: filters.companyId || undefined,
|
||||||
|
branch_id: filters.branchId || undefined,
|
||||||
|
department_id: filters.departmentId || undefined,
|
||||||
|
designation: filters.designationId || undefined,
|
||||||
|
employee_name: filters.employeeName || undefined,
|
||||||
|
employee_code: filters.employeeCode || undefined,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data.roster || [];
|
||||||
|
},
|
||||||
|
enabled: !!filters.date,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Fetch Admin Range Report
|
||||||
|
export interface AdminReportFilters {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
companyId: string;
|
||||||
|
branchId: string;
|
||||||
|
departmentId: string;
|
||||||
|
designation: string;
|
||||||
|
employeeName: string;
|
||||||
|
employeeCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAdminReport = (filters: AdminReportFilters) => {
|
||||||
|
return useQuery<AdminReportRecord[]>({
|
||||||
|
queryKey: ['adminReport', filters],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ams/attendance/admin-report', {
|
||||||
|
params: {
|
||||||
|
from_date: filters.fromDate,
|
||||||
|
to_date: filters.toDate,
|
||||||
|
company_id: filters.companyId || undefined,
|
||||||
|
branch_id: filters.branchId || undefined,
|
||||||
|
department_id: filters.departmentId || undefined,
|
||||||
|
designation: filters.designation || undefined,
|
||||||
|
employee_name: filters.employeeName || undefined,
|
||||||
|
employee_code: filters.employeeCode || undefined,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data.report || [];
|
||||||
|
},
|
||||||
|
enabled: !!filters.fromDate && !!filters.toDate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. Create Regularization Request
|
||||||
|
export interface RegularizationPayload {
|
||||||
|
attendance_id: number;
|
||||||
|
employee_id: number;
|
||||||
|
regularization_type: string;
|
||||||
|
target_date: string;
|
||||||
|
requested_check_in: string;
|
||||||
|
requested_check_out: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateRegularization = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: RegularizationPayload) => {
|
||||||
|
const response = await apiClient.post('/api/ams/attendance/regularize', payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['employeeRangeReport'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['attendanceSummary'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['HRManagerRegularizations'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7. MOCK DATA: Fetch attendance records for other roles (Manager, HR, Director)
|
||||||
|
const MOCK_ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
|
||||||
employee: 'attendance/employee_attendance.json',
|
employee: 'attendance/employee_attendance.json',
|
||||||
manager: 'attendance/manager_attendance.json',
|
manager: 'attendance/manager_attendance.json',
|
||||||
admin: 'attendance/admin_attendance.json',
|
hrmanager: 'attendance/hrmanager_attendance.json',
|
||||||
superadmin: 'attendance/superadmin_attendance.json',
|
director: 'attendance/director_attendance.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAttendanceData = () => {
|
export const useAttendanceData = () => {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
|
|
||||||
return useQuery<AttendanceRecord[]>({
|
return useQuery<AttendanceRecord[]>({
|
||||||
queryKey: ['attendance', role],
|
queryKey: ['attendance', role],
|
||||||
queryFn: () => fetchMockData<AttendanceRecord[]>(ATTENDANCE_ENDPOINTS[role]),
|
queryFn: () => fetchMockData<AttendanceRecord[]>(MOCK_ATTENDANCE_ENDPOINTS[role]),
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SummaryRecord {
|
|
||||||
id: string;
|
|
||||||
employeeName: string;
|
|
||||||
branch: string;
|
|
||||||
department: string;
|
|
||||||
company: string;
|
|
||||||
isCheckedIn: boolean;
|
|
||||||
date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAttendanceSummary = () => {
|
|
||||||
return useQuery<SummaryRecord[]>({
|
|
||||||
queryKey: ['attendanceSummary'],
|
|
||||||
queryFn: () => fetchMockData<SummaryRecord[]>('attendance/attendance_summary.json'),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 8. Fetch HR Manager Regularizations
|
||||||
export interface RegularizationRecord {
|
export interface RegularizationRecord {
|
||||||
id: string;
|
id: string;
|
||||||
empId: string;
|
empId: string;
|
||||||
@ -49,9 +209,33 @@ export interface RegularizationRecord {
|
|||||||
status: 'Pending' | 'Approved' | 'Rejected';
|
status: 'Pending' | 'Approved' | 'Rejected';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAdminRegularizations = () => {
|
export const useHRManagerRegularizations = () => {
|
||||||
return useQuery<RegularizationRecord[]>({
|
return useQuery<RegularizationRecord[]>({
|
||||||
queryKey: ['adminRegularizations'],
|
queryKey: ['HRManagerRegularizations'],
|
||||||
queryFn: () => fetchMockData<RegularizationRecord[]>('attendance/admin_regularizations.json'),
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ams/attendance/regularize/pending');
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
|
||||||
|
return rawData.map((r: any) => {
|
||||||
|
const targetDate = r.targetDate ? new Date(r.targetDate) : new Date();
|
||||||
|
const reqCheckInDate = r.requestedCheckIn ? new Date(r.requestedCheckIn) : new Date();
|
||||||
|
const reqCheckOutDate = r.requestedCheckOut ? new Date(r.requestedCheckOut) : new Date();
|
||||||
|
|
||||||
|
const formattedDate = targetDate.toISOString().split('T')[0];
|
||||||
|
const formatTime = (date: Date) => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||||
|
const capitalizeStatus = (status: string) => status ? status.charAt(0).toUpperCase() + status.slice(1).toLowerCase() : 'Pending';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(r.regularizationId),
|
||||||
|
empId: r.employeeCode || String(r.employeeId),
|
||||||
|
empName: `${r.firstName || ''} ${r.lastName || ''}`.trim(),
|
||||||
|
reqCheckIn: formatTime(reqCheckInDate),
|
||||||
|
reqCheckOut: formatTime(reqCheckOutDate),
|
||||||
|
date: formattedDate,
|
||||||
|
reason: r.reason,
|
||||||
|
status: capitalizeStatus(r.status) as 'Pending' | 'Approved' | 'Rejected',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2 } from 'lucide-react';
|
||||||
import type { AttendanceRecord, SortKey } from '../../types/attendance';
|
import type { AttendanceRecord, SortKey } from '../../types/attendance';
|
||||||
|
|
||||||
type SortDirection = 'ascending' | 'descending';
|
type SortDirection = 'ascending' | 'descending';
|
||||||
@ -7,8 +7,11 @@ type SortDirection = 'ascending' | 'descending';
|
|||||||
interface AttendanceRecordsTableProps {
|
interface AttendanceRecordsTableProps {
|
||||||
records: AttendanceRecord[];
|
records: AttendanceRecord[];
|
||||||
title?: string;
|
title?: string;
|
||||||
initialFromDate?: string;
|
fromDate: string;
|
||||||
initialToDate?: string;
|
toDate: string;
|
||||||
|
onFromDateChange: (date: string) => void;
|
||||||
|
onToDateChange: (date: string) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDaysDifference = (start: string, end: string) => {
|
const getDaysDifference = (start: string, end: string) => {
|
||||||
@ -30,13 +33,12 @@ const getStatusPriority = (status: string) => {
|
|||||||
export default function AttendanceRecordsTable({
|
export default function AttendanceRecordsTable({
|
||||||
records,
|
records,
|
||||||
title = "Attendance Records",
|
title = "Attendance Records",
|
||||||
initialFromDate,
|
fromDate,
|
||||||
initialToDate
|
toDate,
|
||||||
|
onFromDateChange,
|
||||||
|
onToDateChange,
|
||||||
|
isLoading = false
|
||||||
}: AttendanceRecordsTableProps) {
|
}: AttendanceRecordsTableProps) {
|
||||||
// Default to empty strings to show placeholder in date inputs
|
|
||||||
const [fromDate, setFromDate] = useState(initialFromDate || '');
|
|
||||||
const [toDate, setToDate] = useState(initialToDate || '');
|
|
||||||
|
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||||
key: 'date', direction: 'ascending'
|
key: 'date', direction: 'ascending'
|
||||||
});
|
});
|
||||||
@ -44,17 +46,8 @@ export default function AttendanceRecordsTable({
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(5);
|
const [pageSize, setPageSize] = useState(5);
|
||||||
|
|
||||||
// Filter safely by checking if dates exist
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return records.filter((record) => {
|
|
||||||
const isAfterFrom = fromDate ? record.date >= fromDate : true;
|
|
||||||
const isBeforeTo = toDate ? record.date <= toDate : true;
|
|
||||||
return isAfterFrom && isBeforeTo;
|
|
||||||
});
|
|
||||||
}, [records, fromDate, toDate]);
|
|
||||||
|
|
||||||
const sortedData = useMemo(() => {
|
const sortedData = useMemo(() => {
|
||||||
const sortableData = [...filteredData];
|
const sortableData = [...records];
|
||||||
sortableData.sort((a, b) => {
|
sortableData.sort((a, b) => {
|
||||||
if (sortConfig.key === 'status') {
|
if (sortConfig.key === 'status') {
|
||||||
const priorityA = getStatusPriority(a.status);
|
const priorityA = getStatusPriority(a.status);
|
||||||
@ -68,7 +61,7 @@ export default function AttendanceRecordsTable({
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
return sortableData;
|
return sortableData;
|
||||||
}, [filteredData, sortConfig]);
|
}, [records, sortConfig]);
|
||||||
|
|
||||||
const daysDifference = getDaysDifference(fromDate, toDate);
|
const daysDifference = getDaysDifference(fromDate, toDate);
|
||||||
const isPaginationEnabled = daysDifference > 7 || sortedData.length > pageSize;
|
const isPaginationEnabled = daysDifference > 7 || sortedData.length > pageSize;
|
||||||
@ -100,8 +93,8 @@ export default function AttendanceRecordsTable({
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { setFromDate(e.target.value); setCurrentPage(1); };
|
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { onFromDateChange(e.target.value); setCurrentPage(1); };
|
||||||
const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { setToDate(e.target.value); setCurrentPage(1); };
|
const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { onToDateChange(e.target.value); setCurrentPage(1); };
|
||||||
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => { setPageSize(Number(e.target.value)); setCurrentPage(1); };
|
const handlePageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => { setPageSize(Number(e.target.value)); setCurrentPage(1); };
|
||||||
|
|
||||||
const getSortIcon = (key: SortKey) => {
|
const getSortIcon = (key: SortKey) => {
|
||||||
@ -155,11 +148,19 @@ export default function AttendanceRecordsTable({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-app-border">
|
<tbody className="divide-y divide-app-border">
|
||||||
{paginatedData.length > 0 ? (
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="px-6 py-8 text-center text-sm text-text-muted">
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<Loader2 size={20} className="animate-spin text-primary" />
|
||||||
|
<span>Loading records...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : paginatedData.length > 0 ? (
|
||||||
paginatedData.map((record, index) => (
|
paginatedData.map((record, index) => (
|
||||||
<tr key={index} className="hover:bg-app-muted transition-colors">
|
<tr key={index} className="hover:bg-app-muted transition-colors">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">
|
||||||
{/* Table cells format to dd-mm-yyyy */}
|
|
||||||
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeIn}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.timeIn}</td>
|
||||||
@ -193,7 +194,7 @@ export default function AttendanceRecordsTable({
|
|||||||
|
|
||||||
{isPaginationEnabled && totalPages > 1 && (
|
{isPaginationEnabled && totalPages > 1 && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<span className="text-sm text-text-muted mr-2 hidden sm:inline">Page {currentPage} of {totalPages}</span>
|
<span className="text-sm text-text-muted mr-2 hidden sm-inline">Page {currentPage} of {totalPages}</span>
|
||||||
<button onClick={() => setCurrentPage(1)} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsLeft size={16} /></button>
|
<button onClick={() => setCurrentPage(1)} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronsLeft size={16} /></button>
|
||||||
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronLeft size={16} /></button>
|
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border border-app-border rounded-lg text-text-secondary hover:bg-app-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"><ChevronLeft size={16} /></button>
|
||||||
{pageNumbers.map(num => (
|
{pageNumbers.map(num => (
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { CalendarCheck } from 'lucide-react';
|
import { CalendarCheck } from 'lucide-react';
|
||||||
|
|
||||||
interface CalendarProps {
|
interface CalendarProps {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
selectedDate: string;
|
selectedDate: string;
|
||||||
|
currentMonth: number;
|
||||||
|
currentYear: number;
|
||||||
onDateSelect: (date: string) => void;
|
onDateSelect: (date: string) => void;
|
||||||
|
onMonthChange: (month: number, year: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: Date) => {
|
||||||
@ -15,11 +17,7 @@ const formatDate = (date: Date) => {
|
|||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Calendar({ data, selectedDate, onDateSelect }: CalendarProps) {
|
export default function Calendar({ data, selectedDate, currentMonth, currentYear, onDateSelect, onMonthChange }: CalendarProps) {
|
||||||
const initialDate = new Date(selectedDate);
|
|
||||||
const [currentMonth, setCurrentMonth] = useState(initialDate.getMonth());
|
|
||||||
const [currentYear, setCurrentYear] = useState(initialDate.getFullYear());
|
|
||||||
|
|
||||||
const today = formatDate(new Date());
|
const today = formatDate(new Date());
|
||||||
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
||||||
const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay();
|
const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay();
|
||||||
@ -34,11 +32,18 @@ export default function Calendar({ data, selectedDate, onDateSelect }: CalendarP
|
|||||||
|
|
||||||
const goToToday = () => {
|
const goToToday = () => {
|
||||||
const todayDate = new Date();
|
const todayDate = new Date();
|
||||||
setCurrentMonth(todayDate.getMonth());
|
onMonthChange(todayDate.getMonth(), todayDate.getFullYear());
|
||||||
setCurrentYear(todayDate.getFullYear());
|
|
||||||
onDateSelect(formatDate(todayDate));
|
onDateSelect(formatDate(todayDate));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMonthChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onMonthChange(Number(e.target.value), currentYear);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleYearChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onMonthChange(currentMonth, Number(e.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
const getStatusClass = (dateStr: string, dayOfWeek: number) => {
|
const getStatusClass = (dateStr: string, dayOfWeek: number) => {
|
||||||
const dayData = data[dateStr];
|
const dayData = data[dateStr];
|
||||||
|
|
||||||
@ -61,15 +66,15 @@ export default function Calendar({ data, selectedDate, onDateSelect }: CalendarP
|
|||||||
return (
|
return (
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
||||||
<h2 className="text-lg font-semibold text-text-primary">My Attendance</h2>
|
<h2 className="text-lg font-semibold text-text-primary">Attendance</h2>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button onClick={goToToday} className="flex items-center px-3 py-2 text-sm font-medium text-text-secondary border border-app-border rounded-lg hover:bg-app-muted focus:outline-none focus:ring-2 focus:ring-primary transition-colors">
|
<button onClick={goToToday} className="flex items-center px-3 py-2 text-sm font-medium text-text-secondary border border-app-border rounded-lg hover:bg-app-muted focus:outline-none focus:ring-2 focus:ring-primary transition-colors">
|
||||||
<CalendarCheck size={16} className="mr-2 text-text-muted" /> Today
|
<CalendarCheck size={16} className="mr-2 text-text-muted" /> Today
|
||||||
</button>
|
</button>
|
||||||
<select value={currentMonth} onChange={(e) => setCurrentMonth(Number(e.target.value))} className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
<select value={currentMonth} onChange={handleMonthChange} className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
||||||
{monthsArray.map((month, idx) => <option key={idx} value={idx}>{month}</option>)}
|
{monthsArray.map((month, idx) => <option key={idx} value={idx}>{month}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<select value={currentYear} onChange={(e) => setCurrentYear(Number(e.target.value))} className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
<select value={currentYear} onChange={handleYearChange} className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
||||||
{yearsArray.map((year) => <option key={year} value={year}>{year}</option>)}
|
{yearsArray.map((year) => <option key={year} value={year}>{year}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { LogIn, LogOut, Clock, AlertTriangle } from 'lucide-react';
|
import { LogIn, LogOut, Clock, AlertTriangle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import RegularizationModal from './RegularizationModal';
|
import RegularizationModal from './RegularizationModal';
|
||||||
|
import { useCreateRegularization } from '../../api/useAttendanceData';
|
||||||
|
|
||||||
interface DailySummaryProps {
|
interface DailySummaryProps {
|
||||||
selectedDate: string;
|
selectedDate: string;
|
||||||
@ -10,21 +11,31 @@ interface DailySummaryProps {
|
|||||||
login: string;
|
login: string;
|
||||||
logout: string;
|
logout: string;
|
||||||
hours: string;
|
hours: string;
|
||||||
|
attendanceId: number;
|
||||||
} | null;
|
} | null;
|
||||||
|
employeeId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DailySummary({ selectedDate, data }: DailySummaryProps) {
|
export default function DailySummary({ selectedDate, data, employeeId }: DailySummaryProps) {
|
||||||
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-' };
|
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-', attendanceId: 0 };
|
||||||
const isMispunch = selectedData.status === 'Mispunch';
|
const isMispunch = selectedData.status === 'Mispunch';
|
||||||
|
|
||||||
const [isApplied, setIsApplied] = useState(false);
|
const [isApplied, setIsApplied] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
const handleApplyRegularization = (regData: { checkIn: string; checkOut: string; reason: string }) => {
|
const createRegMutation = useCreateRegularization();
|
||||||
console.log("Regularization Data:", { date: selectedDate, ...regData });
|
|
||||||
setIsApplied(true);
|
const handleApplyRegularization = (payload: any) => {
|
||||||
setShowModal(false);
|
createRegMutation.mutate(payload, {
|
||||||
toast.success("Regularization request submitted successfully.");
|
onSuccess: () => {
|
||||||
|
setIsApplied(true);
|
||||||
|
setShowModal(false);
|
||||||
|
toast.success("Regularization request submitted successfully.");
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.response?.data?.message || "Failed to submit request.");
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -102,6 +113,8 @@ export default function DailySummary({ selectedDate, data }: DailySummaryProps)
|
|||||||
{showModal && (
|
{showModal && (
|
||||||
<RegularizationModal
|
<RegularizationModal
|
||||||
selectedDate={selectedDate}
|
selectedDate={selectedDate}
|
||||||
|
attendanceId={selectedData.attendanceId}
|
||||||
|
employeeId={employeeId}
|
||||||
onClose={() => setShowModal(false)}
|
onClose={() => setShowModal(false)}
|
||||||
onApply={handleApplyRegularization}
|
onApply={handleApplyRegularization}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { X } from 'lucide-react';
|
|||||||
|
|
||||||
interface RegularizationModalProps {
|
interface RegularizationModalProps {
|
||||||
selectedDate: string;
|
selectedDate: string;
|
||||||
|
attendanceId: number;
|
||||||
|
employeeId: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onApply: (data: { checkIn: string; checkOut: string; reason: string }) => void;
|
onApply: (payload: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RegularizationModal({ selectedDate, onClose, onApply }: RegularizationModalProps) {
|
export default function RegularizationModal({ selectedDate, attendanceId, employeeId, onClose, onApply }: RegularizationModalProps) {
|
||||||
const [regData, setRegData] = useState({ checkIn: '', checkOut: '', reason: '' });
|
const [regData, setRegData] = useState({ checkIn: '', checkOut: '', reason: '' });
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
@ -16,7 +18,19 @@ export default function RegularizationModal({ selectedDate, onClose, onApply }:
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onApply(regData);
|
|
||||||
|
// Format dates to "YYYY-MM-DD HH:mm:ss" for backend
|
||||||
|
const payload = {
|
||||||
|
attendance_id: attendanceId,
|
||||||
|
employee_id: employeeId,
|
||||||
|
regularization_type: "MISPUNCH",
|
||||||
|
target_date: selectedDate,
|
||||||
|
requested_check_in: `${selectedDate} ${regData.checkIn}:00`,
|
||||||
|
requested_check_out: `${selectedDate} ${regData.checkOut}:00`,
|
||||||
|
reason: regData.reason
|
||||||
|
};
|
||||||
|
|
||||||
|
onApply(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,91 +1,37 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { PieChart } from '@mui/x-charts/PieChart';
|
import { PieChart } from '@mui/x-charts/PieChart';
|
||||||
import { LineChart } from '@mui/x-charts/LineChart';
|
|
||||||
import type { SummaryRecord } from '../../api/useAttendanceData';
|
|
||||||
|
|
||||||
interface AttendanceChartsProps {
|
interface AttendanceChartsProps {
|
||||||
dailyData: SummaryRecord[];
|
checkedIn: number;
|
||||||
trendData: SummaryRecord[];
|
notCheckedIn: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttendanceCharts({ dailyData, trendData }: AttendanceChartsProps) {
|
export default function AttendanceCharts({ checkedIn, notCheckedIn }: AttendanceChartsProps) {
|
||||||
// Pie Chart Data
|
const pieData = [
|
||||||
const { pieData, checkedInCount, notCheckedInCount } = useMemo(() => {
|
{ id: 0, value: checkedIn, label: 'Checked In', color: '#16a34a' },
|
||||||
const checkedIn = dailyData.filter(r => r.isCheckedIn).length;
|
{ id: 1, value: notCheckedIn, label: 'Not Checked In', color: '#dc2626' }
|
||||||
const notCheckedIn = dailyData.filter(r => !r.isCheckedIn).length;
|
];
|
||||||
return {
|
|
||||||
checkedInCount: checkedIn,
|
|
||||||
notCheckedInCount: notCheckedIn,
|
|
||||||
pieData: [
|
|
||||||
{ id: 0, value: checkedIn, label: 'Checked In', color: '#93c5fd' },
|
|
||||||
{ id: 1, value: notCheckedIn, label: 'Not Checked In', color: '#fdba74' },
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}, [dailyData]);
|
|
||||||
|
|
||||||
// Line Chart Data
|
|
||||||
const { xAxisData, lineCheckedInData, lineNotCheckedInData } = useMemo(() => {
|
|
||||||
const trendMap = trendData.reduce((acc, record) => {
|
|
||||||
const day = record.date.split('-')[2]; // Extract "01" from "2024-07-01"
|
|
||||||
if (!acc[day]) {
|
|
||||||
acc[day] = { day, CheckedIn: 0, NotCheckedIn: 0 };
|
|
||||||
}
|
|
||||||
if (record.isCheckedIn) {
|
|
||||||
acc[day].CheckedIn++;
|
|
||||||
} else {
|
|
||||||
acc[day].NotCheckedIn++;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, { day: string; CheckedIn: number; NotCheckedIn: number }>);
|
|
||||||
|
|
||||||
const sortedDays = Object.keys(trendMap).sort((a, b) => a.localeCompare(b));
|
|
||||||
return {
|
|
||||||
xAxisData: sortedDays,
|
|
||||||
lineCheckedInData: sortedDays.map(day => trendMap[day].CheckedIn),
|
|
||||||
lineNotCheckedInData: sortedDays.map(day => trendMap[day].NotCheckedIn),
|
|
||||||
};
|
|
||||||
}, [trendData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
{/* MUI Pie Chart - Daily Ratio */}
|
<h3 className="text-lg font-semibold text-text-primary mb-6">Daily Attendance Ratio</h3>
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
<div className="h-72 w-full flex items-center justify-center">
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-6">Daily Attendance Ratio</h3>
|
{checkedIn === 0 && notCheckedIn === 0 ? (
|
||||||
<div className="h-72 w-full flex items-center justify-center">
|
<p className="text-text-muted text-sm">No data available for selected filters</p>
|
||||||
{checkedInCount === 0 && notCheckedInCount === 0 ? (
|
) : (
|
||||||
<p className="text-text-muted text-sm">No data available for selected date</p>
|
<PieChart
|
||||||
) : (
|
|
||||||
<PieChart
|
|
||||||
series={[
|
|
||||||
{
|
|
||||||
data: pieData,
|
|
||||||
innerRadius: 40,
|
|
||||||
outerRadius: 80,
|
|
||||||
paddingAngle: 2,
|
|
||||||
cornerRadius: 4,
|
|
||||||
arcLabel: (item) => `${item.value}`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
height={280}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* MUI Line Chart - Monthly Trend */}
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-6">Monthly Attendance Trend</h3>
|
|
||||||
<div className="h-72 w-full">
|
|
||||||
<LineChart
|
|
||||||
height={280}
|
|
||||||
series={[
|
series={[
|
||||||
{ data: lineCheckedInData, label: 'Checked In', color: '#93c5fd', area: true },
|
{
|
||||||
{ data: lineNotCheckedInData, label: 'Not Checked In', color: '#fdba74', area: true },
|
data: pieData,
|
||||||
|
innerRadius: 40,
|
||||||
|
outerRadius: 80,
|
||||||
|
paddingAngle: 2,
|
||||||
|
cornerRadius: 4,
|
||||||
|
arcLabel: (item) => `${item.value}`,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
xAxis={[{ scaleType: 'point', data: xAxisData, label: 'Days of Month' }]}
|
height={280}
|
||||||
yAxis={[{ min: 0 }]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,70 +1,84 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
import { useCompanies } from '../../../masterData/api/useCompanyData';
|
||||||
export interface DailyReportFilterValues {
|
import { useBranches } from '../../../masterData/api/useBranchData';
|
||||||
date: string;
|
import { useDepartments } from '../../../masterData/api/useDepartmentData';
|
||||||
company: string;
|
import { useJobRoles } from '../../../masterData/api/useJobRoleData';
|
||||||
branch: string;
|
import type { DailyReportFilters } from '../../api/useAttendanceData';
|
||||||
department: string;
|
|
||||||
designation: string;
|
|
||||||
employeeName: string;
|
|
||||||
employeeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DailyReportFiltersProps {
|
interface DailyReportFiltersProps {
|
||||||
filters: DailyReportFilterValues;
|
filters: DailyReportFilters;
|
||||||
onFilterChange: (newFilters: DailyReportFilterValues) => void;
|
onFilterChange: (newFilters: DailyReportFilters) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DailyReportFilters({ filters, onFilterChange }: DailyReportFiltersProps) {
|
export default function DailyReportFilters({ filters, onFilterChange }: DailyReportFiltersProps) {
|
||||||
|
const { data: companies } = useCompanies();
|
||||||
|
const { data: allBranches } = useBranches();
|
||||||
|
const { data: allDepartments } = useDepartments();
|
||||||
|
const { data: designations } = useJobRoles();
|
||||||
|
|
||||||
|
const filteredBranches = useMemo(() => {
|
||||||
|
if (!allBranches || !filters.companyId) return [];
|
||||||
|
const selectedCompany = companies?.find(c => String(c.id) === filters.companyId);
|
||||||
|
if (!selectedCompany) return [];
|
||||||
|
return allBranches.filter(b => b.companyName === selectedCompany.name);
|
||||||
|
}, [allBranches, filters.companyId, companies]);
|
||||||
|
|
||||||
|
const filteredDepartments = useMemo(() => {
|
||||||
|
if (!allDepartments || !filters.branchId) return [];
|
||||||
|
const selectedBranch = filteredBranches.find(b => String(b.id) === filters.branchId);
|
||||||
|
if (!selectedBranch) return [];
|
||||||
|
return allDepartments.filter(d => d.branchName === selectedBranch.name);
|
||||||
|
}, [allDepartments, filters.branchId, filteredBranches]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
onFilterChange({ ...filters, [e.target.name]: e.target.value });
|
const { name, value } = e.target;
|
||||||
|
const newFilters = { ...filters, [name]: value };
|
||||||
|
|
||||||
|
// Reset child dropdowns if parent changes
|
||||||
|
if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; }
|
||||||
|
if (name === 'branchId') { newFilters.departmentId = ''; }
|
||||||
|
|
||||||
|
onFilterChange(newFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card disabled:opacity-50 disabled:cursor-not-allowed";
|
||||||
const labelClass = "text-xs text-text-muted mb-1";
|
const labelClass = "text-xs text-text-muted mb-1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Date</label>
|
||||||
|
<input type="date" name="date" value={filters.date} onChange={handleChange} className={inputClass} />
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Company</label>
|
<label className={labelClass}>Company</label>
|
||||||
<select name="company" value={filters.company} onChange={handleChange} className={inputClass}>
|
<select name="companyId" value={filters.companyId} onChange={handleChange} className={inputClass}>
|
||||||
<option value="All">All</option>
|
<option value="">All Companies</option>
|
||||||
<option value="CLRI">CLRI</option>
|
{companies?.map(c => <option key={c.id} value={String(c.id)}>{c.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Branch</label>
|
<label className={labelClass}>Branch</label>
|
||||||
<select name="branch" value={filters.branch} onChange={handleChange} className={inputClass}>
|
<select name="branchId" value={filters.branchId} onChange={handleChange} disabled={!filters.companyId} className={inputClass}>
|
||||||
<option value="All">All</option>
|
<option value="">All Branches</option>
|
||||||
<option value="Bengaluru">Bengaluru</option>
|
{filteredBranches.map(b => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
||||||
<option value="Pune">Pune</option>
|
|
||||||
<option value="Hyderabad">Hyderabad</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Department</label>
|
<label className={labelClass}>Department</label>
|
||||||
<select name="department" value={filters.department} onChange={handleChange} className={inputClass}>
|
<select name="departmentId" value={filters.departmentId} onChange={handleChange} disabled={!filters.branchId} className={inputClass}>
|
||||||
<option value="All">All</option>
|
<option value="">All Departments</option>
|
||||||
<option value="IT">IT</option>
|
{filteredDepartments.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||||
<option value="Training">Training</option>
|
|
||||||
<option value="Analytics">Analytics</option>
|
|
||||||
<option value="Placement">Placement</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Designation</label>
|
<label className={labelClass}>Designation</label>
|
||||||
<select name="designation" value={filters.designation} onChange={handleChange} className={inputClass}>
|
<select name="designationId" value={filters.designationId} onChange={handleChange} className={inputClass}>
|
||||||
<option value="All">All</option>
|
<option value="">All Designations</option>
|
||||||
<option value="Software Engineer">Software Engineer</option>
|
{designations?.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||||
<option value="Trainer">Trainer</option>
|
|
||||||
<option value="Data Analyst">Data Analyst</option>
|
|
||||||
<option value="Manager">Manager</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className={labelClass}>Date</label>
|
|
||||||
<input type="date" name="date" value={filters.date} onChange={handleChange} className={inputClass} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Employee Name</label>
|
<label className={labelClass}>Employee Name</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -73,13 +87,12 @@ export default function DailyReportFilters({ filters, onFilterChange }: DailyRep
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Employee ID</label>
|
<label className={labelClass}>Employee Code</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||||
<input type="text" name="employeeId" value={filters.employeeId} onChange={handleChange} placeholder="Search ID..." className={`${inputClass} pl-9`} />
|
<input type="text" name="employeeCode" value={filters.employeeCode} onChange={handleChange} placeholder="Search code..." className={`${inputClass} pl-9`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,88 +1,88 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
import { useCompanies } from '../../../masterData/api/useCompanyData';
|
||||||
|
import { useBranches } from '../../../masterData/api/useBranchData';
|
||||||
|
import { useDepartments } from '../../../masterData/api/useDepartmentData';
|
||||||
|
import { useJobRoles } from '../../../masterData/api/useJobRoleData';
|
||||||
|
import type { AdminReportFilters } from '../../api/useAttendanceData';
|
||||||
|
|
||||||
export interface ReportFilterValues {
|
interface MonthlyReportFiltersProps {
|
||||||
company: string;
|
filters: AdminReportFilters;
|
||||||
branch: string;
|
onFilterChange: (newFilters: AdminReportFilters) => void;
|
||||||
department: string;
|
|
||||||
designation: string;
|
|
||||||
employeeName: string;
|
|
||||||
employeeId: string;
|
|
||||||
fromDate: string;
|
|
||||||
toDate: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReportFiltersProps {
|
export default function MonthlyReportFilters({ filters, onFilterChange }: MonthlyReportFiltersProps) {
|
||||||
filters: ReportFilterValues;
|
const { data: companies } = useCompanies();
|
||||||
onFilterChange: (newFilters: ReportFilterValues) => void;
|
const { data: allBranches } = useBranches();
|
||||||
}
|
const { data: allDepartments } = useDepartments();
|
||||||
|
const { data: designations } = useJobRoles();
|
||||||
|
|
||||||
|
const filteredBranches = useMemo(() => {
|
||||||
|
if (!allBranches || !filters.companyId) return [];
|
||||||
|
const selectedCompany = companies?.find(c => String(c.id) === filters.companyId);
|
||||||
|
if (!selectedCompany) return [];
|
||||||
|
return allBranches.filter(b => b.companyName === selectedCompany.name);
|
||||||
|
}, [allBranches, filters.companyId, companies]);
|
||||||
|
|
||||||
|
const filteredDepartments = useMemo(() => {
|
||||||
|
if (!allDepartments || !filters.branchId) return [];
|
||||||
|
const selectedBranch = filteredBranches.find(b => String(b.id) === filters.branchId);
|
||||||
|
if (!selectedBranch) return [];
|
||||||
|
return allDepartments.filter(d => d.branchName === selectedBranch.name);
|
||||||
|
}, [allDepartments, filters.branchId, filteredBranches]);
|
||||||
|
|
||||||
export default function ReportFilters({ filters, onFilterChange }: ReportFiltersProps) {
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
onFilterChange({ ...filters, [e.target.name]: e.target.value });
|
const { name, value } = e.target;
|
||||||
|
let newFilters = { ...filters, [name]: value };
|
||||||
|
|
||||||
|
// Reset child dropdowns if parent changes
|
||||||
|
if (name === 'companyId') { newFilters.branchId = ''; newFilters.departmentId = ''; }
|
||||||
|
if (name === 'branchId') { newFilters.departmentId = ''; }
|
||||||
|
|
||||||
|
onFilterChange(newFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card disabled:opacity-50 disabled:cursor-not-allowed";
|
||||||
const labelClass = "text-xs text-text-muted mb-1";
|
const labelClass = "text-xs text-text-muted mb-1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
|
||||||
{/* Company Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className={labelClass}>Company</label>
|
|
||||||
<select name="company" value={filters.company} onChange={handleChange} className={inputClass}>
|
|
||||||
<option value="All">All</option>
|
|
||||||
<option value="CLRI">CLRI</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Branch Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className={labelClass}>Branch</label>
|
|
||||||
<select name="branch" value={filters.branch} onChange={handleChange} className={inputClass}>
|
|
||||||
<option value="All">All</option>
|
|
||||||
<option value="Bengaluru">Bengaluru</option>
|
|
||||||
<option value="Pune">Pune</option>
|
|
||||||
<option value="Hyderabad">Hyderabad</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Department Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className={labelClass}>Department</label>
|
|
||||||
<select name="department" value={filters.department} onChange={handleChange} className={inputClass}>
|
|
||||||
<option value="All">All</option>
|
|
||||||
<option value="IT">IT</option>
|
|
||||||
<option value="Training">Training</option>
|
|
||||||
<option value="Analytics">Analytics</option>
|
|
||||||
<option value="Placement">Placement</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Designation Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className={labelClass}>Designation</label>
|
|
||||||
<select name="designation" value={filters.designation} onChange={handleChange} className={inputClass}>
|
|
||||||
<option value="All">All</option>
|
|
||||||
<option value="Software Engineer">Software Engineer</option>
|
|
||||||
<option value="Trainer">Trainer</option>
|
|
||||||
<option value="Data Analyst">Data Analyst</option>
|
|
||||||
<option value="Manager">Manager</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* From Date */}
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>From Date</label>
|
<label className={labelClass}>From Date</label>
|
||||||
<input type="date" name="fromDate" value={filters.fromDate} onChange={handleChange} className={inputClass} />
|
<input type="date" name="fromDate" value={filters.fromDate} onChange={handleChange} className={inputClass} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* To Date */}
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>To Date</label>
|
<label className={labelClass}>To Date</label>
|
||||||
<input type="date" name="toDate" value={filters.toDate} onChange={handleChange} className={inputClass} />
|
<input type="date" name="toDate" value={filters.toDate} onChange={handleChange} className={inputClass} />
|
||||||
</div>
|
</div>
|
||||||
{/* Search by Name */}
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Company</label>
|
||||||
|
<select name="companyId" value={filters.companyId} onChange={handleChange} className={inputClass}>
|
||||||
|
<option value="">All Companies</option>
|
||||||
|
{companies?.map(c => <option key={c.id} value={String(c.id)}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Branch</label>
|
||||||
|
<select name="branchId" value={filters.branchId} onChange={handleChange} disabled={!filters.companyId} className={inputClass}>
|
||||||
|
<option value="">All Branches</option>
|
||||||
|
{filteredBranches.map(b => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Department</label>
|
||||||
|
<select name="departmentId" value={filters.departmentId} onChange={handleChange} disabled={!filters.branchId} className={inputClass}>
|
||||||
|
<option value="">All Departments</option>
|
||||||
|
{filteredDepartments.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className={labelClass}>Designation</label>
|
||||||
|
<select name="designation" value={filters.designation} onChange={handleChange} className={inputClass}>
|
||||||
|
<option value="">All Designations</option>
|
||||||
|
{designations?.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Employee Name</label>
|
<label className={labelClass}>Employee Name</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -90,16 +90,13 @@ export default function ReportFilters({ filters, onFilterChange }: ReportFilters
|
|||||||
<input type="text" name="employeeName" value={filters.employeeName} onChange={handleChange} placeholder="Search name..." className={`${inputClass} pl-9`} />
|
<input type="text" name="employeeName" value={filters.employeeName} onChange={handleChange} placeholder="Search name..." className={`${inputClass} pl-9`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search by ID */}
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className={labelClass}>Employee ID</label>
|
<label className={labelClass}>Employee Code</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||||
<input type="text" name="employeeId" value={filters.employeeId} onChange={handleChange} placeholder="Search ID..." className={`${inputClass} pl-9`} />
|
<input type="text" name="employeeCode" value={filters.employeeCode} onChange={handleChange} placeholder="Search code..." className={`${inputClass} pl-9`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/features/attendance/pages/AttendanceDashboard.tsx
Normal file
126
src/features/attendance/pages/AttendanceDashboard.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Users, UserCheck, UserX } from 'lucide-react';
|
||||||
|
import Loader from '../../../components/ui/Loader';
|
||||||
|
import ErrorState from '../../../components/ui/ErrorState';
|
||||||
|
import { useDashboardMetrics } from '../api/useAttendanceData';
|
||||||
|
import { useCompanies } from '../../masterData/api/useCompanyData';
|
||||||
|
import { useBranches } from '../../masterData/api/useBranchData';
|
||||||
|
import { useDepartments } from '../../masterData/api/useDepartmentData';
|
||||||
|
import StatCard from '../../../components/ui/StatCard';
|
||||||
|
import AttendanceCharts from '../components/summary/AttendanceCharts';
|
||||||
|
|
||||||
|
const formatDate = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AttendanceDashboard() {
|
||||||
|
const [date, setDate] = useState(formatDate(new Date()));
|
||||||
|
// Using strings for IDs to pass to the metrics API
|
||||||
|
const [companyId, setCompanyId] = useState('');
|
||||||
|
const [branchId, setBranchId] = useState('');
|
||||||
|
const [departmentId, setDepartmentId] = useState('');
|
||||||
|
|
||||||
|
// Fetch dropdown data from EMS service
|
||||||
|
const { data: companies } = useCompanies();
|
||||||
|
const { data: allBranches } = useBranches();
|
||||||
|
const { data: allDepartments } = useDepartments();
|
||||||
|
|
||||||
|
// Cascading filter logic (Filter by Name because backend doesn't return parent IDs in list view)
|
||||||
|
const filteredBranches = useMemo(() => {
|
||||||
|
if (!allBranches || !companyId) return [];
|
||||||
|
const selectedCompany = companies?.find(c => String(c.id) === companyId);
|
||||||
|
if (!selectedCompany) return [];
|
||||||
|
// Match by companyName since companyId is missing in branch response
|
||||||
|
return allBranches.filter(b => b.companyName === selectedCompany.name);
|
||||||
|
}, [allBranches, companyId, companies]);
|
||||||
|
|
||||||
|
const filteredDepartments = useMemo(() => {
|
||||||
|
if (!allDepartments || !branchId) return [];
|
||||||
|
const selectedBranch = filteredBranches.find(b => String(b.id) === branchId);
|
||||||
|
if (!selectedBranch) return [];
|
||||||
|
// Match by branchName since branchId is missing in department response
|
||||||
|
return allDepartments.filter(d => d.branchName === selectedBranch.name);
|
||||||
|
}, [allDepartments, branchId, filteredBranches]);
|
||||||
|
|
||||||
|
// Fetch Dashboard Metrics based on selected filters
|
||||||
|
const { data: metrics, isLoading, isError } = useDashboardMetrics(date, companyId, branchId, departmentId);
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: metrics?.total_employees || 0,
|
||||||
|
checkedIn: metrics?.total_checked_in || 0,
|
||||||
|
notCheckedIn: metrics?.total_not_checked_in || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <Loader message="Loading dashboard..." />;
|
||||||
|
if (isError) return <ErrorState message="Failed to load dashboard data." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Professional Grid Filter Bar */}
|
||||||
|
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-xs text-text-muted mb-1">Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={date}
|
||||||
|
onChange={e => setDate(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Company Dropdown */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-xs text-text-muted mb-1">Company</label>
|
||||||
|
<select
|
||||||
|
value={companyId}
|
||||||
|
onChange={e => { setCompanyId(e.target.value); setBranchId(''); setDepartmentId(''); }}
|
||||||
|
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
||||||
|
>
|
||||||
|
<option value="">All Companies</option>
|
||||||
|
{companies?.map(c => <option key={c.id} value={String(c.id)}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Branch Dropdown */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-xs text-text-muted mb-1">Branch</label>
|
||||||
|
<select
|
||||||
|
value={branchId}
|
||||||
|
onChange={e => { setBranchId(e.target.value); setDepartmentId(''); }}
|
||||||
|
disabled={!companyId}
|
||||||
|
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<option value="">All Branches</option>
|
||||||
|
{filteredBranches.map(b => <option key={b.id} value={String(b.id)}>{b.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Department Dropdown */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-xs text-text-muted mb-1">Department</label>
|
||||||
|
<select
|
||||||
|
value={departmentId}
|
||||||
|
onChange={e => setDepartmentId(e.target.value)}
|
||||||
|
disabled={!branchId}
|
||||||
|
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<option value="">All Departments</option>
|
||||||
|
{filteredDepartments.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<StatCard title="Total Employees" value={stats.total} Icon={Users} colorClass="text-primary" bgClass="bg-primary/10" />
|
||||||
|
<StatCard title="Total Checked In" value={stats.checkedIn} Icon={UserCheck} colorClass="text-present-700" bgClass="bg-present-100" />
|
||||||
|
<StatCard title="Total Not Checked In" value={stats.notCheckedIn} Icon={UserX} colorClass="text-absent-700" bgClass="bg-absent-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MUI Charts */}
|
||||||
|
<AttendanceCharts checkedIn={stats.checkedIn} notCheckedIn={stats.notCheckedIn} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
src/features/attendance/pages/DailyReport.tsx
Normal file
124
src/features/attendance/pages/DailyReport.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { FileText, Download, Loader2 } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import DailyReportFilters from '../components/summary/DailyReportFilters';
|
||||||
|
import { useDailyReport, type DailyReportFilters as FilterValues } from '../api/useAttendanceData';
|
||||||
|
|
||||||
|
const getTodayDate = () => {
|
||||||
|
const today = new Date();
|
||||||
|
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DailyReport() {
|
||||||
|
const [filters, setFilters] = useState<FilterValues>({
|
||||||
|
date: getTodayDate(),
|
||||||
|
companyId: '',
|
||||||
|
branchId: '',
|
||||||
|
departmentId: '',
|
||||||
|
designationId: '',
|
||||||
|
employeeName: '',
|
||||||
|
employeeCode: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch data using the hook
|
||||||
|
const { data: reportData, isLoading, isFetching } = useDailyReport(filters);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
toast.success(`Preparing daily report for ${filters.date}...`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'FULL_DAY': return 'bg-present-100 text-present-700';
|
||||||
|
case 'LATE': return 'bg-late-100 text-late-700';
|
||||||
|
case 'ABSENT': return 'bg-absent-100 text-absent-700';
|
||||||
|
case 'MISPUNCH': return 'bg-mispunch-100 text-mispunch-700';
|
||||||
|
case 'HALF_DAY': return 'bg-halfday-100 text-halfday-700';
|
||||||
|
default: return 'bg-app-muted text-text-secondary';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-3 bg-primary/10 rounded-full">
|
||||||
|
<FileText className="text-primary" size={28} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-text-primary">Daily Attendance Report</h2>
|
||||||
|
<p className="text-sm text-text-muted">View daily attendance logs based on filters.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={isLoading || isFetching}
|
||||||
|
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isLoading || isFetching ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||||
|
Download Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DailyReportFilters filters={filters} onFilterChange={setFilters} />
|
||||||
|
|
||||||
|
{/* Data Table */}
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
||||||
|
<h3 className="text-lg font-semibold text-text-primary mb-4">
|
||||||
|
Results for {new Date(filters.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' })} ({reportData?.length || 0})
|
||||||
|
</h3>
|
||||||
|
<table className="min-w-full divide-y divide-app-border">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Status</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-In</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-Out</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Worked Hours</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Company</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Designation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-app-border">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
|
<Loader2 size={20} className="animate-spin mx-auto text-primary" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : reportData && reportData.length > 0 ? (
|
||||||
|
reportData.map((record, index) => (
|
||||||
|
<tr key={index} className="hover:bg-app-muted transition-colors">
|
||||||
|
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeCode}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.fullName}</td>
|
||||||
|
<td className="px-4 py-3 text-sm">
|
||||||
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>
|
||||||
|
{record.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkIn ? record.checkIn.substring(0, 5) : '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkOut ? record.checkOut.substring(0, 5) : '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.workedHours}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.companyName || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.departmentName || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.designation || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={10} className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
|
No attendance records found for this date.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/features/attendance/pages/HRManagerRegularization.tsx
Normal file
48
src/features/attendance/pages/HRManagerRegularization.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
import Loader from '../../../components/ui/Loader';
|
||||||
|
import ErrorState from '../../../components/ui/ErrorState';
|
||||||
|
import RegularizationTable from '../components/summary/RegularizationTable';
|
||||||
|
import { useHRManagerRegularizations, type RegularizationRecord } from '../api/useAttendanceData';
|
||||||
|
|
||||||
|
// In a real app, this would call the /api/ams/attendance/regularize/review endpoint
|
||||||
|
// For now, we just update the local state to show it working
|
||||||
|
const mockReviewAction = (records: RegularizationRecord[], id: string, newStatus: 'Approved' | 'Rejected') => {
|
||||||
|
return records.map(r => r.id === id ? { ...r, status: newStatus } : r);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HRManagerRegularization() {
|
||||||
|
const { data: initialRecords, isLoading, isError } = useHRManagerRegularizations();
|
||||||
|
const [localRecords, setLocalRecords] = useState<RegularizationRecord[]>([]);
|
||||||
|
|
||||||
|
// Once data is loaded, sync it to local state so we can update statuses without refetching immediately
|
||||||
|
useMemo(() => {
|
||||||
|
if (initialRecords) {
|
||||||
|
setLocalRecords(initialRecords);
|
||||||
|
}
|
||||||
|
}, [initialRecords]);
|
||||||
|
|
||||||
|
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||||
|
// TODO: Replace with real API call to POST /api/ams/attendance/regularize/review
|
||||||
|
setLocalRecords(prev => mockReviewAction(prev, id, status));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <Loader message="Loading Regularizations..." />;
|
||||||
|
if (isError) return <ErrorState message="Failed to load regularizations data." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm flex items-center space-x-3">
|
||||||
|
<div className="p-3 bg-primary/10 rounded-full">
|
||||||
|
<Check className="text-primary" size={28} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-text-primary">Regularization Requests</h2>
|
||||||
|
<p className="text-sm text-text-muted">Review and approve pending requests.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RegularizationTable records={localRecords} onAction={handleAction} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/features/attendance/pages/MonthlyReport.tsx
Normal file
111
src/features/attendance/pages/MonthlyReport.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Download, FileText, Loader2 } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import MonthlyReportFilters from '../components/summary/MonthlyReportFilters';
|
||||||
|
import { useAdminReport, type AdminReportFilters } from '../api/useAttendanceData';
|
||||||
|
|
||||||
|
const getTodayDate = () => {
|
||||||
|
const today = new Date();
|
||||||
|
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMonthStart = () => {
|
||||||
|
const today = new Date();
|
||||||
|
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-01`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MonthlyReport() {
|
||||||
|
const [filters, setFilters] = useState<AdminReportFilters>({
|
||||||
|
fromDate: getMonthStart(),
|
||||||
|
toDate: getTodayDate(),
|
||||||
|
companyId: '',
|
||||||
|
branchId: '',
|
||||||
|
departmentId: '',
|
||||||
|
designation: '',
|
||||||
|
employeeName: '',
|
||||||
|
employeeCode: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch data using the hook
|
||||||
|
const { data: reportData, isLoading, isFetching } = useAdminReport(filters);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
toast.success("Preparing report for download...");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-3 bg-primary/10 rounded-full">
|
||||||
|
<FileText className="text-primary" size={28} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-text-primary">Monthly Attendance Report</h2>
|
||||||
|
<p className="text-sm text-text-muted">View monthly attendance metrics based on filters.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={isLoading || isFetching}
|
||||||
|
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isLoading || isFetching ? <Loader2 size={18} className="mr-2 animate-spin" /> : <Download size={18} className="mr-2" />}
|
||||||
|
Download Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MonthlyReportFilters filters={filters} onFilterChange={setFilters} />
|
||||||
|
|
||||||
|
{/* Data Table */}
|
||||||
|
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
||||||
|
<h3 className="text-lg font-semibold text-text-primary mb-4">
|
||||||
|
Filtered Results ({reportData?.length || 0})
|
||||||
|
</h3>
|
||||||
|
<table className="min-w-full divide-y divide-app-border">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Full Days</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Half Days</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Late Arrivals</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Mispunches</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-app-border">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
|
<Loader2 size={20} className="animate-spin mx-auto text-primary" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : reportData && reportData.length > 0 ? (
|
||||||
|
reportData.map((record, index) => (
|
||||||
|
<tr key={index} className="hover:bg-app-muted transition-colors">
|
||||||
|
<td className="px-4 py-3 text-sm text-text-primary">{record.employeeCode}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.firstName} {record.lastName}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.branchName || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.departmentName || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.fullDays || 0}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.halfDays || 0}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.lateArrivals || 0}</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-text-secondary">{record.rangeMetrics?.mispunches || 0}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
|
No attendance records found for this period.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
src/features/attendance/pages/MyAttendance.tsx
Normal file
94
src/features/attendance/pages/MyAttendance.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import type { RootState } from '../../../store/store';
|
||||||
|
import Calendar from '../components/attendance/Calendar';
|
||||||
|
import AttendanceRecordsTable from '../components/attendance/AttendanceRecordsTable';
|
||||||
|
import DailySummary from '../components/attendance/DailySummary';
|
||||||
|
import Loader from '../../../components/ui/Loader';
|
||||||
|
import ErrorState from '../../../components/ui/ErrorState';
|
||||||
|
import { useEmployeeRangeReport } from '../api/useAttendanceData';
|
||||||
|
|
||||||
|
const getTodayDate = () => {
|
||||||
|
const today = new Date();
|
||||||
|
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMonthStart = (year: number, month: number) => `${year}-${String(month + 1).padStart(2, '0')}-01`;
|
||||||
|
const getMonthEnd = (year: number, month: number) => `${year}-${String(month + 1).padStart(2, '0')}-${String(new Date(year, month + 1, 0).getDate()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
export default function MyAttendance() {
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
// Dynamically get the logged-in user's ID from Redux
|
||||||
|
// This is the key to data isolation!
|
||||||
|
const employeeId = useSelector((state: RootState) => Number(state.role.mockUserId));
|
||||||
|
|
||||||
|
const [selectedDate, setSelectedDate] = useState(getTodayDate());
|
||||||
|
const [calendarMonth, setCalendarMonth] = useState(today.getMonth());
|
||||||
|
const [calendarYear, setCalendarYear] = useState(today.getFullYear());
|
||||||
|
|
||||||
|
const [tableFromDate, setTableFromDate] = useState(getMonthStart(today.getFullYear(), today.getMonth()));
|
||||||
|
const [tableToDate, setTableToDate] = useState(getTodayDate());
|
||||||
|
|
||||||
|
const calendarStartDate = getMonthStart(calendarYear, calendarMonth);
|
||||||
|
const calendarEndDate = getMonthEnd(calendarYear, calendarMonth);
|
||||||
|
|
||||||
|
// Pass the dynamically fetched employeeId to the API
|
||||||
|
const { data: calendarRecords, isLoading: calLoading, isError: calError } = useEmployeeRangeReport(employeeId, calendarStartDate, calendarEndDate);
|
||||||
|
const { data: tableRecords, isLoading: tableLoading } = useEmployeeRangeReport(employeeId, tableFromDate, tableToDate);
|
||||||
|
|
||||||
|
const calendarData = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const map: Record<string, any> = {};
|
||||||
|
if (calendarRecords) {
|
||||||
|
calendarRecords.forEach(record => {
|
||||||
|
map[record.date] = {
|
||||||
|
status: record.status,
|
||||||
|
login: record.timeIn,
|
||||||
|
logout: record.timeOut,
|
||||||
|
hours: record.loggedHours,
|
||||||
|
attendanceId: record.attendanceId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [calendarRecords]);
|
||||||
|
|
||||||
|
if (calLoading && !calendarRecords) return <Loader message="Loading attendance data..." />;
|
||||||
|
if (calError) return <ErrorState message="Failed to load attendance data." />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Calendar
|
||||||
|
data={calendarData}
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
currentMonth={calendarMonth}
|
||||||
|
currentYear={calendarYear}
|
||||||
|
onDateSelect={setSelectedDate}
|
||||||
|
onMonthChange={(month, year) => {
|
||||||
|
setCalendarMonth(month);
|
||||||
|
setCalendarYear(year);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DailySummary
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
data={calendarData[selectedDate] || null}
|
||||||
|
employeeId={employeeId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AttendanceRecordsTable
|
||||||
|
records={tableRecords || []}
|
||||||
|
title="Attendance Records"
|
||||||
|
fromDate={tableFromDate}
|
||||||
|
toDate={tableToDate}
|
||||||
|
onFromDateChange={setTableFromDate}
|
||||||
|
onToDateChange={setTableToDate}
|
||||||
|
isLoading={tableLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import Calendar from '../../components/attendance/Calendar';
|
|
||||||
import AttendanceRecordsTable from '../../components/attendance/AttendanceRecordsTable';
|
|
||||||
import DailySummary from '../../components/attendance/DailySummary';
|
|
||||||
import Loader from '../../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
|
||||||
import { useAttendanceData } from '../../api/useAttendanceData';
|
|
||||||
|
|
||||||
const getTodayDate = () => {
|
|
||||||
const today = new Date();
|
|
||||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminAttendance() {
|
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayDate());
|
|
||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const map: Record<string, any> = {};
|
|
||||||
if (attendanceRecords) {
|
|
||||||
attendanceRecords.forEach(record => {
|
|
||||||
map[record.date] = {
|
|
||||||
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [attendanceRecords]);
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading my attendance..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load attendance data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
|
|
||||||
</div>
|
|
||||||
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
|
|
||||||
</div>
|
|
||||||
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import Calendar from '../../components/attendance/Calendar';
|
|
||||||
import AttendanceRecordsTable from '../../components/attendance/AttendanceRecordsTable';
|
|
||||||
import DailySummary from '../../components/attendance/DailySummary';
|
|
||||||
import Loader from '../../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
|
||||||
import { useAttendanceData } from '../../api/useAttendanceData';
|
|
||||||
|
|
||||||
const getTodayDate = () => {
|
|
||||||
const today = new Date();
|
|
||||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EmployeeAttendance() {
|
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayDate());
|
|
||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const map: Record<string, any> = {};
|
|
||||||
if (attendanceRecords) {
|
|
||||||
attendanceRecords.forEach(record => {
|
|
||||||
map[record.date] = {
|
|
||||||
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [attendanceRecords]);
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading attendance data..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load attendance data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
|
|
||||||
</div>
|
|
||||||
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
|
|
||||||
</div>
|
|
||||||
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import Calendar from '../../components/attendance/Calendar';
|
|
||||||
import AttendanceRecordsTable from '../../components/attendance/AttendanceRecordsTable';
|
|
||||||
import DailySummary from '../../components/attendance/DailySummary';
|
|
||||||
import Loader from '../../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
|
||||||
import { useAttendanceData } from '../../api/useAttendanceData';
|
|
||||||
|
|
||||||
const getTodayDate = () => {
|
|
||||||
const today = new Date();
|
|
||||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ManagerAttendance() {
|
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayDate());
|
|
||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const map: Record<string, any> = {};
|
|
||||||
if (attendanceRecords) {
|
|
||||||
attendanceRecords.forEach(record => {
|
|
||||||
map[record.date] = {
|
|
||||||
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [attendanceRecords]);
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading my attendance..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load attendance data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
|
|
||||||
</div>
|
|
||||||
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
|
|
||||||
</div>
|
|
||||||
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import Calendar from '../../components/attendance/Calendar';
|
|
||||||
import AttendanceRecordsTable from '../../components/attendance/AttendanceRecordsTable';
|
|
||||||
import DailySummary from '../../components/attendance/DailySummary';
|
|
||||||
import Loader from '../../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
|
||||||
import { useAttendanceData } from '../../api/useAttendanceData';
|
|
||||||
|
|
||||||
const getTodayDate = () => {
|
|
||||||
const today = new Date();
|
|
||||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SuperAdminAttendance() {
|
|
||||||
const [selectedDate, setSelectedDate] = useState(getTodayDate());
|
|
||||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
|
||||||
|
|
||||||
const calendarData = useMemo(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const map: Record<string, any> = {};
|
|
||||||
if (attendanceRecords) {
|
|
||||||
attendanceRecords.forEach(record => {
|
|
||||||
map[record.date] = {
|
|
||||||
status: record.status, login: record.timeIn, logout: record.timeOut, hours: record.loggedHours
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}, [attendanceRecords]);
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading my attendance..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load attendance data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<Calendar data={calendarData} selectedDate={selectedDate} onDateSelect={setSelectedDate} />
|
|
||||||
</div>
|
|
||||||
<DailySummary selectedDate={selectedDate} data={calendarData[selectedDate] || null} />
|
|
||||||
</div>
|
|
||||||
{attendanceRecords && <AttendanceRecordsTable records={attendanceRecords} title="My Attendance Records" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import Loader from '../../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
|
||||||
import RegularizationTable from '../../components/summary/RegularizationTable';
|
|
||||||
import { useAdminRegularizations, type RegularizationRecord } from '../../api/useAttendanceData';
|
|
||||||
|
|
||||||
export default function AdminRegularization() {
|
|
||||||
const { data: records, isLoading, isError } = useAdminRegularizations();
|
|
||||||
|
|
||||||
// Local state to handle instant UI updates for approvals/rejections
|
|
||||||
const [localOverrides, setLocalOverrides] = useState<Record<string, RegularizationRecord>>({});
|
|
||||||
|
|
||||||
const combinedRecords = useMemo(() => {
|
|
||||||
if (!records) return [];
|
|
||||||
return records.map(r => localOverrides[r.id] || r);
|
|
||||||
}, [records, localOverrides]);
|
|
||||||
|
|
||||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
|
||||||
const targetRecord = combinedRecords.find(r => r.id === id);
|
|
||||||
if (!targetRecord) return;
|
|
||||||
|
|
||||||
setLocalOverrides(prev => ({
|
|
||||||
...prev,
|
|
||||||
[id]: { ...targetRecord, status }
|
|
||||||
}));
|
|
||||||
|
|
||||||
toast.success(`Regularization request ${status.toLowerCase()} successfully.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading regularization requests..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load regularization requests." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<RegularizationTable records={combinedRecords} onAction={handleAction} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Users, UserCheck, UserX } from 'lucide-react';
|
|
||||||
import Loader from '../../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
|
||||||
import { useAttendanceSummary } from '../../api/useAttendanceData';
|
|
||||||
import StatCard from '../../components/summary/StatCard';
|
|
||||||
import AttendanceCharts from '../../components/summary/AttendanceCharts';
|
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AttendanceDashboard() {
|
|
||||||
const { data: records, isLoading, isError } = useAttendanceSummary();
|
|
||||||
|
|
||||||
const [date, setDate] = useState(formatDate(new Date()));
|
|
||||||
const [branch, setBranch] = useState('All');
|
|
||||||
const [department, setDepartment] = useState('All');
|
|
||||||
const [company, setCompany] = useState('All');
|
|
||||||
|
|
||||||
// Dynamically extract unique filter options from the API data
|
|
||||||
const { branches, departments, companies } = useMemo(() => {
|
|
||||||
if (!records) return { branches: ['All'], departments: ['All'], companies: ['All'] };
|
|
||||||
return {
|
|
||||||
branches: ['All', ...Array.from(new Set(records.map(r => r.branch)))],
|
|
||||||
departments: ['All', ...Array.from(new Set(records.map(r => r.department)))],
|
|
||||||
companies: ['All', ...Array.from(new Set(records.map(r => r.company)))],
|
|
||||||
};
|
|
||||||
}, [records]);
|
|
||||||
|
|
||||||
// Filter specifically for the selected date (Pie Chart & Stat Cards)
|
|
||||||
const filteredDailyData = useMemo(() => {
|
|
||||||
if (!records) return [];
|
|
||||||
return records.filter(r =>
|
|
||||||
r.date === date &&
|
|
||||||
(branch === 'All' || r.branch === branch) &&
|
|
||||||
(department === 'All' || r.department === department) &&
|
|
||||||
(company === 'All' || r.company === company)
|
|
||||||
);
|
|
||||||
}, [records, date, branch, department, company]);
|
|
||||||
|
|
||||||
// Filter for the entire selected month (Line Chart)
|
|
||||||
const monthlyTrendData = useMemo(() => {
|
|
||||||
if (!records) return [];
|
|
||||||
const selectedMonth = date.substring(0, 7);
|
|
||||||
return records.filter(r =>
|
|
||||||
r.date.startsWith(selectedMonth) &&
|
|
||||||
(branch === 'All' || r.branch === branch) &&
|
|
||||||
(department === 'All' || r.department === department) &&
|
|
||||||
(company === 'All' || r.company === company)
|
|
||||||
);
|
|
||||||
}, [records, date, branch, department, company]);
|
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
const total = filteredDailyData.length;
|
|
||||||
const checkedIn = filteredDailyData.filter(r => r.isCheckedIn).length;
|
|
||||||
const notCheckedIn = total - checkedIn;
|
|
||||||
return { total, checkedIn, notCheckedIn };
|
|
||||||
}, [filteredDailyData]);
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading dashboard..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load dashboard data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Professional Grid Filter Bar */}
|
|
||||||
<div className="bg-app-card p-4 rounded-lg shadow-sm grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className="text-xs text-text-muted mb-1">Date</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={date}
|
|
||||||
onChange={e => setDate(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dynamic Branch Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className="text-xs text-text-muted mb-1">Branch</label>
|
|
||||||
<select value={branch} onChange={e => setBranch(e.target.value)} className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
|
||||||
{branches.map(b => <option key={b} value={b}>{b}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dynamic Department Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className="text-xs text-text-muted mb-1">Department</label>
|
|
||||||
<select value={department} onChange={e => setDepartment(e.target.value)} className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
|
||||||
{departments.map(d => <option key={d} value={d}>{d}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dynamic Company Dropdown */}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label className="text-xs text-text-muted mb-1">Company</label>
|
|
||||||
<select value={company} onChange={e => setCompany(e.target.value)} className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
|
||||||
{companies.map(c => <option key={c} value={c}>{c}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<StatCard title="Total Employees" value={stats.total} Icon={Users} colorClass="text-primary" bgClass="bg-primary/10" />
|
|
||||||
<StatCard title="Total Checked In" value={stats.checkedIn} Icon={UserCheck} colorClass="text-present-700" bgClass="bg-present-100" />
|
|
||||||
<StatCard title="Total Not Checked In" value={stats.notCheckedIn} Icon={UserX} colorClass="text-absent-700" bgClass="bg-absent-100" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* MUI Charts - Passing Daily data for Pie, Monthly data for Line */}
|
|
||||||
<AttendanceCharts dailyData={filteredDailyData} trendData={monthlyTrendData} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Download, FileText } from 'lucide-react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import DailyReportFilters, { type DailyReportFilterValues } from '../../components/summary/DailyReportFilters';
|
|
||||||
|
|
||||||
// Helper to get current system date in YYYY-MM-DD format
|
|
||||||
const getTodayDate = () => {
|
|
||||||
const today = new Date();
|
|
||||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const todayStr = getTodayDate();
|
|
||||||
const yesterday = new Date();
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
const yesterdayStr = `${yesterday.getFullYear()}-${String(yesterday.getMonth() + 1).padStart(2, '0')}-${String(yesterday.getDate()).padStart(2, '0')}`;
|
|
||||||
|
|
||||||
const dummyDailyData = [
|
|
||||||
{ id: 'EMP001', name: 'Alice', company: 'CLRI', branch: 'Bengaluru', department: 'IT', designation: 'Software Engineer', date: todayStr, checkIn: '09:05 AM', checkOut: '06:15 PM', status: 'Present' },
|
|
||||||
{ id: 'EMP002', name: 'Bob', company: 'CLRI', branch: 'Pune', department: 'Training', designation: 'Trainer', date: todayStr, checkIn: '--', checkOut: '--', status: 'Absent' },
|
|
||||||
{ id: 'EMP003', name: 'Charlie', company: 'CLRI', branch: 'Hyderabad', department: 'Analytics', designation: 'Data Analyst', date: todayStr, checkIn: '09:15 AM', checkOut: '06:05 PM', status: 'Late' },
|
|
||||||
{ id: 'EMP004', name: 'David', company: 'CLRI', branch: 'Bengaluru', department: 'Placement', designation: 'Manager', date: todayStr, checkIn: '09:00 AM', checkOut: '--', status: 'Mispunch' },
|
|
||||||
{ id: 'EMP005', name: 'Eve', company: 'CLRI', branch: 'Pune', department: 'IT', designation: 'Software Engineer', date: todayStr, checkIn: '08:50 AM', checkOut: '05:30 PM', status: 'Present' },
|
|
||||||
{ id: 'EMP006', name: 'Frank', company: 'CLRI', branch: 'Bengaluru', department: 'IT', designation: 'Software Engineer', date: yesterdayStr, checkIn: '09:00 AM', checkOut: '06:00 PM', status: 'Present' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function DailyReport() {
|
|
||||||
const [filters, setFilters] = useState<DailyReportFilterValues>({
|
|
||||||
date: getTodayDate(),
|
|
||||||
company: 'All',
|
|
||||||
branch: 'All',
|
|
||||||
department: 'All',
|
|
||||||
designation: 'All',
|
|
||||||
employeeName: '',
|
|
||||||
employeeId: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return dummyDailyData.filter(record => {
|
|
||||||
const matchesDate = record.date === filters.date;
|
|
||||||
const matchesName = filters.employeeName ? record.name.toLowerCase().includes(filters.employeeName.toLowerCase()) : true;
|
|
||||||
const matchesId = filters.employeeId ? record.id.toLowerCase().includes(filters.employeeId.toLowerCase()) : true;
|
|
||||||
const matchesCompany = filters.company === 'All' || record.company === filters.company;
|
|
||||||
const matchesBranch = filters.branch === 'All' || record.branch === filters.branch;
|
|
||||||
const matchesDept = filters.department === 'All' || record.department === filters.department;
|
|
||||||
const matchesDesg = filters.designation === 'All' || record.designation === filters.designation;
|
|
||||||
|
|
||||||
return matchesDate && matchesName && matchesId && matchesCompany && matchesBranch && matchesDept && matchesDesg;
|
|
||||||
});
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
toast.success(`Preparing daily report for ${filters.date}...`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusClass = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'Present': return 'bg-present-100 text-present-700';
|
|
||||||
case 'Late': return 'bg-late-100 text-late-700';
|
|
||||||
case 'Absent': return 'bg-absent-100 text-absent-700';
|
|
||||||
case 'Mispunch': return 'bg-mispunch-100 text-mispunch-700';
|
|
||||||
default: return 'bg-app-muted text-text-secondary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="p-3 bg-primary/10 rounded-full">
|
|
||||||
<FileText className="text-primary" size={28} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">Daily Attendance Report</h2>
|
|
||||||
<p className="text-sm text-text-muted">View and download daily attendance logs.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleDownload}
|
|
||||||
className="flex items-center justify-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<Download size={18} className="mr-2" />
|
|
||||||
Download Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DailyReportFilters filters={filters} onFilterChange={setFilters} />
|
|
||||||
|
|
||||||
{/* Data Table */}
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">
|
|
||||||
Results for {new Date(filters.date + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' })} ({filteredData.length})
|
|
||||||
</h3>
|
|
||||||
<table className="min-w-full divide-y divide-app-border">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Company</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Designation</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-In</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Check-Out</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-app-border">
|
|
||||||
{filteredData.length > 0 ? (
|
|
||||||
filteredData.map((record, index) => (
|
|
||||||
<tr key={index} className="hover:bg-app-muted transition-colors">
|
|
||||||
<td className="px-4 py-3 text-sm text-text-primary">{record.id}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-primary">{record.name}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.company}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.branch}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.department}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.designation}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkIn}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.checkOut}</td>
|
|
||||||
<td className="px-4 py-3 text-sm">
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>
|
|
||||||
{record.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={9} className="px-4 py-8 text-center text-sm text-text-muted">
|
|
||||||
No attendance records found for this date.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Download, FileText } from 'lucide-react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import ReportFilters, { type ReportFilterValues } from '../../components/summary/MonthlyReportFilters';
|
|
||||||
|
|
||||||
const dummyReportData = [
|
|
||||||
{ id: 'EMP001', name: 'Alice', company: 'CLRI', branch: 'Bengaluru', department: 'IT', designation: 'Software Engineer', date: '2024-07-01', status: 'Present' },
|
|
||||||
{ id: 'EMP002', name: 'Bob', company: 'CLRI', branch: 'Pune', department: 'Training', designation: 'Trainer', date: '2024-07-01', status: 'Absent' },
|
|
||||||
{ id: 'EMP003', name: 'Charlie', company: 'CLRI', branch: 'Hyderabad', department: 'Analytics', designation: 'Data Analyst', date: '2024-07-01', status: 'Present' },
|
|
||||||
{ id: 'EMP004', name: 'David', company: 'CLRI', branch: 'Bengaluru', department: 'Placement', designation: 'Manager', date: '2024-07-02', status: 'Late' },
|
|
||||||
{ id: 'EMP005', name: 'Eve', company: 'CLRI', branch: 'Pune', department: 'IT', designation: 'Software Engineer', date: '2024-07-02', status: 'Present' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function MonthlyReport() {
|
|
||||||
const [filters, setFilters] = useState<ReportFilterValues>({
|
|
||||||
company: 'All',
|
|
||||||
branch: 'All',
|
|
||||||
department: 'All',
|
|
||||||
designation: 'All',
|
|
||||||
employeeName: '',
|
|
||||||
employeeId: '',
|
|
||||||
fromDate: '',
|
|
||||||
toDate: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
return dummyReportData.filter(record => {
|
|
||||||
const matchesName = filters.employeeName ? record.name.toLowerCase().includes(filters.employeeName.toLowerCase()) : true;
|
|
||||||
const matchesId = filters.employeeId ? record.id.toLowerCase().includes(filters.employeeId.toLowerCase()) : true;
|
|
||||||
const matchesCompany = filters.company === 'All' || record.company === filters.company;
|
|
||||||
const matchesBranch = filters.branch === 'All' || record.branch === filters.branch;
|
|
||||||
const matchesDept = filters.department === 'All' || record.department === filters.department;
|
|
||||||
const matchesDesg = filters.designation === 'All' || record.designation === filters.designation;
|
|
||||||
|
|
||||||
const recordDate = new Date(record.date);
|
|
||||||
const matchesFrom = filters.fromDate ? recordDate >= new Date(filters.fromDate) : true;
|
|
||||||
const matchesTo = filters.toDate ? recordDate <= new Date(filters.toDate) : true;
|
|
||||||
|
|
||||||
return matchesName && matchesId && matchesCompany && matchesBranch && matchesDept && matchesDesg && matchesFrom && matchesTo;
|
|
||||||
});
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
toast.success("Preparing report for download...");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="p-3 bg-primary/10 rounded-full">
|
|
||||||
<FileText className="text-primary" size={28} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">Monthly Attendance Report</h2>
|
|
||||||
<p className="text-sm text-text-muted">Filter and download detailed attendance reports.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleDownload}
|
|
||||||
className="flex items-center justify-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<Download size={18} className="mr-2" />
|
|
||||||
Download Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ReportFilters filters={filters} onFilterChange={setFilters} />
|
|
||||||
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm overflow-x-auto">
|
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Filtered Results ({filteredData.length})</h3>
|
|
||||||
<table className="min-w-full divide-y divide-app-border">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Emp CODE</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Name</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Branch</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Department</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Designation</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Date</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-text-muted uppercase">Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-app-border">
|
|
||||||
{filteredData.length > 0 ? (
|
|
||||||
filteredData.map((record, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-primary">{record.id}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-primary">{record.name}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.branch}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.department}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.designation}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.date}</td>
|
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">{record.status}</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={7} className="px-4 py-8 text-center text-sm text-text-muted">No records match your filters.</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,18 +1,40 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import MyAttendance from '../pages/MyAttendance';
|
||||||
import type { RootState } from '../../../store/store';
|
|
||||||
import EmployeeAttendance from '../pages/attendance/EmployeeAttendance';
|
|
||||||
import ManagerAttendance from '../pages/attendance/ManagerAttendance';
|
|
||||||
import AdminAttendance from '../pages/attendance/AdminAttendance';
|
|
||||||
import SuperAdminAttendance from '../pages/attendance/SuperAdminAttendance';
|
|
||||||
|
|
||||||
export default function AttendanceRouter() {
|
export default function AttendanceRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
// Since MyAttendance pulls the ID dynamically from Redux,
|
||||||
|
// it works perfectly for Employee, Manager, HR, and Director.
|
||||||
|
return <MyAttendance />;
|
||||||
|
}
|
||||||
|
|
||||||
switch (role) {
|
// ### How are we ensuring Managers/HR use ONLY their data?
|
||||||
case 'employee': return <EmployeeAttendance />;
|
|
||||||
case 'manager': return <ManagerAttendance />;
|
// We are achieving data isolation through a 3-layer security design that matches your backend developer's architecture perfectly:
|
||||||
case 'admin': return <AdminAttendance />;
|
|
||||||
case 'superadmin': return <SuperAdminAttendance />;
|
// #### Layer 1: Frontend State (Redux)
|
||||||
default: return <div>Unauthorized</div>;
|
// When you log in as a Manager, Redux sets the `mockUserId` to `127` (or whatever the Manager's actual ID is).
|
||||||
}
|
// ```typescript
|
||||||
}
|
// // From roleSlice.ts
|
||||||
|
// manager: { userId: '127', branches: '1' }
|
||||||
|
// ```
|
||||||
|
// The `MyAttendance` component reads this ID:
|
||||||
|
// ```typescript
|
||||||
|
// const employeeId = useSelector((state: RootState) => Number(state.role.mockUserId));
|
||||||
|
// ```
|
||||||
|
// Because it reads from Redux, the frontend *only* ever asks the backend for the logged-in user's data. A Manager cannot trigger an API call for Employee `126` because the frontend simply doesn't have `126` in its state.
|
||||||
|
|
||||||
|
// #### Layer 2: The Axios Interceptor (Mock SSO)
|
||||||
|
// When the API call leaves the frontend, your `client.ts` Axios interceptor attaches the Mock SSO headers:
|
||||||
|
// ```typescript
|
||||||
|
// config.headers['X-Mock-User-Id'] = mockUserId; // e.g., "127"
|
||||||
|
// config.headers['X-Mock-User-Role'] = mockUserRole; // e.g., "MANAGER"
|
||||||
|
// ```
|
||||||
|
|
||||||
|
// #### Layer 3: The API Gateway (The True Enforcer)
|
||||||
|
// Even if a malicious Manager somehow modified their browser code to send `?employee_id=126` in the URL parameters, **the backend API Gateway will reject it.**
|
||||||
|
// According to your architecture diagram, the Gateway reads the `X-Mock-User-Id` and `X-Mock-User-Role` headers.
|
||||||
|
// * The Gateway sees: "A request came in for Employee 126's attendance, but the headers say this is User 127 acting as a MANAGER."
|
||||||
|
// * The Gateway throws a **403 Forbidden** error because the token ID (127) does not match the requested ID (126).
|
||||||
|
|
||||||
|
// *(When real SSO/Keycloak is implemented, this exact same logic applies, just using the JWT token payload instead of mock headers).*
|
||||||
|
|
||||||
|
// By standardizing to `MyAttendance` and relying on Redux for the ID, your code is clean, DRY (Don't Repeat Yourself), and completely secure!
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import AttendanceDashboard from '../pages/summary/AttendanceDashboard';
|
import AttendanceDashboard from '../pages/AttendanceDashboard';
|
||||||
import MonthlyReport from '../pages/summary/MonthlyReport';
|
import MonthlyReport from '../pages/MonthlyReport';
|
||||||
import DailyReport from '../pages/summary/DailyReport';
|
import DailyReport from '../pages/DailyReport';
|
||||||
import AdminRegularization from '../pages/summary/AdminRegularization';
|
import HRManagerRegularization from '../pages/HRManagerRegularization';
|
||||||
|
|
||||||
export default function AttendanceSummaryRouter() {
|
export default function AttendanceSummaryRouter() {
|
||||||
return (
|
return (
|
||||||
@ -11,7 +11,7 @@ export default function AttendanceSummaryRouter() {
|
|||||||
<Route path="/dashboard" element={<AttendanceDashboard />} />
|
<Route path="/dashboard" element={<AttendanceDashboard />} />
|
||||||
<Route path="/monthly" element={<MonthlyReport />} />
|
<Route path="/monthly" element={<MonthlyReport />} />
|
||||||
<Route path="/daily" element={<DailyReport />} />
|
<Route path="/daily" element={<DailyReport />} />
|
||||||
<Route path="/regularization" element={<AdminRegularization />} />
|
<Route path="/regularization" element={<HRManagerRegularization />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// src/features/attendance/types/attendance.ts
|
||||||
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
|
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
|
||||||
|
|
||||||
export interface AttendanceRecord {
|
export interface AttendanceRecord {
|
||||||
@ -6,6 +7,58 @@ export interface AttendanceRecord {
|
|||||||
timeOut: string;
|
timeOut: string;
|
||||||
loggedHours: string;
|
loggedHours: string;
|
||||||
status: AttendanceStatus;
|
status: AttendanceStatus;
|
||||||
|
attendanceId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SortKey = keyof AttendanceRecord;
|
export type SortKey = keyof AttendanceRecord;
|
||||||
|
|
||||||
|
export interface AttendanceMetrics {
|
||||||
|
totalDaysTracked: number;
|
||||||
|
totalHoursWorked: number;
|
||||||
|
fullDays: number;
|
||||||
|
halfDays: number;
|
||||||
|
absences: number;
|
||||||
|
mispunches: number;
|
||||||
|
lateArrivals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttendanceSummary {
|
||||||
|
metrics: AttendanceMetrics;
|
||||||
|
history: AttendanceRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardMetrics {
|
||||||
|
total_employees: number;
|
||||||
|
total_checked_in: number;
|
||||||
|
total_not_checked_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyReportRecord {
|
||||||
|
employeeId: number;
|
||||||
|
employeeCode: string;
|
||||||
|
fullName: string;
|
||||||
|
status: string;
|
||||||
|
checkIn: string;
|
||||||
|
checkOut: string;
|
||||||
|
workedHours: string;
|
||||||
|
companyName?: string;
|
||||||
|
branchName?: string;
|
||||||
|
departmentName?: string;
|
||||||
|
designation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminReportRecord {
|
||||||
|
employeeId: number;
|
||||||
|
employeeCode: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
branchName: string;
|
||||||
|
departmentName: string;
|
||||||
|
rangeMetrics: {
|
||||||
|
fullDays: number;
|
||||||
|
halfDays: number;
|
||||||
|
mispunches: number;
|
||||||
|
lateArrivals: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Note: RegularizationRecord is defined in useAttendanceData.ts to keep types close to the hook.
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { Building, Network, Users, ShieldUser, ArrowRight, ScrollText } from 'lucide-react';
|
import { Building, Network, Users, ShieldUser, ArrowRight, ScrollText } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default function SuperAdminDashboard() {
|
export default function DirectorDashboard() {
|
||||||
const stats = [
|
const stats = [
|
||||||
{ title: 'Total Companies', value: '3', icon: Building, color: 'text-primary', bg: 'bg-primary/10' },
|
{ title: 'Total Companies', value: '3', icon: Building, color: 'text-primary', bg: 'bg-primary/10' },
|
||||||
{ title: 'Total Branches', value: '12', icon: Network, color: 'text-primary', bg: 'bg-primary/10' },
|
{ title: 'Total Branches', value: '12', icon: Network, color: 'text-primary', bg: 'bg-primary/10' },
|
||||||
{ title: 'Global Employees', value: '450', icon: Users, color: 'text-present-700', bg: 'bg-present-100' },
|
{ title: 'Global Employees', value: '450', icon: Users, color: 'text-present-700', bg: 'bg-present-100' },
|
||||||
{ title: 'Total Admins', value: '5', icon: ShieldUser, color: 'text-late-700', bg: 'bg-late-100' },
|
{ title: 'Total HRManagers', value: '5', icon: ShieldUser, color: 'text-late-700', bg: 'bg-late-100' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const companies = [
|
const companies = [
|
||||||
@ -18,7 +18,7 @@ export default function SuperAdminDashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
<h2 className="text-2xl font-bold text-text-primary">Welcome back, SuperAdmin!</h2>
|
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Director!</h2>
|
||||||
<p className="text-text-muted mt-1">Here is the global organizational overview.</p>
|
<p className="text-text-muted mt-1">Here is the global organizational overview.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -56,12 +56,12 @@ export default function SuperAdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">SuperAdmin Actions</h3>
|
<h3 className="text-lg font-semibold text-text-primary mb-4">Director Actions</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Link to="/manage-admins" className="flex items-center justify-between p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
|
<Link to="/manage-hrmanagers" className="flex items-center justify-between p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ShieldUser size={20} className="text-primary mr-3" />
|
<ShieldUser size={20} className="text-primary mr-3" />
|
||||||
<span className="text-sm font-medium text-text-primary">Manage Admins</span>
|
<span className="text-sm font-medium text-text-primary">Manage HR Managers</span>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRight size={18} className="text-text-light" />
|
<ArrowRight size={18} className="text-text-light" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Users, CalendarCheck, ClipboardCheck, Plane, Building } from 'lucide-react';
|
import { Users, CalendarCheck, ClipboardCheck, Plane, Building } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function HRManagerDashboard() {
|
||||||
const stats = [
|
const stats = [
|
||||||
{ title: 'Total Employees', value: '245', icon: Users, color: 'text-primary', bg: 'bg-primary/10' },
|
{ title: 'Total Employees', value: '245', icon: Users, color: 'text-primary', bg: 'bg-primary/10' },
|
||||||
{ title: 'Present Today', value: '198', icon: CalendarCheck, color: 'text-present-700', bg: 'bg-present-100' },
|
{ title: 'Present Today', value: '198', icon: CalendarCheck, color: 'text-present-700', bg: 'bg-present-100' },
|
||||||
@ -12,7 +12,7 @@ export default function AdminDashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Admin!</h2>
|
<h2 className="text-2xl font-bold text-text-primary">Welcome back, HRManager!</h2>
|
||||||
<p className="text-text-muted mt-1">Here is the organizational overview for today.</p>
|
<p className="text-text-muted mt-1">Here is the organizational overview for today.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -53,22 +53,6 @@ export default function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Department-wise Summary</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{[
|
|
||||||
{ dept: 'IT', present: 45, total: 50 },
|
|
||||||
{ dept: 'Training', present: 30, total: 35 },
|
|
||||||
{ dept: 'Analytics', present: 20, total: 25 },
|
|
||||||
{ dept: 'Placement', present: 15, total: 20 },
|
|
||||||
].map((d, idx) => (
|
|
||||||
<div key={idx} className="flex items-center justify-between border-b border-app-border pb-3 last:border-b-0 last:pb-0">
|
|
||||||
<span className="text-sm text-text-secondary">{d.dept} Department</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{d.present} / {d.total} Present</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -56,29 +56,6 @@ export default function ManagerDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h3 className="text-lg font-semibold text-text-primary mb-4">Team Attendance Today</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-text-secondary">Present</span>
|
|
||||||
<div className="w-2/3 bg-app-muted rounded-full h-2.5">
|
|
||||||
<div className="bg-present-500 h-2.5 rounded-full" style={{ width: '90%' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-text-secondary">On Leave</span>
|
|
||||||
<div className="w-2/3 bg-app-muted rounded-full h-2.5">
|
|
||||||
<div className="bg-absent-400 h-2.5 rounded-full" style={{ width: '10%' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm text-text-secondary">Late Arrivals</span>
|
|
||||||
<div className="w-2/3 bg-app-muted rounded-full h-2.5">
|
|
||||||
<div className="bg-late-500 h-2.5 rounded-full" style={{ width: '15%' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import { useSelector } from 'react-redux';
|
|||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import EmployeeDashboard from '../pages/EmployeeDashboard';
|
import EmployeeDashboard from '../pages/EmployeeDashboard';
|
||||||
import ManagerDashboard from '../pages/ManagerDashboard';
|
import ManagerDashboard from '../pages/ManagerDashboard';
|
||||||
import AdminDashboard from '../pages/AdminDashboard';
|
import HRManagerDashboard from '../pages/HRManagerDashboard';
|
||||||
import SuperAdminDashboard from '../pages/SuperAdminDashboard';
|
import DirectorDashboard from '../pages/DirectorDashboard';
|
||||||
|
|
||||||
export default function DashboardRouter() {
|
export default function DashboardRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
@ -13,10 +13,10 @@ export default function DashboardRouter() {
|
|||||||
return <EmployeeDashboard />;
|
return <EmployeeDashboard />;
|
||||||
case 'manager':
|
case 'manager':
|
||||||
return <ManagerDashboard />;
|
return <ManagerDashboard />;
|
||||||
case 'admin':
|
case 'hrmanager':
|
||||||
return <AdminDashboard />;
|
return <HRManagerDashboard />;
|
||||||
case 'superadmin':
|
case 'director':
|
||||||
return <SuperAdminDashboard />;
|
return <DirectorDashboard />;
|
||||||
default:
|
default:
|
||||||
return <div>No role assigned</div>;
|
return <div>No role assigned</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
import type { Employee } from './useEmployeeData';
|
|
||||||
export interface AdminEmployee extends Employee {
|
|
||||||
isAdmin: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useAdminEmployees = () => {
|
|
||||||
return useQuery<AdminEmployee[]>({
|
|
||||||
queryKey: ['adminEmployees'],
|
|
||||||
queryFn: () => fetchMockData<AdminEmployee[]>('master-data/admins.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
|
|
||||||
export interface Branch {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
companyId: string;
|
|
||||||
companyName: string;
|
|
||||||
status: 'Active' | 'Disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useBranches = () => {
|
|
||||||
return useQuery<Branch[]>({
|
|
||||||
queryKey: ['branches'],
|
|
||||||
queryFn: () => fetchMockData<Branch[]>('master-data/branches.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
|
|
||||||
export interface Company {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
status: 'Active' | 'Disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCompanies = () => {
|
|
||||||
return useQuery<Company[]>({
|
|
||||||
queryKey: ['companies'],
|
|
||||||
queryFn: () => fetchMockData<Company[]>('master-data/companies.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
|
|
||||||
export interface Department {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
branchId: string;
|
|
||||||
branchName: string;
|
|
||||||
companyId: string;
|
|
||||||
companyName: string;
|
|
||||||
status: 'Active' | 'Disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDepartments = () => {
|
|
||||||
return useQuery<Department[]>({
|
|
||||||
queryKey: ['departments'],
|
|
||||||
queryFn: () => fetchMockData<Department[]>('master-data/departments.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
|
|
||||||
export interface Designation {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
deptId: string;
|
|
||||||
deptName: string;
|
|
||||||
branchId: string;
|
|
||||||
branchName: string;
|
|
||||||
companyId: string;
|
|
||||||
companyName: string;
|
|
||||||
status: 'Active' | 'Disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDesignations = () => {
|
|
||||||
return useQuery<Designation[]>({
|
|
||||||
queryKey: ['designations'],
|
|
||||||
queryFn: () => fetchMockData<Designation[]>('master-data/designations.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
|
|
||||||
export interface Employee {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
designationId: string;
|
|
||||||
designationName: string;
|
|
||||||
deptId: string;
|
|
||||||
deptName: string;
|
|
||||||
branchId: string;
|
|
||||||
branchName: string;
|
|
||||||
companyId: string;
|
|
||||||
companyName: string;
|
|
||||||
status: 'Active' | 'Disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useEmployees = () => {
|
|
||||||
return useQuery<Employee[]>({
|
|
||||||
queryKey: ['employees'],
|
|
||||||
queryFn: () => fetchMockData<Employee[]>('master-data/employees.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { fetchMockData } from '../../../api/client';
|
|
||||||
|
|
||||||
export interface MasterDataSummary {
|
|
||||||
totalCompanies: number;
|
|
||||||
totalBranches: number;
|
|
||||||
totalDepartments: number;
|
|
||||||
totalDesignations: number;
|
|
||||||
totalEmployees: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMasterDataSummary = () => {
|
|
||||||
return useQuery<MasterDataSummary>({
|
|
||||||
queryKey: ['masterDataSummary'],
|
|
||||||
queryFn: () => fetchMockData<MasterDataSummary>('master-data/summary.json'),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
import type { Designation } from '../../api/useDesignationData';
|
|
||||||
import type { Company } from '../../api/useCompanyData';
|
|
||||||
import type { Branch } from '../../api/useBranchData';
|
|
||||||
import type { Department } from '../../api/useDepartmentData';
|
|
||||||
|
|
||||||
interface DesignationFormModalProps {
|
|
||||||
designation: Designation | null;
|
|
||||||
companies: Company[];
|
|
||||||
branches: Branch[];
|
|
||||||
departments: Department[];
|
|
||||||
onClose: () => void;
|
|
||||||
onSave: (data: Omit<Designation, 'id'>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DesignationFormModal({ designation, companies, branches, departments, onClose, onSave }: DesignationFormModalProps) {
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [companyId, setCompanyId] = useState('');
|
|
||||||
const [branchId, setBranchId] = useState('');
|
|
||||||
const [deptId, setDeptId] = useState('');
|
|
||||||
const [status, setStatus] = useState<'Active' | 'Disabled'>('Active');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (designation) {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setName(designation.name);
|
|
||||||
setCompanyId(designation.companyId);
|
|
||||||
setBranchId(designation.branchId);
|
|
||||||
setDeptId(designation.deptId);
|
|
||||||
setStatus(designation.status);
|
|
||||||
} else {
|
|
||||||
setName('');
|
|
||||||
setCompanyId(companies.length > 0 ? companies[0].id : '');
|
|
||||||
setBranchId('');
|
|
||||||
setDeptId('');
|
|
||||||
setStatus('Active');
|
|
||||||
}
|
|
||||||
}, [designation, companies]);
|
|
||||||
|
|
||||||
const filteredBranches = useMemo(() => branches.filter(b => b.companyId === companyId), [branches, companyId]);
|
|
||||||
const filteredDepartments = useMemo(() => departments.filter(d => d.branchId === branchId), [departments, branchId]);
|
|
||||||
|
|
||||||
const handleCompanyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setCompanyId(e.target.value);
|
|
||||||
setBranchId('');
|
|
||||||
setDeptId('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBranchChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setBranchId(e.target.value);
|
|
||||||
setDeptId('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const selectedCompany = companies.find(c => c.id === companyId);
|
|
||||||
const selectedBranch = branches.find(b => b.id === branchId);
|
|
||||||
const selectedDept = departments.find(d => d.id === deptId);
|
|
||||||
|
|
||||||
onSave({
|
|
||||||
name,
|
|
||||||
companyId,
|
|
||||||
companyName: selectedCompany?.name || '',
|
|
||||||
branchId,
|
|
||||||
branchName: selectedBranch?.name || '',
|
|
||||||
deptId,
|
|
||||||
deptName: selectedDept?.name || '',
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
|
||||||
const labelClass = "block text-sm font-medium text-text-secondary mb-1";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto">
|
|
||||||
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
|
||||||
<X size={24} />
|
|
||||||
</button>
|
|
||||||
<h3 className="text-xl font-bold text-text-primary mb-6">
|
|
||||||
{designation ? 'Edit Designation' : 'Add New Designation'}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Designation Name</label>
|
|
||||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} required className={inputClass} placeholder="Enter designation name..." />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Company</label>
|
|
||||||
<select value={companyId} onChange={handleCompanyChange} required className={inputClass}>
|
|
||||||
{companies.length === 0 ? <option value="">No companies available</option> : companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Branch</label>
|
|
||||||
<select value={branchId} onChange={handleBranchChange} required disabled={!companyId || filteredBranches.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
|
||||||
<option value="">Select Branch</option>
|
|
||||||
{filteredBranches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Department</label>
|
|
||||||
<select value={deptId} onChange={(e) => setDeptId(e.target.value)} required disabled={!branchId || filteredDepartments.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
|
||||||
<option value="">Select Department</option>
|
|
||||||
{filteredDepartments.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Status</label>
|
|
||||||
<select value={status} onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')} className={inputClass}>
|
|
||||||
<option value="Active">Active</option>
|
|
||||||
<option value="Disabled">Disabled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 mt-6">
|
|
||||||
<button type="button" onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">Cancel</button>
|
|
||||||
<button type="submit" disabled={!deptId} className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed">
|
|
||||||
{designation ? 'Update' : 'Add'} Designation
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
import type { Employee } from '../../api/useEmployeeData';
|
|
||||||
import type { Company } from '../../api/useCompanyData';
|
|
||||||
import type { Branch } from '../../api/useBranchData';
|
|
||||||
import type { Department } from '../../api/useDepartmentData';
|
|
||||||
import type { Designation } from '../../api/useDesignationData';
|
|
||||||
|
|
||||||
interface EmployeeFormModalProps {
|
|
||||||
employee: Employee | null;
|
|
||||||
companies: Company[];
|
|
||||||
branches: Branch[];
|
|
||||||
departments: Department[];
|
|
||||||
designations: Designation[];
|
|
||||||
onClose: () => void;
|
|
||||||
onSave: (data: Omit<Employee, 'id'>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EmployeeFormModal({ employee, companies, branches, departments, designations, onClose, onSave }: EmployeeFormModalProps) {
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [companyId, setCompanyId] = useState('');
|
|
||||||
const [branchId, setBranchId] = useState('');
|
|
||||||
const [deptId, setDeptId] = useState('');
|
|
||||||
const [designationId, setDesignationId] = useState('');
|
|
||||||
const [status, setStatus] = useState<'Active' | 'Disabled'>('Active');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (employee) {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
setName(employee.name);
|
|
||||||
setEmail(employee.email);
|
|
||||||
setCompanyId(employee.companyId);
|
|
||||||
setBranchId(employee.branchId);
|
|
||||||
setDeptId(employee.deptId);
|
|
||||||
setDesignationId(employee.designationId);
|
|
||||||
setStatus(employee.status);
|
|
||||||
} else {
|
|
||||||
setName('');
|
|
||||||
setEmail('');
|
|
||||||
setCompanyId(companies.length > 0 ? companies[0].id : '');
|
|
||||||
setBranchId('');
|
|
||||||
setDeptId('');
|
|
||||||
setDesignationId('');
|
|
||||||
setStatus('Active');
|
|
||||||
}
|
|
||||||
}, [employee, companies]);
|
|
||||||
|
|
||||||
const filteredBranches = useMemo(() => branches.filter(b => b.companyId === companyId), [branches, companyId]);
|
|
||||||
const filteredDepartments = useMemo(() => departments.filter(d => d.branchId === branchId), [departments, branchId]);
|
|
||||||
const filteredDesignations = useMemo(() => designations.filter(d => d.deptId === deptId), [designations, deptId]);
|
|
||||||
|
|
||||||
const handleCompanyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setCompanyId(e.target.value);
|
|
||||||
setBranchId('');
|
|
||||||
setDeptId('');
|
|
||||||
setDesignationId('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBranchChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setBranchId(e.target.value);
|
|
||||||
setDeptId('');
|
|
||||||
setDesignationId('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeptChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setDeptId(e.target.value);
|
|
||||||
setDesignationId('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const selectedCompany = companies.find(c => c.id === companyId);
|
|
||||||
const selectedBranch = branches.find(b => b.id === branchId);
|
|
||||||
const selectedDept = departments.find(d => d.id === deptId);
|
|
||||||
const selectedDesg = designations.find(d => d.id === designationId);
|
|
||||||
|
|
||||||
onSave({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
companyId,
|
|
||||||
companyName: selectedCompany?.name || '',
|
|
||||||
branchId,
|
|
||||||
branchName: selectedBranch?.name || '',
|
|
||||||
deptId,
|
|
||||||
deptName: selectedDept?.name || '',
|
|
||||||
designationId,
|
|
||||||
designationName: selectedDesg?.name || '',
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
|
||||||
const labelClass = "block text-sm font-medium text-text-secondary mb-1";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto">
|
|
||||||
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
|
||||||
<X size={24} />
|
|
||||||
</button>
|
|
||||||
<h3 className="text-xl font-bold text-text-primary mb-6">
|
|
||||||
{employee ? 'Edit Employee' : 'Add New Employee'}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Employee Name</label>
|
|
||||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} required className={inputClass} placeholder="Enter employee name..." />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Employee Email</label>
|
|
||||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className={inputClass} placeholder="Enter email address..." />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Company</label>
|
|
||||||
<select value={companyId} onChange={handleCompanyChange} required className={inputClass}>
|
|
||||||
{companies.length === 0 ? <option value="">No companies available</option> : companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Branch</label>
|
|
||||||
<select value={branchId} onChange={handleBranchChange} required disabled={!companyId || filteredBranches.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
|
||||||
<option value="">Select Branch</option>
|
|
||||||
{filteredBranches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Department</label>
|
|
||||||
<select value={deptId} onChange={handleDeptChange} required disabled={!branchId || filteredDepartments.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
|
||||||
<option value="">Select Department</option>
|
|
||||||
{filteredDepartments.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Designation</label>
|
|
||||||
<select value={designationId} onChange={(e) => setDesignationId(e.target.value)} required disabled={!deptId || filteredDesignations.length === 0} className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`}>
|
|
||||||
<option value="">Select Designation</option>
|
|
||||||
{filteredDesignations.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Status</label>
|
|
||||||
<select value={status} onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')} className={inputClass}>
|
|
||||||
<option value="Active">Active</option>
|
|
||||||
<option value="Disabled">Disabled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 mt-6">
|
|
||||||
<button type="button" onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">Cancel</button>
|
|
||||||
<button type="submit" disabled={!designationId} className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed">
|
|
||||||
{employee ? 'Update' : 'Add'} Employee
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import { X } from 'lucide-react';
|
|
||||||
import type { Employee } from '../../api/useEmployeeData';
|
|
||||||
|
|
||||||
interface EmployeeViewModalProps {
|
|
||||||
employee: Employee;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusClass = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'Active': return 'bg-present-100 text-present-700';
|
|
||||||
case 'Disabled': return 'bg-absent-100 text-absent-700';
|
|
||||||
default: return 'bg-app-muted text-text-secondary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EmployeeViewModal({ employee, onClose }: EmployeeViewModalProps) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
|
|
||||||
<button onClick={onClose} className="absolute top-4 right-4 text-text-muted hover:text-text-primary">
|
|
||||||
<X size={24} />
|
|
||||||
</button>
|
|
||||||
<h3 className="text-xl font-bold text-text-primary mb-6">Employee Details</h3>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Employee ID</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.id}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Employee Name</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Employee Email</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.email}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Company</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.companyName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Branch</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.branchName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Department</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.deptName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Designation</span>
|
|
||||||
<span className="text-sm font-medium text-text-primary">{employee.designationName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between border-b border-app-border pb-2">
|
|
||||||
<span className="text-sm text-text-muted">Status</span>
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(employee.status)}`}>{employee.status}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end mt-6">
|
|
||||||
<button onClick={onClose} className="px-4 py-2 bg-app-muted text-text-primary rounded-lg hover:bg-app-border transition-colors text-sm font-medium">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Tag, Plus } from 'lucide-react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import Loader from '../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../components/ui/ErrorState';
|
|
||||||
import ConfirmationModal from '../../../components/ui/ConfirmationModal';
|
|
||||||
import DesignationTable from '../components/designation/DesignationTable';
|
|
||||||
import DesignationFormModal from '../components/designation/DesignationFormModal';
|
|
||||||
import { useDesignations, type Designation } from '../api/useDesignationData';
|
|
||||||
import { useCompanies } from '../api/useCompanyData';
|
|
||||||
import { useBranches } from '../api/useBranchData';
|
|
||||||
import { useDepartments } from '../api/useDepartmentData';
|
|
||||||
|
|
||||||
export default function ManageDesignations() {
|
|
||||||
const { data: apiRecords, isLoading: dsgLoading, isError: dsgError } = useDesignations();
|
|
||||||
const { data: companies, isLoading: compLoading, isError: compError } = useCompanies();
|
|
||||||
const { data: branches, isLoading: branchLoading, isError: branchError } = useBranches();
|
|
||||||
const { data: departments, isLoading: depLoading, isError: depError } = useDepartments();
|
|
||||||
|
|
||||||
const [localRecords, setLocalRecords] = useState<Designation[]>([]);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [editingDesignation, setEditingDesignation] = useState<Designation | null>(null);
|
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useMemo(() => {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-render
|
|
||||||
if (apiRecords) setLocalRecords(apiRecords);
|
|
||||||
}, [apiRecords]);
|
|
||||||
|
|
||||||
const handleAddClick = () => {
|
|
||||||
setEditingDesignation(null);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditClick = (designation: Designation) => {
|
|
||||||
setEditingDesignation(designation);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteClick = (id: string) => {
|
|
||||||
setDeleteId(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (deleteId) {
|
|
||||||
setLocalRecords(prev => prev.filter(d => d.id !== deleteId));
|
|
||||||
toast.success("Designation deleted successfully.");
|
|
||||||
setDeleteId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = (data: Omit<Designation, 'id'>) => {
|
|
||||||
if (editingDesignation) {
|
|
||||||
setLocalRecords(prev => prev.map(d => d.id === editingDesignation.id ? { ...d, ...data } : d));
|
|
||||||
toast.success("Designation updated successfully.");
|
|
||||||
} else {
|
|
||||||
const newId = `DSG${String(localRecords.length + 1).padStart(3, '0')}`;
|
|
||||||
setLocalRecords(prev => [...prev, { id: newId, ...data }]);
|
|
||||||
toast.success("Designation added successfully.");
|
|
||||||
}
|
|
||||||
setIsModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (dsgLoading || compLoading || branchLoading || depLoading) return <Loader message="Loading designations data..." />;
|
|
||||||
if (dsgError || compError || branchError || depError) return <ErrorState message="Failed to load designations data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="p-3 bg-primary/10 rounded-full">
|
|
||||||
<Tag className="text-primary" size={28} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">Manage Designations</h2>
|
|
||||||
<p className="text-sm text-text-muted">Add, edit, or disable job designations.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleAddClick}
|
|
||||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<Plus size={18} className="mr-2" />
|
|
||||||
Add Designation
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DesignationTable records={localRecords} onEdit={handleEditClick} onDelete={handleDeleteClick} />
|
|
||||||
|
|
||||||
{isModalOpen && (
|
|
||||||
<DesignationFormModal
|
|
||||||
designation={editingDesignation}
|
|
||||||
companies={companies || []}
|
|
||||||
branches={branches || []}
|
|
||||||
departments={departments || []}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deleteId && (
|
|
||||||
<ConfirmationModal
|
|
||||||
title="Delete Designation?"
|
|
||||||
message="Are you sure you want to delete this designation? This action cannot be undone."
|
|
||||||
onConfirm={confirmDelete}
|
|
||||||
onClose={() => setDeleteId(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Users, Plus } from 'lucide-react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import Loader from '../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../components/ui/ErrorState';
|
|
||||||
import ConfirmationModal from '../../../components/ui/ConfirmationModal';
|
|
||||||
import EmployeeTable from '../components/employee/EmployeeTable';
|
|
||||||
import EmployeeFormModal from '../components/employee/EmployeeFormModal';
|
|
||||||
import EmployeeViewModal from '../components/employee/EmployeeViewModal';
|
|
||||||
import { useEmployees, type Employee } from '../api/useEmployeeData';
|
|
||||||
import { useCompanies } from '../api/useCompanyData';
|
|
||||||
import { useBranches } from '../api/useBranchData';
|
|
||||||
import { useDepartments } from '../api/useDepartmentData';
|
|
||||||
import { useDesignations } from '../api/useDesignationData';
|
|
||||||
|
|
||||||
export default function ManageEmployees() {
|
|
||||||
const { data: apiRecords, isLoading: empLoading, isError: empError } = useEmployees();
|
|
||||||
const { data: companies, isLoading: compLoading, isError: compError } = useCompanies();
|
|
||||||
const { data: branches, isLoading: branchLoading, isError: branchError } = useBranches();
|
|
||||||
const { data: departments, isLoading: depLoading, isError: depError } = useDepartments();
|
|
||||||
const { data: designations, isLoading: dsgLoading, isError: dsgError } = useDesignations();
|
|
||||||
|
|
||||||
const [localRecords, setLocalRecords] = useState<Employee[]>([]);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
|
|
||||||
const [viewEmployee, setViewEmployee] = useState<Employee | null>(null);
|
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useMemo(() => {
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-render
|
|
||||||
if (apiRecords) setLocalRecords(apiRecords);
|
|
||||||
}, [apiRecords]);
|
|
||||||
|
|
||||||
const handleAddClick = () => {
|
|
||||||
setEditingEmployee(null);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditClick = (employee: Employee) => {
|
|
||||||
setEditingEmployee(employee);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewClick = (employee: Employee) => {
|
|
||||||
setViewEmployee(employee);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteClick = (id: string) => {
|
|
||||||
setDeleteId(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (deleteId) {
|
|
||||||
setLocalRecords(prev => prev.filter(e => e.id !== deleteId));
|
|
||||||
toast.success("Employee deleted successfully.");
|
|
||||||
setDeleteId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = (data: Omit<Employee, 'id'>) => {
|
|
||||||
if (editingEmployee) {
|
|
||||||
setLocalRecords(prev => prev.map(e => e.id === editingEmployee.id ? { ...e, ...data } : e));
|
|
||||||
toast.success("Employee updated successfully.");
|
|
||||||
} else {
|
|
||||||
const newId = `EMP${String(localRecords.length + 1).padStart(3, '0')}`;
|
|
||||||
setLocalRecords(prev => [...prev, { id: newId, ...data }]);
|
|
||||||
toast.success("Employee added successfully.");
|
|
||||||
}
|
|
||||||
setIsModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLoading = empLoading || compLoading || branchLoading || depLoading || dsgLoading;
|
|
||||||
const isError = empError || compError || branchError || depError || dsgError;
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading employees data..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load employees data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="p-3 bg-primary/10 rounded-full">
|
|
||||||
<Users className="text-primary" size={28} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">Manage Employees</h2>
|
|
||||||
<p className="text-sm text-text-muted">Add, edit, or disable employee profiles.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleAddClick}
|
|
||||||
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<Plus size={18} className="mr-2" />
|
|
||||||
Add Employee
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EmployeeTable
|
|
||||||
records={localRecords}
|
|
||||||
onView={handleViewClick}
|
|
||||||
onEdit={handleEditClick}
|
|
||||||
onDelete={handleDeleteClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isModalOpen && (
|
|
||||||
<EmployeeFormModal
|
|
||||||
employee={editingEmployee}
|
|
||||||
companies={companies || []}
|
|
||||||
branches={branches || []}
|
|
||||||
departments={departments || []}
|
|
||||||
designations={designations || []}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewEmployee && (
|
|
||||||
<EmployeeViewModal
|
|
||||||
employee={viewEmployee}
|
|
||||||
onClose={() => setViewEmployee(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deleteId && (
|
|
||||||
<ConfirmationModal
|
|
||||||
title="Delete Employee?"
|
|
||||||
message="Are you sure you want to delete this employee? This action cannot be undone."
|
|
||||||
onConfirm={confirmDelete}
|
|
||||||
onClose={() => setDeleteId(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { Building, Network, Briefcase, Tag, Users } from 'lucide-react';
|
|
||||||
import Loader from '../../../components/ui/Loader';
|
|
||||||
import ErrorState from '../../../components/ui/ErrorState';
|
|
||||||
import StatCard from '../../attendance/components/summary/StatCard';
|
|
||||||
import { useMasterDataSummary } from '../api/useMasterData';
|
|
||||||
|
|
||||||
export default function MasterDataDashboard() {
|
|
||||||
const { data: summary, isLoading, isError } = useMasterDataSummary();
|
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading master data..." />;
|
|
||||||
if (isError) return <ErrorState message="Failed to load master data." />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
|
||||||
<h2 className="text-xl font-bold text-text-primary">Master Data Dashboard</h2>
|
|
||||||
<p className="text-sm text-text-muted mt-2">Overview of organizational structure.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<StatCard
|
|
||||||
title="Total Companies"
|
|
||||||
value={summary?.totalCompanies || 0}
|
|
||||||
Icon={Building}
|
|
||||||
colorClass="text-primary"
|
|
||||||
bgClass="bg-primary/10"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Total Branches"
|
|
||||||
value={summary?.totalBranches || 0}
|
|
||||||
Icon={Network}
|
|
||||||
colorClass="text-primary"
|
|
||||||
bgClass="bg-primary/10"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Total Departments"
|
|
||||||
value={summary?.totalDepartments || 0}
|
|
||||||
Icon={Briefcase}
|
|
||||||
colorClass="text-primary"
|
|
||||||
bgClass="bg-primary/10"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Total Designations"
|
|
||||||
value={summary?.totalDesignations || 0}
|
|
||||||
Icon={Tag}
|
|
||||||
colorClass="text-primary"
|
|
||||||
bgClass="bg-primary/10"
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
title="Total Employees"
|
|
||||||
value={summary?.totalEmployees || 0}
|
|
||||||
Icon={Users}
|
|
||||||
colorClass="text-primary"
|
|
||||||
bgClass="bg-primary/10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { useSelector } from 'react-redux';
|
|
||||||
import type { RootState } from '../../../store/store';
|
|
||||||
import ManageAdmins from '../../../features/employee-management/pages/ManageAdmins';
|
|
||||||
|
|
||||||
export default function ManageAdminsRouter() {
|
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
|
||||||
|
|
||||||
if (role === 'superadmin') {
|
|
||||||
return <ManageAdmins />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: SuperAdmins only.</div>;
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@ import type { RootState } from '../../../store/store';
|
|||||||
import type { UserRole } from '../../../store/roleSlice';
|
import type { UserRole } from '../../../store/roleSlice';
|
||||||
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
|
import type { LeaveSummary, LeaveRequestPayload, LeaveHistoryRecord, ManagerLeaveApproval, ApprovalStatus } from '../types/leave';
|
||||||
import type { LeaveApplicationSummary } from '../types/leave';
|
import type { LeaveApplicationSummary } from '../types/leave';
|
||||||
import type { LeaveLedgerRecord } from '../types/leave';
|
import type { LeaveBalanceRecord } from '../types/leave';
|
||||||
|
|
||||||
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> = {
|
||||||
employee: {
|
employee: {
|
||||||
@ -16,13 +16,13 @@ const LEAVE_ENDPOINTS: Record<UserRole, { summary: string, history: string }> =
|
|||||||
summary: 'leave/manager_leave_summary.json',
|
summary: 'leave/manager_leave_summary.json',
|
||||||
history: 'leave/manager_leave_history.json',
|
history: 'leave/manager_leave_history.json',
|
||||||
},
|
},
|
||||||
admin: {
|
hrmanager: {
|
||||||
summary: 'leave/admin_leave_summary.json',
|
summary: 'leave/hrmanager_leave_summary.json',
|
||||||
history: 'leave/admin_leave_history.json',
|
history: 'leave/hrmanager_leave_history.json',
|
||||||
},
|
},
|
||||||
superadmin: {
|
director: {
|
||||||
summary: 'leave/superadmin_leave_summary.json',
|
summary: 'leave/director_leave_summary.json',
|
||||||
history: 'leave/superadmin_leave_history.json',
|
history: 'leave/director_leave_history.json',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,25 +71,25 @@ export const useLeaveApplications = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLeaveLedger = () => {
|
export const useLeaveBalance = () => {
|
||||||
return useQuery<LeaveLedgerRecord[]>({
|
return useQuery<LeaveBalanceRecord[]>({
|
||||||
queryKey: ['leaveLedger'],
|
queryKey: ['leaveBalance'],
|
||||||
queryFn: () => fetchMockData<LeaveLedgerRecord[]>('leave/leave_ledger.json'),
|
queryFn: () => fetchMockData<LeaveBalanceRecord[]>('leave/leave_balance.json'),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useAdminApprovals = () => {
|
export const useHRManagerApprovals = () => {
|
||||||
return useQuery<ManagerLeaveApproval[]>({
|
return useQuery<ManagerLeaveApproval[]>({
|
||||||
queryKey: ['leaveApprovals', 'admin'],
|
queryKey: ['leaveApprovals', 'hrmanager'],
|
||||||
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/admin_pending_leaves.json'),
|
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/hrmanager_pending_leaves.json'),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSuperAdminApprovals = () => {
|
export const useDirectorApprovals = () => {
|
||||||
return useQuery<ManagerLeaveApproval[]>({
|
return useQuery<ManagerLeaveApproval[]>({
|
||||||
queryKey: ['leaveApprovals', 'superadmin'],
|
queryKey: ['leaveApprovals', 'director'],
|
||||||
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/superadmin_pending_leaves.json'),
|
queryFn: () => fetchMockData<ManagerLeaveApproval[]>('approvals/director_pending_leaves.json'),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -133,7 +133,7 @@ export default function LeaveHistoryTable({ records }: LeaveHistoryTableProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6 relative">
|
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6 relative">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
||||||
<h2 className="text-lg font-semibold text-text-primary">My Leaves Applications</h2>
|
<h2 className="text-lg font-semibold text-text-primary">Leaves Applications</h2>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@ -7,7 +7,7 @@ interface LeaveSummaryTableProps {
|
|||||||
export default function LeaveSummaryTable({ data }: LeaveSummaryTableProps) {
|
export default function LeaveSummaryTable({ data }: LeaveSummaryTableProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-app-card p-6 rounded-lg shadow-sm">
|
<div className="flex-1 bg-app-card p-6 rounded-lg shadow-sm">
|
||||||
<h2 className="text-lg font-semibold text-text-primary mb-6">My Leave Summary</h2>
|
<h2 className="text-lg font-semibold text-text-primary mb-6">Leave Summary</h2>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-app-border">
|
<table className="min-w-full divide-y divide-app-border">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
export interface LedgerFilterValues {
|
export interface BalanceFilterValues {
|
||||||
empId: string;
|
empId: string;
|
||||||
empName: string;
|
empName: string;
|
||||||
month: number;
|
month: number;
|
||||||
year: number;
|
year: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LedgerFiltersProps {
|
interface BalanceFiltersProps {
|
||||||
filters: LedgerFilterValues;
|
filters: BalanceFilterValues;
|
||||||
onFilterChange: (newFilters: LedgerFilterValues) => void;
|
onFilterChange: (newFilters: BalanceFilterValues) => void;
|
||||||
onSearch: (e: React.FormEvent) => void;
|
onSearch: (e: React.FormEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ const monthsArray = [
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const yearsArray = Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
|
const yearsArray = Array.from({ length: 5 }, (_, i) => currentYear - 2 + i);
|
||||||
|
|
||||||
export default function LedgerFilters({ filters, onFilterChange, onSearch }: LedgerFiltersProps) {
|
export default function BalanceFilters({ filters, onFilterChange, onSearch }: BalanceFiltersProps) {
|
||||||
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
||||||
const labelClass = "text-xs text-text-muted mb-1";
|
const labelClass = "text-xs text-text-muted mb-1";
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ export default function LedgerFilters({ filters, onFilterChange, onSearch }: Led
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex items-center justify-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors font-medium h-[38px]"
|
className="flex items-center justify-center px-4 py-2 bg-action text-white rounded-lg hover:bg-action-hover transition-colors font-medium h-[38px]"
|
||||||
>
|
>
|
||||||
<Search size={18} className="mr-2" />
|
<Search size={18} className="mr-2" />
|
||||||
Search
|
Search
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import type { LeaveLedgerRecord } from '../../../types/leave';
|
import type { LeaveBalanceRecord } from '../../../types/leave';
|
||||||
|
|
||||||
interface LedgerTableProps {
|
interface BalanceTableProps {
|
||||||
records: LeaveLedgerRecord[];
|
records: LeaveBalanceRecord[];
|
||||||
isSearched: boolean;
|
isSearched: boolean;
|
||||||
month: number;
|
month: number;
|
||||||
year: number;
|
year: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LedgerTable({ records, isSearched, month, year }: LedgerTableProps) {
|
export default function BalanceTable({ records, isSearched, month, year }: BalanceTableProps) {
|
||||||
const formatMonthYear = () => {
|
const formatMonthYear = () => {
|
||||||
const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'long' });
|
const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'long' });
|
||||||
return `${monthName} ${year}`;
|
return `${monthName} ${year}`;
|
||||||
@ -51,7 +51,7 @@ export default function LedgerTable({ records, isSearched, month, year }: Ledger
|
|||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="px-4 py-8 text-center text-sm text-text-muted">
|
<td colSpan={6} className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
No ledger records found for the selected filters.
|
No Leave Balance records found for the selected filters.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
@ -2,10 +2,10 @@ import { toast } from 'sonner';
|
|||||||
import Loader from '../../../../components/ui/Loader';
|
import Loader from '../../../../components/ui/Loader';
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
import ErrorState from '../../../../components/ui/ErrorState';
|
||||||
import ApprovalTable from '../../components/approval/ApprovalTable';
|
import ApprovalTable from '../../components/approval/ApprovalTable';
|
||||||
import { useSuperAdminApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
import { useDirectorApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
||||||
|
|
||||||
export default function SuperAdminApproval() {
|
export default function DirectorApproval() {
|
||||||
const { data: approvals, isLoading, isError } = useSuperAdminApprovals();
|
const { data: approvals, isLoading, isError } = useDirectorApprovals();
|
||||||
const { mutate } = useUpdateLeaveApproval();
|
const { mutate } = useUpdateLeaveApproval();
|
||||||
|
|
||||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||||
@ -19,7 +19,7 @@ export default function SuperAdminApproval() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading admin leave requests..." />;
|
if (isLoading) return <Loader message="Loading HR Manager leave requests..." />;
|
||||||
if (isError) return <ErrorState message="Failed to load leave requests." />;
|
if (isError) return <ErrorState message="Failed to load leave requests." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -2,10 +2,10 @@ import { toast } from 'sonner';
|
|||||||
import Loader from '../../../../components/ui/Loader';
|
import Loader from '../../../../components/ui/Loader';
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
import ErrorState from '../../../../components/ui/ErrorState';
|
||||||
import ApprovalTable from '../../components/approval/ApprovalTable';
|
import ApprovalTable from '../../components/approval/ApprovalTable';
|
||||||
import { useAdminApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
import { useHRManagerApprovals, useUpdateLeaveApproval } from '../../api/useLeaveData';
|
||||||
|
|
||||||
export default function AdminApproval() {
|
export default function HRManagerApproval() {
|
||||||
const { data: approvals, isLoading, isError } = useAdminApprovals();
|
const { data: approvals, isLoading, isError } = useHRManagerApprovals();
|
||||||
const { mutate } = useUpdateLeaveApproval();
|
const { mutate } = useUpdateLeaveApproval();
|
||||||
|
|
||||||
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
const handleAction = (id: string, status: 'Approved' | 'Rejected') => {
|
||||||
@ -2,17 +2,17 @@ import { useState } from 'react';
|
|||||||
import { FileText } from 'lucide-react';
|
import { FileText } from 'lucide-react';
|
||||||
import Loader from '../../../../components/ui/Loader';
|
import Loader from '../../../../components/ui/Loader';
|
||||||
import ErrorState from '../../../../components/ui/ErrorState';
|
import ErrorState from '../../../../components/ui/ErrorState';
|
||||||
import { useLeaveLedger } from '../../api/useLeaveData';
|
import { useLeaveBalance } from '../../api/useLeaveData';
|
||||||
import type { LeaveLedgerRecord } from '../../types/leave';
|
import type { LeaveBalanceRecord } from '../../types/leave';
|
||||||
import LedgerFilters, { type LedgerFilterValues } from '../../components/leave/ledger/LedgerFilters';
|
import BalanceFilters, { type BalanceFilterValues } from '../../components/leave/balance/BalanceFilters';
|
||||||
import LedgerTable from '../../components/leave/ledger/LedgerTable';
|
import BalanceTable from '../../components/leave/balance/BalanceTable';
|
||||||
|
|
||||||
export default function LedgerReport() {
|
export default function BalanceReport() {
|
||||||
const { data: ledgerData, isLoading, isError } = useLeaveLedger();
|
const { data: balanceData, isLoading, isError } = useLeaveBalance();
|
||||||
|
|
||||||
// Initialize filters with current month and year
|
// Initialize filters with current month and year
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const [filters, setFilters] = useState<LedgerFilterValues>({
|
const [filters, setFilters] = useState<BalanceFilterValues>({
|
||||||
empId: '',
|
empId: '',
|
||||||
empName: '',
|
empName: '',
|
||||||
month: today.getMonth() + 1,
|
month: today.getMonth() + 1,
|
||||||
@ -21,13 +21,13 @@ export default function LedgerReport() {
|
|||||||
|
|
||||||
// Search Trigger State
|
// Search Trigger State
|
||||||
const [isSearched, setIsSearched] = useState(false);
|
const [isSearched, setIsSearched] = useState(false);
|
||||||
const [searchResults, setSearchResults] = useState<LeaveLedgerRecord[]>([]);
|
const [searchResults, setSearchResults] = useState<LeaveBalanceRecord[]>([]);
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!ledgerData) return;
|
if (!balanceData) return;
|
||||||
|
|
||||||
const results = ledgerData.filter(record => {
|
const results = balanceData.filter(record => {
|
||||||
const matchesEmpId = filters.empId ? record.employeeId.toLowerCase().includes(filters.empId.toLowerCase()) : true;
|
const matchesEmpId = filters.empId ? record.employeeId.toLowerCase().includes(filters.empId.toLowerCase()) : true;
|
||||||
const matchesEmpName = filters.empName ? record.employeeName.toLowerCase().includes(filters.empName.toLowerCase()) : true;
|
const matchesEmpName = filters.empName ? record.employeeName.toLowerCase().includes(filters.empName.toLowerCase()) : true;
|
||||||
const matchesMonth = record.month === Number(filters.month);
|
const matchesMonth = record.month === Number(filters.month);
|
||||||
@ -40,8 +40,8 @@ export default function LedgerReport() {
|
|||||||
setIsSearched(true);
|
setIsSearched(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <Loader message="Loading ledger data..." />;
|
if (isLoading) return <Loader message="Loading Leave Balance data..." />;
|
||||||
if (isError) return <ErrorState message="Failed to load ledger data." />;
|
if (isError) return <ErrorState message="Failed to load Leave Balance data." />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -50,18 +50,18 @@ export default function LedgerReport() {
|
|||||||
<FileText className="text-primary" size={28} />
|
<FileText className="text-primary" size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-text-primary">Leave Ledger Report</h2>
|
<h2 className="text-xl font-bold text-text-primary">Leave Balance Report</h2>
|
||||||
<p className="text-sm text-text-muted">Filter by employee and month to view leave balances.</p>
|
<p className="text-sm text-text-muted">Filter by employee and month to view leave balances.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LedgerFilters
|
<BalanceFilters
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onFilterChange={setFilters}
|
onFilterChange={setFilters}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LedgerTable
|
<BalanceTable
|
||||||
records={searchResults}
|
records={searchResults}
|
||||||
isSearched={isSearched}
|
isSearched={isSearched}
|
||||||
month={filters.month}
|
month={filters.month}
|
||||||
@ -5,7 +5,7 @@ import ApplyLeaveForm from '../../components/leave/ApplyLeaveForm';
|
|||||||
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
||||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
||||||
|
|
||||||
export default function SuperAdminLeaveLeave() {
|
export default function DirectorLeaveLeave() {
|
||||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||||
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
||||||
|
|
||||||
@ -5,7 +5,7 @@ import ApplyLeaveForm from '../../components/leave/ApplyLeaveForm';
|
|||||||
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
import LeaveHistoryTable from '../../components/leave/LeaveHistoryTable';
|
||||||
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
import { useLeaveSummary, useLeaveHistory } from '../../api/useLeaveData';
|
||||||
|
|
||||||
export default function AdminLeave() {
|
export default function HRManagerLeave() {
|
||||||
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
const { data: summaryData, isLoading: summaryLoading, isError: summaryError } = useLeaveSummary();
|
||||||
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
const { data: historyData, isLoading: historyLoading, isError: historyError } = useLeaveHistory();
|
||||||
|
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import ManagerApproval from '../pages/approval/ManagerApproval';
|
import ManagerApproval from '../pages/approval/ManagerApproval';
|
||||||
import AdminApproval from '../pages/approval/AdminApproval';
|
import HRManagerApproval from '../pages/approval/HRManagerApproval';
|
||||||
import SuperAdminApproval from '../pages/approval/SuperAdminApproval';
|
import DirectorApproval from '../pages/approval/DirectorApproval';
|
||||||
|
|
||||||
export default function ApprovalRouter() {
|
export default function ApprovalRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
|
|
||||||
if (role === 'manager') return <ManagerApproval />;
|
if (role === 'manager') return <ManagerApproval />;
|
||||||
if (role === 'admin') return <AdminApproval />;
|
if (role === 'hrmanager') return <HRManagerApproval />;
|
||||||
if (role === 'superadmin') return <SuperAdminApproval />;
|
if (role === 'director') return <DirectorApproval />;
|
||||||
|
|
||||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Managers and Admins only.</div>;
|
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Managers and HRManagers only.</div>;
|
||||||
}
|
}
|
||||||
@ -2,16 +2,16 @@ import { useSelector } from 'react-redux';
|
|||||||
import type { RootState } from '../../../store/store';
|
import type { RootState } from '../../../store/store';
|
||||||
import EmployeeLeave from '../pages/leave/EmployeeLeave';
|
import EmployeeLeave from '../pages/leave/EmployeeLeave';
|
||||||
import ManagerLeave from '../pages/leave/ManagerLeave';
|
import ManagerLeave from '../pages/leave/ManagerLeave';
|
||||||
import AdminLeave from '../pages/leave/AdminLeave';
|
import HRManagerLeave from '../pages/leave/HRManagerLeave';
|
||||||
import SuperAdminLeaveLeave from '../pages/leave/SuperAdminLeave';
|
import DirectorLeave from '../pages/leave/DirectorLeave';
|
||||||
|
|
||||||
export default function LeaveRouter() {
|
export default function LeaveRouter() {
|
||||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'employee': return <EmployeeLeave />;
|
case 'employee': return <EmployeeLeave />;
|
||||||
case 'manager': return <ManagerLeave />;
|
case 'manager': return <ManagerLeave />;
|
||||||
case 'admin': return <AdminLeave />;
|
case 'hrmanager': return <HRManagerLeave />;
|
||||||
case 'superadmin': return <SuperAdminLeaveLeave />;
|
case 'director': return <DirectorLeave />;
|
||||||
default: return <div>Unauthorized</div>;
|
default: return <div>Unauthorized</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import LeaveApplications from '../pages/leave/LeaveApplications';
|
import LeaveApplications from '../pages/leave/LeaveApplications';
|
||||||
import LedgerReport from '../pages/leave/LedgerReport';
|
import BalanceReport from '../pages/leave/BalanceReport';
|
||||||
|
|
||||||
export default function LeaveSummaryRouter() {
|
export default function LeaveSummaryRouter() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/leave-summary/applications" replace />} />
|
<Route path="/" element={<Navigate to="/leave-summary/applications" replace />} />
|
||||||
<Route path="/applications" element={<LeaveApplications />} />
|
<Route path="/applications" element={<LeaveApplications />} />
|
||||||
<Route path="/ledger" element={<LedgerReport />} />
|
<Route path="/balance" element={<BalanceReport />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ export interface LeaveApplicationSummary {
|
|||||||
status: 'Approved' | 'Pending' | 'Rejected' | 'Withdrawn';
|
status: 'Approved' | 'Pending' | 'Rejected' | 'Withdrawn';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeaveLedgerRecord {
|
export interface LeaveBalanceRecord {
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
employeeName: string;
|
employeeName: string;
|
||||||
month: number;
|
month: number;
|
||||||
|
|||||||
93
src/features/masterData/api/useBranchData.ts
Normal file
93
src/features/masterData/api/useBranchData.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface Branch {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
companyId: number | null;
|
||||||
|
companyCode: string;
|
||||||
|
companyName: string;
|
||||||
|
status: 'Active' | 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. GET: Fetch all branches
|
||||||
|
export const useBranches = () => {
|
||||||
|
return useQuery<Branch[]>({
|
||||||
|
queryKey: ['branches'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ems/branches');
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return rawData.map((b: any) => ({
|
||||||
|
id: b.branchId,
|
||||||
|
code: b.branchCode || b.code,
|
||||||
|
name: b.branchName,
|
||||||
|
companyId: b.companyId || null,
|
||||||
|
companyCode: b.companyCode || '',
|
||||||
|
companyName: b.companyName || '',
|
||||||
|
status: b.isActive ? 'Active' : 'Disabled',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. POST: Create a branch
|
||||||
|
export const useCreateBranch = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: { name: string; companyId: number; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
branchName: data.name,
|
||||||
|
companyId: data.companyId,
|
||||||
|
isActive: data.status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.post('/api/ems/branches', payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['branches'] });
|
||||||
|
toast.success('Branch created successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. PUT: Update a branch
|
||||||
|
export const useUpdateBranch = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, code, name, companyId, status }: { id: number; code: string; name: string; companyId: number; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
branchName: name,
|
||||||
|
companyId: companyId,
|
||||||
|
code: code,
|
||||||
|
isActive: status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.put(`/api/ems/branches/${id}`, payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['branches'] });
|
||||||
|
toast.success('Branch updated successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. DELETE: Delete a branch
|
||||||
|
export const useDeleteBranch = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const response = await apiClient.delete(`/api/ems/branches/${id}`);
|
||||||
|
if (response.data && !response.data.success) {
|
||||||
|
throw new Error(response.data.message || "Failed to delete branch.");
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['branches'] });
|
||||||
|
toast.success('Branch deleted successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
89
src/features/masterData/api/useCompanyData.ts
Normal file
89
src/features/masterData/api/useCompanyData.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface Company {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
status: 'Active' | 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. GET: Fetch all companies
|
||||||
|
export const useCompanies = () => {
|
||||||
|
return useQuery<Company[]>({
|
||||||
|
queryKey: ['companies'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ems/companies');
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
|
||||||
|
return rawData.map((c: any) => ({
|
||||||
|
id: c.companyId,
|
||||||
|
code: c.companyCode,
|
||||||
|
name: c.name,
|
||||||
|
status: c.isActive ? 'Active' : 'Disabled',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. POST: Create a company
|
||||||
|
export const useCreateCompany = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: { name: string; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
name: data.name,
|
||||||
|
parentId: null,
|
||||||
|
isActive: data.status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.post('/api/ems/companies', payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['companies'] });
|
||||||
|
toast.success('Company created successfully');
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.response?.data?.message || 'Failed to create company');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. PUT: Update a company
|
||||||
|
export const useUpdateCompany = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, name, status }: { id: number; name: string; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
parentId: null,
|
||||||
|
isActive: status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.put(`/api/ems/companies/${id}`, payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['companies'] });
|
||||||
|
toast.success('Company updated successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. DELETE: Delete a company
|
||||||
|
export const useDeleteCompany = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const response = await apiClient.delete(`/api/ems/companies/${id}`);
|
||||||
|
if (response.data && response.data.success === false) {
|
||||||
|
throw new Error(response.data.message || 'Failed to delete company.');
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['companies'] });
|
||||||
|
toast.success('Company deleted successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
100
src/features/masterData/api/useDepartmentData.ts
Normal file
100
src/features/masterData/api/useDepartmentData.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface Department {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
branchId: number | null;
|
||||||
|
branchName: string;
|
||||||
|
companyId: number | null;
|
||||||
|
companyName: string;
|
||||||
|
status: 'Active' | 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. GET: Fetch all departments
|
||||||
|
export const useDepartments = () => {
|
||||||
|
return useQuery<Department[]>({
|
||||||
|
queryKey: ['departments'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ems/departments');
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return rawData.map((d: any) => ({
|
||||||
|
id: d.departmentId,
|
||||||
|
code: d.departmentCode || '',
|
||||||
|
name: d.departmentName || d.name,
|
||||||
|
branchId: d.branchId || null,
|
||||||
|
branchName: d.branchName || '',
|
||||||
|
companyId: d.companyId || null,
|
||||||
|
companyName: d.companyName || '',
|
||||||
|
status: d.isActive ? 'Active' : 'Disabled',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. POST: Create a department
|
||||||
|
export const useCreateDepartment = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: { name: string; companyId: number; branchId: number; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
name: data.name,
|
||||||
|
companyId: data.companyId,
|
||||||
|
branchId: data.branchId,
|
||||||
|
parentId: null,
|
||||||
|
managerId: null,
|
||||||
|
isActive: data.status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.post('/api/ems/departments', payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['departments'] });
|
||||||
|
toast.success('Department created successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. PUT: Update a department
|
||||||
|
export const useUpdateDepartment = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, name, companyId, branchId, status }: { id: number; name: string; companyId: number; branchId: number; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
companyId,
|
||||||
|
branchId,
|
||||||
|
parentId: null,
|
||||||
|
managerId: null,
|
||||||
|
isActive: status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.put(`/api/ems/departments/${id}`, payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['departments'] });
|
||||||
|
toast.success('Department updated successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. DELETE: Delete a department
|
||||||
|
export const useDeleteDepartment = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const response = await apiClient.delete(`/api/ems/departments/${id}`);
|
||||||
|
if (response.data && !response.data.success) {
|
||||||
|
throw new Error(response.data.message || "Failed to delete department.");
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['departments'] });
|
||||||
|
toast.success('Department deleted successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
223
src/features/masterData/api/useEmployeeData.ts
Normal file
223
src/features/masterData/api/useEmployeeData.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface Employee {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
jobId: number | null;
|
||||||
|
jobName: string;
|
||||||
|
deptId: number | null;
|
||||||
|
deptName: string;
|
||||||
|
branchId: number | null;
|
||||||
|
branchName: string;
|
||||||
|
companyId: number | null;
|
||||||
|
companyName: string;
|
||||||
|
status: 'Active' | 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmployeeDetail {
|
||||||
|
employeeId: number;
|
||||||
|
employeeCode: string;
|
||||||
|
isActive: boolean;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
dob: string;
|
||||||
|
gender: string;
|
||||||
|
personalEmail: string;
|
||||||
|
personalPhone: string;
|
||||||
|
workEmail: string;
|
||||||
|
dateJoining: string;
|
||||||
|
probationDays: number;
|
||||||
|
contractStatus: string;
|
||||||
|
salaryStructureId: number;
|
||||||
|
department: string;
|
||||||
|
departmentId: number;
|
||||||
|
jobName: string;
|
||||||
|
jobId: number;
|
||||||
|
managerFirstName: string;
|
||||||
|
managerLastName: string;
|
||||||
|
managerEmployeeCode: string;
|
||||||
|
addresses: Array<{
|
||||||
|
type: string;
|
||||||
|
doorNumber: string;
|
||||||
|
landmark: string;
|
||||||
|
line: string;
|
||||||
|
pincode: string;
|
||||||
|
district: string;
|
||||||
|
state: string;
|
||||||
|
}>;
|
||||||
|
companyName: string;
|
||||||
|
companyId: number;
|
||||||
|
branchName: string;
|
||||||
|
branchId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Manager {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. GET: Fetch all employees (List view)
|
||||||
|
export const useEmployees = () => {
|
||||||
|
return useQuery<Employee[]>({
|
||||||
|
queryKey: ['employees'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ems/employees');
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return rawData.map((e: any) => ({
|
||||||
|
id: e.employeeId,
|
||||||
|
code: e.employeeCode,
|
||||||
|
firstName: e.firstName || "",
|
||||||
|
lastName: e.lastName || "",
|
||||||
|
email: e.workEmail || "",
|
||||||
|
jobId: e.jobId || null,
|
||||||
|
jobName: e.jobName || "",
|
||||||
|
deptId: e.departmentId || null,
|
||||||
|
deptName: e.departmentName || "",
|
||||||
|
branchId: e.branchId || null,
|
||||||
|
branchName: e.branchName || "",
|
||||||
|
companyId: e.companyId || null,
|
||||||
|
companyName: e.companyName || "",
|
||||||
|
status: e.isActive ? 'Active' : 'Disabled',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1.5. GET: Fetch single employee by ID
|
||||||
|
export const useEmployeeById = (id: number | null) => {
|
||||||
|
return useQuery<EmployeeDetail>({
|
||||||
|
queryKey: ['employee', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get(`/api/ems/employees/${id}`);
|
||||||
|
return response.data.data as EmployeeDetail;
|
||||||
|
},
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1.6. GET: Fetch Managers by Department ID
|
||||||
|
export const useManagers = (deptId: number | null) => {
|
||||||
|
return useQuery<Manager[]>({
|
||||||
|
queryKey: ['managers', deptId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!deptId) return [];
|
||||||
|
const response = await apiClient.get(`/api/ems/managers?department_id=${deptId}`);
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return rawData.map((m: any) => ({
|
||||||
|
id: m.employeeId,
|
||||||
|
code: m.employeeCode,
|
||||||
|
firstName: m.firstName,
|
||||||
|
lastName: m.lastName,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
enabled: !!deptId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. POST: Create an employee
|
||||||
|
export const useCreateEmployee = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: any) => {
|
||||||
|
const payload = {
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
dob: data.dob,
|
||||||
|
gender: data.gender,
|
||||||
|
personalEmail: data.personalEmail,
|
||||||
|
personalPhone: data.personalPhone,
|
||||||
|
address: data.address,
|
||||||
|
companyId: data.companyId,
|
||||||
|
branchId: data.branchId,
|
||||||
|
departmentId: data.deptId,
|
||||||
|
jobId: data.jobId,
|
||||||
|
workEmail: data.workEmail,
|
||||||
|
dateJoining: data.dateJoining,
|
||||||
|
probationDays: data.probationDays || 90,
|
||||||
|
salaryStructureId: data.salaryStructureId || 1,
|
||||||
|
reportingToId: data.managerId || null,
|
||||||
|
isActive: data.status === 'Active'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post('/api/ems/employees', payload);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error";
|
||||||
|
throw new Error(backendError, { cause: error });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['employees'] });
|
||||||
|
toast.success('Employee created successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. PUT: Update an employee profile data
|
||||||
|
export const useUpdateEmployee = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: any) => {
|
||||||
|
// Removed isActive from here, status is handled by PATCH endpoint
|
||||||
|
const payload = {
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
dob: data.dob,
|
||||||
|
gender: data.gender,
|
||||||
|
personalEmail: data.personalEmail,
|
||||||
|
personalPhone: data.personalPhone,
|
||||||
|
address: data.address,
|
||||||
|
companyId: data.companyId,
|
||||||
|
branchId: data.branchId,
|
||||||
|
departmentId: data.deptId,
|
||||||
|
jobId: data.jobId,
|
||||||
|
workEmail: data.workEmail,
|
||||||
|
dateJoining: data.dateJoining,
|
||||||
|
reportingToId: data.managerId || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put(`/api/ems/employees/${data.id}`, payload);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error";
|
||||||
|
throw new Error(backendError, { cause: error });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['employees'], refetchType: 'active' });
|
||||||
|
if (variables.id) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['employee', variables.id] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. PATCH: Update Employee Status (Active/Disabled)
|
||||||
|
export const useUpdateEmployeeStatus = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, isActive }: { id: number; isActive: boolean }) => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.patch(`/api/ems/employees/${id}/status`, { isActive });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
const backendError = error?.response?.data?.message || error?.response?.data?.error || "Unknown backend error";
|
||||||
|
throw new Error(backendError, { cause: error });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['employees'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
14
src/features/masterData/api/useHRManagerData.ts
Normal file
14
src/features/masterData/api/useHRManagerData.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { fetchMockData } from '../../../api/client';
|
||||||
|
import type { Employee } from './useEmployeeData';
|
||||||
|
export interface HRManagerEmployee extends Employee {
|
||||||
|
name: any;
|
||||||
|
isHRManager: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHRManagerEmployees = () => {
|
||||||
|
return useQuery<HRManagerEmployee[]>({
|
||||||
|
queryKey: ['HRManagerEmployees'],
|
||||||
|
queryFn: () => fetchMockData<HRManagerEmployee[]>('master-data/hrmanagers.json'),
|
||||||
|
});
|
||||||
|
};
|
||||||
102
src/features/masterData/api/useJobRoleData.ts
Normal file
102
src/features/masterData/api/useJobRoleData.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface JobRole {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
deptId: number | null;
|
||||||
|
deptName: string;
|
||||||
|
branchId: number | null;
|
||||||
|
branchName: string;
|
||||||
|
companyId: number | null;
|
||||||
|
companyName: string;
|
||||||
|
status: 'Active' | 'Disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. GET: Fetch all job roles
|
||||||
|
export const useJobRoles = () => {
|
||||||
|
return useQuery<JobRole[]>({
|
||||||
|
queryKey: ['jobRoles'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ems/jobs');
|
||||||
|
const rawData = response.data.data || [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return rawData.map((d: any) => ({
|
||||||
|
id: d.jobId,
|
||||||
|
code: d.jobCode || '',
|
||||||
|
name: d.jobName || '',
|
||||||
|
description: d.description || '',
|
||||||
|
deptId: d.departmentId || null,
|
||||||
|
deptName: d.departmentName || '',
|
||||||
|
branchId: d.branchId || null,
|
||||||
|
branchName: d.branchName || '',
|
||||||
|
companyId: d.companyId || null,
|
||||||
|
companyName: d.companyName || '',
|
||||||
|
status: d.isActive ? 'Active' : 'Disabled',
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. POST: Create a job role
|
||||||
|
export const useCreateJobRole = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (data: { name: string; description: string; deptId: number; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
departmentId: data.deptId,
|
||||||
|
title: data.name,
|
||||||
|
description: data.description,
|
||||||
|
isActive: data.status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.post('/api/ems/jobs', payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['jobRoles'] });
|
||||||
|
toast.success('Job Role created successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. PUT: Update a job role
|
||||||
|
export const useUpdateJobRole = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, name, description, deptId, status }: { id: number; name: string; description: string; deptId: number; status: 'Active' | 'Disabled' }) => {
|
||||||
|
const payload = {
|
||||||
|
departmentId: deptId,
|
||||||
|
title: name,
|
||||||
|
description: description,
|
||||||
|
isActive: status === 'Active',
|
||||||
|
};
|
||||||
|
const response = await apiClient.put(`/api/ems/jobs/${id}`, payload);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['jobRoles'] });
|
||||||
|
toast.success('Job Role updated successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. DELETE: Delete a job role
|
||||||
|
export const useDeleteJobRole = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const response = await apiClient.delete(`/api/ems/jobs/${id}`);
|
||||||
|
if (response.data && !response.data.success) {
|
||||||
|
throw new Error(response.data.message || "Failed to delete job role.");
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['jobRoles'] });
|
||||||
|
toast.success('Job Role deleted successfully');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
28
src/features/masterData/api/useMasterData.ts
Normal file
28
src/features/masterData/api/useMasterData.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
|
||||||
|
export interface MasterDataSummary {
|
||||||
|
totalCompanies: number;
|
||||||
|
totalBranches: number;
|
||||||
|
totalDepartments: number;
|
||||||
|
totalJobRoles: number;
|
||||||
|
totalEmployees: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMasterDataSummary = () => {
|
||||||
|
return useQuery<MasterDataSummary>({
|
||||||
|
queryKey: ['masterDataSummary'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await apiClient.get('/api/ems/dashboard/metrics');
|
||||||
|
const d = response.data.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCompanies: d.total_companies ?? d.totalCompanies ?? 0,
|
||||||
|
totalBranches: d.total_branches ?? d.totalBranches ?? 0,
|
||||||
|
totalDepartments: d.total_departments ?? d.totalDepartments ?? 0,
|
||||||
|
totalJobRoles: d.total_jobs ?? d.totalJobs ?? 0,
|
||||||
|
totalEmployees: d.total_employees ?? d.totalEmployees ?? 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -4,37 +4,48 @@ import type { Branch } from '../../api/useBranchData';
|
|||||||
import type { Company } from '../../api/useCompanyData';
|
import type { Company } from '../../api/useCompanyData';
|
||||||
|
|
||||||
interface BranchFormModalProps {
|
interface BranchFormModalProps {
|
||||||
branch: Branch | null; // null means Add mode, object means Edit mode
|
branch: Branch | null;
|
||||||
companies: Company[];
|
companies: Company[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (name: string, companyId: string, companyName: string, status: 'Active' | 'Disabled') => void;
|
onSave: (name: string, companyId: number, status: 'Active' | 'Disabled') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BranchFormModal({ branch, companies, onClose, onSave }: BranchFormModalProps) {
|
export default function BranchFormModal({ branch, companies, onClose, onSave }: BranchFormModalProps) {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [companyId, setCompanyId] = useState('');
|
const [companyId, setCompanyId] = useState<string>('');
|
||||||
const [status, setStatus] = useState<'Active' | 'Disabled'>('Active');
|
const [status, setStatus] = useState<'Active' | 'Disabled'>('Active');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setName(branch.name);
|
setName(branch.name);
|
||||||
setCompanyId(branch.companyId);
|
|
||||||
setStatus(branch.status);
|
setStatus(branch.status);
|
||||||
|
|
||||||
|
// If backend GET provided companyId, use it. Otherwise, find it by companyCode.
|
||||||
|
if (branch.companyId) {
|
||||||
|
setCompanyId(String(branch.companyId));
|
||||||
|
} else {
|
||||||
|
const matchedCompany = companies.find(c => c.code === branch.companyCode);
|
||||||
|
setCompanyId(matchedCompany ? String(matchedCompany.id) : (companies.length > 0 ? String(companies[0].id) : ''));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setName('');
|
setName('');
|
||||||
setCompanyId(companies.length > 0 ? companies[0].id : '');
|
setCompanyId(companies.length > 0 ? String(companies[0].id) : '');
|
||||||
setStatus('Active');
|
setStatus('Active');
|
||||||
}
|
}
|
||||||
}, [branch, companies]);
|
}, [branch, companies]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Find the company name from the selected ID to save it alongside
|
if (!companyId) {
|
||||||
const selectedCompany = companies.find(c => c.id === companyId);
|
alert("Please select a company");
|
||||||
onSave(name, companyId, selectedCompany?.name || '', status);
|
return;
|
||||||
|
}
|
||||||
|
onSave(name, Number(companyId), status);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inputClass = "w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
|
<div className="bg-app-card rounded-lg shadow-xl w-full max-w-md p-6 relative">
|
||||||
@ -53,7 +64,7 @@ export default function BranchFormModal({ branch, companies, onClose, onSave }:
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
required
|
required
|
||||||
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
className={inputClass}
|
||||||
placeholder="Enter branch name..."
|
placeholder="Enter branch name..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -64,7 +75,7 @@ export default function BranchFormModal({ branch, companies, onClose, onSave }:
|
|||||||
value={companyId}
|
value={companyId}
|
||||||
onChange={(e) => setCompanyId(e.target.value)}
|
onChange={(e) => setCompanyId(e.target.value)}
|
||||||
required
|
required
|
||||||
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
className={inputClass}
|
||||||
>
|
>
|
||||||
{companies.length === 0 ? (
|
{companies.length === 0 ? (
|
||||||
<option value="">No companies available</option>
|
<option value="">No companies available</option>
|
||||||
@ -79,7 +90,7 @@ export default function BranchFormModal({ branch, companies, onClose, onSave }:
|
|||||||
<select
|
<select
|
||||||
value={status}
|
value={status}
|
||||||
onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')}
|
onChange={(e) => setStatus(e.target.value as 'Active' | 'Disabled')}
|
||||||
className="w-full px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card"
|
className={inputClass}
|
||||||
>
|
>
|
||||||
<option value="Active">Active</option>
|
<option value="Active">Active</option>
|
||||||
<option value="Disabled">Disabled</option>
|
<option value="Disabled">Disabled</option>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user