Compare commits
2 Commits
cf568582de
...
82ebe286a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 82ebe286a8 | |||
| f430c530ca |
194
.gitignore
vendored
194
.gitignore
vendored
@ -1,138 +1,90 @@
|
||||
# ---> Node
|
||||
# =====================
|
||||
# Logs
|
||||
# =====================
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# =====================
|
||||
# Dependencies
|
||||
# =====================
|
||||
node_modules
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
# =====================
|
||||
# Build Output
|
||||
# =====================
|
||||
dist
|
||||
dist-ssr
|
||||
build
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
# =====================
|
||||
# Environment Files
|
||||
# =====================
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
# Local env files
|
||||
*.local
|
||||
|
||||
# =====================
|
||||
# Docker
|
||||
# =====================
|
||||
|
||||
# Docker volumes/data
|
||||
docker-data/
|
||||
volumes/
|
||||
|
||||
# Docker override files
|
||||
docker-compose.override.yml
|
||||
|
||||
# Temporary docker files
|
||||
*.pid
|
||||
|
||||
# =====================
|
||||
# IDEs
|
||||
# =====================
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
.idea
|
||||
|
||||
# =====================
|
||||
# OS Files
|
||||
# =====================
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# =====================
|
||||
# Visual Studio
|
||||
# =====================
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# =====================
|
||||
# Cache
|
||||
# =====================
|
||||
.cache
|
||||
.parcel-cache
|
||||
.eslintcache
|
||||
.vite
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
# =====================
|
||||
# Test Reports
|
||||
# =====================
|
||||
coverage/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# =====================
|
||||
# Misc
|
||||
# =====================
|
||||
*.tgz
|
||||
75
README.md
75
README.md
@ -1,2 +1,75 @@
|
||||
# hrms-frontend
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
22
eslint.config.js
Normal file
22
eslint.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
16
index.html
Normal file
16
index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="clri_logo_fav.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CLRI-HRMS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3310
package-lock.json
generated
Normal file
3310
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
Normal file
42
package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "clri-hrms",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.12.0",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"lucide-react": "^1.23.0",
|
||||
"protobufjs": "^8.6.5",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-redux": "^9.3.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"sonner": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"autoprefixer": "^10.5.2",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"postcss": "^8.5.16",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.62.0",
|
||||
"vite": "^8.1.1"
|
||||
}
|
||||
}
|
||||
94
public/clri_logo.svg
Normal file
94
public/clri_logo.svg
Normal file
@ -0,0 +1,94 @@
|
||||
<svg width="9085" height="1492" viewBox="0 0 9085 1492" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M695.725 800.703C692.581 795.937 688.018 789.547 682.034 782.448C677.673 777.276 658.201 754.558 623.922 734.172C623.922 734.172 598.67 719.162 569.766 710.035C515.407 692.895 434.273 695.43 378.596 718.554C242.799 774.943 148.481 999.383 241.582 1154.45C294.623 1242.89 397.459 1290.15 485.691 1292.18C508.003 1292.69 568.143 1294.01 628.588 1256.28C660.635 1236.2 681.527 1212.26 693.798 1195.63C771.281 1196.24 848.662 1196.95 926.144 1197.56C915.597 1231.43 893.691 1286.7 847.14 1341.77C832.334 1359.32 781.118 1416.93 693.089 1455.36C605.566 1493.6 529.503 1492.08 481.635 1490.56C441.575 1489.34 364.499 1486.2 276.672 1444.21C255.881 1434.27 197.668 1404.55 139.658 1345.02C78.1993 1281.94 48.18 1217.43 35.1987 1185.08C0.412773 1098.67 -0.0943219 1025.55 0.00709474 988.43C0.00709474 947.457 0.91983 874.942 36.5171 790.257C45.5432 768.655 81.7489 687.114 166.432 613.382C196.755 586.912 263.69 535.087 361.557 508.312C415.207 493.708 459.222 491.679 477.984 491.375C494.21 491.071 539.037 491.071 594.613 503.748C632.239 512.369 716.617 532.551 797.548 601.617C883.549 674.943 918.538 762.164 932.026 803.036C885.983 801.616 838.926 800.703 791.057 800.399C758.908 800.196 727.165 800.297 695.827 800.602L695.725 800.703Z" fill="url(#paint0_linear_122_7)"/>
|
||||
<path d="M1040.03 513.992V1471.19H1529.06V1300.6H1227.96C1228.57 1038.43 1229.07 776.161 1229.68 513.992H1040.03Z" fill="url(#paint1_linear_122_7)"/>
|
||||
<path d="M1630.89 513.891C1630.48 833.158 1629.97 1152.32 1629.57 1471.59C1692.95 1471.39 1756.34 1471.29 1819.62 1471.08V513.891C1756.74 513.891 1693.77 513.891 1630.89 513.891Z" fill="url(#paint2_linear_122_7)"/>
|
||||
<path d="M1979.76 513.992V1471.69L2162.51 1471.19V859.628C2292.13 1063.48 2421.84 1267.33 2551.45 1471.19H2723.75V513.992H2541C2541 709.021 2540.8 904.05 2540.8 1099.08C2413.01 904.05 2285.13 709.021 2157.34 513.992H1979.86H1979.76Z" fill="url(#paint3_linear_122_7)"/>
|
||||
<path d="M3073.44 513.992H2885.51V1471.19H3073.44V513.992Z" fill="url(#paint4_linear_122_7)"/>
|
||||
<path d="M3617.74 513.992V1471.69L4103.33 1471.19V1298.67H3800.5V514.093H3617.74V513.992Z" fill="url(#paint5_linear_122_7)"/>
|
||||
<path d="M4166.91 1471.19L4529.78 513.992H4694.28L5054.51 1471.19H4832.61C4803.91 1383.36 4775.21 1295.53 4746.51 1207.7C4700.36 1068.45 4654.32 929.202 4608.18 790.055C4570.55 891.271 4532.93 992.386 4495.3 1093.6C4448.55 1219.46 4401.69 1345.32 4354.94 1471.19H4167.01H4166.91Z" fill="url(#paint6_linear_122_7)"/>
|
||||
<path d="M5248.93 515.108C5248.01 642.592 5247.2 770.075 5246.29 897.559C5247.61 994.617 5248.93 1091.57 5250.25 1188.63C5250.25 1195.12 5251.16 1203.64 5253.69 1213.28C5256.63 1223.93 5260.79 1232.34 5264.34 1238.33C5267.89 1244.72 5273.06 1252.42 5280.47 1260.23C5291.22 1271.49 5302.27 1278.49 5310.28 1282.55C5321.64 1286.4 5338.07 1290.56 5358.15 1291.27C5377.93 1291.98 5394.36 1289.14 5405.92 1286.2C5413.32 1282.85 5425.39 1276.06 5436.85 1263.58C5448.31 1251 5453.89 1238.12 5456.53 1230.42C5458.56 1224.13 5460.58 1217.13 5462.31 1209.42C5464.03 1201.51 5465.15 1194.21 5465.86 1187.52C5468.39 1091.78 5470.93 995.936 5473.46 900.196C5473.06 770.988 5472.55 641.78 5472.14 512.572C5538.27 513.079 5604.39 513.587 5670.52 513.992L5664.03 881.941L5670.52 1136.5C5669.81 1158.21 5668.79 1180.52 5667.27 1203.24C5665.34 1233.66 5664.53 1242.49 5664.03 1247.45C5660.78 1276.46 5658.75 1293.7 5651.15 1314.9C5640.9 1343.4 5626.4 1362.67 5618.49 1373.01C5605.2 1390.35 5593.64 1400.39 5573.46 1418.04C5551.05 1437.62 5535.02 1451.31 5509.97 1463.27C5493.85 1470.98 5479.95 1475.04 5472.55 1477.17C5453.99 1482.54 5437.16 1485.38 5422.96 1486.7C5404.09 1487.31 5384.32 1487.31 5363.63 1486.7C5334.42 1485.89 5307.04 1483.76 5281.48 1480.92C5266.78 1478.99 5246.9 1474.84 5224.89 1465.51C5207.35 1458 5195.99 1450.29 5175.7 1436.6C5154.61 1422.3 5140.11 1412.46 5123.07 1395.73C5111.61 1384.47 5094.57 1367.54 5079.97 1340.25C5076.72 1334.17 5067.39 1316.12 5061 1290.66C5055.42 1268.25 5054.71 1250.7 5054.21 1235.59C5053.4 1207.7 5052.79 1168.45 5054.21 1119.46V513.992C5119.01 514.398 5183.82 514.803 5248.72 515.108H5248.93Z" fill="url(#paint7_linear_122_7)"/>
|
||||
<path d="M5818.89 521.092V1478.79L6001.64 1478.29V866.728C6131.25 1070.58 6260.96 1274.43 6390.58 1478.29H6562.88V521.092H6380.13C6380.13 716.121 6379.93 911.15 6379.93 1106.18C6252.14 911.15 6124.25 716.121 5996.47 521.092H5818.99H5818.89Z" fill="url(#paint8_linear_122_7)"/>
|
||||
<path d="M7380.1 800.703C7376.96 795.937 7372.39 789.547 7366.41 782.448C7362.05 777.276 7342.58 754.558 7308.3 734.172C7308.3 734.172 7283.04 719.162 7254.14 710.035C7199.78 692.895 7118.65 695.43 7062.97 718.554C6927.17 774.943 6832.86 999.383 6925.96 1154.45C6979 1242.89 7081.83 1290.15 7170.07 1292.18C7192.38 1292.69 7252.52 1294.01 7312.96 1256.28C7345.01 1236.2 7365.9 1212.26 7378.17 1195.63C7455.66 1196.24 7533.04 1196.95 7610.52 1197.56C7599.97 1231.43 7578.07 1286.7 7531.52 1341.77C7516.71 1359.32 7465.49 1416.93 7377.46 1455.36C7289.94 1493.6 7213.88 1492.08 7166.01 1490.56C7125.95 1489.34 7048.87 1486.2 6961.05 1444.21C6940.26 1434.27 6882.04 1404.55 6824.03 1345.02C6762.57 1281.94 6732.55 1217.43 6719.57 1185.08C6684.79 1098.67 6684.28 1025.55 6684.38 988.43C6684.38 947.457 6685.29 874.942 6720.89 790.257C6729.92 768.655 6766.12 687.114 6850.81 613.382C6881.13 586.912 6948.07 535.087 7045.93 508.312C7099.58 493.708 7143.6 491.679 7162.36 491.375C7178.59 491.071 7223.41 491.071 7278.99 503.748C7316.61 512.369 7400.99 532.551 7481.92 601.617C7567.92 674.943 7602.91 762.164 7616.4 803.036C7570.36 801.616 7523.3 800.703 7475.43 800.399C7443.28 800.196 7411.54 800.297 7380.2 800.602L7380.1 800.703Z" fill="url(#paint9_linear_122_7)"/>
|
||||
<path d="M7736.27 513.992L7725.83 1471.19H7921.56V1080.82H8213.95V1471.19H8412.32V520.99C8349.64 519.672 8287.07 518.252 8224.39 516.933C8218.41 644.823 8212.53 772.712 8206.54 900.602C8109.89 901.514 8013.14 902.427 7916.49 903.34C7918.22 773.523 7919.84 643.707 7921.56 513.992H7736.17H7736.27Z" fill="url(#paint10_linear_122_7)"/>
|
||||
<path d="M8353.59 71.8047H8464.54V368.151H8548.11V71.8047H8659.06V0H8353.59V71.8047Z" fill="black"/>
|
||||
<path d="M8702.16 0V368.05H8783.09C8781.37 279.207 8779.65 190.364 8777.92 101.52C8801.35 190.364 8824.88 279.207 8848.31 368.05H8926.6C8949.62 277.077 8972.74 186.205 8995.76 95.2325C8998.4 186.205 9000.94 277.077 9003.57 368.05H9084.5V0H8952.66C8930.96 84.6849 8909.26 169.471 8887.55 254.156C8864.94 169.471 8842.22 84.6849 8819.6 0H8702.16Z" fill="black"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_122_7" x1="459.12" y1="601.415" x2="480.013" y2="1520.37" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_122_7" x1="1266.29" y1="583.16" x2="1287.19" y2="1502.02" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_122_7" x1="1715.16" y1="572.917" x2="1735.96" y2="1491.77" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_122_7" x1="2341.82" y1="558.718" x2="2362.71" y2="1477.57" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_122_7" x1="2969.39" y1="544.418" x2="2990.28" y2="1463.27" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_122_7" x1="3840.96" y1="524.641" x2="3861.85" y2="1443.5" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_122_7" x1="4588.81" y1="507.603" x2="4609.7" y2="1426.46" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_122_7" x1="5353.69" y1="490.26" x2="5374.58" y2="1409.12" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_122_7" x1="6178.92" y1="471.498" x2="6199.81" y2="1390.35" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_122_7" x1="7140.05" y1="449.59" x2="7160.94" y2="1368.55" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_122_7" x1="8056.24" y1="428.8" x2="8077.14" y2="1347.66" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.08" stop-color="#00AAE9"/>
|
||||
<stop offset="0.23" stop-color="#039FE1"/>
|
||||
<stop offset="0.5" stop-color="#0B83CB"/>
|
||||
<stop offset="0.84" stop-color="#1955A9"/>
|
||||
<stop offset="1" stop-color="#213E98"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
32
public/clri_logo_fav.svg
Normal file
32
public/clri_logo_fav.svg
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.2" viewBox="0 0 150 150" preserveAspectRatio="xMidYMid meet" id="svg131" baseProfile="tiny-ps" xml:space="preserve">
|
||||
<title>CliniLaunch Logo</title>
|
||||
<circle cx="75" cy="75" r="70" fill="#ffffff" />
|
||||
<defs id="defs131"/>
|
||||
<g id="g234">
|
||||
<g fill="#058dec" id="g119">
|
||||
<path d="m 71.13,72.04 c -0.12,-0.44 -0.12,-2.43 0,-2.86 0.06,-0.19 0.19,-0.34 0.29,-0.34 0.16,0 0.21,0.35 0.21,1.76 0,1.41 -0.04,1.76 -0.21,1.76 -0.1,0 -0.23,-0.15 -0.29,-0.32 z" id="path118"/>
|
||||
<path d="m 118.39,65 0.04,-5.3 0.48,-1 c 0.82,-1.66 2.17,-2.52 3.91,-2.5 1.67,0.01 3.18,1.04 3.93,2.67 0.31,0.66 0.32,0.92 0.37,6.01 0.04,5.27 0.04,5.3 -0.25,5.3 -0.25,0 -0.31,-0.09 -0.31,-0.51 0,-0.48 -0.03,-0.51 -0.47,-0.53 -0.26,0 -1.54,-0.04 -2.83,-0.07 -4.26,-0.13 -4.32,-0.13 -4.32,0.69 0,0.48 -0.04,0.57 -0.31,0.57 -0.29,0 -0.31,-0.01 -0.25,-5.32 z" id="path119"/>
|
||||
</g>
|
||||
<g fill="#0473d9" id="g122">
|
||||
<path d="m 124.29,75.15 c -0.4,-0.07 -1.9,-0.13 -3.33,-0.13 l -2.61,-0.01 0.01,-2.37 c 0.01,-2.23 0.21,-5.49 0.25,-4.38 0.03,0.44 0.06,0.47 0.28,0.29 0.13,-0.12 0.26,-0.19 0.29,-0.16 0.13,0.09 0.34,0.01 0.26,-0.12 -0.04,-0.07 0.01,-0.15 0.15,-0.15 0.12,0 0.26,0.09 0.29,0.19 0.06,0.12 0.48,0.18 1.46,0.16 0.91,0 1.39,0.04 1.39,0.15 0,0.1 0.04,0.12 0.12,0.01 0.15,-0.21 1.54,-0.22 1.67,-0.01 0.07,0.1 0.12,0.1 0.12,-0.03 0,-0.1 0.07,-0.15 0.15,-0.1 0.09,0.06 0.25,0.03 0.34,-0.06 0.13,-0.1 0.31,-0.09 0.57,0.04 0.21,0.1 0.54,0.21 0.76,0.21 0.34,0 0.38,-0.07 0.41,-0.54 0.04,-1.22 0.23,2.17 0.25,4.57 l 0.01,2.59 -1.05,-0.01 c -0.59,-0.01 -1.39,-0.07 -1.8,-0.13 z" id="path120"/>
|
||||
<path d="m 56.94,68.64 c -0.66,-0.25 -1.46,-0.85 -4.83,-3.65 l -2.45,-2.05 0.03,-5.87 0.04,-5.87 0.51,-0.04 c 0.28,-0.03 0.92,0.06 1.42,0.19 0.51,0.15 1.03,0.21 1.14,0.16 0.13,-0.04 0.37,-0.38 0.53,-0.75 l 0.29,-0.67 -0.44,-0.51 c -0.38,-0.42 -0.45,-0.63 -0.45,-1.25 0,-0.66 0.06,-0.81 0.5,-1.25 0.44,-0.44 0.59,-0.5 1.29,-0.5 1.25,0 1.88,0.66 1.88,1.95 0,0.79 -0.34,1.26 -1.16,1.64 -0.47,0.21 -0.63,0.38 -0.75,0.79 -0.26,0.94 -0.19,1.26 0.41,1.8 1.27,1.16 3.03,2.17 3.74,2.17 0.48,0 1.68,-0.64 2.33,-1.23 0.5,-0.45 0.54,-0.56 0.41,-0.89 -0.26,-0.7 -0.18,-1.89 0.19,-2.43 0.72,-1.07 2.18,-1.45 3.33,-0.86 1.44,0.73 1.82,2.58 0.79,3.81 -0.59,0.69 -1.11,0.89 -2.21,0.86 -0.86,-0.03 -0.98,0 -1.23,0.32 -0.41,0.54 -0.72,1.57 -0.72,2.42 0,0.69 0.09,0.89 0.88,2.07 1.54,2.29 1.71,4.41 0.57,6.67 -0.44,0.86 -1.76,2.2 -2.65,2.68 -0.92,0.5 -2.49,0.64 -3.38,0.31 z" id="path121"/>
|
||||
<path d="M 81.96,61.07 C 81.4,61 78.77,60.95 76.1,60.97 L 71.27,61 71.31,60.63 c 0.09,-0.64 0.76,-1.86 1.3,-2.34 0.31,-0.25 0.83,-0.6 1.2,-0.78 0.66,-0.31 0.85,-0.31 11.15,-0.35 13.07,-0.06 14.05,0.03 16.93,1.45 1.29,0.63 2.92,1.85 2.92,2.18 0,0.31 -0.7,0.34 -1.86,0.1 -0.56,-0.12 -1.05,-0.16 -1.17,-0.09 -0.25,0.16 -3.05,0.18 -3.05,0.01 0,-0.15 -7.02,-0.07 -7.4,0.06 -0.13,0.06 -0.78,0.06 -1.42,0 -0.78,-0.06 -1.25,-0.04 -1.36,0.06 -0.13,0.12 -0.92,0.15 -2.33,0.1 -1.17,-0.03 -2.14,0 -2.14,0.07 0,0.13 -0.69,0.12 -2.12,-0.04 z" id="path122"/>
|
||||
</g>
|
||||
<g fill="#055fcb" id="g126">
|
||||
<path d="m 41.15,116.94 c -0.15,-0.03 -0.29,-0.21 -0.34,-0.38 -0.04,-0.18 -0.09,-2.05 -0.09,-4.16 v -3.82 l -1.13,-0.1 c -2.9,-0.23 -6.68,-1.08 -9.16,-2.07 -0.69,-0.26 -0.69,-0.26 -1.03,0.04 -1.26,1.19 -3.4,0.97 -4.32,-0.42 -0.34,-0.5 -0.41,-0.78 -0.41,-1.54 l -0.01,-0.94 -1.3,-0.97 C 19.52,99.77 16.59,96.27 14.47,91.97 13.31,89.63 12.58,87.52 12.07,85 c -0.21,-1.03 -0.38,-1.88 -0.4,-1.88 -0.01,0 -0.29,-0.13 -0.62,-0.29 -1.05,-0.54 -1.6,-1.76 -1.25,-2.9 0.19,-0.64 1,-1.57 1.36,-1.57 0.18,0 0.25,-0.28 0.31,-1.27 0.21,-2.84 0.92,-6.33 1.77,-8.61 1.52,-4.15 3.85,-7.68 7.19,-10.91 1.79,-1.73 2.05,-1.95 4.47,-3.57 3.68,-2.49 9.01,-4.39 13.21,-4.7 0.59,-0.04 1.41,-0.12 1.83,-0.16 l 0.76,-0.07 v -2.83 c 0,-2.62 0.01,-2.86 0.34,-3.46 1.07,-2.09 3.78,-2.15 4.91,-0.09 0.23,0.42 0.25,2.81 0.32,34.37 l 0.07,33.91 39.48,0.07 39.48,0.07 0.63,0.34 c 0.91,0.48 1.49,1.46 1.49,2.48 0,1.22 -0.57,2.12 -1.68,2.72 l -0.59,0.32 -41.89,0.01 c -23.04,0.01 -42,0 -42.13,-0.04 z m -0.42,-15.7 v -4.97 l -0.76,-0.06 -0.78,-0.06 -2.84,2.21 c -1.55,1.2 -3.59,2.78 -4.54,3.49 -0.94,0.7 -1.71,1.41 -1.74,1.57 -0.03,0.16 0.09,0.37 0.29,0.48 0.81,0.53 4.95,1.7 7,1.99 0.6,0.09 1.26,0.19 1.46,0.22 0.21,0.03 0.72,0.06 1.14,0.07 l 0.76,0.01 v -4.97 z m -10.25,0.37 c 1.71,-1.27 7.1,-5.54 7.15,-5.67 0.03,-0.07 -0.04,-0.13 -0.18,-0.13 -0.48,0 -2.72,-0.81 -3.97,-1.42 -1.8,-0.91 -3.31,-1.96 -4.61,-3.28 -1.35,-1.33 -2.21,-2.52 -3.25,-4.45 -0.67,-1.25 -0.85,-1.46 -1.29,-1.63 -0.28,-0.09 -0.85,-0.31 -1.25,-0.47 l -0.73,-0.29 -0.41,0.35 -0.41,0.35 0.53,1.71 c 0.29,0.95 0.92,2.92 1.39,4.37 0.47,1.45 1.36,4.26 1.98,6.27 0.6,1.99 1.17,3.78 1.25,3.97 0.1,0.28 0.26,0.35 0.95,0.42 0.45,0.04 1.03,0.22 1.29,0.38 0.51,0.32 0.45,0.34 1.57,-0.48 z m -4.91,-0.48 c -0.1,-0.29 -3.52,-11 -4.42,-13.95 L 20.59,85.36 20,85.15 c -0.95,-0.32 -1.54,-1.05 -1.54,-1.92 0,-0.42 -0.06,-0.45 -1.93,-1.17 l -1.95,-0.73 -0.34,0.67 -0.35,0.69 0.32,1.61 c 1.29,6.5 4.89,12.26 10.17,16.27 1.1,0.83 1.35,0.95 1.19,0.54 z M 40.73,78.73 V 63.54 l -1.23,0.07 c -1.2,0.09 -1.26,0.1 -1.7,0.62 -0.54,0.62 -1.23,0.79 -2.07,0.54 -0.67,-0.21 -1.11,-0.01 -3.09,1.33 -3.38,2.31 -5.73,5.93 -6.5,10.03 -0.35,1.88 -0.22,4.78 0.29,6.56 0.53,1.83 1.8,4.31 2.93,5.73 2.56,3.21 6.69,5.29 10.83,5.46 l 0.54,0.01 V 78.71 Z m -16.57,4.03 c -0.38,-1.51 -0.54,-4.98 -0.29,-6.52 0.59,-3.82 2.49,-7.41 5.39,-10.18 1.32,-1.26 2.43,-2.05 4.1,-2.93 1.01,-0.53 1.64,-1.04 1.66,-1.33 0,-0.04 -0.51,-0.53 -1.13,-1.05 -1.1,-0.95 -1.14,-0.97 -1.96,-0.97 H 31.1 l -2.62,2.72 -2.61,2.72 v 0.85 c 0,1.36 -0.85,2.26 -2.14,2.29 H 23 l -1.13,1.32 c -2.39,2.74 -6.58,7.63 -7.25,8.45 l -0.69,0.86 0.29,0.41 c 0.16,0.22 0.29,0.51 0.29,0.63 0,0.22 0.82,0.59 3.38,1.51 l 0.97,0.35 0.7,-0.31 c 1.22,-0.54 2.36,-0.04 2.74,1.2 0.15,0.5 0.25,0.59 1.13,0.91 0.53,0.21 0.98,0.35 1.01,0.32 0.01,-0.01 -0.1,-0.59 -0.28,-1.26 z m -9.49,-6.28 c 0.35,-0.44 1.1,-1.33 1.67,-1.98 0.56,-0.64 1.83,-2.12 2.81,-3.3 1,-1.17 2.07,-2.43 2.4,-2.81 l 0.6,-0.7 -0.23,-0.62 C 21.8,66.72 21.67,66.25 21.66,66 c -0.04,-0.41 -0.19,-0.51 -1.77,-1.3 -1.48,-0.75 -1.76,-0.83 -1.93,-0.66 -0.7,0.7 -2.65,4.97 -3.21,7.05 -0.69,2.52 -1.26,6.83 -0.85,6.34 0.07,-0.09 0.41,-0.51 0.78,-0.95 z m 8.12,-12.16 c 0.69,-0.35 1.08,-0.37 1.77,-0.07 l 0.53,0.22 2.59,-2.65 2.58,-2.67 v -0.92 l -0.01,-0.92 -1.36,-1.04 c -0.75,-0.56 -1.46,-1.03 -1.61,-1.03 -0.53,0 -3.16,1.92 -5.05,3.68 -1.14,1.07 -3.46,3.72 -3.46,3.97 0,0.12 3.13,1.73 3.35,1.73 0.06,0 0.35,-0.13 0.67,-0.29 z m 17.36,-3.06 0.51,-0.1 0.03,-2.04 c 0.03,-1.88 0.01,-2.08 -0.26,-2.4 -0.28,-0.34 -0.34,-0.35 -0.91,-0.18 -0.34,0.1 -1.33,0.34 -2.2,0.53 -0.88,0.21 -2.02,0.48 -2.55,0.63 -0.88,0.23 -0.97,0.31 -1.14,0.79 l -0.18,0.53 1.36,1.19 c 0.75,0.64 1.45,1.16 1.55,1.11 0.1,-0.04 0.4,-0.01 0.63,0.07 0.44,0.13 2.21,0.07 3.15,-0.13 z M 35.46,56.48 C 37.64,56 39.8,55.35 40.25,55.04 l 0.48,-0.32 v -1.66 -1.67 l -1.42,0.1 c -2.23,0.15 -4.07,0.45 -5.82,0.97 -1.82,0.51 -5.1,1.86 -5.04,2.05 0.03,0.07 0.56,0.51 1.19,1 l 1.14,0.88 0.92,-0.09 c 0.82,-0.07 0.95,-0.04 1.33,0.28 0.34,0.31 0.51,0.35 0.86,0.26 0.25,-0.06 0.95,-0.22 1.55,-0.37 z" id="path123"/>
|
||||
<path d="m 60.72,108.98 c -0.25,-0.07 -0.64,-0.34 -0.88,-0.59 -0.37,-0.4 -0.42,-0.56 -0.42,-1.26 0.01,-0.78 0,-0.82 -0.7,-1.45 -1.01,-0.91 -1.79,-1.25 -2.87,-1.25 -0.79,0 -1.05,0.09 -2.26,0.72 -1.51,0.79 -3.24,1.41 -3.56,1.27 -0.19,-0.07 -0.22,-0.98 -0.22,-6.34 0,-3.44 0.06,-6.34 0.12,-6.43 0.06,-0.09 0.76,-0.59 1.57,-1.13 0.81,-0.53 2.09,-1.49 2.86,-2.14 1.64,-1.39 2.21,-1.63 3.94,-1.63 1.08,0 1.39,0.06 2.14,0.42 0.95,0.45 1.79,1.25 2.61,2.53 0.28,0.45 0.59,0.86 0.66,0.91 0.09,0.04 0.66,0.16 1.27,0.23 1.13,0.15 1.13,0.15 1.66,-0.29 0.88,-0.72 2.02,-0.54 2.5,0.4 0.51,1.01 -0.18,2.24 -1.26,2.24 -0.56,0 -1.51,-0.45 -1.51,-0.73 0,-0.25 -1.36,-0.16 -2.01,0.13 -0.53,0.25 -0.63,0.37 -0.7,0.85 -0.35,2.3 -0.53,2.59 -2.96,4.94 -2.2,2.12 -2.31,2.55 -1.1,4.34 l 0.54,0.81 0.75,-0.04 c 1.36,-0.09 2.11,0.6 2.11,1.93 0,0.53 -0.09,0.73 -0.45,1.1 -0.25,0.25 -0.53,0.45 -0.62,0.45 -0.1,0 -0.31,0.03 -0.47,0.06 -0.16,0.04 -0.5,0.01 -0.73,-0.06 z" id="path124"/>
|
||||
<path d="m 126.56,80.2 c 0,-0.12 -0.1,-0.22 -0.22,-0.22 -0.12,0 -0.22,-0.06 -0.22,-0.12 0,-0.41 -1.01,-0.5 -5.52,-0.48 l -1.66,0.01 v 0.44 c 0,0.34 -0.06,0.44 -0.29,0.44 -0.28,0 -0.29,-0.1 -0.29,-3.38 0,-2.21 0.04,-3.34 0.15,-3.28 0.09,0.04 0.15,0.25 0.15,0.44 0,0.4 0.23,0.62 0.34,0.34 0.06,-0.15 0.09,-0.15 0.09,0 0.01,0.09 0.09,0.18 0.18,0.18 0.07,0 0.1,-0.06 0.06,-0.15 -0.04,-0.07 -0.03,-0.15 0.06,-0.15 0.07,0 0.18,0.06 0.23,0.13 0.1,0.19 3.25,0.19 3.37,0.01 0.04,-0.09 0.19,-0.15 0.31,-0.15 0.13,0 0.18,0.07 0.12,0.16 -0.07,0.12 0.01,0.15 0.28,0.07 0.21,-0.04 0.42,-0.03 0.47,0.06 0.06,0.09 0.19,0.1 0.34,0.01 0.16,-0.09 0.35,-0.09 0.51,0 0.19,0.12 0.28,0.09 0.34,-0.09 0.07,-0.19 0.13,-0.21 0.22,-0.06 0.07,0.12 0.04,0.25 -0.06,0.31 -0.12,0.07 -0.1,0.12 0.07,0.12 0.13,0 0.26,-0.09 0.31,-0.18 0.04,-0.15 0.1,-0.15 0.29,0.01 0.21,0.16 0.25,0.16 0.25,0 0,-0.16 0.03,-0.16 0.13,0 0.12,0.18 0.16,0.13 0.23,-0.18 0.25,-0.95 0.37,-0.01 0.37,2.84 0,2.96 -0.01,3.06 -0.29,3.06 -0.16,0 -0.29,-0.1 -0.29,-0.22 z" id="path125"/>
|
||||
<path d="m 79.39,67.73 c 0,-0.82 -0.04,-1.01 -0.32,-1.25 -0.19,-0.15 -0.53,-0.26 -0.76,-0.28 -0.25,0 -0.54,-0.13 -0.7,-0.32 -0.21,-0.23 -0.31,-0.28 -0.44,-0.15 -0.22,0.22 -3.78,0.25 -3.78,0.03 0,-0.07 -0.1,-0.15 -0.22,-0.15 -0.12,0 -0.22,0.07 -0.22,0.15 0,0.09 -0.15,0.15 -0.32,0.15 -0.38,0.01 -0.85,0.32 -0.85,0.57 0,0.09 -0.13,0.16 -0.31,0.16 -0.29,0 -0.29,-0.04 -0.25,-3.13 0.03,-1.73 0.09,-3.19 0.16,-3.25 0.06,-0.06 0.19,0.01 0.29,0.15 0.18,0.25 0.22,0.25 0.42,0.03 0.18,-0.18 0.26,-0.19 0.31,-0.04 0.06,0.13 0.09,0.13 0.09,0 0.01,-0.1 0.12,-0.19 0.25,-0.19 0.12,0 0.18,-0.06 0.13,-0.15 -0.04,-0.07 0.01,-0.15 0.13,-0.15 0.29,0 0.29,0.07 0,0.35 -0.21,0.22 -0.21,0.23 0,0.23 0.13,0 0.23,-0.06 0.23,-0.15 0,-0.07 0.1,-0.15 0.23,-0.15 0.12,0 0.18,0.07 0.13,0.15 -0.04,0.09 -0.01,0.15 0.06,0.15 0.09,0 0.16,-0.07 0.16,-0.15 0,-0.09 0.13,-0.07 0.31,0.03 0.25,0.15 0.34,0.15 0.38,0.01 0.04,-0.12 0.18,-0.13 0.41,-0.04 0.47,0.18 1.98,0.13 1.98,-0.06 0,-0.1 0.04,-0.12 0.12,-0.01 0.16,0.23 1.64,0.26 1.64,0.04 0,-0.12 0.04,-0.13 0.12,-0.04 0.15,0.21 0.47,0.25 0.47,0.07 0,-0.07 0.1,-0.15 0.23,-0.15 0.12,0 0.18,0.07 0.12,0.16 -0.12,0.21 0.56,0.12 0.72,-0.09 0.06,-0.09 0.15,-0.07 0.23,0.06 0.09,0.13 0.28,0.18 0.59,0.12 0.23,-0.06 0.47,-0.04 0.51,0.01 0.09,0.16 1.44,0.12 1.68,-0.04 0.16,-0.09 0.19,-0.06 0.12,0.12 -0.04,0.15 -0.03,0.25 0.03,0.25 0.07,0 0.16,-0.06 0.19,-0.15 0.06,-0.16 0.95,-0.23 1.38,-0.09 0.21,0.06 0.4,0.01 0.56,-0.13 0.13,-0.13 0.23,-0.18 0.23,-0.1 0,0.15 0.44,0.13 0.7,-0.03 0.1,-0.06 0.18,-0.03 0.18,0.1 0,0.25 1.19,0.25 1.48,0 0.25,-0.21 1.61,-0.22 1.85,-0.01 0.13,0.1 0.19,0.1 0.19,-0.01 0,-0.09 0.15,-0.16 0.34,-0.16 0.26,0 0.28,0.03 0.1,0.15 -0.19,0.12 -0.19,0.15 0.03,0.15 0.13,0 0.29,-0.07 0.34,-0.15 0.06,-0.09 0.38,-0.15 0.72,-0.13 0.34,0.03 0.69,-0.01 0.78,-0.06 0.09,-0.06 0.19,-0.04 0.22,0.04 0.03,0.09 0.32,0.1 0.82,0.01 0.41,-0.06 0.76,-0.07 0.76,-0.01 0,0.07 0.23,0.06 0.51,0 0.31,-0.07 0.51,-0.06 0.51,0.03 0,0.07 0.09,0.07 0.23,0 0.13,-0.07 0.5,-0.09 0.81,-0.03 0.31,0.06 0.57,0.07 0.57,0.01 0,-0.04 0.22,-0.07 0.47,-0.06 0.26,0.01 0.81,0.03 1.19,0.03 0.42,0.01 0.67,0.07 0.62,0.16 -0.04,0.09 0.01,0.15 0.15,0.15 0.13,0 0.19,-0.06 0.15,-0.15 -0.04,-0.07 0.04,-0.15 0.21,-0.15 0.15,0 0.32,0.09 0.35,0.19 0.06,0.1 0.09,0.09 0.09,-0.06 0.01,-0.37 0.38,-0.44 0.7,-0.13 0.19,0.18 0.37,0.22 0.51,0.13 0.26,-0.15 2.02,-0.12 1.93,0.03 -0.09,0.16 0.88,0.25 1.05,0.12 0.21,-0.16 1.32,1.01 2.07,2.18 0.63,0.98 1.36,2.68 1.36,3.16 0,0.35 -0.06,0.38 -0.59,0.38 -0.32,0 -0.59,-0.06 -0.59,-0.15 0,-0.07 -0.13,-0.15 -0.29,-0.15 -0.16,0 -0.29,-0.06 -0.29,-0.15 0,-0.25 -1.63,-0.18 -1.82,0.07 -0.22,0.29 -2.74,0.45 -3.97,0.25 -0.48,-0.07 -0.91,-0.09 -0.95,-0.03 -0.04,0.07 -0.26,0.18 -0.48,0.22 -0.34,0.09 -0.51,0 -1.27,-0.7 -0.98,-0.91 -2.01,-1.45 -3.3,-1.73 -1.3,-0.28 -13.7,-0.29 -14.03,0 -0.22,0.16 -0.25,0.54 -0.25,2.52 0,2.23 -0.01,2.33 -0.29,2.33 -0.26,0 -0.29,-0.1 -0.29,-0.97 z" id="path126"/>
|
||||
</g>
|
||||
<g fill="#0d4ec9" id="g129">
|
||||
<path d="m 118.37,81.59 c 0.01,-1.42 0.21,-3.76 0.25,-3.18 0.03,0.23 0.19,0.31 0.59,0.26 0.09,-0.01 0.18,0.04 0.18,0.13 0,0.07 0.16,0.15 0.37,0.15 0.21,0 0.37,-0.09 0.37,-0.18 0,-0.15 0.04,-0.13 0.13,0.01 0.12,0.19 0.45,0.18 2.04,-0.03 0.25,-0.04 0.47,-0.01 0.47,0.06 0,0.07 0.13,0.13 0.29,0.13 0.16,0 0.29,-0.07 0.29,-0.16 0,-0.07 0.07,-0.1 0.18,-0.04 0.28,0.18 1.41,0.22 1.51,0.06 0.04,-0.07 0.16,-0.15 0.23,-0.15 0.09,0 0.1,0.07 0.06,0.15 -0.04,0.09 -0.03,0.15 0.06,0.15 0.07,0 0.16,-0.07 0.19,-0.18 0.04,-0.12 0.16,-0.07 0.37,0.12 0.75,0.67 0.73,0.67 0.86,0.09 0.12,-0.54 0.12,-0.53 0.22,0.34 0.06,0.48 0.1,1.55 0.12,2.37 l 0.01,1.49 -4.39,-0.03 -4.39,-0.03 0.01,-1.54 z" id="path127"/>
|
||||
<path d="M 98.57,73.7 C 99.92,71.61 99.9,68.76 98.51,66.68 98.23,66.24 98,65.89 98,65.87 c 0,-0.01 0.18,-0.04 0.4,-0.06 0.22,0 0.42,-0.09 0.45,-0.18 0.03,-0.09 0.15,-0.16 0.26,-0.16 0.13,0 0.18,-0.07 0.13,-0.15 -0.06,-0.09 0.07,-0.15 0.29,-0.15 0.22,0 0.35,0.06 0.29,0.15 -0.1,0.16 0.06,0.19 0.69,0.15 0.21,-0.01 0.42,0.04 0.47,0.13 0.06,0.07 0.47,0.13 0.94,0.13 0.57,0 0.75,-0.04 0.54,-0.12 -0.16,-0.07 -0.22,-0.13 -0.15,-0.13 0.09,-0.01 0.04,-0.09 -0.07,-0.16 -0.16,-0.1 0.04,-0.15 0.66,-0.15 0.54,0 0.81,0.06 0.67,0.13 -0.13,0.09 -0.15,0.16 -0.03,0.22 0.09,0.06 0.21,0 0.25,-0.12 0.07,-0.19 0.35,-0.23 1.48,-0.23 0.88,0 1.35,0.06 1.29,0.15 -0.06,0.09 0.07,0.15 0.29,0.15 0.22,0 0.35,0.06 0.29,0.15 -0.04,0.07 -0.01,0.15 0.07,0.15 0.1,0 0.13,-0.18 0.09,-0.44 -0.09,-0.59 0.12,-0.56 0.4,0.04 0.78,1.63 0.89,5.46 0.25,7.84 l -0.32,1.22 -4.06,-0.01 c -2.24,-0.01 -4.41,0.03 -4.83,0.09 l -0.76,0.1 z" id="path128"/>
|
||||
<path d="m 71.22,70.17 c 0.04,-4.04 0.06,-4.25 0.34,-4.45 0.16,-0.13 0.45,-0.23 0.66,-0.23 0.29,0 0.32,-0.03 0.15,-0.15 -0.16,-0.1 0.01,-0.15 0.62,-0.15 0.53,0 0.85,0.07 0.89,0.19 0.06,0.13 0.09,0.13 0.09,0 0.01,-0.1 0.19,-0.19 0.4,-0.19 0.21,0 0.34,0.06 0.28,0.15 -0.04,0.09 0.01,0.15 0.13,0.15 0.13,0 0.23,-0.07 0.23,-0.18 0,-0.13 0.04,-0.13 0.18,0 0.23,0.23 0.81,0.22 0.89,0 0.06,-0.15 0.09,-0.15 0.09,0 0.01,0.23 0.38,0.22 0.59,-0.03 0.1,-0.12 0.29,-0.19 0.42,-0.16 0.13,0.03 0.45,0.06 0.69,0.07 0.25,0.01 0.37,0.06 0.26,0.1 -0.15,0.04 -0.15,0.1 0,0.25 0.12,0.15 0.26,0.16 0.5,0.04 0.26,-0.12 0.38,-0.07 0.69,0.28 l 0.37,0.42 0.03,-0.59 c 0.06,-1.11 0.23,2.93 0.26,5.83 l 0.01,2.89 H 75.6 71.19 l 0.04,-4.25 z" id="path129"/>
|
||||
</g>
|
||||
<g fill="#2728b2" id="g131">
|
||||
<path d="m 74.25,103.64 c -0.94,-0.29 -2.07,-1.36 -2.58,-2.39 l -0.41,-0.83 -0.04,-13.26 -0.04,-13.26 4.37,0.03 4.37,0.03 0.04,1.41 0.04,1.39 h 6.77 c 6.15,0 6.86,-0.03 7.72,-0.28 1.38,-0.38 2.43,-0.97 3.33,-1.82 l 0.81,-0.75 4.53,-0.01 c 3.71,0 4.53,0.03 4.53,0.19 0,0.12 -0.26,0.72 -0.6,1.36 -1.05,2.02 -2.99,3.84 -5.38,5.02 -1.17,0.59 -3.25,1.3 -4.25,1.48 -0.35,0.06 -0.62,0.16 -0.59,0.22 0.03,0.06 0.6,0.56 1.29,1.11 2.65,2.09 4.79,4.58 7.03,8.19 1.32,2.11 3.05,5.51 3.52,6.94 0.76,2.33 0.16,4 -1.82,5.02 -0.45,0.25 -0.86,0.32 -1.7,0.32 -0.97,0.01 -1.25,-0.06 -2.09,-0.47 -1.64,-0.82 -2.14,-1.48 -6.39,-8.6 C 92.49,87.59 90.95,85.58 89,84.54 88.52,84.29 87.43,83.87 86.58,83.6 85.13,83.13 84.9,83.1 82.52,83.06 L 80,83 79.96,91.77 c -0.04,8.6 -0.04,8.77 -0.35,9.43 -0.94,2.02 -3.28,3.06 -5.35,2.4 z" id="path130"/>
|
||||
<path d="m 120.89,103.34 c -0.92,-0.47 -1.57,-1.17 -2.08,-2.27 l -0.38,-0.81 v -8.79 -8.79 h 4.32 4.32 v 8.94 8.94 l -0.4,0.76 c -0.86,1.63 -1.92,2.29 -3.78,2.36 -1.11,0.04 -1.33,0 -2.01,-0.34 z" id="path131"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
16
public/mockApi/attendance.json
Normal file
16
public/mockApi/attendance.json
Normal file
@ -0,0 +1,16 @@
|
||||
[
|
||||
{ "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" }
|
||||
]
|
||||
24
src/App.tsx
Normal file
24
src/App.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './store/store';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Toaster } from 'sonner';
|
||||
import MainLayout from './components/MainLayout';
|
||||
import { BrowserRouter } from 'react-router-dom'; // <-- Import BrowserRouter
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* Wrap MainLayout in BrowserRouter */}
|
||||
<BrowserRouter>
|
||||
<MainLayout />
|
||||
</BrowserRouter>
|
||||
<Toaster richColors position="top-right" />
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
148
src/components/AttendanceRecordsTable.tsx
Normal file
148
src/components/AttendanceRecordsTable.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown, Calendar } from 'lucide-react';
|
||||
import type { AttendanceRecord, SortKey } from '../types/attendance';
|
||||
|
||||
type SortDirection = 'ascending' | 'descending';
|
||||
|
||||
interface AttendanceRecordsTableProps {
|
||||
records: AttendanceRecord[];
|
||||
title?: string;
|
||||
initialFromDate?: string;
|
||||
initialToDate?: string;
|
||||
}
|
||||
|
||||
export default function AttendanceRecordsTable({
|
||||
records,
|
||||
title = "Attendance Records",
|
||||
initialFromDate,
|
||||
initialToDate
|
||||
}: AttendanceRecordsTableProps) {
|
||||
|
||||
// Default to current month if no initial dates are provided
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth();
|
||||
const defaultFrom = initialFromDate || new Date(currentYear, currentMonth, 1).toISOString().split('T')[0];
|
||||
const defaultTo = initialToDate || new Date(currentYear, currentMonth + 1, 0).toISOString().split('T')[0];
|
||||
|
||||
const [fromDate, setFromDate] = useState(defaultFrom);
|
||||
const [toDate, setToDate] = useState(defaultTo);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: SortDirection }>({
|
||||
key: 'date',
|
||||
direction: 'descending'
|
||||
});
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return records.filter((record) => record.date >= fromDate && record.date <= toDate);
|
||||
}, [records, fromDate, toDate]);
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
const sortableData = [...filteredData];
|
||||
sortableData.sort((a, b) => {
|
||||
if (a[sortConfig.key] < b[sortConfig.key]) return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
if (a[sortConfig.key] > b[sortConfig.key]) return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
return sortableData;
|
||||
}, [filteredData, sortConfig]);
|
||||
|
||||
const requestSort = (key: SortKey) => {
|
||||
let direction: SortDirection = 'ascending';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'ascending') direction = 'descending';
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
const getSortIcon = (key: SortKey) => {
|
||||
if (sortConfig.key !== key) return <ArrowUpDown size={14} className="ml-1 text-text-light" />;
|
||||
if (sortConfig.direction === 'ascending') return <ArrowUp size={14} className="ml-1 text-primary" />;
|
||||
return <ArrowDown size={14} className="ml-1 text-primary" />;
|
||||
};
|
||||
|
||||
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 'Half Day': return 'bg-halfday-100 text-halfday-700';
|
||||
case 'WFH': return 'bg-wfh-100 text-wfh-700';
|
||||
case 'Absent': return 'bg-absent-100 text-absent-700';
|
||||
case 'Leave': return 'bg-leave-100 text-leave-700';
|
||||
case 'Mispunch': return 'bg-mispunch-100 text-mispunch-700';
|
||||
case 'Holiday': return 'bg-holiday-100 text-holiday-700';
|
||||
default: return 'bg-app-muted text-text-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm mt-6">
|
||||
<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">{title}</h2>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative">
|
||||
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||
<input
|
||||
type="date"
|
||||
value={fromDate}
|
||||
onChange={(e) => setFromDate(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-text-muted text-sm">to</span>
|
||||
<div className="relative">
|
||||
<Calendar size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-light" />
|
||||
<input
|
||||
type="date"
|
||||
value={toDate}
|
||||
onChange={(e) => setToDate(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-app-border">
|
||||
<thead>
|
||||
<tr>
|
||||
{(['date', 'timeIn', 'timeOut', 'loggedHours', '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"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase())} {getSortIcon(key)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-app-border">
|
||||
{sortedData.length > 0 ? (
|
||||
sortedData.map((record, index) => (
|
||||
<tr key={index} className="hover:bg-app-muted transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-primary">
|
||||
{new Date(record.date + 'T00:00:00').toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
</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.timeOut}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text-primary">{record.loggedHours}</td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-sm text-text-muted">
|
||||
No records found for the selected period.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
src/components/Calendar.tsx
Normal file
111
src/components/Calendar.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { useState } from 'react';
|
||||
import { CalendarCheck } from 'lucide-react';
|
||||
|
||||
interface CalendarProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Record<string, any>;
|
||||
selectedDate: string;
|
||||
onDateSelect: (date: string) => void;
|
||||
}
|
||||
|
||||
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 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 daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
||||
const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay();
|
||||
|
||||
const monthsArray = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const currentSystemYear = new Date().getFullYear();
|
||||
const yearsArray = Array.from({ length: 16 }, (_, i) => currentSystemYear - 5 + i);
|
||||
|
||||
const handleDateClick = (day: number) => {
|
||||
onDateSelect(formatDate(new Date(currentYear, currentMonth, day)));
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
const todayDate = new Date();
|
||||
setCurrentMonth(todayDate.getMonth());
|
||||
setCurrentYear(todayDate.getFullYear());
|
||||
onDateSelect(formatDate(todayDate));
|
||||
};
|
||||
|
||||
const getStatusClass = (dateStr: string, dayOfWeek: number) => {
|
||||
const dayData = data[dateStr];
|
||||
|
||||
if (dayData?.status === 'Present') return 'bg-present-100 text-present-800 border-present-300 hover:bg-present-200';
|
||||
if (dayData?.status === 'Late') return 'bg-late-100 text-late-800 border-late-300 hover:bg-late-200';
|
||||
if (dayData?.status === 'Half Day') return 'bg-halfday-100 text-halfday-800 border-halfday-300 hover:bg-halfday-200';
|
||||
if (dayData?.status === 'WFH') return 'bg-wfh-100 text-wfh-800 border-wfh-300 hover:bg-wfh-200';
|
||||
if (dayData?.status === 'Absent') return 'bg-absent-100 text-absent-800 border-absent-300 hover:bg-absent-200';
|
||||
if (dayData?.status === 'Leave') return 'bg-leave-100 text-leave-800 border-leave-300 hover:bg-leave-200';
|
||||
if (dayData?.status === 'Mispunch') return 'bg-mispunch-100 text-mispunch-800 border-mispunch-300 hover:bg-mispunch-200';
|
||||
if (dayData?.status === 'Holiday') return 'bg-holiday-100 text-holiday-800 border-holiday-300 hover:bg-holiday-200';
|
||||
|
||||
if (dayOfWeek === 0) return 'bg-app-muted text-text-light border-app-border';
|
||||
if (dateStr === today) return 'bg-today-50 text-today-800 border-today-300 hover:bg-today-100';
|
||||
return 'bg-app-card text-text-secondary border-app-border hover:bg-app-muted';
|
||||
};
|
||||
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
return (
|
||||
<div className="bg-app-card p-6 rounded-lg shadow-sm">
|
||||
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
||||
<h2 className="text-lg font-semibold text-text-primary">My Attendance</h2>
|
||||
<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">
|
||||
<CalendarCheck size={16} className="mr-2 text-text-muted" /> Today
|
||||
</button>
|
||||
<select value={currentMonth} onChange={(e) => setCurrentMonth(Number(e.target.value))} className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
||||
{monthsArray.map((month, idx) => <option key={idx} value={idx}>{month}</option>)}
|
||||
</select>
|
||||
<select value={currentYear} onChange={(e) => setCurrentYear(Number(e.target.value))} className="px-3 py-2 border border-app-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary text-text-secondary bg-app-card">
|
||||
{yearsArray.map((year) => <option key={year} value={year}>{year}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-2 mb-2">
|
||||
{days.map(day => <div key={day} className="text-center text-xs font-semibold text-text-muted uppercase">{day}</div>)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{Array.from({ length: firstDayOfMonth }).map((_, index) => <div key={`empty-${index}`} className="h-12"></div>)}
|
||||
{Array.from({ length: daysInMonth }).map((_, index) => {
|
||||
const day = index + 1;
|
||||
const dateStr = formatDate(new Date(currentYear, currentMonth, day));
|
||||
const dayOfWeek = new Date(currentYear, currentMonth, day).getDay();
|
||||
const isSelected = selectedDate === dateStr;
|
||||
return (
|
||||
<button key={day} onClick={() => handleDateClick(day)} className={`h-12 flex flex-col items-center justify-center rounded-md border text-sm font-medium transition-colors ${getStatusClass(dateStr, dayOfWeek)} ${isSelected ? 'ring-2 ring-primary ring-offset-1' : ''}`}>
|
||||
{day}
|
||||
{data[dateStr] && <span className="w-1.5 h-1.5 rounded-full bg-text-muted mt-0.5"></span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-app-border flex flex-wrap gap-4">
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-present-100 border border-present-300"></div><span className="text-xs text-text-secondary">Present</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-late-100 border border-late-300"></div><span className="text-xs text-text-secondary">Late Arrival</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-halfday-100 border border-halfday-300"></div><span className="text-xs text-text-secondary">Half Day</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-wfh-100 border border-wfh-300"></div><span className="text-xs text-text-secondary">Work From Home</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-absent-100 border border-absent-300"></div><span className="text-xs text-text-secondary">Absent</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-leave-100 border border-leave-300"></div><span className="text-xs text-text-secondary">Leave</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-mispunch-100 border border-mispunch-300"></div><span className="text-xs text-text-secondary">Mispunch</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-holiday-100 border border-holiday-300"></div><span className="text-xs text-text-secondary">Holiday</span></div>
|
||||
<div className="flex items-center space-x-2"><div className="w-4 h-4 rounded bg-app-muted border border-app-border"></div><span className="text-xs text-text-secondary">Weekly Off</span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
src/components/DailySummary.tsx
Normal file
73
src/components/DailySummary.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { LogIn, LogOut, Clock } from 'lucide-react';
|
||||
|
||||
interface DailySummaryProps {
|
||||
selectedDate: string;
|
||||
data: {
|
||||
status: string;
|
||||
login: string;
|
||||
logout: string;
|
||||
hours: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export default function DailySummary({ selectedDate, data }: DailySummaryProps) {
|
||||
// Fallback if no data exists for the selected date
|
||||
const selectedData = data || { status: 'No Data', login: '-', logout: '-', hours: '-' };
|
||||
|
||||
return (
|
||||
<div className="w-full lg:w-96 bg-app-card p-6 rounded-lg shadow-sm h-fit">
|
||||
<h3 className="text-lg font-semibold text-text-primary mb-1">Daily Summary</h3>
|
||||
<p className="text-sm text-text-muted mb-6">
|
||||
{new Date(selectedDate + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
|
||||
</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wide
|
||||
${selectedData.status === 'Present' ? 'bg-present-100 text-present-700' : ''}
|
||||
${selectedData.status === 'Late' ? 'bg-late-100 text-late-700' : ''}
|
||||
${selectedData.status === 'Half Day' ? 'bg-halfday-100 text-halfday-700' : ''}
|
||||
${selectedData.status === 'WFH' ? 'bg-wfh-100 text-wfh-700' : ''}
|
||||
${selectedData.status === 'Absent' ? 'bg-absent-100 text-absent-700' : ''}
|
||||
${selectedData.status === 'Leave' ? 'bg-leave-100 text-leave-700' : ''}
|
||||
${selectedData.status === 'Mispunch' ? 'bg-mispunch-100 text-mispunch-700' : ''}
|
||||
${selectedData.status === 'Holiday' ? 'bg-holiday-100 text-holiday-700' : ''}
|
||||
${selectedData.status === 'No Data' ? 'bg-app-muted text-text-secondary' : ''}
|
||||
`}>
|
||||
{selectedData.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-app-muted rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-present-100 rounded-lg">
|
||||
<LogIn size={20} className="text-present-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-text-secondary">Check-In</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-text-primary">{selectedData.login}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-app-muted rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-absent-100 rounded-lg">
|
||||
<LogOut size={20} className="text-absent-600" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-text-secondary">Check-Out</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-text-primary">{selectedData.logout}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-primary/5 rounded-lg border border-primary/20">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-today-100 rounded-lg">
|
||||
<Clock size={20} className="text-primary" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-text-secondary">Total Hours</span>
|
||||
</div>
|
||||
<span className="text-base font-extrabold text-primary">{selectedData.hours}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
src/components/ErrorState.tsx
Normal file
20
src/components/ErrorState.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
interface ErrorStateProps {
|
||||
message?: string;
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
export default function ErrorState({
|
||||
message = 'Something went wrong. Please try again later.',
|
||||
fullScreen = false
|
||||
}: ErrorStateProps) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center bg-app-card rounded-lg shadow-sm text-absent-500 ${
|
||||
fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
|
||||
}`}>
|
||||
<AlertCircle size={24} />
|
||||
<span className="ml-3">{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/Loader.tsx
Normal file
22
src/components/Loader.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface LoaderProps {
|
||||
message?: string;
|
||||
size?: number;
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
export default function Loader({
|
||||
message = 'Loading...',
|
||||
size = 32,
|
||||
fullScreen = false
|
||||
}: LoaderProps) {
|
||||
return (
|
||||
<div className={`flex flex-col items-center justify-center bg-app-card rounded-lg shadow-sm ${
|
||||
fullScreen ? 'h-screen w-screen fixed top-0 left-0 z-50' : 'h-64 w-full'
|
||||
}`}>
|
||||
<Loader2 className="animate-spin text-primary" size={size} />
|
||||
{message && <span className="mt-3 text-text-muted">{message}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/components/MainLayout.tsx
Normal file
40
src/components/MainLayout.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import Sidebar from './Sidebar';
|
||||
import Navbar from './Navbar';
|
||||
import DashboardRouter from '../features/dashboard/DashboardRouter';
|
||||
import AttendanceRouter from '../features/attendance/AttendanceRouter';
|
||||
import LeaveRouter from '../features/leave/LeaveRouter';
|
||||
import ApprovalRouter from '../features/approval/ApprovalRouter';
|
||||
import AttendanceSummaryRouter from '../features/summary/AttendanceSummaryRouter';
|
||||
import LeaveSummaryRouter from '../features/summary/LeaveSummaryRouter';
|
||||
import ManageEmployeesRouter from '../features/manage/ManageEmployeesRouter';
|
||||
import ManageAdminsRouter from '../features/manage/ManageAdminsRouter';
|
||||
import PolicyRouter from '../features/policy/PolicyRouter';
|
||||
|
||||
export default function MainLayout() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-app-bg overflow-hidden">
|
||||
<Sidebar isCollapsed={isCollapsed} setIsCollapsed={setIsCollapsed} />
|
||||
<div className="flex-1 flex flex-col">
|
||||
<Navbar />
|
||||
<main className="flex-1 overflow-y-auto p-6">
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<DashboardRouter />} />
|
||||
<Route path="/attendance" element={<AttendanceRouter />} />
|
||||
<Route path="/leave" element={<LeaveRouter />} />
|
||||
<Route path="/approval" element={<ApprovalRouter />} />
|
||||
<Route path="/attendance-summary" element={<AttendanceSummaryRouter />} />
|
||||
<Route path="/leave-summary" element={<LeaveSummaryRouter />} />
|
||||
<Route path="/manage-employees" element={<ManageEmployeesRouter />} />
|
||||
<Route path="/manage-admins" element={<ManageAdminsRouter />} />
|
||||
<Route path="/policies" element={<PolicyRouter />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
179
src/components/Navbar.tsx
Normal file
179
src/components/Navbar.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { RootState } from '../store/store';
|
||||
import { setRole, type UserRole } from '../store/roleSlice';
|
||||
import { Search, UserCircle, ChevronDown, X, ArrowRight } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const roleSearchData: Record<UserRole, { name: string; path: string }[]> = {
|
||||
employee: [
|
||||
{ name: 'My Dashboard', path: '/dashboard' },
|
||||
{ name: 'Mark Attendance', path: '/attendance' },
|
||||
{ name: 'Apply for Leave', path: '/leave' },
|
||||
],
|
||||
manager: [
|
||||
{ name: 'Manager Dashboard', path: '/dashboard' },
|
||||
{ name: 'Team Attendance', path: '/attendance' },
|
||||
{ name: 'Team Leaves', path: '/leave' },
|
||||
{ name: 'Pending Approvals', path: '/approval' },
|
||||
],
|
||||
admin: [
|
||||
{ name: 'Admin Dashboard', path: '/dashboard' },
|
||||
{ name: 'All Attendance Logs', path: '/attendance' },
|
||||
{ name: 'All Leave Requests', path: '/leave' },
|
||||
{ name: 'Attendance Summary Report', path: '/attendance-summary' },
|
||||
{ name: 'Leave Summary Report', path: '/leave-summary' },
|
||||
{ name: 'Manage Employees', path: '/manage-employees' },
|
||||
],
|
||||
superadmin: [
|
||||
{ name: 'SuperAdmin Dashboard', path: '/dashboard' },
|
||||
{ name: 'Global Attendance Logs', path: '/attendance' },
|
||||
{ name: 'Global Leave Requests', path: '/leave' },
|
||||
{ name: 'Global Attendance Summary', path: '/attendance-summary' },
|
||||
{ name: 'Global Leave Summary', path: '/leave-summary' },
|
||||
{ name: 'Manage Admins', path: '/manage-admins' },
|
||||
{ name: 'Manage Employees', path: '/manage-employees' },
|
||||
{ name: 'Policy and Rules', path: '/policies' },
|
||||
],
|
||||
};
|
||||
|
||||
export default function Navbar() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const currentRole = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const roles: UserRole[] = ['employee', 'manager', 'admin', 'superadmin'];
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearchOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isSearchOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsSearchOpen(false);
|
||||
setSearchQuery('');
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
const availableItems = roleSearchData[currentRole] || [];
|
||||
const searchResults = availableItems.filter(item =>
|
||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleSearchClick = (path: string) => {
|
||||
navigate(path);
|
||||
setIsSearchOpen(false);
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
<h2 className="text-lg font-bold text-primary tracking-wide">CLRI HRMS</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
<div className="relative flex items-center">
|
||||
{isSearchOpen ? (
|
||||
<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" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-56 px-3 py-2 text-sm focus:outline-none rounded-lg text-text-primary"
|
||||
/>
|
||||
<button
|
||||
onClick={() => { setIsSearchOpen(false); setSearchQuery(''); }}
|
||||
className="p-2 text-text-light hover:text-text-muted"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsSearchOpen(true)}
|
||||
className="p-2 text-text-secondary hover:bg-app-muted rounded-full transition-colors"
|
||||
>
|
||||
<Search size={22} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isSearchOpen && searchQuery && (
|
||||
<div className="absolute right-0 top-14 w-72 bg-app-card border border-app-border rounded-lg shadow-lg overflow-hidden z-50">
|
||||
{searchResults.length > 0 ? (
|
||||
<ul>
|
||||
{searchResults.map((item) => (
|
||||
<li key={item.path}>
|
||||
<button
|
||||
onClick={() => handleSearchClick(item.path)}
|
||||
className="flex items-center justify-between w-full px-4 py-3 text-left text-sm text-text-secondary hover:bg-app-muted transition-colors"
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<ArrowRight size={14} className="text-text-light" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="px-4 py-3 text-sm text-text-muted">
|
||||
No results found for "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="flex items-center space-x-2 p-1 rounded-lg hover:bg-app-muted transition-colors"
|
||||
>
|
||||
<UserCircle size={28} className="text-text-secondary" />
|
||||
<div className="text-left hidden md:block">
|
||||
<p className="text-sm font-medium text-text-primary capitalize">{currentRole}</p>
|
||||
<p className="text-xs text-text-muted">Test Mode</p>
|
||||
</div>
|
||||
<ChevronDown size={16} className="text-text-muted" />
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-app-card border border-app-border rounded-lg shadow-lg z-50">
|
||||
<p className="px-4 py-2 text-xs font-semibold text-text-light uppercase">Switch Role (Test)</p>
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
onClick={() => {
|
||||
dispatch(setRole(role));
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
className={`block w-full text-left px-4 py-2 text-sm capitalize hover:bg-app-muted ${currentRole === role
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
{role}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
src/components/Sidebar.tsx
Normal file
82
src/components/Sidebar.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../store/store';
|
||||
import { getSidebarItems } from '../types/sidebarConfig';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard, CalendarCheck, Plane, CheckCircle, BarChart3, ClipboardList,
|
||||
Menu, Users, ShieldUser, ScrollText
|
||||
} from 'lucide-react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const iconMap: Record<string, any> = {
|
||||
LayoutDashboard,
|
||||
CalendarCheck,
|
||||
Plane,
|
||||
CheckCircle,
|
||||
BarChart3,
|
||||
ClipboardList,
|
||||
Users,
|
||||
ShieldUser,
|
||||
ScrollText,
|
||||
};
|
||||
|
||||
interface SidebarProps {
|
||||
isCollapsed: boolean;
|
||||
setIsCollapsed: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({ isCollapsed, setIsCollapsed }: SidebarProps) {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
const navItems = getSidebarItems(role);
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
|
||||
return (
|
||||
<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'}`}>
|
||||
{!isCollapsed && (
|
||||
<Link to="/dashboard">
|
||||
<img src="clri_logo.svg" alt="Company Logo" className="h-8 w-auto cursor-pointer" />
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-2 rounded hover:bg-sidebar-hover transition-colors"
|
||||
>
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 mt-4 overflow-x-visible">
|
||||
<ul>
|
||||
{navItems.map((item) => {
|
||||
const Icon = iconMap[item.icon];
|
||||
const isActive = currentPath === item.path;
|
||||
|
||||
return (
|
||||
<li key={item.name} className="mb-1 relative group">
|
||||
<Link
|
||||
to={item.path}
|
||||
className={`flex items-center px-4 py-3 transition-colors duration-200 ${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" />}
|
||||
{!isCollapsed && <span className="ml-4 text-sm font-medium">{item.name}</span>}
|
||||
</Link>
|
||||
|
||||
{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>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/features/approval/ApprovalRouter.tsx
Normal file
14
src/features/approval/ApprovalRouter.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
|
||||
const ManagerApproval = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Pending Approvals</h2><p className="mt-2 text-text-muted">Approve or reject team leave and regularization requests.</p></div>;
|
||||
|
||||
export default function ApprovalRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
if (role === 'manager') {
|
||||
return <ManagerApproval />;
|
||||
}
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Managers only.</div>;
|
||||
}
|
||||
18
src/features/attendance/AttendanceRouter.tsx
Normal file
18
src/features/attendance/AttendanceRouter.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
import EmployeeAttendance from './EmployeeAttendance';
|
||||
|
||||
const ManagerAttendance = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Team Attendance</h2><p className="mt-2 text-text-muted">View your team's daily attendance logs.</p></div>;
|
||||
const AdminAttendance = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Branch Attendance</h2><p className="mt-2 text-text-muted">Raw attendance logs for all employees.</p></div>;
|
||||
|
||||
export default function AttendanceRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
switch (role) {
|
||||
case 'employee': return <EmployeeAttendance />;
|
||||
case 'manager': return <ManagerAttendance />;
|
||||
case 'admin':
|
||||
case 'superadmin': return <AdminAttendance />;
|
||||
default: return <div>Unauthorized</div>;
|
||||
}
|
||||
}
|
||||
78
src/features/attendance/EmployeeAttendance.tsx
Normal file
78
src/features/attendance/EmployeeAttendance.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import Calendar from '../../components/Calendar';
|
||||
import AttendanceRecordsTable from '../../components/AttendanceRecordsTable';
|
||||
import DailySummary from '../../components/DailySummary';
|
||||
import Loader from '../../components/Loader';
|
||||
import ErrorState from '../../components/ErrorState';
|
||||
import { useAttendanceData } from './useAttendanceData';
|
||||
|
||||
// Helper to get current system date in YYYY-MM-DD format
|
||||
const getTodayDate = () => {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
export default function EmployeeAttendance() {
|
||||
// Initialize with the current system date
|
||||
const [selectedDate, setSelectedDate] = useState(getTodayDate());
|
||||
|
||||
// Fetch data using TanStack Query
|
||||
const { data: attendanceRecords, isLoading, isError } = useAttendanceData();
|
||||
|
||||
// Map array data to dictionary for the Calendar
|
||||
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]);
|
||||
|
||||
const selectedDayData = calendarData[selectedDate] || null;
|
||||
|
||||
// 1. Loading State
|
||||
if (isLoading) {
|
||||
return <Loader message="Loading attendance data..." />;
|
||||
}
|
||||
|
||||
// 2. Error State
|
||||
if (isError) {
|
||||
return <ErrorState message="Failed to load attendance data. Please try again later." />;
|
||||
}
|
||||
|
||||
// 3. Success State
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
{/* Top Section: Calendar and Daily Summary */}
|
||||
<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={selectedDayData} />
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Reusable Attendance Records Table */}
|
||||
{/* Removed hardcoded dates so it defaults to the current system month */}
|
||||
{attendanceRecords && (
|
||||
<AttendanceRecordsTable
|
||||
records={attendanceRecords}
|
||||
title="My Attendance Records"
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/features/attendance/useAttendanceData.ts
Normal file
18
src/features/attendance/useAttendanceData.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { AttendanceRecord } from '../../types/attendance';
|
||||
|
||||
export const useAttendanceData = () => {
|
||||
return useQuery<AttendanceRecord[]>({
|
||||
queryKey: ['attendanceRecords'],
|
||||
queryFn: async () => {
|
||||
// Simulate network delay (500ms)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const response = await fetch('/mockApi/attendance.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch attendance data');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
};
|
||||
27
src/features/dashboard/AdminDashboard.tsx
Normal file
27
src/features/dashboard/AdminDashboard.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
export default function AdminDashboard() {
|
||||
return (
|
||||
<div className="bg-app-card rounded-lg shadow-sm p-6">
|
||||
<h2 className="text-2xl font-bold text-primary">Admin / SuperAdmin Dashboard</h2>
|
||||
<p className="text-text-muted mt-2">Global branch analytics and summaries.</p>
|
||||
|
||||
<div className="mt-6 grid grid-cols-4 gap-4">
|
||||
<div className="p-4 border rounded-lg bg-app-muted">
|
||||
<h3 className="font-semibold text-sm text-text-muted">Total Employees</h3>
|
||||
<p className="text-2xl text-primary">245</p>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg bg-app-muted">
|
||||
<h3 className="font-semibold text-sm text-text-muted">Present Today</h3>
|
||||
<p className="text-2xl text-present-600">198</p>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg bg-app-muted">
|
||||
<h3 className="font-semibold text-sm text-text-muted">On Leave</h3>
|
||||
<p className="text-2xl text-action">12</p>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg bg-app-muted">
|
||||
<h3 className="font-semibold text-sm text-text-muted">Mispunch</h3>
|
||||
<p className="text-2xl text-absent-500">5</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/features/dashboard/DashboardRouter.tsx
Normal file
21
src/features/dashboard/DashboardRouter.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
import EmployeeDashboard from './EmployeeDashboard';
|
||||
import ManagerDashboard from './ManagerDashboard';
|
||||
import AdminDashboard from './AdminDashboard';
|
||||
|
||||
export default function DashboardRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
switch (role) {
|
||||
case 'employee':
|
||||
return <EmployeeDashboard />;
|
||||
case 'manager':
|
||||
return <ManagerDashboard />;
|
||||
case 'admin':
|
||||
case 'superadmin':
|
||||
return <AdminDashboard />;
|
||||
default:
|
||||
return <div>No role assigned</div>;
|
||||
}
|
||||
}
|
||||
19
src/features/dashboard/EmployeeDashboard.tsx
Normal file
19
src/features/dashboard/EmployeeDashboard.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
export default function EmployeeDashboard() {
|
||||
return (
|
||||
<div className="bg-app-card rounded-lg shadow-sm p-6">
|
||||
<h2 className="text-2xl font-bold text-primary">Employee Dashboard</h2>
|
||||
<p className="text-text-muted mt-2">Welcome! Here you can mark your attendance and check your leave balance.</p>
|
||||
|
||||
<div className="mt-6 grid grid-cols-3 gap-4">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-semibold">Leave Balance</h3>
|
||||
<p className="text-2xl text-action">12 Days</p>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-semibold">Attendance Status</h3>
|
||||
<button className="mt-2 px-4 py-1 bg-action text-white rounded">Check In</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/features/dashboard/ManagerDashboard.tsx
Normal file
22
src/features/dashboard/ManagerDashboard.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
export default function ManagerDashboard() {
|
||||
return (
|
||||
<div className="bg-app-card rounded-lg shadow-sm p-6">
|
||||
<h2 className="text-2xl font-bold text-primary">Manager Dashboard</h2>
|
||||
<p className="text-text-muted mt-2">Welcome Manager! Review your team's pending requests below.</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="font-semibold text-lg">Pending Approvals</h3>
|
||||
<ul className="mt-2 space-y-2">
|
||||
<li className="p-3 border rounded flex justify-between items-center">
|
||||
<span>John Doe - Casual Leave (2 Days)</span>
|
||||
<button className="px-3 py-1 bg-primary text-white rounded text-sm">Review</button>
|
||||
</li>
|
||||
<li className="p-3 border rounded flex justify-between items-center">
|
||||
<span>Jane Smith - Late Regularization</span>
|
||||
<button className="px-3 py-1 bg-primary text-white rounded text-sm">Review</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/features/leave/LeaveRouter.tsx
Normal file
18
src/features/leave/LeaveRouter.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
|
||||
const EmployeeLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">My Leaves</h2><p className="mt-2 text-text-muted">Apply for leave and view your leave history.</p></div>;
|
||||
const ManagerLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Team Leave Calendar</h2><p className="mt-2 text-text-muted">See who is on leave in your team.</p></div>;
|
||||
const AdminLeave = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">All Leave Requests</h2><p className="mt-2 text-text-muted">Manage leave types and balances for branches.</p></div>;
|
||||
|
||||
export default function LeaveRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
switch (role) {
|
||||
case 'employee': return <EmployeeLeave />;
|
||||
case 'manager': return <ManagerLeave />;
|
||||
case 'admin':
|
||||
case 'superadmin': return <AdminLeave />;
|
||||
default: return <div>Unauthorized</div>;
|
||||
}
|
||||
}
|
||||
17
src/features/manage/ManageAdminsRouter.tsx
Normal file
17
src/features/manage/ManageAdminsRouter.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
|
||||
export default function ManageAdminsRouter() {
|
||||
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">Manage Admins</h2>
|
||||
<p className="mt-2 text-text-muted">Create and manage branch administrators and their permissions.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: SuperAdmins only.</div>;
|
||||
}
|
||||
17
src/features/manage/ManageEmployeesRouter.tsx
Normal file
17
src/features/manage/ManageEmployeesRouter.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
|
||||
export default function ManageEmployeesRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
if (role === 'admin' || role === 'superadmin') {
|
||||
return (
|
||||
<div className="bg-app-card p-6 rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold text-primary">Manage Employees</h2>
|
||||
<p className="mt-2 text-text-muted">Add, edit, or remove employee records and manage branch assignments.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Admins only.</div>;
|
||||
}
|
||||
17
src/features/policy/PolicyRouter.tsx
Normal file
17
src/features/policy/PolicyRouter.tsx
Normal 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>;
|
||||
}
|
||||
14
src/features/summary/AttendanceSummaryRouter.tsx
Normal file
14
src/features/summary/AttendanceSummaryRouter.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
|
||||
const AdminAttendanceSummary = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Attendance Summary Report</h2><p className="mt-2 text-text-muted">Generate branch-wise attendance reports.</p></div>;
|
||||
|
||||
export default function AttendanceSummaryRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
if (role === 'admin' || role === 'superadmin') {
|
||||
return <AdminAttendanceSummary />;
|
||||
}
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Admins only.</div>;
|
||||
}
|
||||
14
src/features/summary/LeaveSummaryRouter.tsx
Normal file
14
src/features/summary/LeaveSummaryRouter.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { RootState } from '../../store/store';
|
||||
|
||||
const AdminLeaveSummary = () => <div className="bg-app-card p-6 rounded-lg shadow"><h2 className="text-2xl font-bold text-primary">Leave Summary Report</h2><p className="mt-2 text-text-muted">Analyze leave trends across the organization.</p></div>;
|
||||
|
||||
export default function LeaveSummaryRouter() {
|
||||
const role = useSelector((state: RootState) => state.role.currentRole);
|
||||
|
||||
if (role === 'admin' || role === 'superadmin') {
|
||||
return <AdminLeaveSummary />;
|
||||
}
|
||||
|
||||
return <div className="bg-app-card p-6 rounded-lg shadow text-absent-500">Access Denied: Admins only.</div>;
|
||||
}
|
||||
89
src/index.css
Normal file
89
src/index.css
Normal file
@ -0,0 +1,89 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Brand Colors */
|
||||
--color-primary: #0a66c2;
|
||||
--color-primary-hover: #0854a0;
|
||||
--color-action: #f36f21;
|
||||
--color-action-hover: #d65e15;
|
||||
|
||||
/* Semantic Status Colors */
|
||||
--color-present-100: #dcfce7;
|
||||
--color-present-200: #bbf7d0;
|
||||
--color-present-300: #86efac;
|
||||
--color-present-600: #16a34a;
|
||||
--color-present-700: #15803d;
|
||||
--color-present-800: #166534;
|
||||
|
||||
--color-wfh-100: #e0e7ff;
|
||||
--color-wfh-200: #c7d2fe;
|
||||
--color-wfh-300: #a5b4fc;
|
||||
--color-wfh-700: #4338ca;
|
||||
--color-wfh-800: #3730a3;
|
||||
|
||||
--color-absent-100: #fee2e2;
|
||||
--color-absent-200: #fecaca;
|
||||
--color-absent-300: #fca5a5;
|
||||
--color-absent-500: #ef4444;
|
||||
--color-absent-600: #dc2626;
|
||||
--color-absent-700: #b91c1c;
|
||||
--color-absent-800: #991b1b;
|
||||
|
||||
--color-leave-100: #fef9c3;
|
||||
--color-leave-200: #fef08a;
|
||||
--color-leave-300: #fde047;
|
||||
--color-leave-700: #a16207;
|
||||
--color-leave-800: #854d0e;
|
||||
|
||||
--color-mispunch-100: #ffedd5;
|
||||
--color-mispunch-200: #fed7aa;
|
||||
--color-mispunch-300: #fdba74;
|
||||
--color-mispunch-700: #c2410c;
|
||||
--color-mispunch-800: #9a3412;
|
||||
|
||||
--color-holiday-100: #fce7f3;
|
||||
--color-holiday-200: #fbcfe8;
|
||||
--color-holiday-300: #f9a8d4;
|
||||
--color-holiday-700: #be185d;
|
||||
--color-holiday-800: #9d174d;
|
||||
|
||||
--color-today-50: #eff6ff;
|
||||
--color-today-100: #dbeafe;
|
||||
--color-today-300: #93c5fd;
|
||||
--color-today-700: #1d4ed8;
|
||||
--color-today-800: #1e40af;
|
||||
|
||||
/* Add these inside @theme */
|
||||
--color-late-100: #fef3c7; /* Amber */
|
||||
--color-late-200: #fde68a;
|
||||
--color-late-300: #fcd34d;
|
||||
--color-late-700: #b45309;
|
||||
--color-late-800: #92400e;
|
||||
|
||||
--color-halfday-100: #ccfbf1; /* Teal */
|
||||
--color-halfday-200: #99f6e4;
|
||||
--color-halfday-300: #5eead4;
|
||||
--color-halfday-700: #0f766e;
|
||||
--color-halfday-800: #115e59;
|
||||
|
||||
/* UI Neutral Colors */
|
||||
--color-sidebar: #1e293b;
|
||||
--color-sidebar-hover: #334155;
|
||||
--color-sidebar-active: #0f172a;
|
||||
|
||||
--color-app-bg: #f8fafc;
|
||||
--color-app-card: #ffffff;
|
||||
--color-app-border: #e2e8f0;
|
||||
--color-app-muted: #f1f5f9;
|
||||
|
||||
--color-text-primary: #0f172a;
|
||||
--color-text-secondary: #334155;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-light: #94a3b8;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
26
src/store/roleSlice.ts
Normal file
26
src/store/roleSlice.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export type UserRole = 'employee' | 'manager' | 'admin' | 'superadmin';
|
||||
|
||||
interface RoleState {
|
||||
currentRole: UserRole;
|
||||
}
|
||||
|
||||
// Change this initial state to test different users immediately on load
|
||||
const initialState: RoleState = {
|
||||
currentRole: 'employee',
|
||||
};
|
||||
|
||||
const roleSlice = createSlice({
|
||||
name: 'role',
|
||||
initialState,
|
||||
reducers: {
|
||||
setRole: (state, action: PayloadAction<UserRole>) => {
|
||||
state.currentRole = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setRole } = roleSlice.actions;
|
||||
export default roleSlice.reducer;
|
||||
11
src/store/store.ts
Normal file
11
src/store/store.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import roleReducer from './roleSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
role: roleReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
12
src/types/attendance.ts
Normal file
12
src/types/attendance.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export type AttendanceStatus = 'Present' | 'WFH' | 'Absent' | 'Leave' | 'Mispunch' | 'Holiday' | 'Late' | 'Half Day';
|
||||
|
||||
export interface AttendanceRecord {
|
||||
date: string;
|
||||
timeIn: string;
|
||||
timeOut: string;
|
||||
loggedHours: string;
|
||||
status: AttendanceStatus;
|
||||
}
|
||||
|
||||
// Add this line to fix the error
|
||||
export type SortKey = keyof AttendanceRecord;
|
||||
41
src/types/sidebarConfig.ts
Normal file
41
src/types/sidebarConfig.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { UserRole } from '../store/roleSlice';
|
||||
|
||||
export interface NavItem {
|
||||
name: string;
|
||||
icon: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const getSidebarItems = (role: UserRole): NavItem[] => {
|
||||
const baseItems: NavItem[] = [
|
||||
{ name: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' },
|
||||
{ name: 'Attendance', icon: 'CalendarCheck', path: '/attendance' },
|
||||
{ name: 'Leave', icon: 'Plane', path: '/leave' },
|
||||
];
|
||||
|
||||
if (role === 'manager') {
|
||||
return [...baseItems, { name: 'Approval', icon: 'CheckCircle', path: '/approval' }];
|
||||
}
|
||||
|
||||
if (role === 'admin') {
|
||||
return [
|
||||
...baseItems,
|
||||
{ name: 'Attendance Summary', icon: 'BarChart3', path: '/attendance-summary' },
|
||||
{ name: 'Leave Summary', icon: 'ClipboardList', path: '/leave-summary' },
|
||||
{ name: 'Manage Employees', icon: 'Users', path: '/manage-employees' },
|
||||
];
|
||||
}
|
||||
|
||||
if (role === 'superadmin') {
|
||||
return [
|
||||
...baseItems,
|
||||
{ name: 'Attendance Summary', icon: 'BarChart3', path: '/attendance-summary' },
|
||||
{ name: 'Leave Summary', icon: 'ClipboardList', path: '/leave-summary' },
|
||||
{ name: 'Manage Admins', icon: 'ShieldUser', path: '/manage-admins' }, // New
|
||||
{ name: 'Manage Employees', icon: 'Users', path: '/manage-employees' }, // New
|
||||
{ name: 'Policy and Rules', icon: 'ScrollText', path: '/policies' }, // New
|
||||
];
|
||||
}
|
||||
|
||||
return baseItems; // employee
|
||||
};
|
||||
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"allowArbitraryExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
23
tsconfig.node.json
Normal file
23
tsconfig.node.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"module": "nodenext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user