Compare commits

..

3 Commits

Author SHA1 Message Date
4174687024 revert 0b5b207715e5a26945d8d56393fa05de511a08fb
revert revert 7535624829cd7d4d5776eb8bfde62948da2edddb

revert revert 00929c92d1e9cad7c21037202563d40d123346ef

revert Merge pull request 'feature/leave/admin/approval - added admin,manager leave approval' (#18) from feature/leave/admin/approval into main

Reviewed-on: #18
2026-07-22 10:33:30 +05:30
0b5b207715 revert 7535624829cd7d4d5776eb8bfde62948da2edddb
revert revert 00929c92d1e9cad7c21037202563d40d123346ef

revert Merge pull request 'feature/leave/admin/approval - added admin,manager leave approval' (#18) from feature/leave/admin/approval into main

Reviewed-on: #18
2026-07-22 10:33:16 +05:30
7535624829 revert 00929c92d1e9cad7c21037202563d40d123346ef
revert Merge pull request 'feature/leave/admin/approval - added admin,manager leave approval' (#18) from feature/leave/admin/approval into main

Reviewed-on: #18
2026-07-20 12:03:40 +05:30
156 changed files with 2979 additions and 5691 deletions

View File

@ -1,27 +0,0 @@
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

6
.gitignore vendored
View File

@ -88,9 +88,3 @@ test-results/
# Misc # Misc
# ===================== # =====================
*.tgz *.tgz
# Playwright
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/

View File

@ -1,4 +1,4 @@
version: "3.8" version: '3.8'
services: services:
hrms-frontend: hrms-frontend:

343
package-lock.json generated
View File

@ -14,7 +14,6 @@
"@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",
@ -28,7 +27,6 @@
}, },
"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",
@ -1076,22 +1074,6 @@
"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",
@ -2142,18 +2124,6 @@
"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",
@ -2171,12 +2141,6 @@
"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",
@ -2214,18 +2178,6 @@
"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",
@ -2317,19 +2269,6 @@
"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",
@ -2369,18 +2308,6 @@
"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",
@ -2620,15 +2547,6 @@
"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",
@ -2649,20 +2567,6 @@
"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",
@ -2692,15 +2596,6 @@
"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",
@ -2710,33 +2605,6 @@
"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",
@ -3061,42 +2929,6 @@
"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",
@ -3144,43 +2976,6 @@
"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",
@ -3207,51 +3002,12 @@
"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",
@ -3296,19 +3052,6 @@
"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",
@ -3823,36 +3566,6 @@
"@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",
@ -4052,53 +3765,6 @@
"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",
@ -4173,15 +3839,6 @@
"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",

View File

@ -16,7 +16,6 @@
"@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",
@ -30,7 +29,6 @@
}, },
"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",

View File

@ -1,79 +0,0 @@
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,
// },
});

View File

@ -1,26 +0,0 @@
[
{
"id": "SA001",
"employeeId": "ADM001",
"employeeName": "Alice Williams",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-10",
"fromDate": "2026-08-01",
"toDate": "2026-08-01",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival observance."
},
{
"id": "SA002",
"employeeId": "ADM002",
"employeeName": "Bob Smith",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-12",
"fromDate": "2026-08-05",
"toDate": "2026-08-06",
"leaveDays": 2,
"status": "Pending",
"reason": "Personal work."
}
]

View File

@ -1,26 +0,0 @@
[
{
"id": "AA001",
"employeeId": "MGR001",
"employeeName": "Michael Scott",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-15",
"fromDate": "2026-07-25",
"toDate": "2026-07-26",
"leaveDays": 2,
"status": "Pending",
"reason": "Family trip."
},
{
"id": "AA002",
"employeeId": "MGR002",
"employeeName": "Jane Doe",
"leaveType": "Sick Leave",
"applicationDate": "2026-07-16",
"fromDate": "2026-07-22",
"toDate": "2026-07-22",
"leaveDays": 1,
"status": "Pending",
"reason": "Fever."
}
]

View File

@ -1,242 +1,22 @@
[ [
{ { "id": "MA005", "employeeId": "EMP005", "employeeName": "Michael Brown", "leaveType": "Casual Leave", "applicationDate": "2026-07-16", "fromDate": "2026-07-28", "toDate": "2026-07-29", "leaveDays": 2, "status": "Pending", "reason": "Attending a family wedding." },
"id": "MA005", { "id": "MA006", "employeeId": "EMP006", "employeeName": "Emily Davis", "leaveType": "Optional Holiday", "applicationDate": "2026-07-17", "fromDate": "2026-08-01", "toDate": "2026-08-01", "leaveDays": 1, "status": "Pending", "reason": "Festival celebration." },
"employeeId": "EMP005", { "id": "MA007", "employeeId": "EMP007", "employeeName": "Chris Wilson", "leaveType": "Casual Leave", "applicationDate": "2026-07-18", "fromDate": "2026-08-03", "toDate": "2026-08-04", "leaveDays": 2, "status": "Approved", "reason": "Personal commitments." },
"employeeName": "Michael Brown", { "id": "MA008", "employeeId": "EMP008", "employeeName": "Sophia Johnson", "leaveType": "Optional Holiday", "applicationDate": "2026-07-19", "fromDate": "2026-08-05", "toDate": "2026-08-05", "leaveDays": 1, "status": "Rejected", "reason": "Festival observance." },
"leaveType": "Casual Leave", { "id": "MA009", "employeeId": "EMP009", "employeeName": "Daniel Lee", "leaveType": "Casual Leave", "applicationDate": "2026-07-20", "fromDate": "2026-08-06", "toDate": "2026-08-08", "leaveDays": 3, "status": "Pending", "reason": "Family emergency." },
"applicationDate": "2026-07-16", { "id": "MA010", "employeeId": "EMP010", "employeeName": "Olivia Martin", "leaveType": "Optional Holiday", "applicationDate": "2026-07-21", "fromDate": "2026-08-10", "toDate": "2026-08-10", "leaveDays": 1, "status": "Approved", "reason": "Cultural celebration." },
"fromDate": "2026-07-28", { "id": "MA011", "employeeId": "EMP011", "employeeName": "James Taylor", "leaveType": "Casual Leave", "applicationDate": "2026-07-22", "fromDate": "2026-08-12", "toDate": "2026-08-13", "leaveDays": 2, "status": "Pending", "reason": "Home relocation." },
"toDate": "2026-07-29", { "id": "MA012", "employeeId": "EMP012", "employeeName": "Emma Garcia", "leaveType": "Optional Holiday", "applicationDate": "2026-07-23", "fromDate": "2026-08-14", "toDate": "2026-08-14", "leaveDays": 1, "status": "Pending", "reason": "Religious observance." },
"leaveDays": 2, { "id": "MA013", "employeeId": "EMP013", "employeeName": "William Harris", "leaveType": "Casual Leave", "applicationDate": "2026-07-24", "fromDate": "2026-08-17", "toDate": "2026-08-18", "leaveDays": 2, "status": "Approved", "reason": "Health checkup." },
"status": "Pending", { "id": "MA014", "employeeId": "EMP014", "employeeName": "Ava Clark", "leaveType": "Optional Holiday", "applicationDate": "2026-07-25", "fromDate": "2026-08-19", "toDate": "2026-08-19", "leaveDays": 1, "status": "Pending", "reason": "Community event." },
"reason": "Attending a family wedding." { "id": "MA015", "employeeId": "EMP015", "employeeName": "Benjamin Lewis", "leaveType": "Casual Leave", "applicationDate": "2026-07-26", "fromDate": "2026-08-20", "toDate": "2026-08-22", "leaveDays": 3, "status": "Rejected", "reason": "Vacation with family." },
}, { "id": "MA016", "employeeId": "EMP016", "employeeName": "Mia Walker", "leaveType": "Optional Holiday", "applicationDate": "2026-07-27", "fromDate": "2026-08-24", "toDate": "2026-08-24", "leaveDays": 1, "status": "Approved", "reason": "Festival celebration." },
{ { "id": "MA017", "employeeId": "EMP017", "employeeName": "Ethan Hall", "leaveType": "Casual Leave", "applicationDate": "2026-07-28", "fromDate": "2026-08-25", "toDate": "2026-08-26", "leaveDays": 2, "status": "Pending", "reason": "Personal travel." },
"id": "MA006", { "id": "MA018", "employeeId": "EMP018", "employeeName": "Charlotte Allen", "leaveType": "Optional Holiday", "applicationDate": "2026-07-29", "fromDate": "2026-08-27", "toDate": "2026-08-27", "leaveDays": 1, "status": "Pending", "reason": "Special occasion." },
"employeeId": "EMP006", { "id": "MA019", "employeeId": "EMP019", "employeeName": "Alexander Young", "leaveType": "Casual Leave", "applicationDate": "2026-07-30", "fromDate": "2026-08-28", "toDate": "2026-08-30", "leaveDays": 3, "status": "Approved", "reason": "Family gathering." },
"employeeName": "Emily Davis", { "id": "MA020", "employeeId": "EMP020", "employeeName": "Amelia King", "leaveType": "Optional Holiday", "applicationDate": "2026-07-31", "fromDate": "2026-09-01", "toDate": "2026-09-01", "leaveDays": 1, "status": "Pending", "reason": "Festival leave." },
"leaveType": "Optional Holiday", { "id": "MA021", "employeeId": "EMP021", "employeeName": "Matthew Scott", "leaveType": "Casual Leave", "applicationDate": "2026-08-01", "fromDate": "2026-09-02", "toDate": "2026-09-03", "leaveDays": 2, "status": "Rejected", "reason": "Personal errands." },
"applicationDate": "2026-07-17", { "id": "MA022", "employeeId": "EMP022", "employeeName": "Harper Green", "leaveType": "Optional Holiday", "applicationDate": "2026-08-02", "fromDate": "2026-09-04", "toDate": "2026-09-04", "leaveDays": 1, "status": "Approved", "reason": "Religious holiday." },
"fromDate": "2026-08-01", { "id": "MA023", "employeeId": "EMP023", "employeeName": "David Baker", "leaveType": "Casual Leave", "applicationDate": "2026-08-03", "fromDate": "2026-09-07", "toDate": "2026-09-08", "leaveDays": 2, "status": "Pending", "reason": "Attending a seminar." },
"toDate": "2026-08-01", { "id": "MA024", "employeeId": "EMP024", "employeeName": "Ella Adams", "leaveType": "Optional Holiday", "applicationDate": "2026-08-04", "fromDate": "2026-09-09", "toDate": "2026-09-09", "leaveDays": 1, "status": "Pending", "reason": "Festival observance." }
"leaveDays": 1,
"status": "Pending",
"reason": "Festival celebration."
},
{
"id": "MA007",
"employeeId": "EMP007",
"employeeName": "Chris Wilson",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-18",
"fromDate": "2026-08-03",
"toDate": "2026-08-04",
"leaveDays": 2,
"status": "Approved",
"reason": "Personal commitments."
},
{
"id": "MA008",
"employeeId": "EMP008",
"employeeName": "Sophia Johnson",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-19",
"fromDate": "2026-08-05",
"toDate": "2026-08-05",
"leaveDays": 1,
"status": "Rejected",
"reason": "Festival observance."
},
{
"id": "MA009",
"employeeId": "EMP009",
"employeeName": "Daniel Lee",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-20",
"fromDate": "2026-08-06",
"toDate": "2026-08-08",
"leaveDays": 3,
"status": "Pending",
"reason": "Family emergency."
},
{
"id": "MA010",
"employeeId": "EMP010",
"employeeName": "Olivia Martin",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-21",
"fromDate": "2026-08-10",
"toDate": "2026-08-10",
"leaveDays": 1,
"status": "Approved",
"reason": "Cultural celebration."
},
{
"id": "MA011",
"employeeId": "EMP011",
"employeeName": "James Taylor",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-22",
"fromDate": "2026-08-12",
"toDate": "2026-08-13",
"leaveDays": 2,
"status": "Pending",
"reason": "Home relocation."
},
{
"id": "MA012",
"employeeId": "EMP012",
"employeeName": "Emma Garcia",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-23",
"fromDate": "2026-08-14",
"toDate": "2026-08-14",
"leaveDays": 1,
"status": "Pending",
"reason": "Religious observance."
},
{
"id": "MA013",
"employeeId": "EMP013",
"employeeName": "William Harris",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-24",
"fromDate": "2026-08-17",
"toDate": "2026-08-18",
"leaveDays": 2,
"status": "Approved",
"reason": "Health checkup."
},
{
"id": "MA014",
"employeeId": "EMP014",
"employeeName": "Ava Clark",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-25",
"fromDate": "2026-08-19",
"toDate": "2026-08-19",
"leaveDays": 1,
"status": "Pending",
"reason": "Community event."
},
{
"id": "MA015",
"employeeId": "EMP015",
"employeeName": "Benjamin Lewis",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-26",
"fromDate": "2026-08-20",
"toDate": "2026-08-22",
"leaveDays": 3,
"status": "Rejected",
"reason": "Vacation with family."
},
{
"id": "MA016",
"employeeId": "EMP016",
"employeeName": "Mia Walker",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-27",
"fromDate": "2026-08-24",
"toDate": "2026-08-24",
"leaveDays": 1,
"status": "Approved",
"reason": "Festival celebration."
},
{
"id": "MA017",
"employeeId": "EMP017",
"employeeName": "Ethan Hall",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-28",
"fromDate": "2026-08-25",
"toDate": "2026-08-26",
"leaveDays": 2,
"status": "Pending",
"reason": "Personal travel."
},
{
"id": "MA018",
"employeeId": "EMP018",
"employeeName": "Charlotte Allen",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-29",
"fromDate": "2026-08-27",
"toDate": "2026-08-27",
"leaveDays": 1,
"status": "Pending",
"reason": "Special occasion."
},
{
"id": "MA019",
"employeeId": "EMP019",
"employeeName": "Alexander Young",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-30",
"fromDate": "2026-08-28",
"toDate": "2026-08-30",
"leaveDays": 3,
"status": "Approved",
"reason": "Family gathering."
},
{
"id": "MA020",
"employeeId": "EMP020",
"employeeName": "Amelia King",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-31",
"fromDate": "2026-09-01",
"toDate": "2026-09-01",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival leave."
},
{
"id": "MA021",
"employeeId": "EMP021",
"employeeName": "Matthew Scott",
"leaveType": "Casual Leave",
"applicationDate": "2026-08-01",
"fromDate": "2026-09-02",
"toDate": "2026-09-03",
"leaveDays": 2,
"status": "Rejected",
"reason": "Personal errands."
},
{
"id": "MA022",
"employeeId": "EMP022",
"employeeName": "Harper Green",
"leaveType": "Optional Holiday",
"applicationDate": "2026-08-02",
"fromDate": "2026-09-04",
"toDate": "2026-09-04",
"leaveDays": 1,
"status": "Approved",
"reason": "Religious holiday."
},
{
"id": "MA023",
"employeeId": "EMP023",
"employeeName": "David Baker",
"leaveType": "Casual Leave",
"applicationDate": "2026-08-03",
"fromDate": "2026-09-07",
"toDate": "2026-09-08",
"leaveDays": 2,
"status": "Pending",
"reason": "Attending a seminar."
},
{
"id": "MA024",
"employeeId": "EMP024",
"employeeName": "Ella Adams",
"leaveType": "Optional Holiday",
"applicationDate": "2026-08-04",
"fromDate": "2026-09-09",
"toDate": "2026-09-09",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival observance."
}
] ]

View File

@ -0,0 +1,6 @@
[
{ "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" }
]

View File

@ -0,0 +1,5 @@
[
{ "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" }
]

View File

@ -0,0 +1,91 @@
[
{ "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" }
]

View File

@ -0,0 +1,22 @@
[
{ "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" }
]

View File

@ -0,0 +1,8 @@
[
{ "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" }
]

View File

@ -0,0 +1,5 @@
[
{ "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" }
]

View File

@ -0,0 +1,4 @@
[
{ "id": "A001", "leaveType": "Casual Leave", "applicationDate": "2026-05-10", "fromDate": "2026-06-10", "toDate": "2026-06-11", "leaveDays": 2, "status": "Approved", "reason": "Medical appointment." },
{ "id": "A002", "leaveType": "Casual Leave", "applicationDate": "2026-06-20", "fromDate": "2026-07-01", "toDate": "2026-07-02", "leaveDays": 2, "status": "Withdrawn", "reason": "Changed plans." }
]

View File

@ -0,0 +1,4 @@
[
{ "leaveType": "Casual Leave", "credited": 15, "utilized": 5, "lapsed": 0, "balance": 10 },
{ "leaveType": "Optional Holiday", "credited": 4, "utilized": 0, "lapsed": 0, "balance": 4 }
]

View File

@ -1,12 +0,0 @@
[
{
"id": "S001",
"leaveType": "Optional Holiday",
"applicationDate": "2026-06-22",
"fromDate": "2026-07-21",
"toDate": "2026-07-21",
"leaveDays": 1,
"status": "Pending",
"reason": "Festival."
}
]

View File

@ -1,16 +0,0 @@
[
{
"leaveType": "Casual Leave",
"credited": 20,
"utilized": 2,
"lapsed": 0,
"balance": 18
},
{
"leaveType": "Optional Holiday",
"credited": 4,
"utilized": 2,
"lapsed": 0,
"balance": 2
}
]

View File

@ -1,82 +1,10 @@
[ [
{ { "id": "L001", "leaveType": "Casual Leave", "applicationDate": "2026-07-10", "fromDate": "2026-07-04", "toDate": "2026-07-05", "leaveDays": 2, "status": "Approved", "reason": "Family function out of town." },
"id": "L001", { "id": "L002", "leaveType": "Optional Holiday", "applicationDate": "2026-07-15", "fromDate": "2026-07-14", "toDate": "2026-07-14", "leaveDays": 1, "status": "Pending", "reason": "Personal work." },
"leaveType": "Casual Leave", { "id": "L003", "leaveType": "Casual Leave", "applicationDate": "2026-07-20", "fromDate": "2026-06-10", "toDate": "2026-06-11", "leaveDays": 2, "status": "Withdrawn", "reason": "Changed plans." },
"applicationDate": "2026-07-10", { "id": "L004", "leaveType": "Casual Leave", "applicationDate": "2026-07-22", "fromDate": "2026-06-20", "toDate": "2026-06-20", "leaveDays": 1, "status": "Approved", "reason": "Medical appointment." },
"fromDate": "2026-07-04", { "id": "L005", "leaveType": "Optional Holiday", "applicationDate": "2026-07-18", "fromDate": "2026-07-01", "toDate": "2026-07-02", "leaveDays": 2, "status": "Pending", "reason": "Religious festival." },
"toDate": "2026-07-05", { "id": "L006", "leaveType": "Casual Leave", "applicationDate": "2026-07-02", "fromDate": "2026-05-15", "toDate": "2026-05-16", "leaveDays": 2, "status": "Approved", "reason": "Friend's wedding." },
"leaveDays": 2, { "id": "L007", "leaveType": "Casual Leave", "applicationDate": "2026-07-10", "fromDate": "2026-04-25", "toDate": "2026-04-26", "leaveDays": 2, "status": "Approved", "reason": "House shifting." },
"status": "Approved", { "id": "L008", "leaveType": "Optional Holiday", "applicationDate": "2026-07-22", "fromDate": "2026-07-21", "toDate": "2026-07-21", "leaveDays": 1, "status": "Pending", "reason": "Personal work." }
"reason": "Family function out of town."
},
{
"id": "L002",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-15",
"fromDate": "2026-07-14",
"toDate": "2026-07-14",
"leaveDays": 1,
"status": "Pending",
"reason": "Personal work."
},
{
"id": "L003",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-20",
"fromDate": "2026-06-10",
"toDate": "2026-06-11",
"leaveDays": 2,
"status": "Withdrawn",
"reason": "Changed plans."
},
{
"id": "L004",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-22",
"fromDate": "2026-06-20",
"toDate": "2026-06-20",
"leaveDays": 1,
"status": "Approved",
"reason": "Medical appointment."
},
{
"id": "L005",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-18",
"fromDate": "2026-07-01",
"toDate": "2026-07-02",
"leaveDays": 2,
"status": "Pending",
"reason": "Religious festival."
},
{
"id": "L006",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-02",
"fromDate": "2026-05-15",
"toDate": "2026-05-16",
"leaveDays": 2,
"status": "Approved",
"reason": "Friend's wedding."
},
{
"id": "L007",
"leaveType": "Casual Leave",
"applicationDate": "2026-07-10",
"fromDate": "2026-04-25",
"toDate": "2026-04-26",
"leaveDays": 2,
"status": "Approved",
"reason": "House shifting."
},
{
"id": "L008",
"leaveType": "Optional Holiday",
"applicationDate": "2026-07-22",
"fromDate": "2026-07-21",
"toDate": "2026-07-21",
"leaveDays": 1,
"status": "Pending",
"reason": "Personal work."
}
] ]

View File

@ -1,16 +1,4 @@
[ [
{ { "leaveType": "Casual Leave", "credited": 10, "utilized": 8, "lapsed": 0, "balance": 2 },
"leaveType": "Casual Leave", { "leaveType": "Optional Holiday", "credited": 4, "utilized": 3, "lapsed": 0, "balance": 1 }
"credited": 10,
"utilized": 8,
"lapsed": 0,
"balance": 2
},
{
"leaveType": "Optional Holiday",
"credited": 4,
"utilized": 3,
"lapsed": 0,
"balance": 1
}
] ]

View File

@ -1,22 +0,0 @@
[
{
"id": "A001",
"leaveType": "Casual Leave",
"applicationDate": "2026-05-10",
"fromDate": "2026-06-10",
"toDate": "2026-06-11",
"leaveDays": 2,
"status": "Approved",
"reason": "Medical appointment."
},
{
"id": "A002",
"leaveType": "Casual Leave",
"applicationDate": "2026-06-20",
"fromDate": "2026-07-01",
"toDate": "2026-07-02",
"leaveDays": 2,
"status": "Withdrawn",
"reason": "Changed plans."
}
]

View File

@ -1,16 +0,0 @@
[
{
"leaveType": "Casual Leave",
"credited": 15,
"utilized": 5,
"lapsed": 0,
"balance": 10
},
{
"leaveType": "Optional Holiday",
"credited": 4,
"utilized": 0,
"lapsed": 0,
"balance": 4
}
]

View File

@ -1,134 +1,14 @@
[ [
{ { "id": "LA001", "employeeId": "EMP001", "employeeName": "Alice Williams", "designation": "Software Engineer", "department": "IT", "managerName": "Bob Smith", "leaveType": "Casual Leave", "leaveDays": 2, "status": "Approved" },
"id": "LA001", { "id": "LA002", "employeeId": "EMP002", "employeeName": "Bob Smith", "designation": "Senior Software Engineer", "department": "IT", "managerName": "Charlie Brown", "leaveType": "Sick Leave", "leaveDays": 1, "status": "Pending" },
"employeeId": "EMP001", { "id": "LA003", "employeeId": "EMP003", "employeeName": "Charlie Brown", "designation": "Trainer", "department": "Training", "managerName": "Diana Prince", "leaveType": "Optional Holiday", "leaveDays": 1, "status": "Rejected" },
"employeeName": "Alice Williams", { "id": "LA004", "employeeId": "EMP004", "employeeName": "Diana Prince", "designation": "Data Analyst", "department": "Analytics", "managerName": "Evan Wright", "leaveType": "Casual Leave", "leaveDays": 3, "status": "Approved" },
"designation": "Software Engineer", { "id": "LA005", "employeeId": "EMP005", "employeeName": "Evan Wright", "designation": "QA Tester", "department": "Software Dev", "managerName": "Fiona Gallagher", "leaveType": "Sick Leave", "leaveDays": 2, "status": "Pending" },
"department": "IT", { "id": "LA006", "employeeId": "EMP006", "employeeName": "Fiona Gallagher", "designation": "HR Executive", "department": "HR", "managerName": "George Costanza", "leaveType": "Casual Leave", "leaveDays": 1, "status": "Withdrawn" },
"managerName": "Bob Smith", { "id": "LA007", "employeeId": "EMP007", "employeeName": "George Costanza", "designation": "Placement Officer", "department": "Placement", "managerName": "Hannah Abbott", "leaveType": "Optional Holiday", "leaveDays": 1, "status": "Approved" },
"leaveType": "Casual Leave", { "id": "LA008", "employeeId": "EMP008", "employeeName": "Hannah Abbott", "designation": "Support Engineer", "department": "Support", "managerName": "Ian Malcolm", "leaveType": "Sick Leave", "leaveDays": 4, "status": "Pending" },
"leaveDays": 2, { "id": "LA009", "employeeId": "EMP009", "employeeName": "Ian Malcolm", "designation": "Lead Trainer", "department": "Training", "managerName": "Jane Porter", "leaveType": "Casual Leave", "leaveDays": 2, "status": "Approved" },
"status": "Approved" { "id": "LA010", "employeeId": "EMP010", "employeeName": "Jane Porter", "designation": "Project Manager", "department": "Software Dev", "managerName": "Kevin Hart", "leaveType": "Optional Holiday", "leaveDays": 1, "status": "Rejected" },
}, { "id": "LA011", "employeeId": "EMP011", "employeeName": "Kevin Hart", "designation": "Software Engineer", "department": "IT", "managerName": "Laura Croft", "leaveType": "Sick Leave", "leaveDays": 1, "status": "Pending" },
{ { "id": "LA012", "employeeId": "EMP012", "employeeName": "Laura Croft", "designation": "Data Analyst", "department": "Analytics", "managerName": "Alice Williams", "leaveType": "Casual Leave", "leaveDays": 2, "status": "Approved" }
"id": "LA002",
"employeeId": "EMP002",
"employeeName": "Bob Smith",
"designation": "Senior Software Engineer",
"department": "IT",
"managerName": "Charlie Brown",
"leaveType": "Sick Leave",
"leaveDays": 1,
"status": "Pending"
},
{
"id": "LA003",
"employeeId": "EMP003",
"employeeName": "Charlie Brown",
"designation": "Trainer",
"department": "Training",
"managerName": "Diana Prince",
"leaveType": "Optional Holiday",
"leaveDays": 1,
"status": "Rejected"
},
{
"id": "LA004",
"employeeId": "EMP004",
"employeeName": "Diana Prince",
"designation": "Data Analyst",
"department": "Analytics",
"managerName": "Evan Wright",
"leaveType": "Casual Leave",
"leaveDays": 3,
"status": "Approved"
},
{
"id": "LA005",
"employeeId": "EMP005",
"employeeName": "Evan Wright",
"designation": "QA Tester",
"department": "Software Dev",
"managerName": "Fiona Gallagher",
"leaveType": "Sick Leave",
"leaveDays": 2,
"status": "Pending"
},
{
"id": "LA006",
"employeeId": "EMP006",
"employeeName": "Fiona Gallagher",
"designation": "HR Executive",
"department": "HR",
"managerName": "George Costanza",
"leaveType": "Casual Leave",
"leaveDays": 1,
"status": "Withdrawn"
},
{
"id": "LA007",
"employeeId": "EMP007",
"employeeName": "George Costanza",
"designation": "Placement Officer",
"department": "Placement",
"managerName": "Hannah Abbott",
"leaveType": "Optional Holiday",
"leaveDays": 1,
"status": "Approved"
},
{
"id": "LA008",
"employeeId": "EMP008",
"employeeName": "Hannah Abbott",
"designation": "Support Engineer",
"department": "Support",
"managerName": "Ian Malcolm",
"leaveType": "Sick Leave",
"leaveDays": 4,
"status": "Pending"
},
{
"id": "LA009",
"employeeId": "EMP009",
"employeeName": "Ian Malcolm",
"designation": "Lead Trainer",
"department": "Training",
"managerName": "Jane Porter",
"leaveType": "Casual Leave",
"leaveDays": 2,
"status": "Approved"
},
{
"id": "LA010",
"employeeId": "EMP010",
"employeeName": "Jane Porter",
"designation": "Project Manager",
"department": "Software Dev",
"managerName": "Kevin Hart",
"leaveType": "Optional Holiday",
"leaveDays": 1,
"status": "Rejected"
},
{
"id": "LA011",
"employeeId": "EMP011",
"employeeName": "Kevin Hart",
"designation": "Software Engineer",
"department": "IT",
"managerName": "Laura Croft",
"leaveType": "Sick Leave",
"leaveDays": 1,
"status": "Pending"
},
{
"id": "LA012",
"employeeId": "EMP012",
"employeeName": "Laura Croft",
"designation": "Data Analyst",
"department": "Analytics",
"managerName": "Alice Williams",
"leaveType": "Casual Leave",
"leaveDays": 2,
"status": "Approved"
}
] ]

View File

@ -1,52 +0,0 @@
[
{
"employeeId": "EMP001",
"employeeName": "Alice Williams",
"month": 7,
"year": 2026,
"openingBalance": 12,
"closingBalance": 10,
"casualLeave": 8,
"optionalLeave": 2
},
{
"employeeId": "EMP002",
"employeeName": "Bob Smith",
"month": 7,
"year": 2026,
"openingBalance": 15,
"closingBalance": 14,
"casualLeave": 10,
"optionalLeave": 4
},
{
"employeeId": "EMP001",
"employeeName": "Alice Williams",
"month": 6,
"year": 2026,
"openingBalance": 15,
"closingBalance": 12,
"casualLeave": 10,
"optionalLeave": 2
},
{
"employeeId": "EMP003",
"employeeName": "Charlie Brown",
"month": 7,
"year": 2026,
"openingBalance": 10,
"closingBalance": 8,
"casualLeave": 6,
"optionalLeave": 2
},
{
"employeeId": "EMP004",
"employeeName": "Diana Prince",
"month": 7,
"year": 2026,
"openingBalance": 12,
"closingBalance": 12,
"casualLeave": 12,
"optionalLeave": 0
}
]

View File

@ -0,0 +1,7 @@
[
{ "employeeId": "EMP001", "employeeName": "Alice Williams", "month": 7, "year": 2026, "openingBalance": 12, "closingBalance": 10, "casualLeave": 8, "optionalLeave": 2 },
{ "employeeId": "EMP002", "employeeName": "Bob Smith", "month": 7, "year": 2026, "openingBalance": 15, "closingBalance": 14, "casualLeave": 10, "optionalLeave": 4 },
{ "employeeId": "EMP001", "employeeName": "Alice Williams", "month": 6, "year": 2026, "openingBalance": 15, "closingBalance": 12, "casualLeave": 10, "optionalLeave": 2 },
{ "employeeId": "EMP003", "employeeName": "Charlie Brown", "month": 7, "year": 2026, "openingBalance": 10, "closingBalance": 8, "casualLeave": 6, "optionalLeave": 2 },
{ "employeeId": "EMP004", "employeeName": "Diana Prince", "month": 7, "year": 2026, "openingBalance": 12, "closingBalance": 12, "casualLeave": 12, "optionalLeave": 0 }
]

View File

@ -1,22 +1,4 @@
[ [
{ { "id": "M001", "leaveType": "Casual Leave", "applicationDate": "2026-06-12", "fromDate": "2026-07-04", "toDate": "2026-07-05", "leaveDays": 2, "status": "Approved", "reason": "Family function." },
"id": "M001", { "id": "M002", "leaveType": "Optional Holiday", "applicationDate": "2026-06-18", "fromDate": "2026-07-14", "toDate": "2026-07-14", "leaveDays": 1, "status": "Pending", "reason": "Personal work." }
"leaveType": "Casual Leave",
"applicationDate": "2026-06-12",
"fromDate": "2026-07-04",
"toDate": "2026-07-05",
"leaveDays": 2,
"status": "Approved",
"reason": "Family function."
},
{
"id": "M002",
"leaveType": "Optional Holiday",
"applicationDate": "2026-06-18",
"fromDate": "2026-07-14",
"toDate": "2026-07-14",
"leaveDays": 1,
"status": "Pending",
"reason": "Personal work."
}
] ]

View File

@ -1,16 +1,4 @@
[ [
{ { "leaveType": "Casual Leave", "credited": 12, "utilized": 4, "lapsed": 0, "balance": 8 },
"leaveType": "Casual Leave", { "leaveType": "Optional Holiday", "credited": 4, "utilized": 1, "lapsed": 0, "balance": 3 }
"credited": 12,
"utilized": 4,
"lapsed": 0,
"balance": 8
},
{
"leaveType": "Optional Holiday",
"credited": 4,
"utilized": 1,
"lapsed": 0,
"balance": 3
}
] ]

View File

@ -0,0 +1,3 @@
[
{ "id": "S001", "leaveType": "Optional Holiday", "applicationDate": "2026-06-22", "fromDate": "2026-07-21", "toDate": "2026-07-21", "leaveDays": 1, "status": "Pending", "reason": "Festival." }
]

View File

@ -0,0 +1,4 @@
[
{ "leaveType": "Casual Leave", "credited": 20, "utilized": 2, "lapsed": 0, "balance": 18 },
{ "leaveType": "Optional Holiday", "credited": 4, "utilized": 2, "lapsed": 0, "balance": 2 }
]

View File

@ -0,0 +1,14 @@
[
{ "id": "EMP001", "name": "Alice Williams", "designationName": "Software Engineer", "deptName": "IT", "branchName": "Bengaluru", "companyName": "CLRI", "status": "Active", "isAdmin": true },
{ "id": "EMP002", "name": "Bob Smith", "designationName": "Senior Software Engineer", "deptName": "IT", "branchName": "Bengaluru", "companyName": "CLRI", "status": "Active", "isAdmin": false },
{ "id": "EMP003", "name": "Charlie Brown", "designationName": "Trainer", "deptName": "Training", "branchName": "Pune", "companyName": "CLRI", "status": "Disabled", "isAdmin": false },
{ "id": "EMP004", "name": "Diana Prince", "designationName": "Data Analyst", "deptName": "Analytics", "branchName": "Bengaluru", "companyName": "CLRI", "status": "Active", "isAdmin": true },
{ "id": "EMP005", "name": "Evan Wright", "designationName": "QA Tester", "deptName": "Software Dev", "branchName": "Mumbai", "companyName": "TechNova Solutions", "status": "Active", "isAdmin": false },
{ "id": "EMP006", "name": "Fiona Gallagher", "designationName": "HR Executive", "deptName": "HR", "branchName": "Chennai", "companyName": "Infotech Ltd", "status": "Active", "isAdmin": false },
{ "id": "EMP007", "name": "George Costanza", "designationName": "Placement Officer", "deptName": "Placement", "branchName": "Bengaluru", "companyName": "CLRI", "status": "Active", "isAdmin": true },
{ "id": "EMP008", "name": "Hannah Abbott", "designationName": "Support Engineer", "deptName": "Support", "branchName": "Ahmedabad", "companyName": "Global Systems", "status": "Disabled", "isAdmin": false },
{ "id": "EMP009", "name": "Ian Malcolm", "designationName": "Lead Trainer", "deptName": "Training", "branchName": "Pune", "companyName": "CLRI", "status": "Active", "isAdmin": false },
{ "id": "EMP010", "name": "Jane Porter", "designationName": "Project Manager", "deptName": "Software Dev", "branchName": "Mumbai", "companyName": "TechNova Solutions", "status": "Active", "isAdmin": true },
{ "id": "EMP011", "name": "Kevin Hart", "designationName": "Software Engineer", "deptName": "IT", "branchName": "Bengaluru", "companyName": "CLRI", "status": "Active", "isAdmin": false },
{ "id": "EMP012", "name": "Laura Croft", "designationName": "Data Analyst", "deptName": "Analytics", "branchName": "Bengaluru", "companyName": "CLRI", "status": "Active", "isAdmin": false }
]

View File

@ -0,0 +1,14 @@
[
{ "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" }
]

View File

@ -0,0 +1,14 @@
[
{ "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" }
]

View File

@ -0,0 +1,14 @@
[
{ "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" }
]

View File

@ -0,0 +1,14 @@
[
{ "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" }
]

View File

@ -0,0 +1,14 @@
[
{ "id": "EMP001", "name": "Alice Williams", "designationId": "DSG001", "designationName": "Software Engineer", "deptId": "DEP001", "deptName": "IT", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
{ "id": "EMP002", "name": "Bob Smith", "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", "designationId": "DSG004", "designationName": "Trainer", "deptId": "DEP002", "deptName": "Training", "branchId": "BR002", "branchName": "Pune", "companyId": "CMP001", "companyName": "CLRI", "status": "Disabled" },
{ "id": "EMP004", "name": "Diana Prince", "designationId": "DSG006", "designationName": "Data Analyst", "deptId": "DEP003", "deptName": "Analytics", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
{ "id": "EMP005", "name": "Evan Wright", "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", "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", "designationId": "DSG007", "designationName": "Placement Officer", "deptId": "DEP004", "deptName": "Placement", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
{ "id": "EMP008", "name": "Hannah Abbott", "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", "designationId": "DSG005", "designationName": "Lead Trainer", "deptId": "DEP002", "deptName": "Training", "branchId": "BR002", "branchName": "Pune", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
{ "id": "EMP010", "name": "Jane Porter", "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", "designationId": "DSG001", "designationName": "Software Engineer", "deptId": "DEP001", "deptName": "IT", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" },
{ "id": "EMP012", "name": "Laura Croft", "designationId": "DSG006", "designationName": "Data Analyst", "deptId": "DEP003", "deptName": "Analytics", "branchId": "BR001", "branchName": "Bengaluru", "companyId": "CMP001", "companyName": "CLRI", "status": "Active" }
]

View File

@ -1,134 +0,0 @@
[
{
"id": "EMP001",
"name": "Alice Williams",
"email": "alice@example.com",
"designationName": "Software Engineer",
"deptName": "IT",
"branchName": "Bengaluru",
"companyName": "CLRI",
"status": "Active",
"isHRManager": true
},
{
"id": "EMP002",
"name": "Bob Smith",
"email": "bob@example.com",
"designationName": "Senior Software Engineer",
"deptName": "IT",
"branchName": "Bengaluru",
"companyName": "CLRI",
"status": "Active",
"isHRManager": false
},
{
"id": "EMP003",
"name": "Charlie Brown",
"email": "charlie@example.com",
"designationName": "Trainer",
"deptName": "Training",
"branchName": "Pune",
"companyName": "CLRI",
"status": "Disabled",
"isHRManager": false
},
{
"id": "EMP004",
"name": "Diana Prince",
"email": "diana@example.com",
"designationName": "Data Analyst",
"deptName": "Analytics",
"branchName": "Bengaluru",
"companyName": "CLRI",
"status": "Active",
"isHRManager": true
},
{
"id": "EMP005",
"name": "Evan Wright",
"email": "evan@example.com",
"designationName": "QA Tester",
"deptName": "Software Dev",
"branchName": "Mumbai",
"companyName": "TechNova Solutions",
"status": "Active",
"isHRManager": false
},
{
"id": "EMP006",
"name": "Fiona Gallagher",
"email": "fiona@example.com",
"designationName": "HR Executive",
"deptName": "HR",
"branchName": "Chennai",
"companyName": "Infotech Ltd",
"status": "Active",
"isHRManager": false
},
{
"id": "EMP007",
"name": "George Costanza",
"email": "george@example.com",
"designationName": "Placement Officer",
"deptName": "Placement",
"branchName": "Bengaluru",
"companyName": "CLRI",
"status": "Active",
"isHRManager": true
},
{
"id": "EMP008",
"name": "Hannah Abbott",
"email": "hannah@example.com",
"designationName": "Support Engineer",
"deptName": "Support",
"branchName": "Ahmedabad",
"companyName": "Global Systems",
"status": "Disabled",
"isHRManager": false
},
{
"id": "EMP009",
"name": "Ian Malcolm",
"email": "ian@example.com",
"designationName": "Lead Trainer",
"deptName": "Training",
"branchName": "Pune",
"companyName": "CLRI",
"status": "Active",
"isHRManager": false
},
{
"id": "EMP010",
"name": "Jane Porter",
"email": "jane@example.com",
"designationName": "Project Manager",
"deptName": "Software Dev",
"branchName": "Mumbai",
"companyName": "TechNova Solutions",
"status": "Active",
"isHRManager": true
},
{
"id": "EMP011",
"name": "Kevin Hart",
"email": "kevin@example.com",
"designationName": "Software Engineer",
"deptName": "IT",
"branchName": "Bengaluru",
"companyName": "CLRI",
"status": "Active",
"isHRManager": false
},
{
"id": "EMP012",
"name": "Laura Croft",
"email": "laura@example.com",
"designationName": "Data Analyst",
"deptName": "Analytics",
"branchName": "Bengaluru",
"companyName": "CLRI",
"status": "Active",
"isHRManager": false
}
]

View File

@ -1,74 +0,0 @@
[
{
"companyId": "CMP001",
"companyName": "CliniLaunch Research Institute - CLRI",
"policies": [
{
"id": "POL001",
"title": "Loss of Pay (LOP)",
"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."
},
{
"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",
"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",
"companyName": "Win in Life Academy - WILA",
"policies": [
{
"id": "POL001",
"title": "Loss of Pay (LOP)",
"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."
},
{
"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",
"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."
}
]
}
]

View File

@ -0,0 +1,7 @@
{
"totalCompanies": 1,
"totalBranches": 3,
"totalDepartments": 5,
"totalDesignations": 4,
"totalEmployees": 245
}

View File

@ -1,34 +1,6 @@
import axios from 'axios'; // src/api/client.ts
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)
);
// Generic GET request for any module
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}`);
@ -36,6 +8,7 @@ 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);

View File

@ -1,59 +1,32 @@
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';
// Fallback map for URL segments if dynamic lookup fails (e.g., for IDs in URL) // Map URL segments to readable names
const routeNameMap: Record<string, string> = { const routeNameMap: Record<string, string> = {
dashboard: 'Dashboard', dashboard: 'Dashboard',
attendance: 'Attendance Summary', attendance: 'Attendance',
leave: 'Leave Summary', leave: 'Leave',
approval: 'Approve Leaves', approval: 'Approval',
'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 Service', 'master-data': 'Master Data',
company: 'Manage Company', company: 'Manage Company',
branches: 'Manage Branches', branches: 'Manage Branches',
departments: 'Manage Departments', departments: 'Manage Departments',
'job-roles': 'Manage Job Roles', designations: 'Manage Designations',
employees: 'Manage Employees', employees: 'Manage Employees',
'manage-hrmanagers': 'Manage HR Managers', 'manage-admins': 'Manage Admins',
policies: 'Policy and Rules', policies: 'Policy and Rules',
applications: 'Leave Applications',
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);
// Generate a map of paths to breadcrumb trails based on the user's role sidebar config // If we are exactly at /dashboard, just show a static breadcrumb
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">
@ -63,36 +36,6 @@ 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">
@ -100,8 +43,11 @@ 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 (

View File

@ -3,16 +3,16 @@ import { Routes, Route, Navigate } from 'react-router-dom';
import { useIsFetching } from '@tanstack/react-query'; import { useIsFetching } from '@tanstack/react-query';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import Navbar from './Navbar'; import Navbar from './Navbar';
import Breadcrumbs from './Breadcrumbs'; import Breadcrumbs from './Breadcrumbs'; // <-- Import Breadcrumbs
import DashboardRouter from '../../features/dashboard/routes/DashboardRouter'; import DashboardRouter from '../../features/dashboard/routes/DashboardRouter';
import AttendanceRouter from '../../features/attendance/routes/AttendanceRouter'; import AttendanceRouter from '../../features/attendance/routes/AttendanceRouter';
import LeaveRouter from '../../features/leave/routes/LeaveRouter'; 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/masterData/routes/MasterDataRouter'; import MasterDataRouter from '../../features/employee-management/routes/MasterDataRouter';
import PolicyRouter from '../../features/masterData/routes/PolicyRouter'; import PolicyRouter from '../../features/dashboard/routes/PolicyRouter';
import ManageHRManagersRouter from '../../features/masterData/routes/ManageHRManagersRouter'; import ManageAdminsRouter from '../../features/employee-management/routes/ManageAdminsRouter';
export default function MainLayout() { export default function MainLayout() {
const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false);
@ -30,6 +30,7 @@ export default function MainLayout() {
)} )}
<main className="flex-1 overflow-y-auto p-6"> <main className="flex-1 overflow-y-auto p-6">
{/* Add Breadcrumbs here so it shows on every page */}
<Breadcrumbs /> <Breadcrumbs />
<Routes> <Routes>
@ -40,7 +41,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-hrmanagers" element={<ManageHRManagersRouter />} /> <Route path="/manage-admins" element={<ManageAdminsRouter />} />
<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>

View File

@ -2,22 +2,71 @@ 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, useMemo } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getSidebarItems, type NavItem } from './types/sidebarConfig';
// Helper function to flatten the nested sidebar items into a simple list for search // Role-specific search data
const flattenNavItems = (items: NavItem[]): { name: string; path: string }[] => { const roleSearchData: Record<UserRole, { name: string; path: string }[]> = {
let result: { name: string; path: string }[] = []; employee: [
for (const item of items) { { name: 'My Dashboard', path: '/dashboard' },
if (item.path) { { name: 'My Attendance', path: '/attendance' },
result.push({ name: item.name, path: item.path }); { name: 'Apply for Leave', path: '/leave' },
} ],
if (item.children && item.children.length > 0) { manager: [
result = result.concat(flattenNavItems(item.children)); { name: 'Manager Dashboard', path: '/dashboard' },
} { name: 'My Attendance', path: '/attendance' },
} { name: 'My Leaves', path: '/leave' },
return result; { name: 'Pending Approvals', path: '/approval' },
],
admin: [
{ name: 'Admin Dashboard', path: '/dashboard' },
{ name: 'All Attendance Logs', path: '/attendance' },
{ name: 'All Leave Requests', path: '/leave' },
// Attendance Summary
{ 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' },
// Leave Summary
{ name: 'Leave Applications', path: '/leave-summary/applications' },
{ name: 'Leave Ledger Report', path: '/leave-summary/ledger' },
// Master Data
{ 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' },
// Attendance Summary
{ name: 'Attendance Summary Dashboard', path: '/attendance-summary/dashboard' },
{ name: 'Monthly Attendance Report', path: '/attendance-summary/monthly' },
{ name: 'Daily Attendance Report', path: '/attendance-summary/daily' },
// Leave Summary
{ name: 'Leave Applications', path: '/leave-summary/applications' },
{ name: 'Leave Ledger Report', path: '/leave-summary/ledger' },
// Master Data
{ 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 Specific
{ name: 'Manage Admins', path: '/manage-admins' },
{ name: 'Policy and Rules', path: '/policies' },
],
}; };
export default function Navbar() { export default function Navbar() {
@ -30,7 +79,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', 'hrmanager', 'director']; const roles: UserRole[] = ['employee', 'manager', 'admin', 'superadmin'];
useEffect(() => { useEffect(() => {
if (isSearchOpen && searchInputRef.current) { if (isSearchOpen && searchInputRef.current) {
@ -49,18 +98,11 @@ export default function Navbar() {
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, []); }, []);
// Dynamically generate search items based on the current role's sidebar configuration // Get search items based on the current role
const availableItems = useMemo(() => { const availableItems = roleSearchData[currentRole] || [];
const sidebarItems = getSidebarItems(currentRole); const searchResults = availableItems.filter(item =>
return flattenNavItems(sidebarItems); item.name.toLowerCase().includes(searchQuery.toLowerCase())
}, [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);
@ -69,29 +111,29 @@ export default function Navbar() {
}; };
return ( return (
<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-6 border-b border-app-border relative z-30">
<div className="flex items-center shrink-0"> <div className="flex items-center">
<h2 className="text-lg font-bold text-primary tracking-wide">CliniLaunch HRMS</h2> <h2 className="text-lg font-bold text-primary tracking-wide">CLRI 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-4">
<div className="relative flex items-center"> <div className="relative flex items-center">
{isSearchOpen ? ( {isSearchOpen ? (
<div className="flex items-center border border-app-border rounded-lg shadow-sm bg-app-card"> <div className="flex items-center border border-app-border rounded-lg shadow-sm bg-app-card">
<Search size={18} className="ml-3 text-text-light shrink-0" /> <Search size={18} className="ml-3 text-text-light" />
<input <input
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
placeholder="Search ....." placeholder="Search modules..."
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-56 px-3 py-2 text-sm focus:outline-none rounded-lg text-text-primary"
/> />
<button <button
onClick={() => { setIsSearchOpen(false); setSearchQuery(''); }} onClick={() => { setIsSearchOpen(false); setSearchQuery(''); }}
className="p-2 text-text-light hover:text-text-muted shrink-0" className="p-2 text-text-light hover:text-text-muted"
> >
<X size={18} /> <X size={18} />
</button> </button>
@ -99,7 +141,7 @@ export default function Navbar() {
) : ( ) : (
<button <button
onClick={() => setIsSearchOpen(true)} onClick={() => setIsSearchOpen(true)}
className="p-2 text-text-secondary hover:bg-app-muted rounded-full transition-colors shrink-0" className="p-2 text-text-secondary hover:bg-app-muted rounded-full transition-colors"
> >
<Search size={22} /> <Search size={22} />
</button> </button>
@ -130,7 +172,7 @@ export default function Navbar() {
)} )}
</div> </div>
<div className="relative shrink-0"> <div className="relative">
<button <button
onClick={() => setDropdownOpen(!dropdownOpen)} onClick={() => setDropdownOpen(!dropdownOpen)}
className="flex items-center space-x-2 p-1 rounded-lg hover:bg-app-muted transition-colors" className="flex items-center space-x-2 p-1 rounded-lg hover:bg-app-muted transition-colors"
@ -145,7 +187,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</p> <p className="px-4 py-2 text-xs font-semibold text-text-light uppercase">Switch Role (Test)</p>
{roles.map((role) => ( {roles.map((role) => (
<button <button
key={role} key={role}

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { RootState } from '../../store/store'; import type { RootState } from '../../store/store';
import { getSidebarItems, type NavItem } from './types/sidebarConfig'; import { getSidebarItems, type NavItem } from './types/sidebarConfig';
@ -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, ClipboardType, ClipboardMinus Database, Building, Network, Briefcase, Tag, Receipt
} 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, ClipboardType,ClipboardMinus Database, Building, Network, Briefcase, Tag, Receipt
}; };
export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) { export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed: boolean; setIsCollapsed: (v: boolean) => void }) {
@ -23,13 +23,6 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
const currentPath = location.pathname; const currentPath = location.pathname;
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({}); const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
const [pinnedFlyout, setPinnedFlyout] = useState<string | null>(null);
// Close any pinned flyout when the route changes or the sidebar is expanded
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setPinnedFlyout(null);
}, [location.pathname, isCollapsed]);
const toggleDropdown = (name: string) => { const toggleDropdown = (name: string) => {
setOpenDropdowns(prev => ({ ...prev, [name]: !prev[name] })); setOpenDropdowns(prev => ({ ...prev, [name]: !prev[name] }));
@ -39,92 +32,52 @@ export default function Sidebar({ isCollapsed, setIsCollapsed }: { isCollapsed:
const Icon = iconMap[item.icon]; const Icon = iconMap[item.icon];
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
const isDropdownOpen = openDropdowns[item.name]; const isDropdownOpen = openDropdowns[item.name];
const isPinned = pinnedFlyout === item.name;
const isChildActive = hasChildren && item.children!.some(child => currentPath === child.path); const isChildActive = hasChildren && item.children!.some(child => currentPath === child.path);
const isActive = currentPath === item.path; const isActive = currentPath === item.path;
// COLLAPSED STATE: Render Flyout Submenus if (hasChildren && isCollapsed) {
if (isCollapsed) { return item.children!.map(child => renderNavItem(child));
return (
<li key={item.name} className="mb-1 relative group">
{hasChildren ? (
<button
// Toggle pin on click
onClick={() => setPinnedFlyout(prev => prev === item.name ? null : item.name)}
className={`w-full flex items-center justify-center px-4 py-3 transition-colors duration-200 ${isChildActive || isPinned ? 'text-white border-l-4 border-action' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
}`}
>
{Icon && <Icon size={20} className="shrink-0" />}
</button>
) : (
<Link
to={item.path!}
className={`w-full flex items-center justify-center px-4 py-3 transition-colors duration-200 ${isActive ? 'bg-sidebar-active text-white border-l-4 border-action' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
}`}
>
{Icon && <Icon size={20} className="flex-shrink-0" />}
</Link>
)}
{/* Flyout Submenu or Tooltip on Hover/Pin */}
{/* If pinned, it forces 'block'. If not pinned, 'hidden' but 'group-hover:block' shows it on hover */}
<div
className={`${isPinned ? 'block' : 'hidden'} group-hover:block absolute left-full ml-4 top-0 z-50 shadow-lg`}
>
{hasChildren ? (
<ul className="bg-sidebar min-w-[200px] rounded-lg border border-sidebar-hover py-2">
{item.children!.map(child => {
const ChildIcon = iconMap[child.icon];
return (
<li key={child.name}>
<Link
to={child.path!}
className={`flex items-center px-4 py-2 text-sm w-full transition-colors ${currentPath === child.path ? 'bg-sidebar-active text-white' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white'
}`}
>
{ChildIcon && <ChildIcon size={16} className="mr-3" />}
{child.name}
</Link>
</li>
);
})}
</ul>
) : (
<div className="bg-sidebar-active text-white text-sm px-3 py-1 rounded whitespace-nowrap pointer-events-none">
{item.name}
</div>
)}
</div>
</li>
);
} }
// EXPANDED STATE: Render Normal Dropdowns
return ( return (
<li key={item.name} className="mb-1 relative group"> <li key={item.name} className="mb-1 relative group">
{hasChildren ? ( {hasChildren ? (
<button <button
onClick={() => toggleDropdown(item.name)} onClick={() => toggleDropdown(item.name)}
className={`w-full flex items-center px-4 py-3 transition-colors duration-200 text-left ${isChildActive ? 'text-white' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white' className={`w-full flex items-center px-4 py-3 transition-colors duration-200 text-left ${
} border-l-4 ${isChildActive ? 'border-action' : 'border-transparent'}`} isCollapsed ? 'justify-center' : ''
} ${
isChildActive ? 'text-white' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white'
} border-l-4 ${isChildActive ? 'border-action' : 'border-transparent'}`}
> >
{Icon && <Icon size={20} className="flex-shrink-0" />} {Icon && <Icon size={20} className="flex-shrink-0" />}
<span className="ml-4 text-sm font-medium flex-grow">{item.name}</span> {!isCollapsed && <span className="ml-4 text-sm font-medium flex-grow">{item.name}</span>}
<ChevronDown size={16} className={`transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} /> {!isCollapsed && (
<ChevronDown size={16} className={`transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />
)}
</button> </button>
) : ( ) : (
<Link <Link
to={item.path!} to={item.path!}
className={`flex items-center px-4 py-3 transition-colors duration-200 text-left ${isActive ? 'bg-sidebar-active text-white border-l-4 border-action' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent' className={`flex items-center px-4 py-3 transition-colors duration-200 text-left ${
}`} isCollapsed ? 'justify-center' : ''
} ${
isActive ? 'bg-sidebar-active text-white border-l-4 border-action' : 'text-blue-100 hover:bg-sidebar-hover hover:text-white border-l-4 border-transparent'
}`}
> >
{Icon && <Icon size={20} className="flex-shrink-0" />} {Icon && <Icon size={20} className="flex-shrink-0" />}
<span className="ml-4 text-sm font-medium">{item.name}</span> {!isCollapsed && <span className="ml-4 text-sm font-medium">{item.name}</span>}
</Link> </Link>
)} )}
{isDropdownOpen && hasChildren && ( {isCollapsed && (
<div className="absolute left-full ml-4 top-1/2 -translate-y-1/2 z-50 bg-sidebar-active text-white text-sm px-3 py-1 rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap">
{item.name}
</div>
)}
{isDropdownOpen && !isCollapsed && (
<ul className="mt-1 ml-4 border-l border-blue-800/50 pl-2"> <ul className="mt-1 ml-4 border-l border-blue-800/50 pl-2">
{item.children!.map(child => renderNavItem(child))} {item.children!.map(child => renderNavItem(child))}
</ul> </ul>
@ -137,21 +90,17 @@ 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 flex-shrink-0"> <Link to="/dashboard" className="flex items-center">
<img src="logo.gif" alt="Company Logo" className="h-10 w-auto max-w-[140px] object-contain cursor-pointer" /> {/* Fixed Tailwind sizing for GIF alignment */}
<img src="logo.gif" alt="Company Logo" className="h-25 w-25 object-contain cursor-pointer" />
</Link> </Link>
)} )}
<button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors flex-shrink-0"> <button onClick={() => setIsCollapsed(!isCollapsed)} className="p-2 rounded hover:bg-sidebar-hover transition-colors">
<Menu size={24} className="text-white" /> <Menu size={24} />
</button> </button>
</div> </div>
{/* <nav className="flex-1 mt-4 overflow-x-visible">
Conditional Overflow Logic:
- Collapsed: 'overflow-x-visible' allows flyout menus to appear outside the sidebar bounds.
- Expanded: 'overflow-y-auto overflow-x-hidden' enables vertical scrolling to see all dropdowns.
*/}
<nav className={`flex-1 mt-4 ${isCollapsed ? 'overflow-x-visible' : 'overflow-y-auto overflow-x-hidden'}`}>
<ul> <ul>
{navItems.map(item => renderNavItem(item))} {navItems.map(item => renderNavItem(item))}
</ul> </ul>

View File

@ -8,94 +8,61 @@ export interface NavItem {
} }
export const getSidebarItems = (role: UserRole): NavItem[] => { export const getSidebarItems = (role: UserRole): NavItem[] => {
if (role === 'employee') { const baseItems: NavItem[] = [
return [ { name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' }, { name: 'Attendance', icon: 'CalendarCheck', path: '/attendance' },
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' }, { name: 'Leave', icon: 'Plane', path: '/leave' },
{ name: 'Leave Summary', icon: 'ClipboardMinus', path: '/leave' }, ];
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
];
}
if (role === 'manager') { if (role === 'manager') {
return [ return [...baseItems, { name: 'Approval', icon: 'CheckCircle', path: '/approval' }];
{ 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 adminItems: NavItem[] = [
const attendanceSummaryItems: NavItem = { {
name: 'Attendance Service', name: 'Attendance Summary',
icon: 'BarChart3', icon: 'BarChart3',
children: [ children: [
{ name: 'Attendance Dashboard', icon: 'LayoutDashboard', path: '/attendance-summary/dashboard' }, { name: 'Dashboard', icon: 'LayoutDashboard', path: '/attendance-summary/dashboard' },
{ name: 'Attendance Summary', icon: 'CalendarCheck', path: '/attendance' }, { name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' },
{ name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' }, { name: 'Daily Report', icon: 'CalendarDays', path: '/attendance-summary/daily' },
{ name: 'Monthly Report', icon: 'FileText', path: '/attendance-summary/monthly' }, ...(role === 'admin' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : [])
...(role === 'hrmanager' ? [{ name: 'Regularization', icon: 'ClipboardCheck', path: '/attendance-summary/regularization' }] : []) ]
] },
}; {
name: 'Leave Summary',
const leaveSummaryItems: NavItem = { icon: 'ClipboardList',
name: 'Leave Service', children: [
icon: 'ClipboardList', { name: 'Leave Applications', icon: 'FileText', path: '/leave-summary/applications' },
children: [ { name: 'Ledger Report', icon: 'Receipt', path: '/leave-summary/ledger' },
{ 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' }, name: 'Master Data',
] icon: 'Database',
}; children: [
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/master-data/dashboard' },
const masterDataItems: NavItem = { { name: 'Manage Company', icon: 'Building', path: '/master-data/company' },
name: 'Master Data Service', { name: 'Manage Branches', icon: 'Network', path: '/master-data/branches' },
icon: 'Database', { name: 'Manage Departments', icon: 'Briefcase', path: '/master-data/departments' },
children: [ { name: 'Manage Designations', icon: 'Tag', path: '/master-data/designations' },
{ name: 'Master Data Dashboard', icon: 'LayoutDashboard', path: '/master-data/dashboard' }, { name: 'Manage Employees', icon: 'Users', path: '/master-data/employees' },
{ 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' },
]; ];
if (role === 'admin') {
return [...baseItems, ...adminItems];
}
if (role === 'superadmin') {
return [
...baseItems,
...adminItems,
{ name: 'Manage Admins', icon: 'ShieldUser', path: '/manage-admins' },
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' },
];
}
return baseItems; // employee
} }

View File

@ -5,10 +5,9 @@ interface ConfirmationModalProps {
message: string; message: string;
onConfirm: () => void; onConfirm: () => void;
onClose: () => void; onClose: () => void;
confirmText?: string;
} }
export default function ConfirmationModal({ title, message, onConfirm, onClose, confirmText = "Yes, Delete" }: ConfirmationModalProps) { export default function ConfirmationModal({ title, message, onConfirm, onClose }: 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">
@ -34,7 +33,7 @@ export default function ConfirmationModal({ title, message, onConfirm, onClose,
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"
> >
{confirmText} Yes, Delete
</button> </button>
</div> </div>
</div> </div>

View File

@ -10,8 +10,10 @@ export default function ErrorState({
fullScreen = false fullScreen = false
}: ErrorStateProps) { }: ErrorStateProps) {
return ( return (
<div className={`flex flex-col items-center justify-center bg-app-card rounded-xl shadow-md border border-app-border p-8 text-absent-600 ${fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full' <div className={`flex flex-col items-center justify-center bg-app-card rounded-xl shadow-md border border-app-border p-8 text-absent-600 ${
}`}> fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
}`}>
{/* Icon wrapped in a circular background to look like a card illustration */}
<div className="p-3 bg-absent-100 rounded-full mb-4"> <div className="p-3 bg-absent-100 rounded-full mb-4">
<AlertCircle size={32} className="text-absent-500" /> <AlertCircle size={32} className="text-absent-500" />
</div> </div>

View File

@ -8,12 +8,13 @@ interface LoaderProps {
export default function Loader({ export default function Loader({
message = 'Loading...', message = 'Loading...',
color = '#001045', color = '#0A66C2', // Default to your primary brand color
fullScreen = false fullScreen = false
}: LoaderProps) { }: LoaderProps) {
return ( return (
<div className={`flex flex-col items-center justify-center bg-app-card rounded-xl shadow-md border border-app-border p-8 ${fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full' <div className={`flex flex-col items-center justify-center bg-app-card rounded-xl shadow-md border border-app-border p-8 ${
}`}> fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
}`}>
<FadeLoader <FadeLoader
color={color} color={color}
height={15} height={15}

View File

@ -1,203 +1,49 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { apiClient, fetchMockData } from '../../../api/client'; import { 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, AttendanceStatus, AttendanceSummary, DashboardMetrics, DailyReportRecord, AdminReportRecord } from '../types/attendance'; import type { AttendanceRecord } from '../types/attendance';
// Helper to map backend statuses to frontend statuses // 1. Define endpoints per role (Easy to swap to real API URLs later)
const mapStatus = (backendStatus: string): AttendanceStatus => { const ATTENDANCE_ENDPOINTS: Record<UserRole, string> = {
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',
hrmanager: 'attendance/hrmanager_attendance.json', admin: 'attendance/admin_attendance.json',
director: 'attendance/director_attendance.json', superadmin: 'attendance/superadmin_attendance.json',
// Future real backend example:
// employee: '/api/v1/attendance/me',
// manager: '/api/v1/attendance/team',
}; };
// 2. Create a single dynamic hook
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[]>(MOCK_ATTENDANCE_ENDPOINTS[role]), queryFn: () => fetchMockData<AttendanceRecord[]>(ATTENDANCE_ENDPOINTS[role]),
});
};
export interface SummaryRecord {
id: string;
employeeName: string;
branch: string;
department: string;
company: string;
isCheckedIn: boolean;
date: string; // Added date field
}
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;
@ -209,33 +55,9 @@ export interface RegularizationRecord {
status: 'Pending' | 'Approved' | 'Rejected'; status: 'Pending' | 'Approved' | 'Rejected';
} }
export const useHRManagerRegularizations = () => { export const useAdminRegularizations = () => {
return useQuery<RegularizationRecord[]>({ return useQuery<RegularizationRecord[]>({
queryKey: ['HRManagerRegularizations'], queryKey: ['adminRegularizations'],
queryFn: async () => { queryFn: () => fetchMockData<RegularizationRecord[]>('attendance/admin_regularizations.json'),
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',
};
});
},
}); });
}; };

View File

@ -1,5 +1,5 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2 } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } 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,13 +7,12 @@ type SortDirection = 'ascending' | 'descending';
interface AttendanceRecordsTableProps { interface AttendanceRecordsTableProps {
records: AttendanceRecord[]; records: AttendanceRecord[];
title?: string; title?: string;
fromDate: string; initialFromDate?: string;
toDate: string; initialToDate?: string;
onFromDateChange: (date: string) => void;
onToDateChange: (date: string) => void;
isLoading?: boolean;
} }
// Updated to handle empty strings safely
const getDaysDifference = (start: string, end: string) => { const getDaysDifference = (start: string, end: string) => {
if (!start || !end) return 0; if (!start || !end) return 0;
const startDate = new Date(start + 'T00:00:00'); const startDate = new Date(start + 'T00:00:00');
@ -33,12 +32,13 @@ const getStatusPriority = (status: string) => {
export default function AttendanceRecordsTable({ export default function AttendanceRecordsTable({
records, records,
title = "Attendance Records", title = "Attendance Records",
fromDate, initialFromDate,
toDate, initialToDate
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'
}); });
@ -46,8 +46,17 @@ 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 = [...records]; const sortableData = [...filteredData];
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);
@ -61,7 +70,7 @@ export default function AttendanceRecordsTable({
return 0; return 0;
}); });
return sortableData; return sortableData;
}, [records, sortConfig]); }, [filteredData, 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;
@ -93,8 +102,8 @@ export default function AttendanceRecordsTable({
setCurrentPage(1); setCurrentPage(1);
}; };
const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { onFromDateChange(e.target.value); setCurrentPage(1); }; const handleFromDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { setFromDate(e.target.value); setCurrentPage(1); };
const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { onToDateChange(e.target.value); setCurrentPage(1); }; const handleToDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { setToDate(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) => {
@ -148,19 +157,11 @@ export default function AttendanceRecordsTable({
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-app-border"> <tbody className="divide-y divide-app-border">
{isLoading ? ( {paginatedData.length > 0 ? (
<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>
@ -194,7 +195,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 => (

View File

@ -1,13 +1,11 @@
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) => {
@ -17,7 +15,11 @@ const formatDate = (date: Date) => {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };
export default function Calendar({ data, selectedDate, currentMonth, currentYear, onDateSelect, onMonthChange }: CalendarProps) { export default function Calendar({ data, selectedDate, onDateSelect }: 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();
@ -32,18 +34,11 @@ export default function Calendar({ data, selectedDate, currentMonth, currentYear
const goToToday = () => { const goToToday = () => {
const todayDate = new Date(); const todayDate = new Date();
onMonthChange(todayDate.getMonth(), todayDate.getFullYear()); setCurrentMonth(todayDate.getMonth());
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];
@ -66,15 +61,15 @@ export default function Calendar({ data, selectedDate, currentMonth, currentYear
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">Attendance</h2> <h2 className="text-lg font-semibold text-text-primary">My 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={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"> <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">
{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={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"> <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">
{yearsArray.map((year) => <option key={year} value={year}>{year}</option>)} {yearsArray.map((year) => <option key={year} value={year}>{year}</option>)}
</select> </select>
</div> </div>

View File

@ -2,7 +2,6 @@ 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;
@ -11,31 +10,21 @@ 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, employeeId }: DailySummaryProps) { export default function DailySummary({ selectedDate, data }: DailySummaryProps) {
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-', attendanceId: 0 }; const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-' };
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 createRegMutation = useCreateRegularization(); const handleApplyRegularization = (regData: { checkIn: string; checkOut: string; reason: string }) => {
console.log("Regularization Data:", { date: selectedDate, ...regData });
const handleApplyRegularization = (payload: any) => { setIsApplied(true);
createRegMutation.mutate(payload, { setShowModal(false);
onSuccess: () => { toast.success("Regularization request submitted successfully.");
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 (
@ -110,11 +99,10 @@ export default function DailySummary({ selectedDate, data, employeeId }: DailySu
</div> </div>
)} )}
{/* Render Separated Modal */}
{showModal && ( {showModal && (
<RegularizationModal <RegularizationModal
selectedDate={selectedDate} selectedDate={selectedDate}
attendanceId={selectedData.attendanceId}
employeeId={employeeId}
onClose={() => setShowModal(false)} onClose={() => setShowModal(false)}
onApply={handleApplyRegularization} onApply={handleApplyRegularization}
/> />

View File

@ -3,13 +3,11 @@ import { X } from 'lucide-react';
interface RegularizationModalProps { interface RegularizationModalProps {
selectedDate: string; selectedDate: string;
attendanceId: number;
employeeId: number;
onClose: () => void; onClose: () => void;
onApply: (payload: any) => void; onApply: (data: { checkIn: string; checkOut: string; reason: string }) => void;
} }
export default function RegularizationModal({ selectedDate, attendanceId, employeeId, onClose, onApply }: RegularizationModalProps) { export default function RegularizationModal({ selectedDate, 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>) => {
@ -18,19 +16,7 @@ export default function RegularizationModal({ selectedDate, attendanceId, employ
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 (

View File

@ -1,37 +1,91 @@
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 {
checkedIn: number; dailyData: SummaryRecord[];
notCheckedIn: number; trendData: SummaryRecord[];
} }
export default function AttendanceCharts({ checkedIn, notCheckedIn }: AttendanceChartsProps) { export default function AttendanceCharts({ dailyData, trendData }: AttendanceChartsProps) {
const pieData = [ // Memoize Pie Chart Data
{ id: 0, value: checkedIn, label: 'Checked In', color: '#16a34a' }, const { pieData, checkedInCount, notCheckedInCount } = useMemo(() => {
{ id: 1, value: notCheckedIn, label: 'Not Checked In', color: '#dc2626' } const checkedIn = dailyData.filter(r => r.isCheckedIn).length;
]; 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]);
// Memoize 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="bg-app-card p-6 rounded-lg shadow-sm"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<h3 className="text-lg font-semibold text-text-primary mb-6">Daily Attendance Ratio</h3> {/* MUI Pie Chart - Daily Ratio */}
<div className="h-72 w-full flex items-center justify-center"> <div className="bg-app-card p-6 rounded-lg shadow-sm">
{checkedIn === 0 && notCheckedIn === 0 ? ( <h3 className="text-lg font-semibold text-text-primary mb-6">Daily Attendance Ratio</h3>
<p className="text-text-muted text-sm">No data available for selected filters</p> <div className="h-72 w-full flex items-center justify-center">
) : ( {checkedInCount === 0 && notCheckedInCount === 0 ? (
<PieChart <p className="text-text-muted text-sm">No data available for selected date</p>
series={[ ) : (
{ <PieChart
data: pieData, series={[
innerRadius: 40, {
outerRadius: 80, data: pieData,
paddingAngle: 2, innerRadius: 40,
cornerRadius: 4, outerRadius: 80,
arcLabel: (item) => `${item.value}`, 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} height={280}
series={[
{ data: lineCheckedInData, label: 'Checked In', color: '#93c5fd', area: true },
{ data: lineNotCheckedInData, label: 'Not Checked In', color: '#fdba74', area: true },
]}
xAxis={[{ scaleType: 'point', data: xAxisData, label: 'Days of Month' }]}
yAxis={[{ min: 0 }]}
/> />
)} </div>
</div> </div>
</div> </div>
); );

View File

@ -1,84 +1,70 @@
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'; export interface DailyReportFilterValues {
import { useDepartments } from '../../../masterData/api/useDepartmentData'; date: string;
import { useJobRoles } from '../../../masterData/api/useJobRoleData'; company: string;
import type { DailyReportFilters } from '../../api/useAttendanceData'; branch: string;
department: string;
designation: string;
employeeName: string;
employeeId: string;
}
interface DailyReportFiltersProps { interface DailyReportFiltersProps {
filters: DailyReportFilters; filters: DailyReportFilterValues;
onFilterChange: (newFilters: DailyReportFilters) => void; onFilterChange: (newFilters: DailyReportFilterValues) => 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>) => {
const { name, value } = e.target; onFilterChange({ ...filters, [e.target.name]: e.target.value });
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 disabled:opacity-50 disabled:cursor-not-allowed"; 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";
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="companyId" value={filters.companyId} onChange={handleChange} className={inputClass}> <select name="company" value={filters.company} onChange={handleChange} className={inputClass}>
<option value="">All Companies</option> <option value="All">All</option>
{companies?.map(c => <option key={c.id} value={String(c.id)}>{c.name}</option>)} <option value="CLRI">CLRI</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="branchId" value={filters.branchId} onChange={handleChange} disabled={!filters.companyId} className={inputClass}> <select name="branch" value={filters.branch} onChange={handleChange} className={inputClass}>
<option value="">All Branches</option> <option value="All">All</option>
{filteredBranches.map(b => <option key={b.id} value={String(b.id)}>{b.name}</option>)} <option value="Bengaluru">Bengaluru</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="departmentId" value={filters.departmentId} onChange={handleChange} disabled={!filters.branchId} className={inputClass}> <select name="department" value={filters.department} onChange={handleChange} className={inputClass}>
<option value="">All Departments</option> <option value="All">All</option>
{filteredDepartments.map(d => <option key={d.id} value={String(d.id)}>{d.name}</option>)} <option value="IT">IT</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="designationId" value={filters.designationId} onChange={handleChange} className={inputClass}> <select name="designation" value={filters.designation} onChange={handleChange} className={inputClass}>
<option value="">All Designations</option> <option value="All">All</option>
{designations?.map(d => <option key={d.id} value={String(d.id)}>{d.name}</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> </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">
@ -87,12 +73,13 @@ 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 Code</label> <label className={labelClass}>Employee ID</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="employeeCode" value={filters.employeeCode} onChange={handleChange} placeholder="Search code..." className={`${inputClass} pl-9`} /> <input type="text" name="employeeId" value={filters.employeeId} onChange={handleChange} placeholder="Search ID..." className={`${inputClass} pl-9`} />
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -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';
interface MonthlyReportFiltersProps { export interface ReportFilterValues {
filters: AdminReportFilters; company: string;
onFilterChange: (newFilters: AdminReportFilters) => void; branch: string;
department: string;
designation: string;
employeeName: string;
employeeId: string;
fromDate: string;
toDate: string;
} }
export default function MonthlyReportFilters({ filters, onFilterChange }: MonthlyReportFiltersProps) { interface ReportFiltersProps {
const { data: companies } = useCompanies(); filters: ReportFilterValues;
const { data: allBranches } = useBranches(); onFilterChange: (newFilters: ReportFilterValues) => void;
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>) => {
const { name, value } = e.target; onFilterChange({ ...filters, [e.target.name]: e.target.value });
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 disabled:opacity-50 disabled:cursor-not-allowed"; 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";
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>
<div className="flex flex-col"> {/* Search by Name */}
<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,13 +90,16 @@ export default function MonthlyReportFilters({ filters, onFilterChange }: Monthl
<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 Code</label> <label className={labelClass}>Employee ID</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="employeeCode" value={filters.employeeCode} onChange={handleChange} placeholder="Search code..." className={`${inputClass} pl-9`} /> <input type="text" name="employeeId" value={filters.employeeId} onChange={handleChange} placeholder="Search ID..." className={`${inputClass} pl-9`} />
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@ -27,10 +27,12 @@ export default function RegularizationModal({ record, onClose, onAction }: Regul
<p className="text-sm text-text-muted mb-6">Request ID: {record.id}</p> <p className="text-sm text-text-muted mb-6">Request ID: {record.id}</p>
<div className="space-y-4"> <div className="space-y-4">
{/* Separated Employee ID */}
<div className="flex justify-between border-b border-app-border pb-2"> <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 text-text-muted">Employee ID</span>
<span className="text-sm font-medium text-text-primary">{record.empId}</span> <span className="text-sm font-medium text-text-primary">{record.empId}</span>
</div> </div>
{/* Separated Employee Name */}
<div className="flex justify-between border-b border-app-border pb-2"> <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 text-text-muted">Employee Name</span>
<span className="text-sm font-medium text-text-primary">{record.empName}</span> <span className="text-sm font-medium text-text-primary">{record.empName}</span>

View File

@ -24,9 +24,9 @@ export default function RegularizationTable({ records, onAction }: Regularizatio
const [viewRecord, setViewRecord] = useState<RegularizationRecord | null>(null); const [viewRecord, setViewRecord] = useState<RegularizationRecord | null>(null);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10); // Default 10 entries per page
// Default sort: date descending // Default sort: date descending (latest first)
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({ const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
key: 'date', key: 'date',
direction: 'descending' direction: 'descending'

View File

@ -1,126 +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 { 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>
);
}

View File

@ -1,124 +0,0 @@
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>
);
}

View File

@ -1,48 +0,0 @@
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>
);
}

View File

@ -1,111 +0,0 @@
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>
);
}

View File

@ -1,94 +0,0 @@
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>
);
}

View File

@ -0,0 +1,45 @@
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>
);
}

View File

@ -0,0 +1,46 @@
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]);
// Use the global Loader component here
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>
);
}

View File

@ -0,0 +1,45 @@
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>
);
}

View File

@ -0,0 +1,45 @@
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>
);
}

View File

@ -0,0 +1,39 @@
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>
);
}

View File

@ -0,0 +1,117 @@
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); // Extracts "YYYY-MM"
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>
{/* Reusable Stat Cards */}
<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>
);
}

View File

@ -0,0 +1,147 @@
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')}`;
// Dummy Data - Dynamically using today's and yesterday's date
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() {
// Manage all filter state in one object
const [filters, setFilters] = useState<DailyReportFilterValues>({
date: getTodayDate(),
company: 'All',
branch: 'All',
department: 'All',
designation: 'All',
employeeName: '',
employeeId: '',
});
// Client-side filtering logic
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">
{/* Header & Download Button */}
<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>
{/* Reusable Filters Component */}
<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 ID</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>
);
}

View File

@ -0,0 +1,116 @@
import { useState, useMemo } from 'react';
import { Download, FileText } from 'lucide-react';
import { toast } from 'sonner';
import ReportFilters, { type ReportFilterValues } from '../../components/summary/MonthlyReportFilters';
// 1. Dummy Data for testing
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() {
// 2. Default Filters State
const [filters, setFilters] = useState<ReportFilterValues>({
company: 'All',
branch: 'All',
department: 'All',
designation: 'All',
employeeName: '',
employeeId: '',
fromDate: '',
toDate: '',
});
// 3. Client-side filtering logic (for current dummy data testing)
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 = () => {
// In the future, this will trigger an API call to get an Excel/CSV file
toast.success("Preparing report for download...");
};
return (
<div className="space-y-6">
{/* Header & Download Button */}
<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>
{/* Reusable Filters Component */}
<ReportFilters filters={filters} onFilterChange={setFilters} />
{/* Dummy Data Table (Proves filters are working) */}
<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 ID</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>
);
}

View File

@ -1,40 +1,18 @@
import MyAttendance from '../pages/MyAttendance'; import { useSelector } from 'react-redux';
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() {
// Since MyAttendance pulls the ID dynamically from Redux, const role = useSelector((state: RootState) => state.role.currentRole);
// it works perfectly for Employee, Manager, HR, and Director.
return <MyAttendance />; switch (role) {
case 'employee': return <EmployeeAttendance />;
case 'manager': return <ManagerAttendance />;
case 'admin': return <AdminAttendance />;
case 'superadmin': return <SuperAdminAttendance />;
default: return <div>Unauthorized</div>;
}
} }
// ### How are we ensuring Managers/HR use ONLY their data?
// We are achieving data isolation through a 3-layer security design that matches your backend developer's architecture perfectly:
// #### Layer 1: Frontend State (Redux)
// 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!

View File

@ -1,8 +1,8 @@
import { Routes, Route, Navigate } from 'react-router-dom'; import { Routes, Route, Navigate } from 'react-router-dom';
import AttendanceDashboard from '../pages/AttendanceDashboard'; import AttendanceDashboard from '../pages/summary/AttendanceDashboard';
import MonthlyReport from '../pages/MonthlyReport'; import MonthlyReport from '../pages/summary/MonthlyReport';
import DailyReport from '../pages/DailyReport'; import DailyReport from '../pages/summary/DailyReport';
import HRManagerRegularization from '../pages/HRManagerRegularization'; import AdminRegularization from '../pages/summary/AdminRegularization'; // New Import
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={<HRManagerRegularization />} /> <Route path="/regularization" element={<AdminRegularization />} /> {/* New Route */}
</Routes> </Routes>
); );
} }

View File

@ -1,4 +1,3 @@
// 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 {
@ -7,58 +6,7 @@ export interface AttendanceRecord {
timeOut: string; timeOut: string;
loggedHours: string; loggedHours: string;
status: AttendanceStatus; status: AttendanceStatus;
attendanceId: number;
} }
// Add this line to fix the error
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.

View File

@ -0,0 +1,8 @@
export default function AdminDashboard() {
return (
<div className="bg-app-card rounded-lg shadow-sm p-6">
<h2 className="text-2xl font-bold text-default">Admin Dashboard</h2>
<p className="text-text-muted mt-2">Welcome Admin!</p>
</div>
);
}

View File

@ -1,87 +0,0 @@
import { Building, Network, Users, ShieldUser, ArrowRight, ScrollText } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function DirectorDashboard() {
const stats = [
{ 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: 'Global Employees', value: '450', icon: Users, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Total HRManagers', value: '5', icon: ShieldUser, color: 'text-late-700', bg: 'bg-late-100' },
];
const companies = [
{ name: 'CLRI', employees: 245, status: 'Active' },
{ name: 'TechNova Solutions', employees: 120, status: 'Active' },
{ name: 'Infotech Ltd', employees: 85, status: 'Active' },
];
return (
<div className="space-y-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<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>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Company Overview</h3>
<div className="space-y-3">
{companies.map((c, idx) => (
<div key={idx} className="flex items-center justify-between p-3 border border-app-border rounded-lg">
<div className="flex items-center">
<Building size={18} className="text-text-light mr-3" />
<span className="text-sm font-medium text-text-primary">{c.name}</span>
</div>
<div className="flex items-center space-x-4">
<span className="text-sm text-text-secondary">{c.employees} Employees</span>
<span className="px-2 py-1 rounded-full text-xs font-medium bg-present-100 text-present-700">{c.status}</span>
</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">Director Actions</h3>
<div className="space-y-3">
<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">
<ShieldUser size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Manage HR Managers</span>
</div>
<ArrowRight size={18} className="text-text-light" />
</Link>
<Link to="/policies" 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">
<ScrollText size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Policy and Rules</span>
</div>
<ArrowRight size={18} className="text-text-light" />
</Link>
<Link to="/master-data/company" 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">
<Building size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Manage Master Data</span>
</div>
<ArrowRight size={18} className="text-text-light" />
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,49 +1,8 @@
import { CalendarCheck, Plane, Clock } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function EmployeeDashboard() { export default function EmployeeDashboard() {
const stats = [
{ title: 'Attendance Status', value: 'Present', icon: CalendarCheck, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Leave Balance', value: '12 Days', icon: Plane, color: 'text-primary', bg: 'bg-primary/10' },
{ title: 'Check-In Time', value: '09:05 AM', icon: Clock, color: 'text-late-700', bg: 'bg-late-100' },
];
return ( return (
<div className="space-y-6"> <div className="bg-app-card rounded-lg shadow-sm p-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm"> <h2 className="text-2xl font-bold text-default">Employee Dashboard</h2>
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Employee!</h2> <p className="text-text-muted mt-2">Welcome User!</p>
<p className="text-text-muted mt-1">Here is your attendance and leave summary for today.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Quick Actions</h3>
<div className="grid grid-cols-2 gap-4">
<Link to="/leave" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<Plane size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Check Leave Balance</span>
</Link>
<Link to="/attendance" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<CalendarCheck size={20} className="text-present-700 mr-3" />
<span className="text-sm font-medium text-text-primary">View Attendance</span>
</Link>
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -1,59 +0,0 @@
import { Users, CalendarCheck, ClipboardCheck, Plane, Building } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function HRManagerDashboard() {
const stats = [
{ 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: 'Pending Regularizations', value: '4', icon: ClipboardCheck, color: 'text-late-700', bg: 'bg-late-100' },
{ title: 'On Leave', value: '12', icon: Plane, color: 'text-absent-700', bg: 'bg-absent-100' },
];
return (
<div className="space-y-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<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>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold text-text-primary mb-4">Quick Links</h3>
<div className="grid grid-cols-2 gap-4">
<Link to="/master-data/employees" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<Users size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Manage Employees</span>
</Link>
<Link to="/attendance-summary/dashboard" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<CalendarCheck size={20} className="text-present-700 mr-3" />
<span className="text-sm font-medium text-text-primary">Attendance Summary</span>
</Link>
<Link to="/leave-summary/applications" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<ClipboardCheck size={20} className="text-late-700 mr-3" />
<span className="text-sm font-medium text-text-primary">Leave Applications</span>
</Link>
<Link to="/master-data/company" className="flex items-center p-4 border border-app-border rounded-lg hover:bg-app-muted transition-colors">
<Building size={20} className="text-primary mr-3" />
<span className="text-sm font-medium text-text-primary">Master Data</span>
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,62 +1,8 @@
import { CheckCircle, Users, Plane, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function ManagerDashboard() { export default function ManagerDashboard() {
const stats = [
{ title: 'Pending Approvals', value: '5', icon: CheckCircle, color: 'text-late-700', bg: 'bg-late-100' },
{ title: 'Team Present Today', value: '18 / 20', icon: Users, color: 'text-present-700', bg: 'bg-present-100' },
{ title: 'Team On Leave', value: '2', icon: Plane, color: 'text-absent-700', bg: 'bg-absent-100' },
];
const recentApprovals = [
{ id: 'LA001', name: 'Alice Williams', type: 'Casual Leave', days: 2 },
{ id: 'LA002', name: 'Bob Smith', type: 'Sick Leave', days: 1 },
{ id: 'LA004', name: 'Diana Prince', type: 'Casual Leave', days: 3 },
];
return ( return (
<div className="space-y-6"> <div className="bg-app-card rounded-lg shadow-sm p-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm"> <h2 className="text-2xl font-bold text-default">Manager Dashboard</h2>
<h2 className="text-2xl font-bold text-text-primary">Welcome back, Manager!</h2> <p className="text-text-muted mt-2">Welcome Manager!</p>
<p className="text-text-muted mt-1">Here is your team overview for today.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{stats.map((stat, index) => (
<div key={index} className="bg-app-card p-6 rounded-lg shadow-sm flex items-center justify-between transition-transform hover:scale-[1.02]">
<div>
<p className="text-sm text-text-muted font-medium">{stat.title}</p>
<p className={`text-2xl font-bold mt-2 ${stat.color}`}>{stat.value}</p>
</div>
<div className={`p-3 rounded-full ${stat.bg}`}>
<stat.icon className={stat.color} size={28} />
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-app-card p-6 rounded-lg shadow-sm">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-text-primary">Pending Leave Requests</h3>
<Link to="/approval" className="text-primary text-sm font-medium hover:text-primary-hover flex items-center">
View All <ArrowRight size={14} className="ml-1" />
</Link>
</div>
<div className="space-y-3">
{recentApprovals.map((req) => (
<div key={req.id} className="flex items-center justify-between p-3 border border-app-border rounded-lg">
<div>
<p className="text-sm font-medium text-text-primary">{req.name}</p>
<p className="text-xs text-text-muted">{req.type} &bull; {req.days} Day(s)</p>
</div>
<Link to="/approval" className="px-3 py-1 bg-primary/10 text-primary text-xs font-medium rounded-full">Review</Link>
</div>
))}
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,8 @@
export default function SuperAdminDashboard() {
return (
<div className="bg-app-card rounded-lg shadow-sm p-6">
<h2 className="text-2xl font-bold text-default">Super Admin Dashboard</h2>
<p className="text-text-muted mt-2">Welcome Super Admin!</p>
</div>
);
}

View File

@ -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 HRManagerDashboard from '../pages/HRManagerDashboard'; import AdminDashboard from '../pages/AdminDashboard';
import DirectorDashboard from '../pages/DirectorDashboard'; import SuperAdminDashboard from '../pages/SuperAdminDashboard';
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 'hrmanager': case 'admin':
return <HRManagerDashboard />; return <AdminDashboard />;
case 'director': case 'superadmin':
return <DirectorDashboard />; return <SuperAdminDashboard />;
default: default:
return <div>No role assigned</div>; return <div>No role assigned</div>;
} }

View File

@ -0,0 +1,17 @@
import { useSelector } from 'react-redux';
import type { RootState } from '../../../store/store';
export default function PolicyRouter() {
const role = useSelector((state: RootState) => state.role.currentRole);
if (role === 'superadmin') {
return (
<div className="bg-app-card p-6 rounded-lg shadow">
<h2 className="text-2xl font-bold text-primary">Policy and Rules</h2>
<p className="mt-2 text-text-muted">Configure global leave policies, LOP rules, and attendance shift timings.</p>
</div>
);
}
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: SuperAdmins only.</div>;
}

View File

@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMockData } from '../../../api/client';
import type { Employee } from './useEmployeeData';
// Extends Employee to include admin status
export interface AdminEmployee extends Employee {
isAdmin: boolean;
}
export const useAdminEmployees = () => {
return useQuery<AdminEmployee[]>({
queryKey: ['adminEmployees'],
queryFn: () => fetchMockData<AdminEmployee[]>('master-data/admins.json'),
});
};

View File

@ -0,0 +1,17 @@
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'),
});
};

View File

@ -0,0 +1,15 @@
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'),
});
};

View File

@ -0,0 +1,19 @@
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'),
});
};

View File

@ -0,0 +1,21 @@
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'),
});
};

View File

@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMockData } from '../../../api/client';
export interface Employee {
id: string;
name: 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'),
});
};

View File

@ -0,0 +1,17 @@
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'),
});
};

View File

@ -4,48 +4,37 @@ 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; branch: Branch | null; // null means Add mode, object means Edit mode
companies: Company[]; companies: Company[];
onClose: () => void; onClose: () => void;
onSave: (name: string, companyId: number, status: 'Active' | 'Disabled') => void; onSave: (name: string, companyId: string, companyName: string, 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<string>(''); const [companyId, setCompanyId] = useState('');
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 ? String(companies[0].id) : ''); setCompanyId(companies.length > 0 ? 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();
if (!companyId) { // Find the company name from the selected ID to save it alongside
alert("Please select a company"); const selectedCompany = companies.find(c => c.id === companyId);
return; onSave(name, companyId, selectedCompany?.name || '', status);
}
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">
@ -64,7 +53,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={inputClass} 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"
placeholder="Enter branch name..." placeholder="Enter branch name..."
/> />
</div> </div>
@ -75,7 +64,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={inputClass} 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.length === 0 ? ( {companies.length === 0 ? (
<option value="">No companies available</option> <option value="">No companies available</option>
@ -90,7 +79,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={inputClass} 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="Active">Active</option> <option value="Active">Active</option>
<option value="Disabled">Disabled</option> <option value="Disabled">Disabled</option>

View File

@ -5,10 +5,10 @@ import type { Branch } from '../../api/useBranchData';
interface BranchTableProps { interface BranchTableProps {
records: Branch[]; records: Branch[];
onEdit: (branch: Branch) => void; onEdit: (branch: Branch) => void;
onDelete: (branch: Branch) => void; onDelete: (id: string) => void;
} }
type SortKey = 'code' | 'name' | 'companyName' | 'status'; type SortKey = 'id' | 'name' | 'companyName' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
@ -32,7 +32,7 @@ export default function BranchTable({ records, onEdit, onDelete }: BranchTablePr
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!searchQuery) return records; if (!searchQuery) return records;
return records.filter(record => return records.filter(record =>
record.code.toLowerCase().includes(searchQuery.toLowerCase()) || record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.name.toLowerCase().includes(searchQuery.toLowerCase()) || record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) || record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.status.toLowerCase().includes(searchQuery.toLowerCase()) record.status.toLowerCase().includes(searchQuery.toLowerCase())
@ -96,7 +96,7 @@ export default function BranchTable({ records, onEdit, onDelete }: BranchTablePr
<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 <input
type="text" type="text"
placeholder="Search code, name, company..." placeholder="Search id, name, company..."
value={searchQuery} value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }} onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
className="pl-9 pr-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 w-full" className="pl-9 pr-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 w-full"
@ -108,14 +108,14 @@ export default function BranchTable({ records, onEdit, onDelete }: BranchTablePr
<table className="min-w-full divide-y divide-app-border"> <table className="min-w-full divide-y divide-app-border">
<thead> <thead>
<tr> <tr>
{(['code', 'name', 'companyName', 'status'] as SortKey[]).map((key) => ( {(['id', 'name', 'companyName', 'status'] as SortKey[]).map((key) => (
<th <th
key={key} key={key}
onClick={() => requestSort(key)} onClick={() => requestSort(key)}
className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors" className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
> >
<div className="flex items-center"> <div className="flex items-center">
{key === 'code' ? 'Branch Code' : key === 'companyName' ? 'Company' : key.charAt(0).toUpperCase() + key.slice(1)} {getSortIcon(key)} {key === 'id' ? 'Branch ID' : key === 'companyName' ? 'Company' : key.charAt(0).toUpperCase() + key.slice(1)} {getSortIcon(key)}
</div> </div>
</th> </th>
))} ))}
@ -126,7 +126,7 @@ export default function BranchTable({ records, onEdit, onDelete }: BranchTablePr
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((record) => ( paginatedData.map((record) => (
<tr key={record.id} className="hover:bg-app-muted transition-colors"> <tr key={record.id} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.code}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm"> <td className="px-6 py-4 whitespace-nowrap text-sm">
@ -136,7 +136,7 @@ export default function BranchTable({ records, onEdit, onDelete }: BranchTablePr
<button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center"> <button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center">
<Pencil size={16} className="mr-1" /> Edit <Pencil size={16} className="mr-1" /> Edit
</button> </button>
<button onClick={() => onDelete(record)} className="text-absent-500 hover:text-absent-700 flex items-center"> <button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
<Trash2 size={16} className="mr-1" /> Delete <Trash2 size={16} className="mr-1" /> Delete
</button> </button>
</td> </td>

View File

@ -5,10 +5,10 @@ import type { Company } from '../../api/useCompanyData';
interface CompanyTableProps { interface CompanyTableProps {
records: Company[]; records: Company[];
onEdit: (company: Company) => void; onEdit: (company: Company) => void;
onDelete: (company: Company) => void; onDelete: (id: string) => void;
} }
type SortKey = 'code' | 'name' | 'status'; type SortKey = 'id' | 'name' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
@ -32,7 +32,7 @@ export default function CompanyTable({ records, onEdit, onDelete }: CompanyTable
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!searchQuery) return records; if (!searchQuery) return records;
return records.filter(record => return records.filter(record =>
record.code.toLowerCase().includes(searchQuery.toLowerCase()) || record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.name.toLowerCase().includes(searchQuery.toLowerCase()) || record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.status.toLowerCase().includes(searchQuery.toLowerCase()) record.status.toLowerCase().includes(searchQuery.toLowerCase())
); );
@ -95,7 +95,7 @@ export default function CompanyTable({ records, onEdit, onDelete }: CompanyTable
<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 <input
type="text" type="text"
placeholder="Search code, name, status..." placeholder="Search id, name, status..."
value={searchQuery} value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }} onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
className="pl-9 pr-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 w-full" className="pl-9 pr-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 w-full"
@ -107,14 +107,14 @@ export default function CompanyTable({ records, onEdit, onDelete }: CompanyTable
<table className="min-w-full divide-y divide-app-border"> <table className="min-w-full divide-y divide-app-border">
<thead> <thead>
<tr> <tr>
{(['code', 'name', 'status'] as SortKey[]).map((key) => ( {(['id', 'name', 'status'] as SortKey[]).map((key) => (
<th <th
key={key} key={key}
onClick={() => requestSort(key)} onClick={() => requestSort(key)}
className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors" className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
> >
<div className="flex items-center"> <div className="flex items-center">
{key === 'code' ? 'Company Code' : key.charAt(0).toUpperCase() + key.slice(1)} {getSortIcon(key)} {key === 'id' ? 'Company ID' : key.charAt(0).toUpperCase() + key.slice(1)} {getSortIcon(key)}
</div> </div>
</th> </th>
))} ))}
@ -125,7 +125,7 @@ export default function CompanyTable({ records, onEdit, onDelete }: CompanyTable
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((record) => ( paginatedData.map((record) => (
<tr key={record.id} className="hover:bg-app-muted transition-colors"> <tr key={record.id} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.code}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm"> <td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span> <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
@ -134,7 +134,7 @@ export default function CompanyTable({ records, onEdit, onDelete }: CompanyTable
<button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center"> <button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center">
<Pencil size={16} className="mr-1" /> Edit <Pencil size={16} className="mr-1" /> Edit
</button> </button>
<button onClick={() => onDelete(record)} className="text-absent-500 hover:text-absent-700 flex items-center"> <button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
<Trash2 size={16} className="mr-1" /> Delete <Trash2 size={16} className="mr-1" /> Delete
</button> </button>
</td> </td>

View File

@ -5,52 +5,38 @@ import type { Company } from '../../api/useCompanyData';
import type { Branch } from '../../api/useBranchData'; import type { Branch } from '../../api/useBranchData';
interface DepartmentFormModalProps { interface DepartmentFormModalProps {
department: Department | null; department: Department | null; // null means Add mode
companies: Company[]; companies: Company[];
branches: Branch[]; branches: Branch[];
onClose: () => void; onClose: () => void;
onSave: (data: { name: string; companyId: number; branchId: number; status: 'Active' | 'Disabled' }) => void; onSave: (data: Omit<Department, 'id'>) => void;
} }
export default function DepartmentFormModal({ department, companies, branches, onClose, onSave }: DepartmentFormModalProps) { export default function DepartmentFormModal({ department, companies, branches, onClose, onSave }: DepartmentFormModalProps) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [companyId, setCompanyId] = useState<string>(''); const [companyId, setCompanyId] = useState('');
const [branchId, setBranchId] = useState<string>(''); const [branchId, setBranchId] = useState('');
const [status, setStatus] = useState<'Active' | 'Disabled'>('Active'); const [status, setStatus] = useState<'Active' | 'Disabled'>('Active');
useEffect(() => { useEffect(() => {
if (department) { if (department) {
// eslint-disable-next-line react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect
setName(department.name); setName(department.name);
setCompanyId(department.companyId);
setBranchId(department.branchId);
setStatus(department.status); setStatus(department.status);
if (department.companyId) {
setCompanyId(String(department.companyId));
} else {
const matchedCompany = companies.find(c => c.name === department.companyName);
setCompanyId(matchedCompany ? String(matchedCompany.id) : (companies.length > 0 ? String(companies[0].id) : ''));
}
if (department.branchId) {
setBranchId(String(department.branchId));
} else {
const matchedBranch = branches.find(b => b.name === department.branchName);
setBranchId(matchedBranch ? String(matchedBranch.id) : '');
}
} else { } else {
setName(''); setName('');
setCompanyId(companies.length > 0 ? String(companies[0].id) : ''); setCompanyId(companies.length > 0 ? companies[0].id : '');
setBranchId(''); setBranchId('');
setStatus('Active'); setStatus('Active');
} }
}, [department, companies, branches]); }, [department, companies]);
// Filter branches by matching the Company Name // Filter branches based on selected company
const filteredBranches = useMemo(() => { const filteredBranches = useMemo(() => {
const selectedCompany = companies.find(c => String(c.id) === companyId); return branches.filter(b => b.companyId === companyId);
if (!selectedCompany) return []; }, [branches, companyId]);
return branches.filter(b => b.companyName === selectedCompany.name);
}, [branches, companyId, companies]);
const handleCompanyChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleCompanyChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setCompanyId(e.target.value); setCompanyId(e.target.value);
@ -59,21 +45,19 @@ export default function DepartmentFormModal({ department, companies, branches, o
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!companyId || !branchId) { const selectedCompany = companies.find(c => c.id === companyId);
alert("Please select a company and branch"); const selectedBranch = branches.find(b => b.id === branchId);
return;
}
onSave({ onSave({
name, name,
companyId: Number(companyId), companyId,
branchId: Number(branchId), companyName: selectedCompany?.name || '',
branchId,
branchName: selectedBranch?.name || '',
status, 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">
@ -92,7 +76,7 @@ export default function DepartmentFormModal({ department, companies, branches, o
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
required required
className={inputClass} 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"
placeholder="Enter department name..." placeholder="Enter department name..."
/> />
</div> </div>
@ -103,7 +87,7 @@ export default function DepartmentFormModal({ department, companies, branches, o
value={companyId} value={companyId}
onChange={handleCompanyChange} onChange={handleCompanyChange}
required required
className={inputClass} 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.length === 0 ? ( {companies.length === 0 ? (
<option value="">No companies available</option> <option value="">No companies available</option>
@ -120,7 +104,7 @@ export default function DepartmentFormModal({ department, companies, branches, o
onChange={(e) => setBranchId(e.target.value)} onChange={(e) => setBranchId(e.target.value)}
required required
disabled={!companyId || filteredBranches.length === 0} disabled={!companyId || filteredBranches.length === 0}
className={`${inputClass} disabled:opacity-50 disabled:cursor-not-allowed`} 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="">Select Branch</option> <option value="">Select Branch</option>
{filteredBranches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)} {filteredBranches.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
@ -135,7 +119,7 @@ export default function DepartmentFormModal({ department, companies, branches, o
<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={inputClass} 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="Active">Active</option> <option value="Active">Active</option>
<option value="Disabled">Disabled</option> <option value="Disabled">Disabled</option>

View File

@ -5,11 +5,10 @@ import type { Department } from '../../api/useDepartmentData';
interface DepartmentTableProps { interface DepartmentTableProps {
records: Department[]; records: Department[];
onEdit: (department: Department) => void; onEdit: (department: Department) => void;
onDelete: (department: Department) => void; onDelete: (id: string) => void;
} }
// Removed 'id', added 'code' type SortKey = 'id' | 'name' | 'branchName' | 'companyName' | 'status';
type SortKey = 'code' | 'name' | 'branchName' | 'companyName' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
@ -33,7 +32,7 @@ export default function DepartmentTable({ records, onEdit, onDelete }: Departmen
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!searchQuery) return records; if (!searchQuery) return records;
return records.filter(record => return records.filter(record =>
record.code.toLowerCase().includes(searchQuery.toLowerCase()) || record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.name.toLowerCase().includes(searchQuery.toLowerCase()) || record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) || record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) || record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -98,7 +97,7 @@ export default function DepartmentTable({ records, onEdit, onDelete }: Departmen
<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 <input
type="text" type="text"
placeholder="Search code, name, branch..." placeholder="Search id, name, branch..."
value={searchQuery} value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }} onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
className="pl-9 pr-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 w-full" className="pl-9 pr-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 w-full"
@ -110,14 +109,14 @@ export default function DepartmentTable({ records, onEdit, onDelete }: Departmen
<table className="min-w-full divide-y divide-app-border"> <table className="min-w-full divide-y divide-app-border">
<thead> <thead>
<tr> <tr>
{(['code', 'name', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => ( {(['id', 'name', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => (
<th <th
key={key} key={key}
onClick={() => requestSort(key)} onClick={() => requestSort(key)}
className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors" className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"
> >
<div className="flex items-center"> <div className="flex items-center">
{key === 'code' ? 'Dept Code' : key === 'name' ? 'Department' : key === 'branchName' ? 'Branch' : key === 'companyName' ? 'Company' : key.charAt(0).toUpperCase() + key.slice(1)} {getSortIcon(key)} {key === 'id' ? 'Dept ID' : key === 'name' ? 'Department' : key === 'branchName' ? 'Branch' : key === 'companyName' ? 'Company' : key.charAt(0).toUpperCase() + key.slice(1)} {getSortIcon(key)}
</div> </div>
</th> </th>
))} ))}
@ -128,7 +127,7 @@ export default function DepartmentTable({ records, onEdit, onDelete }: Departmen
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((record) => ( paginatedData.map((record) => (
<tr key={record.id} className="hover:bg-app-muted transition-colors"> <tr key={record.id} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.code}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td>
@ -139,7 +138,7 @@ export default function DepartmentTable({ records, onEdit, onDelete }: Departmen
<button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center"> <button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center">
<Pencil size={16} className="mr-1" /> Edit <Pencil size={16} className="mr-1" /> Edit
</button> </button>
<button onClick={() => onDelete(record)} className="text-absent-500 hover:text-absent-700 flex items-center"> <button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
<Trash2 size={16} className="mr-1" /> Delete <Trash2 size={16} className="mr-1" /> Delete
</button> </button>
</td> </td>

View File

@ -0,0 +1,133 @@
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>
);
}

View File

@ -1,14 +1,14 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Trash2 } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Pencil, Trash2 } from 'lucide-react';
import type { JobRole } from '../../api/useJobRoleData'; import type { Designation } from '../../api/useDesignationData';
interface JobRoleTableProps { interface DesignationTableProps {
records: JobRole[]; records: Designation[];
onView: (jobRole: JobRole) => void; onEdit: (designation: Designation) => void;
onDelete: (jobRole: JobRole) => void; onDelete: (id: string) => void;
} }
type SortKey = 'code' | 'name' | 'deptName' | 'branchName' | 'companyName' | 'status'; type SortKey = 'id' | 'name' | 'deptName' | 'branchName' | 'companyName' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
@ -19,7 +19,7 @@ const getStatusClass = (status: string) => {
} }
}; };
export default function JobRoleTable({ records, onView, onDelete }: JobRoleTableProps) { export default function DesignationTable({ records, onEdit, onDelete }: DesignationTableProps) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
@ -31,7 +31,7 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!searchQuery) return records; if (!searchQuery) return records;
return records.filter(record => return records.filter(record =>
record.code.toLowerCase().includes(searchQuery.toLowerCase()) || record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.name.toLowerCase().includes(searchQuery.toLowerCase()) || record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.deptName.toLowerCase().includes(searchQuery.toLowerCase()) || record.deptName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) || record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -90,8 +90,8 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
}; };
const formatHeader = (key: string) => { const formatHeader = (key: string) => {
if (key === 'code') return 'Job Code'; if (key === 'id') return 'Designation ID';
if (key === 'name') return 'Job Role'; if (key === 'name') return 'Designation';
if (key === 'deptName') return 'Department'; if (key === 'deptName') return 'Department';
if (key === 'branchName') return 'Branch'; if (key === 'branchName') return 'Branch';
if (key === 'companyName') return 'Company'; if (key === 'companyName') return 'Company';
@ -101,12 +101,12 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
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 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">Job Roles List</h2> <h2 className="text-lg font-semibold text-text-primary">Designations List</h2>
<div className="relative w-full md:w-64"> <div className="relative w-full md:w-64">
<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 <input
type="text" type="text"
placeholder="Search code, role, dept..." placeholder="Search id, name, dept..."
value={searchQuery} value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }} onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
className="pl-9 pr-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 w-full" className="pl-9 pr-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 w-full"
@ -118,7 +118,7 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
<table className="min-w-full divide-y divide-app-border"> <table className="min-w-full divide-y divide-app-border">
<thead> <thead>
<tr> <tr>
{(['code', 'name', 'deptName', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => ( {(['id', 'name', 'deptName', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => (
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"> <th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
<div className="flex items-center"> <div className="flex items-center">
{formatHeader(key)} {getSortIcon(key)} {formatHeader(key)} {getSortIcon(key)}
@ -132,7 +132,7 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((record) => ( paginatedData.map((record) => (
<tr key={record.id} className="hover:bg-app-muted transition-colors"> <tr key={record.id} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.code}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.deptName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.deptName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td>
@ -141,10 +141,10 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span> <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm flex space-x-3"> <td className="px-6 py-4 whitespace-nowrap text-sm flex space-x-3">
<button onClick={() => onView(record)} className="text-primary hover:text-primary-hover flex items-center"> <button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center">
<Eye size={16} className="mr-1" /> View <Pencil size={16} className="mr-1" /> Edit
</button> </button>
<button onClick={() => onDelete(record)} className="text-absent-500 hover:text-absent-700 flex items-center"> <button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
<Trash2 size={16} className="mr-1" /> Delete <Trash2 size={16} className="mr-1" /> Delete
</button> </button>
</td> </td>
@ -152,7 +152,7 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan={7} className="px-6 py-8 text-center text-sm text-text-muted">No job roles found.</td> <td colSpan={7} className="px-6 py-8 text-center text-sm text-text-muted">No designations found.</td>
</tr> </tr>
)} )}
</tbody> </tbody>
@ -172,7 +172,7 @@ export default function JobRoleTable({ records, onView, onDelete }: JobRoleTable
{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 => (

View File

@ -0,0 +1,157 @@
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 [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);
setCompanyId(employee.companyId);
setBranchId(employee.branchId);
setDeptId(employee.deptId);
setDesignationId(employee.designationId);
setStatus(employee.status);
} else {
setName('');
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,
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}>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>
);
}

View File

@ -1,13 +1,15 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye } from 'lucide-react'; import { ArrowUpDown, ArrowUp, ArrowDown, Search, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Pencil, Trash2 } from 'lucide-react'; // Added Eye
import type { Employee } from '../../api/useEmployeeData'; import type { Employee } from '../../api/useEmployeeData';
interface EmployeeTableProps { interface EmployeeTableProps {
records: Employee[]; records: Employee[];
onView: (employee: Employee) => void; onView: (employee: Employee) => void; // Added onView
onEdit: (employee: Employee) => void;
onDelete: (id: string) => void;
} }
type SortKey = 'code' | 'firstName' | 'lastName' | 'email' | 'jobName' | 'deptName' | 'branchName' | 'companyName' | 'status'; type SortKey = 'id' | 'name' | 'designationName' | 'deptName' | 'branchName' | 'companyName' | 'status';
type SortDirection = 'ascending' | 'descending'; type SortDirection = 'ascending' | 'descending';
const getStatusClass = (status: string) => { const getStatusClass = (status: string) => {
@ -18,23 +20,21 @@ const getStatusClass = (status: string) => {
} }
}; };
export default function EmployeeTable({ records, onView }: EmployeeTableProps) { export default function EmployeeTable({ records, onView, onEdit, onDelete }: EmployeeTableProps) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({ const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
key: 'firstName', direction: 'ascending' key: 'name', direction: 'ascending'
}); });
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
if (!searchQuery) return records; if (!searchQuery) return records;
return records.filter(record => return records.filter(record =>
record.code.toLowerCase().includes(searchQuery.toLowerCase()) || record.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.firstName.toLowerCase().includes(searchQuery.toLowerCase()) || record.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.lastName.toLowerCase().includes(searchQuery.toLowerCase()) || record.designationName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.jobName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.deptName.toLowerCase().includes(searchQuery.toLowerCase()) || record.deptName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) || record.branchName.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) || record.companyName.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -92,11 +92,9 @@ export default function EmployeeTable({ records, onView }: EmployeeTableProps) {
}; };
const formatHeader = (key: string) => { const formatHeader = (key: string) => {
if (key === 'code') return 'Emp Code'; if (key === 'id') return 'Emp ID';
if (key === 'firstName') return 'First Name'; if (key === 'name') return 'Employee Name';
if (key === 'lastName') return 'Last Name'; if (key === 'designationName') return 'Designation';
if (key === 'email') return 'Work Email';
if (key === 'jobName') return 'Job Role';
if (key === 'deptName') return 'Department'; if (key === 'deptName') return 'Department';
if (key === 'branchName') return 'Branch'; if (key === 'branchName') return 'Branch';
if (key === 'companyName') return 'Company'; if (key === 'companyName') return 'Company';
@ -111,7 +109,7 @@ export default function EmployeeTable({ records, onView }: EmployeeTableProps) {
<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 <input
type="text" type="text"
placeholder="Search code, name, role..." placeholder="Search id, name, role..."
value={searchQuery} value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }} onChange={(e) => { setSearchQuery(e.target.value); setCurrentPage(1); }}
className="pl-9 pr-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 w-full" className="pl-9 pr-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 w-full"
@ -123,7 +121,7 @@ export default function EmployeeTable({ records, onView }: EmployeeTableProps) {
<table className="min-w-full divide-y divide-app-border"> <table className="min-w-full divide-y divide-app-border">
<thead> <thead>
<tr> <tr>
{(['code', 'firstName', 'lastName', 'email', 'jobName', 'deptName', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => ( {(['id', 'name', 'designationName', 'deptName', 'branchName', 'companyName', 'status'] as SortKey[]).map((key) => (
<th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors"> <th key={key} onClick={() => requestSort(key)} className="px-6 py-3 text-left text-xs font-semibold text-text-muted uppercase tracking-wider cursor-pointer hover:bg-app-muted transition-colors">
<div className="flex items-center"> <div className="flex items-center">
{formatHeader(key)} {getSortIcon(key)} {formatHeader(key)} {getSortIcon(key)}
@ -137,27 +135,31 @@ export default function EmployeeTable({ records, onView }: EmployeeTableProps) {
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((record) => ( paginatedData.map((record) => (
<tr key={record.id} className="hover:bg-app-muted transition-colors"> <tr key={record.id} className="hover:bg-app-muted transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.code}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">{record.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.firstName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.lastName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.designationName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.email}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.jobName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.deptName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.deptName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.branchName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">{record.companyName}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm"> <td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span> <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(record.status)}`}>{record.status}</span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm"> <td className="px-6 py-4 whitespace-nowrap text-sm flex space-x-3">
<button onClick={() => onView(record)} className="text-primary hover:text-primary-hover flex items-center"> <button onClick={() => onView(record)} className="text-primary hover:text-primary-hover flex items-center">
<Eye size={16} className="mr-1" /> View <Eye size={16} className="mr-1" /> View
</button> </button>
<button onClick={() => onEdit(record)} className="text-primary hover:text-primary-hover flex items-center">
<Pencil size={16} className="mr-1" /> Edit
</button>
<button onClick={() => onDelete(record.id)} className="text-absent-500 hover:text-absent-700 flex items-center">
<Trash2 size={16} className="mr-1" /> Delete
</button>
</td> </td>
</tr> </tr>
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan={10} className="px-6 py-8 text-center text-sm text-text-muted">No employees found.</td> <td colSpan={8} className="px-6 py-8 text-center text-sm text-text-muted">No employees found.</td>
</tr> </tr>
)} )}
</tbody> </tbody>

Some files were not shown because too many files have changed in this diff Show More