Compare commits
43 Commits
main
...
features-a
Author | SHA1 | Date | |
---|---|---|---|
|
f400490b65 | ||
700a9261a8 | |||
ffae2f728b | |||
307d2621ce | |||
6b19adf03e | |||
dcfa7a46d8 | |||
2afefc7442 | |||
d4fd7601f1 | |||
bd32eb02d6 | |||
bb108f809f | |||
abad63787b | |||
9f4059e2c6 | |||
962102d44c | |||
03c4988ff1 | |||
27f4597348 | |||
265d0b2209 | |||
a56f643301 | |||
8b34a69dca | |||
f7fea99f30 | |||
bd461995c7 | |||
17681e64ad | |||
154eba0474 | |||
dea0047007 | |||
f4b7027708 | |||
b4b193a9fe | |||
9d33cb5372 | |||
48b9b70c4a | |||
cc3943196e | |||
71ece53f65 | |||
7ae4e8dbef | |||
a3d4f2b11c | |||
c1df68b27a | |||
44112f91bd | |||
9d72dc6868 | |||
8cee8e0383 | |||
a6a67d69d5 | |||
bfe22a61a5 | |||
442b8e52dd | |||
03747b5251 | |||
650a6dbdd0 | |||
2b940c3d43 | |||
96784c02c7 | |||
835a3cc7fd |
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
@@ -1,8 +1,10 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
@@ -22,17 +24,17 @@ export default [
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
prettier,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
...prettierConfig.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
287
package-lock.json
generated
287
package-lock.json
generated
@@ -8,11 +8,13 @@
|
||||
"name": "osaka",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"lucide-react": "^0.446.0",
|
||||
"motion": "^11.15.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -26,11 +28,14 @@
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
@@ -1088,6 +1093,19 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
|
||||
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
|
||||
@@ -1663,6 +1681,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
@@ -1717,6 +1741,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1933,6 +1968,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@@ -2116,6 +2163,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -2452,6 +2508,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-config-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
|
||||
"integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.11.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-plugin-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/eslint": ">=8.0.0",
|
||||
"eslint": ">=8.0.0",
|
||||
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
|
||||
"prettier": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/eslint": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.0.tgz",
|
||||
@@ -2698,6 +2801,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
@@ -2816,6 +2926,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@@ -2843,6 +2973,20 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -2857,6 +3001,33 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "11.15.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
|
||||
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^11.14.3",
|
||||
"motion-utils": "^11.14.3",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -3905,6 +4076,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -3928,6 +4120,44 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/motion": {
|
||||
"version": "11.15.0",
|
||||
"resolved": "https://registry.npmjs.org/motion/-/motion-11.15.0.tgz",
|
||||
"integrity": "sha512-iZ7dwADQJWGsqsSkBhNHdI2LyYWU+hA1Nhy357wCLZq1yHxGImgt3l7Yv0HT/WOskcYDq9nxdedyl4zUv7UFFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.15.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
|
||||
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -4495,6 +4725,35 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -4506,6 +4765,12 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -5205,6 +5470,22 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
|
||||
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
|
||||
@@ -5327,6 +5608,12 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
@@ -7,14 +7,17 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"lucide-react": "^0.446.0",
|
||||
"motion": "^11.15.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -28,11 +31,14 @@
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
|
@@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
BIN
public/audio/error.mp3
Normal file
BIN
public/audio/error.mp3
Normal file
Binary file not shown.
BIN
public/audio/warning.mp3
Normal file
BIN
public/audio/warning.mp3
Normal file
Binary file not shown.
@@ -1,50 +1,54 @@
|
||||
{
|
||||
"appName": "সমন্বিত PACS কিষাণ সমাধান",
|
||||
"username": "ব্যবহারকারীর নাম",
|
||||
"pacsName": "PACS নাম",
|
||||
"userType": "ব্যবহারকারীর ধরন",
|
||||
"moduleName": "মডিউলের নাম",
|
||||
"date": "তারিখ",
|
||||
"home": "হোম",
|
||||
"moduleList": "মডিউল তালিকা",
|
||||
"enquiry": "অনুসন্ধান",
|
||||
"lockerOperation": "লকার অপারেশন",
|
||||
"reports": "রিপোর্ট",
|
||||
"worklist": "কাজের",
|
||||
"userManagement": "ব্যবহারকারী ব্যবস্থাপনা",
|
||||
"logout": "প্রস্থান",
|
||||
"kcc": "কেসিসি",
|
||||
"trading": "বাণিজ্য",
|
||||
"asset": "সম্পদ",
|
||||
"locker": "লকার",
|
||||
"pacsTeller": "PACS টেলার",
|
||||
"authoriser": "অনুমোদনকারী",
|
||||
"cashOfficer": "নগদ কর্মকর্তা",
|
||||
"selfHelpGroup": "স্বনির্ভর গোষ্ঠী",
|
||||
"deposit": "আমানত",
|
||||
"loan": "ঋণ",
|
||||
"share": "শেয়ার",
|
||||
"lockerEnquiry": "লকার অনুসন্ধান",
|
||||
"accountEnquiry": "অ্যাকাউন্ট অনুসন্ধান",
|
||||
"accountCreation": "অ্যাকাউন্ট তৈরি",
|
||||
"cabinetMaintenance": "ক্যাবিনেট রক্ষণাবেক্ষণ",
|
||||
"lockerMaintenance": "লকার রক্ষণাবেক্ষণ",
|
||||
"rentPenaltyCollection": "ভাড়া / জরিমানা সংগ্রহ",
|
||||
"chargeManagement": "চার্জ ব্যবস্থাপনা",
|
||||
"checkInOutManagement": "চেক ইন/আউট ব্যবস্থাপনা",
|
||||
"accountSurrender": "অ্যাকাউন্ট সমর্পণ",
|
||||
"myIntimation": "আমার বিজ্ঞপ্তি",
|
||||
"changePassword": "পাসওয়ার্ড পরিবর্তন",
|
||||
"resetLogin": "লগইন স্ট্যাটাস রিসেট করুন",
|
||||
"currentDate" : "{{val, datetime}}",
|
||||
"notifications": "বিজ্ঞপ্তি",
|
||||
"holidayList": "ছুটির তালিকা",
|
||||
"information": "তথ্য",
|
||||
"hpn_complete_before_31": "অনুগ্রহ করে ৩১শে মার্চের সমস্ত লেনদেন ২১:00 ঘটিকার আগে সম্পন্ন করুন",
|
||||
"hpn_npa": "আগামী ৩১শে মার্চ ২১:00 ঘটিকার আগে ঋণ এনপিএ রিপোর্ট থেকে প্রস্তাবিত এনপিএ স্থিতি যাচাই করার পরে প্রযোজ্য ঋণ অ্যাকাউন্টের জন্য এনপিএ চিহ্নিত/অচিহ্নিত করুন",
|
||||
"hpn_helpdesk_contact": "ফোনে সমস্যার সমাধানের জন্য আমাদের সাথে যোগাযোগ করুন 033-4065-9546 / 4071-8135 (সকাল ৯:00 থেকে রাত ৯:00 পর্যন্ত)",
|
||||
"hpn_rupay_kcc_time": "সমস্ত রূপে কিসান ক্রেডিট কার্ড লেনদেন ১৯:00 ঘটিকার আগে সম্পন্ন করুন",
|
||||
"hpn_rupay_kcc_atm": "কিছু অভ্যন্তরীণ পরিবর্তনের কারণে, মাইক্রো-এটিএম এর মাধ্যমে রূপে কিসান ক্রেডিট কার্ড কার্যক্রম বন্ধ করা হয়েছে। সেবা আগামীকাল থেকে পুনরায় চালু হবে।",
|
||||
"copyright_statement": "কপিরাইট © ২০২৩, টাটা কনসাল্টেন্সি সার্ভিসেস। সমস্ত অধিকার সংরক্ষিত",
|
||||
"privacy_policy": "গোপনীয়তা নীতি"
|
||||
"appName": "সমন্বিত PACS কিষাণ সমাধান",
|
||||
"username": "ব্যবহারকারীর নাম",
|
||||
"pacsName": "PACS নাম",
|
||||
"userType": "ব্যবহারকারীর ধরন",
|
||||
"moduleName": "মডিউলের নাম",
|
||||
"date": "তারিখ",
|
||||
"home": "হোম",
|
||||
"moduleList": "মডিউল তালিকা",
|
||||
"enquiry": "অনুসন্ধান",
|
||||
"lockerOperation": "লকার অপারেশন",
|
||||
"reports": "রিপোর্ট",
|
||||
"worklist": "কাজের",
|
||||
"userManagement": "ব্যবহারকারী ব্যবস্থাপনা",
|
||||
"logout": "প্রস্থান",
|
||||
"kcc": "কেসিসি",
|
||||
"trading": "বাণিজ্য",
|
||||
"asset": "সম্পদ",
|
||||
"locker": "লকার",
|
||||
"pacsTeller": "PACS টেলার",
|
||||
"authoriser": "অনুমোদনকারী",
|
||||
"cashOfficer": "নগদ কর্মকর্তা",
|
||||
"selfHelpGroup": "স্বনির্ভর গোষ্ঠী",
|
||||
"deposit": "আমানত",
|
||||
"loan": "ঋণ",
|
||||
"share": "শেয়ার",
|
||||
"lockerEnquiry": "লকার অনুসন্ধান",
|
||||
"accountEnquiry": "অ্যাকাউন্ট অনুসন্ধান",
|
||||
"accountCreation": "অ্যাকাউন্ট তৈরি",
|
||||
"cabinetMaintenance": "ক্যাবিনেট রক্ষণাবেক্ষণ",
|
||||
"lockerMaintenance": "লকার রক্ষণাবেক্ষণ",
|
||||
"rentPenaltyCollection": "ভাড়া / জরিমানা সংগ্রহ",
|
||||
"chargeManagement": "চার্জ ব্যবস্থাপনা",
|
||||
"checkInOutManagement": "চেক ইন/আউট ব্যবস্থাপনা",
|
||||
"accountSurrender": "অ্যাকাউন্ট সমর্পণ",
|
||||
"myIntimation": "আমার বিজ্ঞপ্তি",
|
||||
"changePassword": "পাসওয়ার্ড পরিবর্তন",
|
||||
"resetLogin": "লগইন স্ট্যাটাস রিসেট করুন",
|
||||
"currentDate": "{{val, datetime}}",
|
||||
"notifications": "বিজ্ঞপ্তি",
|
||||
"holidayList": "ছুটির তালিকা",
|
||||
"information": "তথ্য",
|
||||
"hpn_complete_before_31": "অনুগ্রহ করে ৩১শে মার্চের সমস্ত লেনদেন ২১:00 ঘটিকার আগে সম্পন্ন করুন",
|
||||
"hpn_npa": "আগামী ৩১শে মার্চ ২১:00 ঘটিকার আগে ঋণ এনপিএ রিপোর্ট থেকে প্রস্তাবিত এনপিএ স্থিতি যাচাই করার পরে প্রযোজ্য ঋণ অ্যাকাউন্টের জন্য এনপিএ চিহ্নিত/অচিহ্নিত করুন",
|
||||
"hpn_helpdesk_contact": "ফোনে সমস্যার সমাধানের জন্য আমাদের সাথে যোগাযোগ করুন 033-4065-9546 / 4071-8135 (সকাল ৯:00 থেকে রাত ৯:00 পর্যন্ত)",
|
||||
"hpn_rupay_kcc_time": "সমস্ত রূপে কিসান ক্রেডিট কার্ড লেনদেন ১৯:00 ঘটিকার আগে সম্পন্ন করুন",
|
||||
"hpn_rupay_kcc_atm": "কিছু অভ্যন্তরীণ পরিবর্তনের কারণে, মাইক্রো-এটিএম এর মাধ্যমে রূপে কিসান ক্রেডিট কার্ড কার্যক্রম বন্ধ করা হয়েছে। সেবা আগামীকাল থেকে পুনরায় চালু হবে।",
|
||||
"copyright_statement": "কপিরাইট © ২০২৩, টাটা কনসাল্টেন্সি সার্ভিসেস। সমস্ত অধিকার সংরক্ষিত",
|
||||
"privacy_policy": "গোপনীয়তা নীতি",
|
||||
"create": "তৈরি করুন",
|
||||
"operation": "অপারেশন",
|
||||
"next": "পরবর্তী",
|
||||
"select": "নির্বাচন করুন"
|
||||
}
|
||||
|
@@ -1,51 +1,83 @@
|
||||
{
|
||||
"appName": "Integrated PACS Kisan Solution",
|
||||
"username": "Username",
|
||||
"pacsName": "PACS Name",
|
||||
"userType": "User Type",
|
||||
"moduleName": "Module Name",
|
||||
"languageSelect": "Language",
|
||||
"date": "Date",
|
||||
"home": "Home",
|
||||
"moduleList": "Module List",
|
||||
"enquiry": "Enquiry",
|
||||
"lockerOperation": "Locker Operation",
|
||||
"reports": "Reports",
|
||||
"worklist": "Worklist",
|
||||
"userManagement": "User Management",
|
||||
"logout": "Logout",
|
||||
"kcc": "KCCP",
|
||||
"trading": "Trading",
|
||||
"asset": "Asset",
|
||||
"selfHelpGroup": "Self Help Group",
|
||||
"deposit": "Deposit",
|
||||
"loan": "Loan",
|
||||
"share": "Share",
|
||||
"locker": "Locker",
|
||||
"pacsTeller": "PACS Teller",
|
||||
"authoriser": "Authoriser",
|
||||
"cashOfficer": "Cash Officer",
|
||||
"lockerEnquiry": "Locker Enquiry",
|
||||
"accountEnquiry": "Account Enquiry",
|
||||
"accountCreation": "Account Creation",
|
||||
"cabinetMaintenance": "Cabinet Maintenance",
|
||||
"lockerMaintenance": "Locker Maintenance",
|
||||
"rentPenaltyCollection": "Rent / Penalty Collection",
|
||||
"chargeManagement": "Charge Management",
|
||||
"checkInOutManagement": "Check in/out Management",
|
||||
"accountSurrender": "Account Surrender",
|
||||
"myIntimation": "My Intimation",
|
||||
"changePassword": "Change Password",
|
||||
"resetLogin": "Reset Login Status",
|
||||
"currentDate" : "{{val, datetime}}",
|
||||
"notifications": "Notification",
|
||||
"holidayList": "Holiday List",
|
||||
"information": "Information",
|
||||
"hpn_complete_before_31": "Please complete all transactions of 31st March before 21:00 Hrs",
|
||||
"hpn_npa": "Do Mark/Unmark NPA for applicable loan accounts after verifying suggestive NPA status from loan NPA reports before upcoming 31st March 21:00 Hrs",
|
||||
"hpn_helpdesk_contact": "For on call issue resolution reach us at 033-4065-9546 / 4071-8135 (9:00 AM to 9:00 PM)",
|
||||
"hpn_rupay_kcc_time": "Complete all Rupay KCC transactions before 19:00 Hrs",
|
||||
"hpn_rupay_kcc_atm": "Due to some internal changes, Rupay KCC Operations through Micro-ATM has been stopped. The service will resume starting tomorrow.",
|
||||
"copyright_statement": "Copyright © 2023, Tata Consultancy Services. All rights reserved",
|
||||
"privacy_policy": "Privacy Policy"
|
||||
}
|
||||
"appName": "Integrated PACS Kisan Solution",
|
||||
"username": "Username",
|
||||
"pacsName": "PACS Name",
|
||||
"userType": "User Type",
|
||||
"moduleName": "Module Name",
|
||||
"languageSelect": "Language",
|
||||
"date": "Date",
|
||||
"home": "Home",
|
||||
"moduleList": "Module List",
|
||||
"enquiry": "Enquiry",
|
||||
"lockerOperation": "Locker Operation",
|
||||
"reports": "Reports",
|
||||
"worklist": "Worklist",
|
||||
"userManagement": "User Management",
|
||||
"logout": "Logout",
|
||||
"kcc": "KCCP",
|
||||
"trading": "Trading",
|
||||
"asset": "Asset",
|
||||
"selfHelpGroup": "Self Help Group",
|
||||
"deposit": "Deposit",
|
||||
"loan": "Loan",
|
||||
"share": "Share",
|
||||
"locker": "Locker",
|
||||
"pacsTeller": "PACS Teller",
|
||||
"authoriser": "Authoriser",
|
||||
"cashOfficer": "Cash Officer",
|
||||
"lockerEnquiry": "Locker Enquiry",
|
||||
"accountEnquiry": "Account Enquiry",
|
||||
"accountCreation": "Account Creation",
|
||||
"cabinetMaintenance": "Cabinet Maintenance",
|
||||
"lockerMaintenance": "Locker Maintenance",
|
||||
"rentPenaltyCollection": "Rent / Penalty Collection",
|
||||
"chargeManagement": "Charge Management",
|
||||
"checkInOutManagement": "Check in/out Management",
|
||||
"accountSurrender": "Account Surrender",
|
||||
"myIntimation": "My Intimation",
|
||||
"changePassword": "Change Password",
|
||||
"resetLogin": "Reset Login Status",
|
||||
"currentDate": "{{val, datetime}}",
|
||||
"notifications": "Notification",
|
||||
"holidayList": "Holiday List",
|
||||
"information": "Information",
|
||||
"hpn_complete_before_31": "Please complete all transactions of 31st March before 21:00 Hrs",
|
||||
"hpn_npa": "Do Mark/Unmark NPA for applicable loan accounts after verifying suggestive NPA status from loan NPA reports before upcoming 31st March 21:00 Hrs",
|
||||
"hpn_helpdesk_contact": "For on call issue resolution reach us at 033-4065-9546 / 4071-8135 (9:00 AM to 9:00 PM)",
|
||||
"hpn_rupay_kcc_time": "Complete all Rupay KCC transactions before 19:00 Hrs",
|
||||
"hpn_rupay_kcc_atm": "Due to some internal changes, Rupay KCC Operations through Micro-ATM has been stopped. The service will resume starting tomorrow.",
|
||||
"copyright_statement": "Copyright © 2023, Tata Consultancy Services. All rights reserved",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"create": "Create",
|
||||
"operation": "Operation",
|
||||
"next": "Next",
|
||||
"select": "Select",
|
||||
"cabinetCreation": "Cabinet Creation",
|
||||
"cabinetId": "Cabinet ID",
|
||||
"cabinetKeyId": "Cabinet Key ID",
|
||||
"noOfLockers": "No of Lockers",
|
||||
"productCode": "Product Code",
|
||||
"interestCategory": "Interest Category",
|
||||
"segmentCode": "Segment Code",
|
||||
"accountHolderType": "Account Holder Type",
|
||||
"primaryCifNumber": "Primary CIF Number",
|
||||
"nomineeCifNumber": "Nominee CIF Number",
|
||||
"activityCode": "Activity Code",
|
||||
"customerType": "Customer Type",
|
||||
"collateralFDAccount": "Collateral FD Account",
|
||||
"rentPayAccount": "Rent Pay Account",
|
||||
"submit": "Submit",
|
||||
"lockerId": "Locker ID",
|
||||
"status": "Status",
|
||||
"lockerStatus": "Locker Status",
|
||||
"open": "Open",
|
||||
"close": "Close",
|
||||
"reasonForChange": "Reason for Change",
|
||||
"oldKeyId": "Old Key ID",
|
||||
"newKeyId": "New Key ID",
|
||||
"confirmNewKeyId": "Confirm New Key ID",
|
||||
"highlightedFieldsInvalid": "Highlighted fields are invalid",
|
||||
"changeStatus": "Change Status",
|
||||
"keySwap": "Key Swap",
|
||||
"chargeEdit": "Edit Charges"
|
||||
}
|
||||
|
@@ -1,51 +1,56 @@
|
||||
{
|
||||
"appName": "समन्वित PACS किसान समाधान",
|
||||
"username": "उपयोगकर्ता नाम",
|
||||
"pacsName": "PACS नाम",
|
||||
"userType": "उपयोगकर्ता प्रकार",
|
||||
"moduleName": "मॉड्यूल नाम",
|
||||
"languageSelect": "भाषा",
|
||||
"date": "तारीख",
|
||||
"home": "होम",
|
||||
"moduleList": "मॉड्यूल सूची",
|
||||
"enquiry": "पूछताछ",
|
||||
"lockerOperation": "लॉकर ऑपरेशन",
|
||||
"reports": "रिपोर्ट",
|
||||
"worklist": "कार्य सूची",
|
||||
"userManagement": "उपयोगकर्ता प्रबंधन",
|
||||
"logout": "लॉगआउट",
|
||||
"kcc": "केसीसी",
|
||||
"trading": "व्यापार",
|
||||
"asset": "संपत्ति",
|
||||
"selfHelpGroup": "स्वयं सहायता समूह",
|
||||
"deposit": "जमा",
|
||||
"loan": "ऋण",
|
||||
"share": "शेयर",
|
||||
"locker": "लॉकर",
|
||||
"pacsTeller": "PACS टेलर",
|
||||
"authoriser": "PACS अधिकृत",
|
||||
"cashOfficer": "कैश अधिकारी",
|
||||
"lockerEnquiry": "लॉकर पूछताछ",
|
||||
"accountEnquiry": "खाता पूछताछ",
|
||||
"accountCreation": "खाता निर्माण",
|
||||
"cabinetMaintenance": "कैबिनेट रखरखाव",
|
||||
"lockerMaintenance": "लॉकर रखरखाव",
|
||||
"rentPenaltyCollection": "किराया / जुर्माना संग्रह",
|
||||
"chargeManagement": "शुल्क प्रबंधन",
|
||||
"checkInOutManagement": "चेक इन/आउट प्रबंधन",
|
||||
"accountSurrender": "खाता समर्पण",
|
||||
"myIntimation": "मेरा सूचितकरण",
|
||||
"changePassword": "पासवर्ड बदलें",
|
||||
"resetLogin": "लॉगिन स्थिति रीसेट करें",
|
||||
"currentDate" : "{{val, datetime}}",
|
||||
"notifications": "सूचना",
|
||||
"holidayList": "छुट्टी की सूची",
|
||||
"information": "जानकारी",
|
||||
"hpn_complete_before_31": "कृपया 31 मार्च के सभी लेनदेन 21:00 बजे से पहले पूरा करें",
|
||||
"hpn_npa": "कृपया आगामी 31 मार्च 21:00 बजे से पहले ऋण एनपीए रिपोर्ट से सुझावित एनपीए स्थिति की जांच के बाद संबंधित ऋण खातों के लिए एनपीए को चिह्नित/अचिह्नित करें",
|
||||
"hpn_helpdesk_contact": "किसी भी समस्या के समाधान के लिए हमें 033-4065-9546 / 4071-8135 (सुबह 9:00 से रात 9:00 बजे तक) पर संपर्क करें",
|
||||
"hpn_rupay_kcc_time": "सभी रुपे केसीसी लेनदेन 19:00 बजे से पहले पूरा करें",
|
||||
"hpn_rupay_kcc_atm": "कुछ आंतरिक बदलावों के कारण, माइक्रो-एटीएम के माध्यम से रुपे केसीसी संचालन बंद कर दिया गया है। सेवा कल से फिर से शुरू होगी।",
|
||||
"copyright_statement": "कॉपीराइट © 2023, टाटा कंसल्टेंसी सर्विसेज। सभी अधिकार सुरक्षित",
|
||||
"privacy_policy": "गोपनीयता नीति"
|
||||
}
|
||||
"appName": "समन्वित PACS किसान समाधान",
|
||||
"username": "उपयोगकर्ता नाम",
|
||||
"pacsName": "PACS नाम",
|
||||
"userType": "उपयोगकर्ता प्रकार",
|
||||
"moduleName": "मॉड्यूल नाम",
|
||||
"languageSelect": "भाषा",
|
||||
"date": "तारीख",
|
||||
"home": "होम",
|
||||
"moduleList": "मॉड्यूल सूची",
|
||||
"enquiry": "पूछताछ",
|
||||
"lockerOperation": "लॉकर ऑपरेशन",
|
||||
"reports": "रिपोर्ट",
|
||||
"worklist": "कार्य सूची",
|
||||
"userManagement": "उपयोगकर्ता प्रबंधन",
|
||||
"logout": "लॉगआउट",
|
||||
"kcc": "केसीसी",
|
||||
"trading": "व्यापार",
|
||||
"asset": "संपत्ति",
|
||||
"selfHelpGroup": "स्वयं सहायता समूह",
|
||||
"deposit": "जमा",
|
||||
"loan": "ऋण",
|
||||
"share": "शेयर",
|
||||
"locker": "लॉकर",
|
||||
"pacsTeller": "PACS टेलर",
|
||||
"authoriser": "PACS अधिकृत",
|
||||
"cashOfficer": "कैश अधिकारी",
|
||||
"lockerEnquiry": "लॉकर पूछताछ",
|
||||
"accountEnquiry": "खाता पूछताछ",
|
||||
"accountCreation": "खाता निर्माण",
|
||||
"cabinetMaintenance": "कैबिनेट रखरखाव",
|
||||
"lockerMaintenance": "लॉकर रखरखाव",
|
||||
"rentPenaltyCollection": "किराया / जुर्माना संग्रह",
|
||||
"chargeManagement": "शुल्क प्रबंधन",
|
||||
"checkInOutManagement": "चेक इन/आउट प्रबंधन",
|
||||
"accountSurrender": "खाता समर्पण",
|
||||
"myIntimation": "मेरा सूचितकरण",
|
||||
"changePassword": "पासवर्ड बदलें",
|
||||
"resetLogin": "लॉगिन स्थिति रीसेट करें",
|
||||
"currentDate": "{{val, datetime}}",
|
||||
"notifications": "सूचना",
|
||||
"holidayList": "छुट्टी की सूची",
|
||||
"information": "जानकारी",
|
||||
"hpn_complete_before_31": "कृपया 31 मार्च के सभी लेनदेन 21:00 बजे से पहले पूरा करें",
|
||||
"hpn_npa": "कृपया आगामी 31 मार्च 21:00 बजे से पहले ऋण एनपीए रिपोर्ट से सुझावित एनपीए स्थिति की जांच के बाद संबंधित ऋण खातों के लिए एनपीए को चिह्नित/अचिह्नित करें",
|
||||
"hpn_helpdesk_contact": "किसी भी समस्या के समाधान के लिए हमें 033-4065-9546 / 4071-8135 (सुबह 9:00 से रात 9:00 बजे तक) पर संपर्क करें",
|
||||
"hpn_rupay_kcc_time": "सभी रुपे केसीसी लेनदेन 19:00 बजे से पहले पूरा करें",
|
||||
"hpn_rupay_kcc_atm": "कुछ आंतरिक बदलावों के कारण, माइक्रो-एटीएम के माध्यम से रुपे केसीसी संचालन बंद कर दिया गया है। सेवा कल से फिर से शुरू होगी।",
|
||||
"copyright_statement": "कॉपीराइट © 2023, टाटा कंसल्टेंसी सर्विसेज। सभी अधिकार सुरक्षित",
|
||||
"privacy_policy": "गोपनीयता नीति",
|
||||
"create": "बनाएं",
|
||||
"operation": "ऑपरेशन",
|
||||
"next": "अगला",
|
||||
"select": "चुनें",
|
||||
"productCode": "प्रोडक्ट कोड"
|
||||
}
|
||||
|
61
src/App.jsx
61
src/App.jsx
@@ -1,15 +1,52 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import Header from "./components/Header"
|
||||
import Footer from "./components/Footer"
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
import { useState } from 'react';
|
||||
import Header from './components/Header';
|
||||
import Footer from './components/Footer';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { motion } from 'motion/react';
|
||||
import { ToastProvider } from './contexts/Toast';
|
||||
import { useLoading } from './hooks/useLoading';
|
||||
import LoadingBar from './components/LoadingBar';
|
||||
import { LoadingProvider } from './contexts/Loading';
|
||||
|
||||
const AnimatedOutlet = () => {
|
||||
const o = useOutlet();
|
||||
const [outlet] = useState(o);
|
||||
|
||||
return <div>{outlet}</div>;
|
||||
};
|
||||
|
||||
function LoadingBarWrapper() {
|
||||
const { isLoading } = useLoading();
|
||||
return isLoading ? <LoadingBar /> : null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
}
|
||||
const location = useLocation();
|
||||
return (
|
||||
<LoadingProvider>
|
||||
<div className="flex flex-col min-h-screen scrollbar">
|
||||
<Header />
|
||||
<LoadingBarWrapper />
|
||||
<main className="overflow-hidden flex flex-grow transition-color-mode md:p-10 2xl:px-70 bg-surface dark:bg-surface-dark">
|
||||
<ToastProvider>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.div
|
||||
className="w-full ovwerflow-hidden"
|
||||
key={location.pathname}
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
>
|
||||
<AnimatedOutlet />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</ToastProvider>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</LoadingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
@@ -3,19 +3,18 @@ import DarkModeToggle from './DarkModeToggle';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
function AppTitle() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className='flex items-center justify-between pt-3 pb-5'>
|
||||
<div className='flex items-center gap-5'>
|
||||
<img src={IpksLogo} alt="IPKS Logo" className="h-16" />
|
||||
<h1 className="font-bold text-title font-display">{t('appName')}</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<DarkModeToggle />
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-3 pb-5">
|
||||
<div className="flex items-center gap-5">
|
||||
<img src={IpksLogo} alt="IPKS Logo" className="h-16" />
|
||||
<h1 className="font-bold text-title font-display">{t('appName')}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<DarkModeToggle />
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,31 +1,30 @@
|
||||
import PropTypes from "prop-types";
|
||||
import BannerInfoElement from "./BannerInfoElement";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from 'prop-types';
|
||||
import BannerInfoElement from './BannerInfoElement';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function BannerInfo({info}) {
|
||||
const {t} = useTranslation();
|
||||
function BannerInfo({ info }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const infoElements = Object.keys(info).map((key) => (
|
||||
<BannerInfoElement key={key} title={t(key)} description={t(info[key])} />
|
||||
))
|
||||
infoElements.push(
|
||||
<BannerInfoElement
|
||||
key="date"
|
||||
title={t('date')}
|
||||
description={t('currentDate',{val: new Date(), formatParams: {
|
||||
val: { month: 'long', day: '2-digit', year: 'numeric'},
|
||||
},})}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className="flex justify-between pb-1">
|
||||
{infoElements}
|
||||
</div>
|
||||
)
|
||||
const infoElements = Object.keys(info).map((key) => (
|
||||
<BannerInfoElement key={key} title={t(key)} description={t(info[key])} />
|
||||
));
|
||||
infoElements.push(
|
||||
<BannerInfoElement
|
||||
key="date"
|
||||
title={t('date')}
|
||||
description={t('currentDate', {
|
||||
val: new Date(),
|
||||
formatParams: {
|
||||
val: { month: 'long', day: '2-digit', year: 'numeric' },
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
return <div className="flex justify-between pb-1">{infoElements}</div>;
|
||||
}
|
||||
|
||||
BannerInfo.propTypes = {
|
||||
info: PropTypes.object.isRequired
|
||||
}
|
||||
info: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default BannerInfo;
|
||||
export default BannerInfo;
|
||||
|
@@ -2,16 +2,16 @@ import PropTypes from 'prop-types';
|
||||
|
||||
function BannerInfoElement({ title, description }) {
|
||||
return (
|
||||
<div className='font-body'>
|
||||
<div className='text-base '>{title}</div>
|
||||
<div className='text-[18px] font-medium'>{description}</div>
|
||||
<div className="font-body">
|
||||
<div className="text-base ">{title}</div>
|
||||
<div className="text-[18px] font-medium">{description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BannerInfoElement.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default BannerInfoElement;
|
||||
export default BannerInfoElement;
|
||||
|
28
src/components/Button.jsx
Normal file
28
src/components/Button.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { motion } from 'motion/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function Button({ text, onClick, disabled }) {
|
||||
return (
|
||||
<motion.button
|
||||
whileHover={!disabled && { scale: 1.05 }}
|
||||
whileTap={!disabled && { scale: 0.95 }}
|
||||
className={clsx(
|
||||
'px-12 py-2 text-lg text-white dark:text-primary-dark rounded-full bg-primary dark:bg-secondary-dark',
|
||||
disabled && 'bg-[#cccccc] dark:bg-[#cccccc]'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{text}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Button;
|
@@ -17,7 +17,7 @@ const DarkModeToggle = () => {
|
||||
}, [darkMode]);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(prevMode => !prevMode);
|
||||
setDarkMode((prevMode) => !prevMode);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -31,4 +31,4 @@ const DarkModeToggle = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DarkModeToggle;
|
||||
export default DarkModeToggle;
|
||||
|
21
src/components/FieldError.jsx
Normal file
21
src/components/FieldError.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
function FieldError({ text }) {
|
||||
return (
|
||||
<motion.div
|
||||
className="text-sm text-error ml-3 pt-1"
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
>
|
||||
{text}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
FieldError.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FieldError;
|
12
src/components/FieldsWrapper.jsx
Normal file
12
src/components/FieldsWrapper.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function FieldsWrapper({ children, className = '' }) {
|
||||
return <div className={`flex flex-col gap-4 m-2 my-7 ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
FieldsWrapper.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FieldsWrapper;
|
@@ -10,4 +10,4 @@ function Footer() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
export default Footer;
|
||||
|
@@ -3,12 +3,25 @@ import clsx from 'clsx';
|
||||
|
||||
function FormBox({ title, children, alt = false }) {
|
||||
return (
|
||||
<div className={clsx(alt ? 'bg-secondary-variant dark:bg-secondary-variant-dark border-secondary-variant dark:border-secondary-variant-dark' : 'bg-surface-variant dark:bg-surface-variant-dark', 'transition-color-mode font-body border-secondary dark:border-secondary-dark border-2 p-4 rounded-3xl relative h-full')}>
|
||||
<label className={clsx(alt && 'bg-surface dark:bg-surface-dark border-3 border-secondary-variant dark:border-secondary-variant-dark', 'font-body absolute left-11 -top-4 bg-secondary dark:bg-secondary-dark text-primary dark:text-primary-dark font-medium py-1 px-4 rounded-full')}>
|
||||
<form
|
||||
className={clsx(
|
||||
alt
|
||||
? 'bg-secondary-variant dark:bg-secondary-variant-dark border-secondary-variant dark:border-secondary-variant-dark'
|
||||
: 'bg-surface-variant dark:bg-surface-variant-dark',
|
||||
'transition-color-mode font-body border-secondary dark:border-secondary-dark border-2 p-4 rounded-3xl relative h-full'
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={clsx(
|
||||
alt &&
|
||||
'bg-surface dark:bg-surface-dark border-3 border-secondary-variant dark:border-secondary-variant-dark',
|
||||
'font-body absolute left-11 -top-4 bg-secondary dark:bg-secondary-dark text-primary dark:text-primary-dark font-medium py-1 px-4 rounded-full z-20'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,4 +31,4 @@ FormBox.propTypes = {
|
||||
alt: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default FormBox;
|
||||
export default FormBox;
|
||||
|
44
src/components/FormField.jsx
Normal file
44
src/components/FormField.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { motion } from 'motion/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FormField({ label, children, icon, variant }) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<label
|
||||
className={clsx(
|
||||
'mr-20 text-lg text-black dark:text-primary-dark whitespace-nowrap',
|
||||
variant === 'long' && 'sm:w-[5%]'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<div className={clsx('flex w-full gap-4 items-center', variant === 'long' && 'gap-10')}>
|
||||
{children}
|
||||
{icon && (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={clsx(
|
||||
icon.mode === 'plain'
|
||||
? 'text-[#444]'
|
||||
: 'bg-primary rounded-full p-2 text-white cursor-pointer'
|
||||
)}
|
||||
onClick={icon.onClick}
|
||||
>
|
||||
{icon.icon}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormField.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
icon: PropTypes.object,
|
||||
variant: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormField;
|
11
src/components/FormHeader.jsx
Normal file
11
src/components/FormHeader.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function FormHeader({ text }) {
|
||||
return <h1 className="text-2xl font-medium text-primary mt-5">{text}</h1>;
|
||||
}
|
||||
|
||||
FormHeader.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FormHeader;
|
38
src/components/FormInput.jsx
Normal file
38
src/components/FormInput.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function FormInput({ props, valid = true, className = '' }) {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
{...props}
|
||||
className={clsx(
|
||||
`w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`,
|
||||
!valid && 'border-error'
|
||||
)}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{!valid && (
|
||||
<motion.div
|
||||
className="text-sm text-error ml-3 pt-1"
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
key="cabinetIdError"
|
||||
>
|
||||
Invalid Value
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormInput.propTypes = {
|
||||
props: PropTypes.object,
|
||||
valid: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormInput;
|
44
src/components/FormSelect.jsx
Normal file
44
src/components/FormSelect.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import clsx from 'clsx';
|
||||
import FieldError from './FieldError';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function FormSelect({ props, valid = true, className = '', options }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
{...props}
|
||||
className={clsx(
|
||||
`w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`,
|
||||
!valid && 'border-error'
|
||||
)}
|
||||
>
|
||||
<option disabled value="">
|
||||
{t('select')}
|
||||
</option>
|
||||
{options?.map(({ value, label }) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<AnimatePresence>{!valid && <FieldError text={'Invalid value'} />}</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormSelect.propTypes = {
|
||||
props: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
valid: PropTypes.bool,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default FormSelect;
|
@@ -1,44 +1,52 @@
|
||||
import { getUserInfoFromSession } from "../util/util";
|
||||
import AppTitle from "./AppTitle";
|
||||
import BannerInfo from "./BannerInfo";
|
||||
import MenuBar from "./MenuBar";
|
||||
import Separator from "./Separator";
|
||||
import { getUserInfoFromSession } from '../util/util';
|
||||
import AppTitle from './AppTitle';
|
||||
import BannerInfo from './BannerInfo';
|
||||
import MenuBar from './MenuBar';
|
||||
import Separator from './Separator';
|
||||
|
||||
function Header() {
|
||||
const userInfo = getUserInfoFromSession();
|
||||
const menuItems = [
|
||||
{ name: "home", submenu: [], path: "/" },
|
||||
{ name: 'home', submenu: [], path: '/' },
|
||||
{
|
||||
name: "moduleList",
|
||||
name: 'moduleList',
|
||||
submenu: [
|
||||
{ name: "kcc", path: "kcc" },
|
||||
{ name: "trading", path: "trading" },
|
||||
{ name: "asset", path: "asset" },
|
||||
{ name: "selfHelpGroup", path: "shg" },
|
||||
{ name: "deposit", path: "deposit" },
|
||||
{ name: "loan", path: "loan" },
|
||||
{ name: "share", path: "share" },
|
||||
{ name: 'kcc', path: 'kcc' },
|
||||
{ name: 'trading', path: 'trading' },
|
||||
{ name: 'asset', path: 'asset' },
|
||||
{ name: 'selfHelpGroup', path: 'shg' },
|
||||
{ name: 'deposit', path: 'deposit' },
|
||||
{ name: 'loan', path: 'loan' },
|
||||
{ name: 'share', path: 'share' },
|
||||
],
|
||||
},
|
||||
{ name: "enquiry", submenu: [{ name: "lockerEnquiry", path: "locker-enquiry" }, { name: "accountEnquiry", path: "account-enquiry" }] },
|
||||
{
|
||||
name: "lockerOperation",
|
||||
name: 'enquiry',
|
||||
submenu: [
|
||||
{ name: "accountCreation", path: "account-creation" },
|
||||
{ name: "cabinetMaintenance", path: "cabinet-maintenance" },
|
||||
{ name: "lockerMaintenance", path: "locker-maintenance" },
|
||||
{ name: "rentPenaltyCollection", path: "rent-collection" },
|
||||
{ name: "chargeManagement", path: "charge-management" },
|
||||
{ name: "checkInOutManagement", path: "check-in-out" },
|
||||
{ name: "accountSurrender", path: "account-surrender" }
|
||||
{ name: 'lockerEnquiry', path: 'locker-enquiry' },
|
||||
{ name: 'accountEnquiry', path: 'account-enquiry' },
|
||||
],
|
||||
},
|
||||
{ name: "worklist", submenu: [{ name: "myIntimation", path: "my-intimation" }] },
|
||||
{
|
||||
name: "userManagement",
|
||||
submenu: [{ name: "resetLogin", path: "reset-login" }, { name: "changePassword", path: "change-password" }],
|
||||
name: 'lockerOperation',
|
||||
submenu: [
|
||||
{ name: 'accountCreation', path: 'operation/account' },
|
||||
{ name: 'cabinetMaintenance', path: 'operation/cabinet' },
|
||||
{ name: 'lockerMaintenance', path: 'operation/locker' },
|
||||
{ name: 'chargeManagement', path: 'operation/charge-management' },
|
||||
{ name: 'checkInOutManagement', path: 'operation/check-in-out' },
|
||||
{ name: 'accountSurrender', path: 'operation/account-surrender' },
|
||||
],
|
||||
},
|
||||
{ name: "logout", submenu: [] },
|
||||
{ name: 'worklist', submenu: [{ name: 'myIntimation', path: 'my-intimation' }] },
|
||||
{
|
||||
name: 'userManagement',
|
||||
submenu: [
|
||||
{ name: 'resetLogin', path: 'reset-login' },
|
||||
{ name: 'changePassword', path: 'change-password' },
|
||||
],
|
||||
},
|
||||
{ name: 'logout', submenu: [] },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -48,7 +56,7 @@ function Header() {
|
||||
<Separator />
|
||||
<MenuBar menuItems={menuItems} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
@@ -9,7 +9,12 @@ const LanguageSelector = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select className='rounded-md font-body bg-secondary dark:bg-secondary-dark focus:outline-none' id="language-select" onChange={changeLanguage} value={i18n.language}>
|
||||
<select
|
||||
className="rounded-md font-body bg-secondary dark:bg-secondary-dark focus:outline-none"
|
||||
id="language-select"
|
||||
onChange={changeLanguage}
|
||||
value={i18n.language}
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="bn">বাংলা</option>
|
||||
<option value="hi">हिन्दी</option>
|
||||
@@ -18,4 +23,4 @@ const LanguageSelector = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
export default LanguageSelector;
|
||||
|
15
src/components/LoadingBar.jsx
Normal file
15
src/components/LoadingBar.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
function LoadingBar() {
|
||||
return (
|
||||
<div className="h-1 bg-grey relative overflow-hidden">
|
||||
<motion.div
|
||||
className="bg-primary dark:bg-primary-dark w-full h-full rounded-sm absolute"
|
||||
animate={{ x: ['-100%', '100%'] }}
|
||||
transition={{ repeat: Infinity, duration: 2, ease: 'anticipate' }}
|
||||
></motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingBar;
|
@@ -1,33 +1,72 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
|
||||
function SubMenu({ items }) {
|
||||
function SubMenu({ items, isVisible, onLinkClick }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="absolute left-0 z-50 invisible transition-all duration-200 -translate-y-6 shadow-sm opacity-0 top-full border-t-3 border-primary dark:border-primary-dark bg-secondary dark:bg-secondary-dark rounded-2xl shadow-surface-variant-dark dark:shadow-primary group-hover:visible group-hover:translate-y-0 group-hover:opacity-100">
|
||||
{items.map((subItem, index) => (
|
||||
<div key={index} className="px-6 py-2 cursor-pointer text-nowrap hover:bg-white dark:hover:bg-secondary-variant-dark first:rounded-t-2xl second-last:rounded-b-2xl first:pt-4 last:pb-4">
|
||||
<Link to={subItem.path}>{t(subItem.name)}</Link>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{isVisible && (
|
||||
<div>
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'absolute left-0 z-50 shadow-sm top-full border-t-3 border-primary dark:border-primary-dark bg-secondary dark:bg-secondary-dark rounded-2xl shadow-surface-variant-dark dark:shadow-primary'
|
||||
)}
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
transition={{ duration: 0.3, type: 'spring' }}
|
||||
>
|
||||
{items.map((subItem, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="px-3 py-2 cursor-pointer text-nowrap dark:hover:bg-secondary-variant-dark first:rounded-t-2xl first:pt-4 last:pb-4 second-last:rounded-b-2xl hover:bg-white"
|
||||
onClick={onLinkClick}
|
||||
>
|
||||
<Link to={subItem.path}>{t(subItem.name)}</Link>
|
||||
</div>
|
||||
))}
|
||||
<svg
|
||||
className="absolute top-0 left-6 mt-[-13px] fill-primary dark:fill-primary-dark"
|
||||
width="16"
|
||||
height="13"
|
||||
viewBox="0 0 16 13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6.26795 1C7.03775 -0.333332 8.96225 -0.333334 9.73205 0.999999L14.9282 10C15.698 11.3333 14.7358 13 13.1962 13H2.80385C1.26425 13 0.301996 11.3333 1.0718 10L6.26795 1Z" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
<svg className="absolute top-0 left-6 mt-[-13px] fill-primary dark:fill-primary-dark" width="16" height="13" viewBox="0 0 16 13" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.26795 1C7.03775 -0.333332 8.96225 -0.333334 9.73205 0.999999L14.9282 10C15.698 11.3333 14.7358 13 13.1962 13H2.80385C1.26425 13 0.301996 11.3333 1.0718 10L6.26795 1Z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function MenuBar({ menuItems }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeMenu, setActiveMenu] = useState(null);
|
||||
return (
|
||||
<div className="flex justify-between pt-5 pb-2 text-base font-body">
|
||||
{menuItems.map((item, index) =>
|
||||
<div key={index} className="relative pb-3 cursor-pointer group">
|
||||
{t(item.name)}
|
||||
{item.submenu.length > 0 && <SubMenu items={item.submenu} />}
|
||||
{menuItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative pb-3 cursor-pointer"
|
||||
onMouseEnter={() => setActiveMenu(index)}
|
||||
onMouseLeave={() => setActiveMenu(null)}
|
||||
>
|
||||
<Link to={item.path}>{t(item.name)}</Link>
|
||||
{item.submenu.length > 0 && (
|
||||
<SubMenu
|
||||
items={item.submenu}
|
||||
isVisible={activeMenu === index}
|
||||
onLinkClick={() => setActiveMenu(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,18 +81,20 @@ MenuBar.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
path: PropTypes.string
|
||||
path: PropTypes.string,
|
||||
})
|
||||
).isRequired
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
SubMenu.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired
|
||||
path: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired
|
||||
).isRequired,
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
onLinkClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MenuBar;
|
||||
|
48
src/components/Notification.jsx
Normal file
48
src/components/Notification.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import clsx from 'clsx';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function Notification({ message, type }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
className={clsx(
|
||||
'p-2 pl-8 mb-8 font-body text-lg border-2 rounded-3xl flex items-center gap-2',
|
||||
type === 'error'
|
||||
? 'bg-error-surface text-white border-error'
|
||||
: type === 'success'
|
||||
? 'bg-success text-white border-green-700'
|
||||
: type === 'warning'
|
||||
? 'bg-warning-surface text-white border-warning'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{message.split(':').map((msg, index) => {
|
||||
return index === 1 ? (
|
||||
<span key={index} className="border-b border-dashed">
|
||||
{msg}
|
||||
</span>
|
||||
) : (
|
||||
<span key={index}>{msg}</span>
|
||||
);
|
||||
})}
|
||||
{message.split(':')[1] && (
|
||||
<Copy
|
||||
cursor={'pointer'}
|
||||
size={15}
|
||||
onClick={navigator.clipboard.writeText(message.split(':')[1].trim())}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Notification.propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Notification;
|
74
src/components/ProductListTable.jsx
Normal file
74
src/components/ProductListTable.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ProductListTable({ productInfo, onSelectProduct }) {
|
||||
return (
|
||||
<table className="w-11/12 border-separate border-spacing-0 rounded-2xl overflow-hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-t-2 border-l-2 border-primary bg-secondary rounded-ss-2xl font-medium text-[#111]">
|
||||
Product Code
|
||||
</th>
|
||||
<th className="border border-t-2 p-2 border-primary bg-secondary font-medium text-[#111]">
|
||||
Description
|
||||
</th>
|
||||
<th className="border border-t-2 p-2 border-primary bg-secondary font-medium text-[#111]">
|
||||
Interest Category
|
||||
</th>
|
||||
<th className="border border-t-2 border-r-2 p-2 border-primary bg-secondary rounded-se-2xl font-medium text-[#111]">
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{productInfo.map((prod, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className="cursor-pointer hover:bg-grey/[0.2] border-b"
|
||||
onClick={() => onSelectProduct(prod)}
|
||||
>
|
||||
<td
|
||||
className={clsx(
|
||||
'border border-l-2 border-primary p-2',
|
||||
idx === productInfo.length - 1 && 'rounded-bl-2xl border-b-2'
|
||||
)}
|
||||
>
|
||||
{prod.productCode}
|
||||
</td>
|
||||
<td
|
||||
className={clsx(
|
||||
'border border-primary p-2',
|
||||
idx === productInfo.length - 1 && 'border-b-2'
|
||||
)}
|
||||
>
|
||||
{prod.productCodeDescription}
|
||||
</td>
|
||||
<td
|
||||
className={clsx(
|
||||
'border border-primary p-2',
|
||||
idx === productInfo.length - 1 && 'border-b-2'
|
||||
)}
|
||||
>
|
||||
{prod.interestCategory}
|
||||
</td>
|
||||
<td
|
||||
className={clsx(
|
||||
'border border-r-2 border-primary p-2',
|
||||
idx === productInfo.length - 1 && 'rounded-br-2xl border-b-2'
|
||||
)}
|
||||
>
|
||||
{prod.interestCategoryDescription}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
ProductListTable.propTypes = {
|
||||
productInfo: PropTypes.array.isRequired,
|
||||
onSelectProduct: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProductListTable;
|
32
src/components/ProductModal.jsx
Normal file
32
src/components/ProductModal.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { motion } from 'motion/react';
|
||||
import ProductListTable from './ProductListTable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ProductModal({ productInfo, handleProductSelect }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
key="productList"
|
||||
className="fixed z-50 inset-0 flex items-center justify-center bg-black/50"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: 15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 15, opacity: 0 }}
|
||||
className="flex flex-col items-center bg-white p-4 py-8 rounded-3xl w-[60%] max-h-[80%] overflow-auto font-body"
|
||||
>
|
||||
<h2 className="text-xl mb-4">Select Product</h2>
|
||||
<ProductListTable productInfo={productInfo} onSelectProduct={handleProductSelect} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
ProductModal.propTypes = {
|
||||
productInfo: PropTypes.object.isRequired,
|
||||
handleProductSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProductModal;
|
@@ -3,7 +3,7 @@ function Separator() {
|
||||
<div className="h-[2px]">
|
||||
<div className="w-full h-full bg-white rounded-md dark:bg-primary-dark"></div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Separator;
|
||||
export default Separator;
|
||||
|
18
src/contexts/Loading.jsx
Normal file
18
src/contexts/Loading.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const LoadingContext = createContext();
|
||||
|
||||
export function LoadingProvider({ children }) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={{ isLoading, setIsLoading }}>
|
||||
{children}
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
LoadingProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
69
src/contexts/Toast.jsx
Normal file
69
src/contexts/Toast.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createContext, useState } from 'react';
|
||||
import { CircleAlert, X, CircleX, Check } from 'lucide-react';
|
||||
import { toTitleCase } from '../util/util';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ToastContext = createContext();
|
||||
|
||||
export const ToastProvider = ({ children }) => {
|
||||
const [toast, setToast] = useState({ show: false, message: '', type: '' });
|
||||
|
||||
const playAudio = (type) => {
|
||||
let audioSrc;
|
||||
if (type === 'warning') audioSrc = '/audio/warning.mp3';
|
||||
else if (type === 'error') audioSrc = '/audio/error.mp3';
|
||||
|
||||
if (audioSrc) {
|
||||
const audio = new Audio(audioSrc);
|
||||
audio.play().catch((error) => console.error('Error playing audio:', error));
|
||||
}
|
||||
};
|
||||
|
||||
const showToast = (message, type) => {
|
||||
playAudio(type);
|
||||
setToast({ show: true, message, type });
|
||||
setTimeout(() => {
|
||||
setToast({ show: false, message: '', type: '' });
|
||||
}, 7000);
|
||||
};
|
||||
let toastIcon;
|
||||
let surfaceColor;
|
||||
let borderColor;
|
||||
if (toast.type === 'warning') {
|
||||
toastIcon = <CircleAlert size={30} fill="#EA7000" stroke="#FDF1E5" />;
|
||||
surfaceColor = 'bg-warning-surface';
|
||||
borderColor = 'border-warning';
|
||||
} else if (toast.type === 'success') {
|
||||
toastIcon = <Check size={30} color="green" />;
|
||||
surfaceColor = 'bg-success-surface';
|
||||
borderColor = 'border-success';
|
||||
} else if (toast.type === 'error') {
|
||||
toastIcon = <CircleX size={30} fill="#E5254B" stroke="#FCE9ED" />;
|
||||
surfaceColor = 'bg-error-surface';
|
||||
borderColor = 'border-error';
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={showToast}>
|
||||
{children}
|
||||
{toast.show && (
|
||||
<div
|
||||
className={`fixed bottom-10 right-5 px-5 py-2 ${surfaceColor} border-2 border-l-8 ${borderColor} rounded-xl font-medium text-onToast z-10 flex gap-5 items-center animate-[slideIn_0.5s]`}
|
||||
role="alert"
|
||||
>
|
||||
{toastIcon}
|
||||
<div>
|
||||
<div className="text-lg text-onToast">{toTitleCase(toast.type)}</div>
|
||||
<div className="text-sm font-body">{toast.message}</div>
|
||||
</div>
|
||||
<X onClick={() => setToast({ show: false, message: '', type: '' })} />
|
||||
</div>
|
||||
)}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ToastProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
4
src/hooks/useLoading.js
Normal file
4
src/hooks/useLoading.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { LoadingContext } from '../contexts/Loading';
|
||||
|
||||
export const useLoading = () => useContext(LoadingContext);
|
4
src/hooks/useToast.js
Normal file
4
src/hooks/useToast.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { ToastContext } from '../contexts/Toast';
|
||||
|
||||
export const useToast = () => useContext(ToastContext);
|
@@ -16,4 +16,4 @@ i18n
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export default i18n;
|
||||
|
88
src/main.jsx
88
src/main.jsx
@@ -1,33 +1,81 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
import './i18n'
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './index.css';
|
||||
import './i18n';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import Placeholder from './pages/Placeholder.jsx';
|
||||
import Home from './pages/Home.jsx'
|
||||
import Home from './pages/Home.jsx';
|
||||
import CabinetMaintenace from './pages/CabinetMaintenance.jsx';
|
||||
import CabinetCreation from './pages/CabinetCreation.jsx';
|
||||
import LockersRegistration from './pages/LockersRegistration.jsx';
|
||||
import AccountCreation from './pages/AccountCreation.jsx';
|
||||
import LockerMaintenance from './pages/LockerMaintenance.jsx';
|
||||
import LockerStatus from './pages/LockerStatus.jsx';
|
||||
import KeySwap from './pages/KeySwap.jsx';
|
||||
import ChargeManagement from './pages/ChargeManagement.jsx';
|
||||
import ChargeEdit from './pages/ChargeEdit.jsx';
|
||||
import CheckInOutManagement from './pages/CheckInOutManagement.jsx';
|
||||
import CheckInOutLog from './pages/CheckInOutLog.jsx';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <App/>,
|
||||
path: '/',
|
||||
element: <App />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: 'cabinet-maintenance',
|
||||
element: <Placeholder />
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
path: 'operation/cabinet',
|
||||
element: <CabinetMaintenace />,
|
||||
},
|
||||
{
|
||||
path: 'operation/cabinet/create',
|
||||
element: <CabinetCreation />,
|
||||
},
|
||||
{
|
||||
path: 'operation/cabinet/create/register-lockers',
|
||||
element: <LockersRegistration />,
|
||||
},
|
||||
{
|
||||
path: 'operation/account',
|
||||
element: <AccountCreation />,
|
||||
},
|
||||
{
|
||||
path: 'operation/locker',
|
||||
element: <LockerMaintenance />,
|
||||
},
|
||||
{
|
||||
path: 'operation/locker/status',
|
||||
element: <LockerStatus />,
|
||||
},
|
||||
{
|
||||
path: 'operation/locker/key-swap',
|
||||
element: <KeySwap />,
|
||||
},
|
||||
{
|
||||
path: 'operation/charge-management',
|
||||
element: <ChargeManagement />,
|
||||
},
|
||||
{
|
||||
path: 'operation/charge-management/change',
|
||||
element: <ChargeEdit />,
|
||||
},
|
||||
{
|
||||
path: 'operation/check-in-out',
|
||||
element: <CheckInOutManagement />,
|
||||
},
|
||||
{
|
||||
path: 'operation/check-in-out/log',
|
||||
element: <CheckInOutLog />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
)
|
||||
</StrictMode>
|
||||
);
|
||||
|
234
src/pages/AccountCreation.jsx
Normal file
234
src/pages/AccountCreation.jsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { PackageSearch, UserSearch } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import FormBox from '../components/FormBox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import Button from '../components/Button';
|
||||
import productInfo from '../util/productList';
|
||||
import Notification from '../components/Notification';
|
||||
import ProductModal from '../components/ProductModal';
|
||||
import FormHeader from '../components/FormHeader';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function AccountCreation() {
|
||||
const { t } = useTranslation();
|
||||
const [notification] = useState({ message: '', type: '' });
|
||||
const [showProductModal, setShowProductModal] = useState(false);
|
||||
const [accountDetails, setAccountDetails] = useState({
|
||||
productCode: '',
|
||||
interestCategory: '',
|
||||
segmentCode: '',
|
||||
accountHolderType: '',
|
||||
primaryCifNumber: '',
|
||||
nomineeCifNumber: '',
|
||||
activityCode: '',
|
||||
customerType: '',
|
||||
collateralFDAccount: '',
|
||||
rentPayAccount: '',
|
||||
productCodeValid: true,
|
||||
interestCategoryValid: true,
|
||||
segmentCodeValid: true,
|
||||
accountHolderTypeValid: true,
|
||||
primaryCifNumberValid: true,
|
||||
nomineeCifNumberValid: true,
|
||||
activityCodeValid: true,
|
||||
customerTypeValid: true,
|
||||
collateralFDAccountValid: true,
|
||||
rentPayAccountValid: true,
|
||||
});
|
||||
|
||||
const handleProductSelect = (product) => {
|
||||
const newAccountDetails = { ...accountDetails };
|
||||
newAccountDetails.productCode = product.productCode;
|
||||
newAccountDetails.interestCategory = product.interestCategory;
|
||||
newAccountDetails.productCodeValid = true;
|
||||
newAccountDetails.interestCategoryValid = true;
|
||||
setAccountDetails(newAccountDetails);
|
||||
setShowProductModal(false);
|
||||
};
|
||||
|
||||
const accountDetailsFields = [
|
||||
{
|
||||
label: t('productCode'),
|
||||
name: 'productCode',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
readOnly: true,
|
||||
validate: (value) => value !== '',
|
||||
icon: {
|
||||
icon: <PackageSearch size={18} />,
|
||||
onClick: () => setShowProductModal(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('interestCategory'),
|
||||
name: 'interestCategory',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
readOnly: true,
|
||||
validate: (value) => value !== '',
|
||||
},
|
||||
{
|
||||
label: t('segmentCode'),
|
||||
name: 'segmentCode',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '0706', label: '0706: Individual' },
|
||||
{ value: '0306', label: '0306: Staff' },
|
||||
{ value: '5003', label: '5003: Senior Citizen' },
|
||||
{ value: '5010', label: '5010: SHG' },
|
||||
{ value: '5000', label: '5000: Bank' },
|
||||
{ value: '5009', label: '5009: Institutions' },
|
||||
{ value: '5050', label: '5050: Others' },
|
||||
{ value: '5007', label: '5007: Society' },
|
||||
],
|
||||
validate: (value) => value !== '',
|
||||
},
|
||||
{
|
||||
label: t('accountHolderType'),
|
||||
name: 'accountHolderType',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '1', label: 'Single' },
|
||||
{ value: '2', label: 'Joint' },
|
||||
],
|
||||
validate: (value) => value === '1' || value === '2',
|
||||
},
|
||||
{
|
||||
label: t('primaryCifNumber'),
|
||||
name: 'primaryCifNumber',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
maxLength: 17,
|
||||
validate: (value) => /^[0-9]{17}$/.test(value),
|
||||
icon: { icon: <UserSearch size={18} /> },
|
||||
},
|
||||
{
|
||||
label: t('nomineeCifNumber'),
|
||||
name: 'nomineeCifNumber',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
maxLength: 17,
|
||||
validate: (value) => /^[0-9]{17}$/.test(value),
|
||||
},
|
||||
];
|
||||
const additionalDetailsFields = [
|
||||
{
|
||||
label: t('activityCode'),
|
||||
name: 'activityCode',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '0701', label: 'Direct Agriculture' },
|
||||
{ value: '0702', label: 'Indirect Agriculture' },
|
||||
{ value: '0703', label: 'Agricultural Services Unit' },
|
||||
{ value: '0704', label: 'Farm Irrigation' },
|
||||
{ value: '0705', label: 'Fruits & Vegetables' },
|
||||
{ value: '0706', label: 'Non-Agriculture' },
|
||||
],
|
||||
validate: (value) => value !== '',
|
||||
},
|
||||
{
|
||||
label: t('customerType'),
|
||||
name: 'customerType',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: '0709', label: 'Individual' },
|
||||
{ value: '0701', label: 'Corporate' },
|
||||
],
|
||||
validate: (value) => value === '0709' || value === '0701',
|
||||
},
|
||||
{
|
||||
label: t('collateralFDAccount'),
|
||||
name: 'collateralFDAccount',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
maxLength: 17,
|
||||
validate: (value) => /^[0-9]{17}$/.test(value),
|
||||
},
|
||||
{
|
||||
label: t('rentPayAccount'),
|
||||
name: 'rentPayAccount',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
maxLength: 17,
|
||||
validate: (value) => /^[0-9]{17}$/.test(value),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let isValid = true;
|
||||
const newValidationState = { ...accountDetails };
|
||||
|
||||
// Validate account details fields
|
||||
[...accountDetailsFields, ...additionalDetailsFields].forEach((field) => {
|
||||
if (field.validate) {
|
||||
const fieldIsValid = field.validate(accountDetails[field.name]);
|
||||
newValidationState[`${field.name}Valid`] = fieldIsValid;
|
||||
if (!fieldIsValid) isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
setAccountDetails(newValidationState);
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
console.log('Form is valid', accountDetails);
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: accountDetails[field.name],
|
||||
onChange: (e) => {
|
||||
const newAccountDetails = { ...accountDetails };
|
||||
newAccountDetails[field.name] = e.target.value;
|
||||
newAccountDetails[`${field.name}Valid`] = true;
|
||||
setAccountDetails(newAccountDetails);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
type: field.subType,
|
||||
readOnly: field.readOnly,
|
||||
};
|
||||
const valid = accountDetails[`${field.name}Valid`];
|
||||
|
||||
return (
|
||||
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} valid={valid} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} valid={valid} options={field.options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{showProductModal && (
|
||||
<ProductModal productInfo={productInfo} handleProductSelect={handleProductSelect} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<FormBox title="Account Creation">
|
||||
<FieldsWrapper>
|
||||
<FormHeader text={'Account Details'} />
|
||||
{accountDetailsFields.map(renderField)}
|
||||
<FormHeader text={'Additional Details'} />
|
||||
{additionalDetailsFields.map(renderField)}
|
||||
</FieldsWrapper>
|
||||
<Button text={t('submit')} onClick={handleSubmit} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountCreation;
|
107
src/pages/CabinetCreation.jsx
Normal file
107
src/pages/CabinetCreation.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormBox from '../components/FormBox';
|
||||
import Button from '../components/Button';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
|
||||
function CabinetCreation() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [cabinetDetails, setCabinetDetails] = useState({
|
||||
cabinetId: '',
|
||||
cabinetIdValid: true,
|
||||
cabinetKeyId: '',
|
||||
cabinetKeyIdValid: true,
|
||||
noOfLockers: 0,
|
||||
noOfLockersValid: true,
|
||||
});
|
||||
|
||||
const handleNext = (e) => {
|
||||
e.preventDefault();
|
||||
let isFormValid = true;
|
||||
const newValues = { ...cabinetDetails };
|
||||
formFields.forEach((field) => {
|
||||
if (field.validate) {
|
||||
const isFieldValid = field.validate(cabinetDetails[field.name]);
|
||||
newValues[`${field.name}Valid`] = isFieldValid;
|
||||
if (!isFieldValid) isFormValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isFormValid) {
|
||||
setCabinetDetails(newValues);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('register-lockers', {
|
||||
state: {
|
||||
cabinetId: cabinetDetails.cabinetId,
|
||||
cabinetKeyId: cabinetDetails.cabinetKeyId,
|
||||
noOfLockers: parseInt(cabinetDetails.noOfLockers),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'cabinetId',
|
||||
label: t('cabinetId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
validate: (v) => /^\w{2}\d{4}$/.test(v),
|
||||
},
|
||||
{
|
||||
name: 'cabinetKeyId',
|
||||
label: t('cabinetKeyId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
validate: (v) => /^\w{2}\d{4}$/.test(v),
|
||||
},
|
||||
{
|
||||
name: 'noOfLockers',
|
||||
label: t('noOfLockers'),
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
validate: (v) => parseInt(v) > 0,
|
||||
},
|
||||
];
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: cabinetDetails[field.name],
|
||||
onChange: (e) => {
|
||||
const newCabinetDetails = { ...cabinetDetails };
|
||||
newCabinetDetails[field.name] = e.target.value.toUpperCase();
|
||||
newCabinetDetails[`${field.name}Valid`] = true;
|
||||
setCabinetDetails(newCabinetDetails);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
type: field.subType,
|
||||
};
|
||||
const valid = cabinetDetails[`${field.name}Valid`];
|
||||
|
||||
return (
|
||||
<FormField key={field.name} label={field.label}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} valid={valid} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} valid={valid} options={field.options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormBox title={t('cabinetCreation')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button text={t('next')} onClick={handleNext} />
|
||||
</FormBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default CabinetCreation;
|
65
src/pages/CabinetMaintenance.jsx
Normal file
65
src/pages/CabinetMaintenance.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormBox from '../components/FormBox';
|
||||
import Button from '../components/Button';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
import FormField from '../components/FormField';
|
||||
|
||||
function CabinetMaintenace() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [operation, setOperation] = useState({ value: '', valid: true });
|
||||
|
||||
const handleNext = (e) => {
|
||||
e.preventDefault();
|
||||
if (operation.value === '') {
|
||||
setOperation({ value: operation.value, valid: false });
|
||||
}
|
||||
navigate(operation.value);
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'value',
|
||||
label: t('operation'),
|
||||
options: [{ label: t('create'), value: 'create' }],
|
||||
type: 'select',
|
||||
},
|
||||
];
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: operation[field.name],
|
||||
onChange: (e) => {
|
||||
const newValues = { ...operation };
|
||||
newValues[field.name] = e.target.value;
|
||||
newValues[`${field.name}Valid`] = true;
|
||||
setOperation(newValues);
|
||||
},
|
||||
};
|
||||
const options = field.options;
|
||||
return (
|
||||
<FormField label={field.label}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} options={options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormBox title={t('cabinetMaintenance')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button text={t('next')} onClick={(e) => handleNext(e)} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CabinetMaintenace;
|
175
src/pages/ChargeEdit.jsx
Normal file
175
src/pages/ChargeEdit.jsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLoading } from '../hooks/useLoading';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import Button from '../components/Button';
|
||||
import FormBox from '../components/FormBox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { lockerService } from '../services/locker.service';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import Notification from '../components/Notification';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function ChargeEdit() {
|
||||
const [chargeDetails, setChargeDetails] = useState({
|
||||
rentAmount: '',
|
||||
rentAmountEdit: false,
|
||||
penaltyAmount: '',
|
||||
penaltyAmountEdit: false,
|
||||
});
|
||||
|
||||
const [notification, setNotification] = useState({ message: '', type: '' });
|
||||
|
||||
const { setIsLoading } = useLoading();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const productCode = location.state?.productCode;
|
||||
const interestCategory = location.state?.interestCategory;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCharges = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await lockerService.getCharges(productCode, interestCategory);
|
||||
if (response.status === 200) {
|
||||
const { rent, penalty } = response.data;
|
||||
setChargeDetails({
|
||||
rentAmount: rent.toString(),
|
||||
rentAmountEdit: false,
|
||||
penaltyAmount: penalty.toString(),
|
||||
penaltyAmountEdit: false,
|
||||
});
|
||||
} else {
|
||||
setNotification({
|
||||
message: response.data.message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNotification({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchCharges();
|
||||
}, [productCode, interestCategory, setIsLoading]);
|
||||
|
||||
if (!location.state) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'rentAmount',
|
||||
label: 'Rent Amount',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
readOnly: !chargeDetails.rentAmountEdit,
|
||||
icon: {
|
||||
icon: <Pencil size={22} />,
|
||||
mode: 'plain',
|
||||
onClick: () => {
|
||||
setChargeDetails({ ...chargeDetails, rentAmountEdit: true });
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'penaltyAmount',
|
||||
label: 'Penalty Amount',
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
readOnly: !chargeDetails.penaltyAmountEdit,
|
||||
icon: {
|
||||
icon: <Pencil size={22} />,
|
||||
mode: 'plain',
|
||||
onClick: () => {
|
||||
setChargeDetails({ ...chargeDetails, penaltyAmountEdit: true });
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await lockerService.updateCharges(
|
||||
productCode,
|
||||
interestCategory,
|
||||
chargeDetails.rentAmount,
|
||||
chargeDetails.penaltyAmount
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setNotification({
|
||||
message: response.data.message,
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
setNotification({
|
||||
message: response.data.message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNotification({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: chargeDetails[field.name],
|
||||
onChange: (e) => {
|
||||
setChargeDetails({
|
||||
...chargeDetails,
|
||||
[field.name]: e.target.value,
|
||||
});
|
||||
},
|
||||
readOnly: field.readOnly,
|
||||
|
||||
type: field.subType,
|
||||
};
|
||||
const className = field.readOnly ? 'bg-grey/[0.3]' : '';
|
||||
return (
|
||||
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} className={className} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} options={field.options} className={className} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
|
||||
<FormBox title={t('chargeEdit')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button
|
||||
text={t('submit')}
|
||||
onClick={handleSubmit}
|
||||
disabled={!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit}
|
||||
/>
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChargeEdit;
|
109
src/pages/ChargeManagement.jsx
Normal file
109
src/pages/ChargeManagement.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import Button from '../components/Button';
|
||||
import FormBox from '../components/FormBox';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useToast } from '../hooks/useToast';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function ChargeManagement() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const showToast = useToast();
|
||||
|
||||
const [productDetails, setProductDetails] = useState({
|
||||
productCode: '',
|
||||
interestCategory: '',
|
||||
productCodeValid: true,
|
||||
interestCategoryValid: true,
|
||||
});
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'productCode',
|
||||
label: t('productCode'),
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
maxLength: 4,
|
||||
validate: (value) => /^[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'interestCategory',
|
||||
label: t('interestCategory'),
|
||||
type: 'input',
|
||||
subType: 'number',
|
||||
maxLength: 4,
|
||||
validate: (value) => /^[0-9]{4}/.test(value),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const newProductDetails = { ...productDetails };
|
||||
let isValid = true;
|
||||
|
||||
formFields.forEach((field) => {
|
||||
if (!field.validate(newProductDetails[field.name])) {
|
||||
isValid = false;
|
||||
newProductDetails[`${field.name}Valid`] = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
setProductDetails(newProductDetails);
|
||||
showToast(t('highlightedFieldsInvalid'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('change', {
|
||||
state: {
|
||||
productCode: productDetails.productCode,
|
||||
interestCategory: productDetails.interestCategory,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: productDetails[field.name],
|
||||
onChange: (e) => {
|
||||
const newLockerDetails = { ...productDetails };
|
||||
if (field.subType === 'number') {
|
||||
e.target.value = e.target.value.replace(/\D/g, '');
|
||||
if (e.target.value.length > field.maxLength) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
newLockerDetails[field.name] = e.target.value;
|
||||
newLockerDetails[`${field.name}Valid`] = true;
|
||||
setProductDetails(newLockerDetails);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
type: field.subType,
|
||||
readOnly: field.readOnly,
|
||||
};
|
||||
const valid = productDetails[`${field.name}Valid`];
|
||||
return (
|
||||
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} valid={valid} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} valid={valid} options={field.options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormBox title={t('chargeManagement')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button text={t('submit')} onClick={handleSubmit} />
|
||||
</FormBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChargeManagement;
|
84
src/pages/CheckInOutLog.jsx
Normal file
84
src/pages/CheckInOutLog.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import FormBox from '../components/FormBox';
|
||||
import Button from '../components/Button';
|
||||
import Notification from '../components/Notification';
|
||||
import { lockerService } from '../services/locker.service';
|
||||
import { useToast } from '../hooks/useToast';
|
||||
import { useLoading } from '../hooks/useLoading';
|
||||
|
||||
function CheckInOutLog() {
|
||||
const [time, setTime] = useState(null);
|
||||
const [checkType, setCheckType] = useState('');
|
||||
const [notification, setNotification] = useState({ message: '', type: '' });
|
||||
|
||||
const location = useLocation();
|
||||
const showToast = useToast();
|
||||
const { setIsLoading } = useLoading();
|
||||
const accountNumber = location.state?.accountNumber;
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (time === null || checkType === '') {
|
||||
showToast('Please fill in all fields', 'error');
|
||||
return;
|
||||
}
|
||||
// Add your logic here
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await lockerService.checkInOut(accountNumber, time, checkType);
|
||||
if (response.status === 200) {
|
||||
setNotification({ message: response.data.message, type: 'success' });
|
||||
} else {
|
||||
console.log(response);
|
||||
setNotification({ message: response.data.message, type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setNotification({ message: error.message, type: 'error' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
<FormBox title="Check In/Out Log">
|
||||
<div className="px-4 pt-7 text-2xl font-display font-bold text-primary dark:text-primary-dark">
|
||||
{accountNumber}
|
||||
</div>
|
||||
<div className="p-2 pt-7 flex flex-col gap-4">
|
||||
<div className="flex">
|
||||
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">Time</label>
|
||||
<input
|
||||
type="time"
|
||||
className="w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey text-black"
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
value={time}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
|
||||
Check Type
|
||||
</label>
|
||||
<select
|
||||
className="w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey"
|
||||
onChange={(e) => setCheckType(e.target.value)}
|
||||
value={checkType}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select
|
||||
</option>
|
||||
<option value="check-in">Check In</option>
|
||||
<option value="check-out">Check Out</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<Button text="Submit" onClick={handleSubmit} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckInOutLog;
|
86
src/pages/CheckInOutManagement.jsx
Normal file
86
src/pages/CheckInOutManagement.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import FormBox from '../components/FormBox';
|
||||
import { useState } from 'react';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import { Search } from 'lucide-react';
|
||||
import Button from '../components/Button';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import Notification from '../components/Notification';
|
||||
import { useToast } from '../hooks/useToast';
|
||||
import { lockerService } from '../services/locker.service';
|
||||
import { useLoading } from '../hooks/useLoading';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function CheckInOutManagement() {
|
||||
const [accountNumber, setAccountNumber] = useState('');
|
||||
const [notification, setNotification] = useState({ message: '', type: '' });
|
||||
|
||||
const showToast = useToast();
|
||||
const { setIsLoading } = useLoading();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNext = async (e) => {
|
||||
e.preventDefault();
|
||||
if (accountNumber === '') {
|
||||
showToast('Account Number is required', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await lockerService.preCheckIn(accountNumber);
|
||||
console.log(response.data);
|
||||
if (response.status === 200) {
|
||||
const data = response.data;
|
||||
if (data.code === 1) {
|
||||
navigate('log', { state: { accountNumber } });
|
||||
} else if (data.code === 2) {
|
||||
setNotification({
|
||||
visible: true,
|
||||
message:
|
||||
'Monthly access limit exceeded. A fine will be charged for each additional access.',
|
||||
type: 'warning',
|
||||
});
|
||||
} else if (data.code === 3) {
|
||||
setNotification({
|
||||
visible: true,
|
||||
message:
|
||||
'Rent for this account is due. Please pay the rent amount in full to access the locker.',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setNotification(error.message, 'error');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
<FormBox title="Check In/Out">
|
||||
<FieldsWrapper>
|
||||
<FormField
|
||||
label="Account Number"
|
||||
icon={{ icon: <Search size={17} />, onClick: () => {} }}
|
||||
>
|
||||
<FormInput
|
||||
props={{
|
||||
type: 'text',
|
||||
value: accountNumber,
|
||||
onChange: (e) => setAccountNumber(e.target.value),
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</FieldsWrapper>
|
||||
<Button text="Next" onClick={handleNext} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckInOutManagement;
|
@@ -1,11 +1,20 @@
|
||||
import FormBox from "../components/FormBox";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getUserInfoFromSession } from "../util/util";
|
||||
import FormBox from '../components/FormBox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getUserInfoFromSession } from '../util/util';
|
||||
|
||||
function Home() {
|
||||
const { t } = useTranslation();
|
||||
const holidayList = [{ date: '23 May, 2024', name: 'Buddha Purnima' }, { date: '15 June, 2024', name: 'Raja Sankranti' }];
|
||||
const homePageNotifications = ['hpn_complete_before_31', 'hpn_npa', 'hpn_helpdesk_contact', 'hpn_rupay_kcc_time', 'hpn_rupay_kcc_atm'];
|
||||
const holidayList = [
|
||||
{ date: '23 May, 2024', name: 'Buddha Purnima' },
|
||||
{ date: '15 June, 2024', name: 'Raja Sankranti' },
|
||||
];
|
||||
const homePageNotifications = [
|
||||
'hpn_complete_before_31',
|
||||
'hpn_npa',
|
||||
'hpn_helpdesk_contact',
|
||||
'hpn_rupay_kcc_time',
|
||||
'hpn_rupay_kcc_atm',
|
||||
];
|
||||
const userInformation = getUserInfoFromSession();
|
||||
|
||||
return (
|
||||
@@ -15,7 +24,12 @@ function Home() {
|
||||
<FormBox title={t('holidayList')} alt={true}>
|
||||
<ul className="px-4 list-disc list-inside">
|
||||
{holidayList.map((holiday, index) => (
|
||||
<li key={index} className="py-2 font-body text-surface dark:text-surface-dark text-lg/loose">{t(holiday.date)} - {t(holiday.name)}</li>
|
||||
<li
|
||||
key={index}
|
||||
className="py-2 font-body text-surface dark:text-surface-dark text-lg/loose"
|
||||
>
|
||||
{t(holiday.date)} - {t(holiday.name)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</FormBox>
|
||||
@@ -23,11 +37,14 @@ function Home() {
|
||||
<div className="flex-grow">
|
||||
<FormBox title={t('information')}>
|
||||
<ul className="px-4 list-disc list-inside">
|
||||
{
|
||||
Object.keys(userInformation).map((key, index) => (
|
||||
<li key={index} className="py-2 font-body text-surface-dark dark:text-surface text-lg/loose">{t(key)}: {t(userInformation[key])}</li>
|
||||
))
|
||||
}
|
||||
{Object.keys(userInformation).map((key, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="py-2 font-body text-surface-dark dark:text-surface text-lg/loose"
|
||||
>
|
||||
{t(key)}: {t(userInformation[key])}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</FormBox>
|
||||
</div>
|
||||
@@ -37,7 +54,12 @@ function Home() {
|
||||
<FormBox title={t('notifications')}>
|
||||
<ul className="px-4 list-disc list-inside">
|
||||
{homePageNotifications.map((notification, index) => (
|
||||
<li key={index} className="py-2 font-body text-surface-dark dark:text-surface text-lg/relaxed">{t(notification)}</li>
|
||||
<li
|
||||
key={index}
|
||||
className="py-2 font-body text-surface-dark dark:text-surface text-lg/relaxed"
|
||||
>
|
||||
{t(notification)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</FormBox>
|
||||
@@ -47,4 +69,4 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default Home;
|
||||
|
173
src/pages/KeySwap.jsx
Normal file
173
src/pages/KeySwap.jsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useState } from 'react';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from '../hooks/useToast';
|
||||
import { useLoading } from '../hooks/useLoading';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import Button from '../components/Button';
|
||||
import { lockerService } from '../services/locker.service';
|
||||
import FormBox from '../components/FormBox';
|
||||
import Notification from '../components/Notification';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function KeySwap() {
|
||||
const { t } = useTranslation();
|
||||
const showToast = useToast();
|
||||
const { isLoading, setIsLoading } = useLoading();
|
||||
const [notification, setNotification] = useState({ message: '', type: '' });
|
||||
const [keySwapDetails, setKeySwapDetails] = useState({
|
||||
cabinetId: '',
|
||||
lockerId: '',
|
||||
reason: '',
|
||||
oldKey: '',
|
||||
newKey: '',
|
||||
newKeyConfirm: '',
|
||||
cabinetIdValid: true,
|
||||
lockerIdValid: true,
|
||||
reasonValid: true,
|
||||
oldKeyValid: true,
|
||||
newKeyValid: true,
|
||||
newKeyConfirmValid: true,
|
||||
});
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'cabinetId',
|
||||
label: t('cabinetId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'lockerId',
|
||||
label: t('lockerId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
label: t('reasonForChange'),
|
||||
type: 'input',
|
||||
maxLength: 50,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => value !== '',
|
||||
},
|
||||
{
|
||||
name: 'oldKey',
|
||||
label: t('oldKeyId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'newKey',
|
||||
label: t('newKeyId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'newKeyConfirm',
|
||||
label: t('confirmNewKeyId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => value !== '' && value === keySwapDetails.newKey,
|
||||
},
|
||||
];
|
||||
|
||||
const handleKeySwap = async (e) => {
|
||||
e.preventDefault();
|
||||
let valid = true;
|
||||
|
||||
for (const field of formFields) {
|
||||
if (!field.validate(keySwapDetails[field.name])) {
|
||||
valid = false;
|
||||
setKeySwapDetails((prev) => ({
|
||||
...prev,
|
||||
[`${field.name}Valid`]: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
showToast(t('highlightedFieldsInvalid'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await lockerService.keySwap(
|
||||
keySwapDetails.cabinetId,
|
||||
keySwapDetails.lockerId,
|
||||
keySwapDetails.reason,
|
||||
keySwapDetails.oldKey,
|
||||
keySwapDetails.newKey
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setNotification({
|
||||
message: response.data.message,
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
setNotification({
|
||||
message: response.data.message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setNotification({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: keySwapDetails[field.name],
|
||||
onChange: (e) => {
|
||||
const newLockerDetails = { ...keySwapDetails };
|
||||
newLockerDetails[field.name] = e.target.value.toUpperCase();
|
||||
newLockerDetails[`${field.name}Valid`] = true;
|
||||
setKeySwapDetails(newLockerDetails);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
type: field.subType,
|
||||
readOnly: field.readOnly,
|
||||
};
|
||||
const valid = keySwapDetails[`${field.name}Valid`];
|
||||
return (
|
||||
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} valid={valid} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} valid={valid} options={field.options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
<FormBox title={t('lockerStatus')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button text={t('submit')} onClick={handleKeySwap} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeySwap;
|
68
src/pages/LockerMaintenance.jsx
Normal file
68
src/pages/LockerMaintenance.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormBox from '../components/FormBox';
|
||||
import Button from '../components/Button';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function LockerMaintenance() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [operation, setOperation] = useState({ value: '', valid: true });
|
||||
|
||||
const handleNext = (e) => {
|
||||
e.preventDefault();
|
||||
if (operation.value === '') {
|
||||
setOperation({ value: operation.value, valid: false });
|
||||
}
|
||||
navigate(operation.value);
|
||||
};
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'value',
|
||||
label: t('operation'),
|
||||
options: [
|
||||
{ label: t('status'), value: 'status' },
|
||||
{ label: t('keySwap'), value: 'key-swap' },
|
||||
],
|
||||
type: 'select',
|
||||
},
|
||||
];
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: operation[field.name],
|
||||
onChange: (e) => {
|
||||
const newValues = { ...operation };
|
||||
newValues[field.name] = e.target.value;
|
||||
newValues[`${field.name}Valid`] = true;
|
||||
setOperation(newValues);
|
||||
},
|
||||
};
|
||||
const options = field.options;
|
||||
return (
|
||||
<FormField label={field.label}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} options={options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormBox title={t('lockerMaintenance')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button text={t('next')} onClick={(e) => handleNext(e)} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LockerMaintenance;
|
137
src/pages/LockerStatus.jsx
Normal file
137
src/pages/LockerStatus.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import FormBox from '../components/FormBox';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import Button from '../components/Button';
|
||||
import { lockerService } from '../services/locker.service';
|
||||
import { useLoading } from '../hooks/useLoading';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import Notification from '../components/Notification';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function LockerStatus() {
|
||||
const { t } = useTranslation();
|
||||
const [lockerDetails, setLockerDetails] = useState({
|
||||
cabinetId: '',
|
||||
lockerId: '',
|
||||
status: '',
|
||||
cabinetIdValid: true,
|
||||
lockerIdValid: true,
|
||||
statusValid: true,
|
||||
});
|
||||
const { isLoading, setIsLoading } = useLoading();
|
||||
const [notification, setNotification] = useState({ message: '', type: '' });
|
||||
|
||||
const formFields = [
|
||||
{
|
||||
name: 'cabinetId',
|
||||
label: t('cabinetId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'lockerId',
|
||||
label: t('lockerId'),
|
||||
type: 'input',
|
||||
maxLength: 6,
|
||||
readOnly: isLoading,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: t('status'),
|
||||
type: 'select',
|
||||
readOnly: isLoading,
|
||||
options: [
|
||||
{ value: 'open', label: t('open') },
|
||||
{ value: 'close', label: t('close') },
|
||||
],
|
||||
validate: (value) => value !== '',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
let isValid = true;
|
||||
const newValidationState = { ...lockerDetails };
|
||||
|
||||
// Validate account details fields
|
||||
formFields.forEach((field) => {
|
||||
if (field.validate) {
|
||||
const fieldIsValid = field.validate(lockerDetails[field.name]);
|
||||
newValidationState[`${field.name}Valid`] = fieldIsValid;
|
||||
if (!fieldIsValid) isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
setLockerDetails(newValidationState);
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await lockerService.changeLockerStatus(
|
||||
lockerDetails.cabinetId,
|
||||
lockerDetails.lockerId,
|
||||
lockerDetails.status
|
||||
);
|
||||
setNotification({
|
||||
message: response.data.message,
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
setNotification({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonProps = {
|
||||
value: lockerDetails[field.name],
|
||||
onChange: (e) => {
|
||||
const newLockerDetails = { ...lockerDetails };
|
||||
newLockerDetails[field.name] = e.target.value.toUpperCase();
|
||||
newLockerDetails[`${field.name}Valid`] = true;
|
||||
setLockerDetails(newLockerDetails);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
type: field.subType,
|
||||
readOnly: field.readOnly,
|
||||
};
|
||||
const valid = lockerDetails[`${field.name}Valid`];
|
||||
return (
|
||||
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||
{field.type === 'input' ? (
|
||||
<FormInput props={commonProps} valid={valid} />
|
||||
) : (
|
||||
<FormSelect props={commonProps} options={field.options} />
|
||||
)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
|
||||
<FormBox title={t('lockerStatus')}>
|
||||
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
|
||||
<Button text={t('submit')} onClick={handleSubmit} />
|
||||
</FormBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LockerStatus;
|
193
src/pages/LockersRegistration.jsx
Normal file
193
src/pages/LockersRegistration.jsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useLoading } from '../hooks/useLoading';
|
||||
import FormBox from '../components/FormBox';
|
||||
import Button from '../components/Button';
|
||||
import { lockerService } from '../services/locker.service';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import Notification from '../components/Notification';
|
||||
import FormField from '../components/FormField';
|
||||
import FormInput from '../components/FormInput';
|
||||
import FormSelect from '../components/FormSelect';
|
||||
import FormHeader from '../components/FormHeader';
|
||||
import FieldsWrapper from '../components/FieldsWrapper';
|
||||
|
||||
function LockersRegistration() {
|
||||
const location = useLocation();
|
||||
const { setIsLoading } = useLoading();
|
||||
const [notification, setNotification] = useState({ message: '', type: '' });
|
||||
|
||||
const noOfLockers = location.state?.noOfLockers;
|
||||
const cabinetId = location.state?.cabinetId;
|
||||
|
||||
const initLockers = Array(noOfLockers)
|
||||
.fill()
|
||||
.map(() => ({
|
||||
id: '',
|
||||
size: '',
|
||||
keyId: '',
|
||||
idValid: true,
|
||||
sizeValid: true,
|
||||
keyIdValid: true,
|
||||
}));
|
||||
|
||||
const [lockerValues, setLockerValues] = useState(initLockers);
|
||||
|
||||
if (!location.state) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const idRegex = /^[A-Z]{2}[0-9]{4}$/;
|
||||
let valid = true;
|
||||
|
||||
// Helper function to find duplicates
|
||||
const findDuplicates = (arr, key) => {
|
||||
const values = arr.map((item) => item[key]);
|
||||
return values.filter((item, index) => values.indexOf(item) !== index);
|
||||
};
|
||||
|
||||
// Find duplicates
|
||||
const duplicateLockerIds = findDuplicates(lockerValues, 'id');
|
||||
const duplicateKeyIds = findDuplicates(lockerValues, 'keyId');
|
||||
|
||||
const newValues = lockerValues.map((locker) => {
|
||||
const newLocker = { ...locker };
|
||||
|
||||
// Check ID
|
||||
if (locker.id === '' || !idRegex.test(locker.id) || duplicateLockerIds.includes(locker.id)) {
|
||||
newLocker.idValid = false;
|
||||
valid = false;
|
||||
} else {
|
||||
newLocker.idValid = true;
|
||||
}
|
||||
|
||||
// Check size
|
||||
if (locker.size === '') {
|
||||
newLocker.sizeValid = false;
|
||||
valid = false;
|
||||
} else {
|
||||
newLocker.sizeValid = true;
|
||||
}
|
||||
|
||||
// Check keyId
|
||||
if (
|
||||
locker.keyId === '' ||
|
||||
!idRegex.test(locker.keyId) ||
|
||||
duplicateKeyIds.includes(locker.keyId)
|
||||
) {
|
||||
newLocker.keyIdValid = false;
|
||||
valid = false;
|
||||
} else {
|
||||
newLocker.keyIdValid = true;
|
||||
}
|
||||
|
||||
return newLocker;
|
||||
});
|
||||
|
||||
setLockerValues(newValues);
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await lockerService.registerLockers(cabinetId, lockerValues);
|
||||
setNotification({
|
||||
message: `Cabinet creation successful. Cabinet ID: ${response.data.cabinetId}`,
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNotification({
|
||||
message: `Error registering lockers. ${error.message}`,
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formRow = [
|
||||
{
|
||||
label: 'Locker ID',
|
||||
type: 'input',
|
||||
subType: 'text',
|
||||
name: 'id',
|
||||
maxLength: 6,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}$/.test(value),
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
type: 'select',
|
||||
subType: 'text',
|
||||
name: 'size',
|
||||
options: [
|
||||
{ label: 'Small', value: '1' },
|
||||
{ label: 'Medium', value: '2' },
|
||||
{ label: 'Large', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Key ID',
|
||||
type: 'input',
|
||||
subType: 'text',
|
||||
name: 'keyId',
|
||||
maxLength: 6,
|
||||
validate: (value) => /^[A-Z]{2}[0-9]{4}$/.test(value),
|
||||
},
|
||||
];
|
||||
const renderFormRow = (field) => {
|
||||
const commonProps = {
|
||||
value: lockerValues[field.name],
|
||||
onChange: (e) => {
|
||||
const newLockerValues = [...lockerValues];
|
||||
newLockerValues[field.name] = e.target.value;
|
||||
newLockerValues[`${field.name}Valid`] = true;
|
||||
setLockerValues(newLockerValues);
|
||||
},
|
||||
maxLength: field.maxLength,
|
||||
type: field.subType,
|
||||
};
|
||||
|
||||
const valid = lockerValues[`${field.name}Valid`];
|
||||
|
||||
return field.type === 'input' ? (
|
||||
<FormInput key={field.name} props={commonProps} valid={valid} />
|
||||
) : (
|
||||
<FormSelect key={field.name} props={commonProps} valid={valid} options={field.options} />
|
||||
);
|
||||
};
|
||||
|
||||
const formFields = Array(noOfLockers).fill(formRow);
|
||||
const renderFormFields = (row, index) => {
|
||||
return (
|
||||
<FormField key={index} label={`Locker ${index + 1}`} variant="long">
|
||||
{row.map(renderFormRow)}
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{notification.message !== '' && <Notification {...notification} />}
|
||||
</AnimatePresence>
|
||||
<div className="relative">
|
||||
{notification.type === 'success' && (
|
||||
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
|
||||
)}
|
||||
<FormBox title="Locker Registration">
|
||||
<FormHeader text={cabinetId} />
|
||||
<FieldsWrapper className="">{formFields.map(renderFormFields)}</FieldsWrapper>
|
||||
<Button text="Register" onClick={handleSubmit} />
|
||||
</FormBox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LockersRegistration;
|
@@ -1,9 +1,9 @@
|
||||
function Placeholder() {
|
||||
return (
|
||||
<div className="text-2xl text-center h-max font-body text-primary dark:text-primary-dark">
|
||||
<h1>Placeholder</h1>
|
||||
</div >
|
||||
<div className="text-center h-full">
|
||||
<p className="text-3xl text-primary font-bold">Placeholder</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Placeholder;
|
||||
export default Placeholder;
|
||||
|
10
src/services/api.js
Normal file
10
src/services/api.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8081/api/v1',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export default api;
|
47
src/services/locker.service.js
Normal file
47
src/services/locker.service.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import api from './api';
|
||||
|
||||
export const lockerService = {
|
||||
registerLockers: async (cabinetId, lockers) => {
|
||||
return api.post('/cabinet', {
|
||||
cabinetId,
|
||||
lockers: lockers.map(({ id, size, keyId }) => ({
|
||||
id,
|
||||
size,
|
||||
keyId,
|
||||
})),
|
||||
});
|
||||
},
|
||||
|
||||
changeLockerStatus: async (cabinetId, lockerId, status) => {
|
||||
return api.patch(`/locker/status`, { cabinetId, lockerId, status });
|
||||
},
|
||||
|
||||
keySwap: async (cabinetId, lockerId, reason, oldKey, newKey) => {
|
||||
return api.patch(`/locker/key`, {
|
||||
cabinetId,
|
||||
lockerId,
|
||||
reason,
|
||||
oldKey,
|
||||
newKey,
|
||||
});
|
||||
},
|
||||
|
||||
updateCharges: async (productCode, interestCategory, rent, penalty) => {
|
||||
return api.patch(`/charge/${productCode}${interestCategory}`, {
|
||||
rent,
|
||||
penalty,
|
||||
});
|
||||
},
|
||||
|
||||
getCharges: async (productCode, interestCategory) => {
|
||||
return api.get(`/charge/${productCode}${interestCategory}`);
|
||||
},
|
||||
|
||||
preCheckIn: async (accountNumber) => {
|
||||
return api.post(`/pre-checkin/${accountNumber}`);
|
||||
},
|
||||
|
||||
checkInOut: async (accountNumber, time, checkType) => {
|
||||
return api.post(`/check-in-out/${accountNumber}`, { time, checkType });
|
||||
},
|
||||
};
|
43
src/util/productList.js
Normal file
43
src/util/productList.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const productInfo = [
|
||||
{
|
||||
productCode: '3001',
|
||||
productCodeDescription: 'RECURRING DEPOSIT',
|
||||
interestCategory: '1004',
|
||||
interestCategoryDescription: 'NON MEMBER - SENIOR CITIZEN',
|
||||
payableGl: '16010',
|
||||
paidGl: '62110',
|
||||
},
|
||||
{
|
||||
productCode: '1101',
|
||||
productCodeDescription: 'CURRENT ACCOUNT',
|
||||
interestCategory: '1009',
|
||||
interestCategoryDescription: 'GENERAL',
|
||||
payableGl: '16018',
|
||||
paidGl: '62115',
|
||||
},
|
||||
{
|
||||
productCode: '1101',
|
||||
productCodeDescription: 'SAVINGS DEPOSIT',
|
||||
interestCategory: '1007',
|
||||
interestCategoryDescription: 'NON MEMBER',
|
||||
payableGl: '16301',
|
||||
paidGl: '62117',
|
||||
},
|
||||
{
|
||||
productCode: '2002',
|
||||
productCodeDescription: 'CASH CERTIFICATE -GENERAL',
|
||||
interestCategory: '1047',
|
||||
interestCategoryDescription: 'COMPOUNDING',
|
||||
payableGl: '16011',
|
||||
paidGl: '62111',
|
||||
},
|
||||
{
|
||||
productCode: '2002',
|
||||
productCodeDescription: 'CASH CERTIFICATE',
|
||||
interestCategory: '1005',
|
||||
interestCategoryDescription: 'NON MEMBER - SENIOR CITIZEN',
|
||||
payableGl: '16011',
|
||||
paidGl: '62111',
|
||||
},
|
||||
];
|
||||
export default productInfo;
|
@@ -1,10 +1,16 @@
|
||||
const getUserInfoFromSession = () => {
|
||||
return {
|
||||
username: "Rajesh Kumar",
|
||||
pacsName: "Demo SKUS Ltd.",
|
||||
userType: "pacsTeller",
|
||||
moduleName: "locker",
|
||||
}
|
||||
}
|
||||
return {
|
||||
username: 'Rajesh Kumar',
|
||||
pacsName: 'Demo SKUS Ltd.',
|
||||
userType: 'pacsTeller',
|
||||
moduleName: 'locker',
|
||||
};
|
||||
};
|
||||
|
||||
export { getUserInfoFromSession };
|
||||
const toTitleCase = (str) => {
|
||||
return str.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
export { getUserInfoFromSession, toTitleCase };
|
||||
|
@@ -1,23 +1,40 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: 'selector',
|
||||
theme: {
|
||||
colors: {
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
grey: '#979797',
|
||||
error: {
|
||||
DEFAULT: '#A14444',
|
||||
dark: '#E5254B',
|
||||
surface: { DEFAULT: '#D26464', dark: '#FCE9ED' },
|
||||
},
|
||||
onToast: {
|
||||
DEFAULT: '#646564',
|
||||
dark: '#646564',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: '#A5A513',
|
||||
dark: '#EA7000',
|
||||
surface: { DEFAULT: '#C8C820', dark: '#FDF1E5' },
|
||||
},
|
||||
success: {
|
||||
DEFAULT: '#038100',
|
||||
dark: '#038100',
|
||||
surface: { DEFAULT: '#E6F2E5', dark: '#E6F2E5' },
|
||||
},
|
||||
transparent: 'transparent',
|
||||
primary: {
|
||||
DEFAULT: '#008C46',
|
||||
dark: '#E6F4E1'
|
||||
dark: '#E6F4E1',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#B1E1B3',
|
||||
dark: '#7DBD80',
|
||||
variant: {DEFAULT: '#80AE82', dark: '#5B875D'}
|
||||
variant: { DEFAULT: '#80AE82', dark: '#5B875D' },
|
||||
},
|
||||
tertiary: {
|
||||
DEFAULT: '#f2f2df',
|
||||
@@ -26,21 +43,36 @@ export default {
|
||||
surface: {
|
||||
DEFAULT: '#F6F6F6',
|
||||
dark: '#2d332d',
|
||||
variant: {DEFAULT: '#F4FFF4', dark: '#2b372c'}
|
||||
variant: { DEFAULT: '#F4FFF4', dark: '#2b372c' },
|
||||
},
|
||||
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['Montserrat', 'sans-serif'],
|
||||
body: ['Rubik', 'Noto Sans','Noto Sans Bengali', 'sans-serif'],
|
||||
body: ['Rubik', 'Noto Sans', 'Noto Sans Bengali', 'sans-serif'],
|
||||
},
|
||||
extend: {
|
||||
borderWidth: {
|
||||
'3': '3px',
|
||||
3: '3px',
|
||||
},
|
||||
fontSize: {
|
||||
title: '40px',
|
||||
},
|
||||
keyframes: {
|
||||
slideIn: {
|
||||
'0%': { transform: 'translateX(100%)', opacity: '0' },
|
||||
'100%': { transform: 'translateX(0)', opacity: '1' },
|
||||
},
|
||||
fadeOut: {
|
||||
'0%': { opacity: '1' },
|
||||
'100%': { opacity: '0' },
|
||||
},
|
||||
loading: {
|
||||
'0%': { left: '-10%' },
|
||||
'30%': { width: '30%' },
|
||||
'80%': { width: '10%' },
|
||||
'100%': { left: '100%' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -48,5 +80,4 @@ export default {
|
||||
addVariant('second-last', '&:nth-last-child(2)');
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
};
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
});
|
||||
|
Reference in New Issue
Block a user