Compare commits
10 Commits
835a3cc7fd
...
44112f91bd
Author | SHA1 | Date | |
---|---|---|---|
44112f91bd | |||
9d72dc6868 | |||
8cee8e0383 | |||
a6a67d69d5 | |||
bfe22a61a5 | |||
442b8e52dd | |||
03747b5251 | |||
650a6dbdd0 | |||
2b940c3d43 | |||
96784c02c7 |
172
package-lock.json
generated
172
package-lock.json
generated
@ -8,11 +8,13 @@
|
|||||||
"name": "osaka",
|
"name": "osaka",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-http-backend": "^2.6.1",
|
"i18next-http-backend": "^2.6.1",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
|
"motion": "^11.15.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@ -1663,6 +1665,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.20",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||||
@ -1717,6 +1725,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -1933,6 +1952,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@ -2116,6 +2147,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@ -2816,6 +2856,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@ -2843,6 +2903,20 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
@ -2857,6 +2931,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"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": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@ -3905,6 +4006,27 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@ -3928,6 +4050,44 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -4506,6 +4666,12 @@
|
|||||||
"react-is": "^16.13.1"
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@ -5327,6 +5493,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
@ -10,11 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-http-backend": "^2.6.1",
|
"i18next-http-backend": "^2.6.1",
|
||||||
"lucide-react": "^0.446.0",
|
"lucide-react": "^0.446.0",
|
||||||
|
"motion": "^11.15.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
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.
@ -46,5 +46,9 @@
|
|||||||
"hpn_rupay_kcc_time": "সমস্ত রূপে কিসান ক্রেডিট কার্ড লেনদেন ১৯:00 ঘটিকার আগে সম্পন্ন করুন",
|
"hpn_rupay_kcc_time": "সমস্ত রূপে কিসান ক্রেডিট কার্ড লেনদেন ১৯:00 ঘটিকার আগে সম্পন্ন করুন",
|
||||||
"hpn_rupay_kcc_atm": "কিছু অভ্যন্তরীণ পরিবর্তনের কারণে, মাইক্রো-এটিএম এর মাধ্যমে রূপে কিসান ক্রেডিট কার্ড কার্যক্রম বন্ধ করা হয়েছে। সেবা আগামীকাল থেকে পুনরায় চালু হবে।",
|
"hpn_rupay_kcc_atm": "কিছু অভ্যন্তরীণ পরিবর্তনের কারণে, মাইক্রো-এটিএম এর মাধ্যমে রূপে কিসান ক্রেডিট কার্ড কার্যক্রম বন্ধ করা হয়েছে। সেবা আগামীকাল থেকে পুনরায় চালু হবে।",
|
||||||
"copyright_statement": "কপিরাইট © ২০২৩, টাটা কনসাল্টেন্সি সার্ভিসেস। সমস্ত অধিকার সংরক্ষিত",
|
"copyright_statement": "কপিরাইট © ২০২৩, টাটা কনসাল্টেন্সি সার্ভিসেস। সমস্ত অধিকার সংরক্ষিত",
|
||||||
"privacy_policy": "গোপনীয়তা নীতি"
|
"privacy_policy": "গোপনীয়তা নীতি",
|
||||||
|
"create": "তৈরি করুন",
|
||||||
|
"operation": "অপারেশন",
|
||||||
|
"next": "পরবর্তী",
|
||||||
|
"select": "নির্বাচন করুন"
|
||||||
}
|
}
|
||||||
|
@ -47,5 +47,36 @@
|
|||||||
"hpn_rupay_kcc_time": "Complete all Rupay KCC transactions before 19:00 Hrs",
|
"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.",
|
"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",
|
"copyright_statement": "Copyright © 2023, Tata Consultancy Services. All rights reserved",
|
||||||
"privacy_policy": "Privacy Policy"
|
"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"
|
||||||
}
|
}
|
@ -47,5 +47,9 @@
|
|||||||
"hpn_rupay_kcc_time": "सभी रुपे केसीसी लेनदेन 19:00 बजे से पहले पूरा करें",
|
"hpn_rupay_kcc_time": "सभी रुपे केसीसी लेनदेन 19:00 बजे से पहले पूरा करें",
|
||||||
"hpn_rupay_kcc_atm": "कुछ आंतरिक बदलावों के कारण, माइक्रो-एटीएम के माध्यम से रुपे केसीसी संचालन बंद कर दिया गया है। सेवा कल से फिर से शुरू होगी।",
|
"hpn_rupay_kcc_atm": "कुछ आंतरिक बदलावों के कारण, माइक्रो-एटीएम के माध्यम से रुपे केसीसी संचालन बंद कर दिया गया है। सेवा कल से फिर से शुरू होगी।",
|
||||||
"copyright_statement": "कॉपीराइट © 2023, टाटा कंसल्टेंसी सर्विसेज। सभी अधिकार सुरक्षित",
|
"copyright_statement": "कॉपीराइट © 2023, टाटा कंसल्टेंसी सर्विसेज। सभी अधिकार सुरक्षित",
|
||||||
"privacy_policy": "गोपनीयता नीति"
|
"privacy_policy": "गोपनीयता नीति",
|
||||||
|
"create": "बनाएं",
|
||||||
|
"operation": "ऑपरेशन",
|
||||||
|
"next": "अगला",
|
||||||
|
"select": "चुनें"
|
||||||
}
|
}
|
61
src/App.jsx
61
src/App.jsx
@ -1,15 +1,52 @@
|
|||||||
import { Outlet } from "react-router-dom"
|
import { useLocation, useOutlet } from "react-router";
|
||||||
import Header from "./components/Header"
|
import { useState } from "react";
|
||||||
import Footer from "./components/Footer"
|
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() {
|
function App() {
|
||||||
return <div className="flex flex-col min-h-screen">
|
const location = useLocation();
|
||||||
<Header />
|
return (
|
||||||
<main className="flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark">
|
<LoadingProvider>
|
||||||
<Outlet />
|
<div className="flex flex-col min-h-screen">
|
||||||
</main>
|
<Header />
|
||||||
<Footer />
|
<LoadingBarWrapper />
|
||||||
</div>
|
<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={{ opacity: 0, x: 50 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 50 }}
|
||||||
|
>
|
||||||
|
<AnimatedOutlet />
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</ToastProvider>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</LoadingProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
@ -6,7 +6,7 @@ function BannerInfo({info}) {
|
|||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const infoElements = Object.keys(info).map((key) => (
|
const infoElements = Object.keys(info).map((key) => (
|
||||||
<BannerInfoElement key={key} title={t(key)} description={t(info[key])} />
|
<BannerInfoElement key={key} title={t(key)} description={info[key]} />
|
||||||
))
|
))
|
||||||
infoElements.push(
|
infoElements.push(
|
||||||
<BannerInfoElement
|
<BannerInfoElement
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
function Button({text, onClick}) {
|
function Button({text, onClick, disabled}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<motion.button
|
||||||
className="px-12 py-2 text-lg text-white rounded-full bg-primary dark:bg-primary-dark"
|
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-[#ccc] dark:bg-[#ccc]")}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</motion.button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.propTypes = {
|
Button.propTypes = {
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string.isRequired,
|
||||||
onClick: PropTypes.func.isRequired
|
onClick: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
import { PropTypes } from 'prop-types';
|
import { PropTypes } from "prop-types";
|
||||||
import clsx from 'clsx';
|
import clsx from "clsx";
|
||||||
|
|
||||||
function FormBox({ title, children, alt = false }) {
|
function FormBox({ title, children, alt = false }) {
|
||||||
return (
|
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')}>
|
<form
|
||||||
<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')}>
|
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}
|
{title}
|
||||||
</label>
|
</label>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,4 +31,4 @@ FormBox.propTypes = {
|
|||||||
alt: PropTypes.bool,
|
alt: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FormBox;
|
export default FormBox;
|
||||||
|
35
src/components/FormField.jsx
Normal file
35
src/components/FormField.jsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
function FormField({ label, children, icon }) {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[17%]">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="flex w-full gap-4 items-center">
|
||||||
|
{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.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormField;
|
32
src/components/FormInput.jsx
Normal file
32
src/components/FormInput.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
function FormInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
maxLength=17,
|
||||||
|
readOnly = false,
|
||||||
|
className = "",
|
||||||
|
type = "text",
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
readOnly={readOnly}
|
||||||
|
value={value}
|
||||||
|
className={`w-1/4 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`}
|
||||||
|
onChange={onChange}
|
||||||
|
type={type}
|
||||||
|
maxLength={maxLength}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormInput.propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
readOnly: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
maxLength: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormInput;
|
37
src/components/FormSelect.jsx
Normal file
37
src/components/FormSelect.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
function FormSelect({ value, onChange, options, className }) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
className={
|
||||||
|
"w-1/4 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey " +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<option disabled value="">
|
||||||
|
Select
|
||||||
|
</option>
|
||||||
|
{options.map(({ value, label }) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormSelect.propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
options: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormSelect;
|
@ -24,13 +24,12 @@ function Header() {
|
|||||||
{
|
{
|
||||||
name: "lockerOperation",
|
name: "lockerOperation",
|
||||||
submenu: [
|
submenu: [
|
||||||
{ name: "accountCreation", path: "account-creation" },
|
{ name: "accountCreation", path: "operation/account" },
|
||||||
{ name: "cabinetMaintenance", path: "operation/cabinet" },
|
{ name: "cabinetMaintenance", path: "operation/cabinet" },
|
||||||
{ name: "lockerMaintenance", path: "locker-maintenance" },
|
{ name: "lockerMaintenance", path: "operation/locker" },
|
||||||
{ name: "rentPenaltyCollection", path: "rent-collection" },
|
{ name: "chargeManagement", path: "operation/charge-management" },
|
||||||
{ name: "chargeManagement", path: "charge-management" },
|
{ name: "checkInOutManagement", path: "operation/check-in-out" },
|
||||||
{ name: "checkInOutManagement", path: "check-in-out" },
|
{ name: "accountSurrender", path: "operation/account-surrender" }
|
||||||
{ name: "accountSurrender", path: "account-surrender" }
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: "worklist", submenu: [{ name: "myIntimation", path: "my-intimation" }] },
|
{ name: "worklist", submenu: [{ name: "myIntimation", path: "my-intimation" }] },
|
||||||
|
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,56 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
function SubMenu({ items }) {
|
function SubMenu({ items, isVisible, onLinkClick }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
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">
|
<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",
|
||||||
|
isVisible ? "block" : "hidden"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{items.map((subItem, index) => (
|
{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">
|
<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"
|
||||||
|
onClick={onLinkClick}
|
||||||
|
>
|
||||||
<Link to={subItem.path}>{t(subItem.name)}</Link>
|
<Link to={subItem.path}>{t(subItem.name)}</Link>
|
||||||
</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">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function MenuBar({ menuItems }) {
|
function MenuBar({ menuItems }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [activeMenu, setActiveMenu] = useState(null);
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between pt-5 pb-2 text-base font-body">
|
<div className="flex justify-between pt-5 pb-2 text-base font-body">
|
||||||
{menuItems.map((item, index) =>
|
{menuItems.map((item, index) => (
|
||||||
<div key={index} className="relative pb-3 cursor-pointer group">
|
<div
|
||||||
{t(item.name)}
|
key={index}
|
||||||
{item.submenu.length > 0 && <SubMenu items={item.submenu} />}
|
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>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -42,18 +65,20 @@ MenuBar.propTypes = {
|
|||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
path: PropTypes.string
|
path: PropTypes.string,
|
||||||
})
|
})
|
||||||
).isRequired
|
).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
SubMenu.propTypes = {
|
SubMenu.propTypes = {
|
||||||
items: PropTypes.arrayOf(
|
items: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired,
|
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;
|
export default MenuBar;
|
||||||
|
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;
|
19
src/contexts/Loading.jsx
Normal file
19
src/contexts/Loading.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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);
|
79
src/main.jsx
79
src/main.jsx
@ -1,33 +1,72 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from "react-dom/client";
|
||||||
import App from './App.jsx'
|
import App from "./App.jsx";
|
||||||
import './index.css'
|
import "./index.css";
|
||||||
import './i18n'
|
import "./i18n";
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import Home from './pages/Home.jsx'
|
import Home from "./pages/Home.jsx";
|
||||||
import CabinetMaintenace from './pages/CabinetMaintenance.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";
|
||||||
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <App/>,
|
element: <App />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <Home />
|
element: <Home />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'operation/cabinet',
|
path: "operation/cabinet",
|
||||||
element: <CabinetMaintenace />
|
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 />,
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")).render(
|
||||||
createRoot(document.getElementById('root')).render(
|
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
)
|
);
|
||||||
|
310
src/pages/AccountCreation.jsx
Normal file
310
src/pages/AccountCreation.jsx
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
import { AnimatePresence } from "motion/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { PackageSearch, Copy, UserSearch } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "motion/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 { useToast } from "../hooks/useToast";
|
||||||
|
import productInfo from "../util/productList";
|
||||||
|
import ProductListTable from "../components/ProductListTable";
|
||||||
|
|
||||||
|
function AccountCreation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const showToast = useToast();
|
||||||
|
const [notification] = useState({
|
||||||
|
visible: false,
|
||||||
|
message: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
const [showProductModal, setShowProductModal] = useState(false);
|
||||||
|
const [submitting] = 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;
|
||||||
|
setAccountDetails(newAccountDetails);
|
||||||
|
setShowProductModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const accountDetailsFields = [
|
||||||
|
{
|
||||||
|
label: t("productCode"),
|
||||||
|
name: "productCode",
|
||||||
|
type: "input",
|
||||||
|
subType: "number",
|
||||||
|
readOnly: true,
|
||||||
|
validate: (value) => value !== "",
|
||||||
|
icon: (
|
||||||
|
<PackageSearch
|
||||||
|
size={18}
|
||||||
|
onClick={() => {
|
||||||
|
console.log("Product search clicked");
|
||||||
|
console.log(showProductModal);
|
||||||
|
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: <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) {
|
||||||
|
showToast("Highlighted fields are invalid", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Form is valid", accountDetails);
|
||||||
|
};
|
||||||
|
const renderProductModal = () => {
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{showProductModal && (
|
||||||
|
<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={{ scale: 0.8 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
exit={{ scale: 0.8 }}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renerField = (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,
|
||||||
|
className: clsx(!accountDetails[`${field.name}Valid`] && "border-error"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||||
|
{field.type === "input" ? (
|
||||||
|
<FormInput
|
||||||
|
{...commonProps}
|
||||||
|
type={field.subType}
|
||||||
|
readOnly={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormSelect {...commonProps} options={field.options} />
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{notification.visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className={clsx(
|
||||||
|
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
|
||||||
|
notification.type === "error"
|
||||||
|
? "bg-error-surface text-error"
|
||||||
|
: "bg-success-surface text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{notification.message.split(":").map((msg, index) => {
|
||||||
|
return index === 1 ? (
|
||||||
|
<span key={index} className="border-b border-dashed">
|
||||||
|
{msg}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key={index}>{msg}</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Copy
|
||||||
|
cursor={"pointer"}
|
||||||
|
size={15}
|
||||||
|
onClick={navigator.clipboard.writeText(
|
||||||
|
notification.message.split(":")[1].trim()
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
{renderProductModal()}
|
||||||
|
<FormBox title="Account Creation" disabled={submitting}>
|
||||||
|
<div className="p-2 pt-7 ">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h1 className="text-2xl font-medium text-primary py-2">
|
||||||
|
Account Details
|
||||||
|
</h1>
|
||||||
|
{accountDetailsFields.map(renerField)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h1 className="text-2xl font-medium text-primary py-2 pt-6">
|
||||||
|
Additional Details
|
||||||
|
</h1>
|
||||||
|
{additionalDetailsFields.map(renerField)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
text={t("submit")}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
</FormBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountCreation;
|
139
src/pages/CabinetCreation.jsx
Normal file
139
src/pages/CabinetCreation.jsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
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 { AnimatePresence, motion } from "motion/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
function CabinetCreation() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [cabinetId, setCabinetId] = useState({ id: "", valid: true });
|
||||||
|
const [cabinetKeyId, setCabinetKeyId] = useState({ id: "", valid: true });
|
||||||
|
const [noOfLockers, setNoOfLockers] = useState({ number: 0, valid: true });
|
||||||
|
|
||||||
|
const handleNext = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const idRegex = /^[A-Z]{2}[0-9]{4}$/;
|
||||||
|
if (!idRegex.test(cabinetId.id)) {
|
||||||
|
setCabinetId({ id: cabinetId.id, valid: false });
|
||||||
|
return;
|
||||||
|
} else if (!idRegex.test(cabinetKeyId.id)) {
|
||||||
|
setCabinetKeyId({ id: cabinetKeyId.id, valid: false });
|
||||||
|
return;
|
||||||
|
} else if (noOfLockers.number === 0) {
|
||||||
|
setNoOfLockers({ number: noOfLockers.number, valid: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate("register-lockers", {
|
||||||
|
state: {
|
||||||
|
cabinetId: cabinetId.id,
|
||||||
|
cabinetKeyId: cabinetKeyId.id,
|
||||||
|
noOfLockers: noOfLockers.number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<motion.div className="w-full h-fit" initial={{ scale: 0.9 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
||||||
|
<FormBox title={t("cabinetCreation")}>
|
||||||
|
<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%]">
|
||||||
|
{t("cabinetId")}
|
||||||
|
</label>
|
||||||
|
<div className="w-full">
|
||||||
|
<input
|
||||||
|
value={cabinetId.id}
|
||||||
|
className={clsx(
|
||||||
|
"w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey",
|
||||||
|
!cabinetId.valid && "border-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCabinetId({
|
||||||
|
id: e.target.value.toUpperCase(),
|
||||||
|
valid: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{!cabinetId.valid && (
|
||||||
|
<motion.div
|
||||||
|
className="w-1/5 text-sm text-error ml-3 pt-1"
|
||||||
|
initial={{ opacity: 0,scale: 0 }}
|
||||||
|
animate={{ opacity: 1,scale: 1 }}
|
||||||
|
exit={{ opacity: 0,scale: 0 }}
|
||||||
|
key="cabinetIdError"
|
||||||
|
>
|
||||||
|
Invalid Cabinet Id
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
|
||||||
|
{t("cabinetKeyId")}
|
||||||
|
</label>
|
||||||
|
<div className="w-full">
|
||||||
|
<input
|
||||||
|
value={cabinetKeyId.id}
|
||||||
|
className={clsx(
|
||||||
|
"w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey",
|
||||||
|
!cabinetKeyId.valid && "border-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCabinetKeyId({
|
||||||
|
id: e.target.value.toUpperCase(),
|
||||||
|
valid: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
{!cabinetKeyId.valid && (
|
||||||
|
<div className="text-sm text-error ml-3 pt-1">
|
||||||
|
Invalid Key Id
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
|
||||||
|
{t("noOfLockers")}
|
||||||
|
</label>
|
||||||
|
<div className="w-full">
|
||||||
|
<input
|
||||||
|
value={noOfLockers.number}
|
||||||
|
className={clsx(
|
||||||
|
"w-1/5 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 border-grey text-grey focus:outline-grey",
|
||||||
|
!noOfLockers.valid && "border-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNoOfLockers({ number: e.target.value, valid: true })
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
{!noOfLockers.valid && (
|
||||||
|
<div className="text-sm text-error ml-3 pt-1">
|
||||||
|
Invalid Number of Lockers
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
text={t("next")}
|
||||||
|
onClick={(e) => {
|
||||||
|
handleNext(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormBox>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CabinetCreation;
|
@ -1,38 +1,68 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import FormBox from "../components/FormBox";
|
import FormBox from "../components/FormBox";
|
||||||
import Button from "../components/Button";
|
import Button from "../components/Button";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { AnimatePresence } from "motion/react";
|
||||||
|
|
||||||
function CabinetMaintenace() {
|
function CabinetMaintenace() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [operation, setOperation] = useState("");
|
const { t } = useTranslation();
|
||||||
|
const [operation, setOperation] = useState({ value: "", valid: true });
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = (e) => {
|
||||||
if (operation === "") {
|
e.preventDefault();
|
||||||
alert("Please select an operation.");
|
if (operation.value === "") {
|
||||||
}
|
setOperation({ value: operation.value, valid: false });
|
||||||
navigate(operation)
|
}
|
||||||
|
navigate(operation.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-fit">
|
<div>
|
||||||
<FormBox title="Cabinet Maintenance">
|
<FormBox title={t("cabinetMaintenance")}>
|
||||||
<div className="p-2">
|
<div className="p-2 pt-7">
|
||||||
<div className="my-5">
|
<div className="flex">
|
||||||
<label className="mr-4 text-lg ">Operation</label>
|
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
|
||||||
<select
|
{t("operation")}
|
||||||
className="w-1/5 h-10 px-2 rounded-full border-2 border-grey text-grey"
|
</label>
|
||||||
onChange={(e) => setOperation(e.target.value)}
|
<div className="w-full">
|
||||||
defaultValue={operation}
|
<select
|
||||||
>
|
className={clsx(
|
||||||
<option value="" disabled>
|
"w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey",
|
||||||
Select
|
!operation.valid && "border-error"
|
||||||
</option>
|
)}
|
||||||
<option value="create">Create</option>
|
onChange={(e) =>
|
||||||
</select>
|
setOperation({ value: e.target.value, valid: true })
|
||||||
|
}
|
||||||
|
defaultValue={operation.value}
|
||||||
|
value={operation.value}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{t("select")}
|
||||||
|
</option>
|
||||||
|
<option value="create">{t("create")}</option>
|
||||||
|
</select>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{!operation.valid && (
|
||||||
|
<motion.div
|
||||||
|
className="w-1/5 text-sm text-error ml-3 pt-1"
|
||||||
|
initial={{ opacity: 0,scale: 0 }}
|
||||||
|
animate={{ opacity: 1,scale: 1 }}
|
||||||
|
exit={{ opacity: 0,scale: 0 }}
|
||||||
|
key="cabinetIdError"
|
||||||
|
>
|
||||||
|
Invalid Operation
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button text="Next" onClick={handleNext} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button text={t("next")} onClick={(e) => handleNext(e)} />
|
||||||
</FormBox>
|
</FormBox>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
198
src/pages/ChargeEdit.jsx
Normal file
198
src/pages/ChargeEdit.jsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
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 { useToast } from "../hooks/useToast";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { lockerService } from "../services/locker.service";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { Pencil } from "lucide-react";
|
||||||
|
|
||||||
|
function ChargeEdit() {
|
||||||
|
const [chargeDetails, setChargeDetails] = useState({
|
||||||
|
rentAmount: "",
|
||||||
|
rentAmountEdit: false,
|
||||||
|
penaltyAmount: "",
|
||||||
|
penaltyAmountEdit: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [notification, setNotification] = useState({
|
||||||
|
visible: false,
|
||||||
|
message: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setIsLoading } = useLoading();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const showToast = useToast();
|
||||||
|
const location = useLocation();
|
||||||
|
const { productCode, interestCategory } = location.state;
|
||||||
|
|
||||||
|
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,
|
||||||
|
rentAmountEdit: false,
|
||||||
|
penaltyAmount: penalty,
|
||||||
|
penaltyAmountEdit: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: response.data.message,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: error.message,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchCharges();
|
||||||
|
}, [productCode, interestCategory, setIsLoading]);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if(!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit) {
|
||||||
|
showToast("No changes made", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await lockerService.updateCharges(
|
||||||
|
productCode,
|
||||||
|
interestCategory,
|
||||||
|
chargeDetails.rentAmount,
|
||||||
|
chargeDetails.penaltyAmount
|
||||||
|
);
|
||||||
|
if (response.status === 200) {
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: response.data.message,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: response.data.message,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
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,
|
||||||
|
className: field.readOnly ? "bg-grey/[0.3]" : "",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||||
|
{field.type === "input" ? (
|
||||||
|
<FormInput
|
||||||
|
{...commonProps}
|
||||||
|
type={field.subType}
|
||||||
|
readOnly={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormSelect {...commonProps} options={field.options} />
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{notification.visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className={clsx(
|
||||||
|
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
|
||||||
|
notification.type === "error"
|
||||||
|
? "bg-error-surface text-error"
|
||||||
|
: "bg-success-surface text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{notification.message}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div className="relative">
|
||||||
|
{notification.type === "success" && (
|
||||||
|
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
|
||||||
|
)}
|
||||||
|
<FormBox title={t("chargeEdit")}>
|
||||||
|
<div className="p-2 pt-7 flex flex-col gap-4">
|
||||||
|
{formFields.map(renderField)}
|
||||||
|
</div>
|
||||||
|
<Button text={t("submit")} onClick={handleSubmit} disabled={!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit}/>
|
||||||
|
</FormBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChargeEdit;
|
114
src/pages/ChargeManagement.jsx
Normal file
114
src/pages/ChargeManagement.jsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
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";
|
||||||
|
|
||||||
|
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,
|
||||||
|
className: clsx(!productDetails[`${field.name}Valid`] && "border-error"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||||
|
{field.type === "input" ? (
|
||||||
|
<FormInput
|
||||||
|
{...commonProps}
|
||||||
|
type={field.subType}
|
||||||
|
readOnly={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormSelect {...commonProps} options={field.options} />
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormBox title={t("lockerStatus")}>
|
||||||
|
<div className="p-2 pt-7 flex flex-col gap-4">
|
||||||
|
{formFields.map(renderField)}
|
||||||
|
</div>
|
||||||
|
<Button text={t("submit")} onClick={handleSubmit} />
|
||||||
|
</FormBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChargeManagement;
|
219
src/pages/KeySwap.jsx
Normal file
219
src/pages/KeySwap.jsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion, 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 clsx from "clsx";
|
||||||
|
import FormBox from "../components/FormBox";
|
||||||
|
import { Copy } from "lucide-react";
|
||||||
|
|
||||||
|
function KeySwap() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const showToast = useToast();
|
||||||
|
const { isLoading, setIsLoading } = useLoading();
|
||||||
|
const [notification, setNotification] = useState({
|
||||||
|
visible: false,
|
||||||
|
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({
|
||||||
|
visible: true,
|
||||||
|
message: response.data.message,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: response.data.message,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
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,
|
||||||
|
className: clsx(!keySwapDetails[`${field.name}Valid`] && "border-error"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||||
|
{field.type === "input" ? (
|
||||||
|
<FormInput
|
||||||
|
{...commonProps}
|
||||||
|
type={field.subType}
|
||||||
|
readOnly={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormSelect {...commonProps} options={field.options} />
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{notification.visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className={clsx(
|
||||||
|
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
|
||||||
|
notification.type === "error"
|
||||||
|
? "bg-error-surface text-error"
|
||||||
|
: "bg-success-surface text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{notification.message.split(":").map((msg, index) => {
|
||||||
|
return index === 1 ? (
|
||||||
|
<span key={index} className="border-b border-dashed">
|
||||||
|
{msg}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key={index}>{msg}</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Copy
|
||||||
|
cursor={"pointer"}
|
||||||
|
size={15}
|
||||||
|
onClick={navigator.clipboard.writeText(
|
||||||
|
notification.message.split(":")[1].trim()
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div className="relative">
|
||||||
|
{notification.type === "success" && (
|
||||||
|
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
|
||||||
|
)}
|
||||||
|
<FormBox title={t("lockerStatus")}>
|
||||||
|
<div className="p-2 pt-7 flex flex-col gap-4">
|
||||||
|
{formFields.map(renderField)}
|
||||||
|
</div>
|
||||||
|
<Button text={t("submit")} onClick={handleKeySwap} />
|
||||||
|
</FormBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeySwap;
|
70
src/pages/LockerMaintenance.jsx
Normal file
70
src/pages/LockerMaintenance.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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 { motion, AnimatePresence } from "motion/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormBox title={t("cabinetMaintenance")}>
|
||||||
|
<div className="p-2 pt-7">
|
||||||
|
<div className="flex">
|
||||||
|
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
|
||||||
|
{t("operation")}
|
||||||
|
</label>
|
||||||
|
<div className="w-full">
|
||||||
|
<select
|
||||||
|
className={clsx(
|
||||||
|
"w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey",
|
||||||
|
!operation.valid && "border-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setOperation({ value: e.target.value, valid: true })
|
||||||
|
}
|
||||||
|
defaultValue={operation.value}
|
||||||
|
value={operation.value}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{t("select")}
|
||||||
|
</option>
|
||||||
|
<option value="status">{t("changeStatus")}</option>
|
||||||
|
<option value="key-swap">{t("keySwap")}</option>
|
||||||
|
</select>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{!operation.valid && (
|
||||||
|
<motion.div
|
||||||
|
className="w-1/5 text-sm text-error ml-3 pt-1"
|
||||||
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0 }}
|
||||||
|
key="cabinetIdError"
|
||||||
|
>
|
||||||
|
Invalid Operation
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button text={t("next")} onClick={(e) => handleNext(e)} />
|
||||||
|
</FormBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LockerMaintenance;
|
164
src/pages/LockerStatus.jsx
Normal file
164
src/pages/LockerStatus.jsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
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 { useToast } from "../hooks/useToast";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Button from "../components/Button";
|
||||||
|
import { lockerService } from "../services/locker.service";
|
||||||
|
import { useLoading } from "../hooks/useLoading";
|
||||||
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
|
||||||
|
function LockerStatus() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const showToast = useToast();
|
||||||
|
const [lockerDetails, setLockerDetails] = useState({
|
||||||
|
cabinetId: "",
|
||||||
|
lockerId: "",
|
||||||
|
status: "",
|
||||||
|
cabinetIdValid: true,
|
||||||
|
lockerIdValid: true,
|
||||||
|
statusValid: true,
|
||||||
|
});
|
||||||
|
const { isLoading, setIsLoading } = useLoading();
|
||||||
|
const [notification, setNotification] = useState({
|
||||||
|
visible: false,
|
||||||
|
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) {
|
||||||
|
showToast("Highlighted fields are invalid", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await lockerService.changeLockerStatus(
|
||||||
|
lockerDetails.cabinetId,
|
||||||
|
lockerDetails.lockerId,
|
||||||
|
lockerDetails.status
|
||||||
|
);
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: response.data.message,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showToast(error.response.data.message, "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,
|
||||||
|
className: clsx(!lockerDetails[`${field.name}Valid`] && "border-error"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField key={field.name} label={field.label} icon={field.icon}>
|
||||||
|
{field.type === "input" ? (
|
||||||
|
<FormInput
|
||||||
|
{...commonProps}
|
||||||
|
type={field.subType}
|
||||||
|
readOnly={field.readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormSelect {...commonProps} options={field.options} />
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{notification.visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className={clsx(
|
||||||
|
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
|
||||||
|
notification.type === "error"
|
||||||
|
? "bg-error-surface text-error"
|
||||||
|
: "bg-success-surface text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{notification.message}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div className="relative">
|
||||||
|
{notification.type === "success" && (
|
||||||
|
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
|
||||||
|
)}
|
||||||
|
<FormBox title={t("lockerStatus")}>
|
||||||
|
<div className="p-2 pt-7 flex flex-col gap-4">
|
||||||
|
{formFields.map(renderField)}
|
||||||
|
</div>
|
||||||
|
<Button text={t("submit")} onClick={handleSubmit} />
|
||||||
|
</FormBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LockerStatus;
|
262
src/pages/LockersRegistration.jsx
Normal file
262
src/pages/LockersRegistration.jsx
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useState } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import FormBox from "../components/FormBox";
|
||||||
|
import Button from "../components/Button";
|
||||||
|
import { useToast } from "../hooks/useToast";
|
||||||
|
import { lockerService } from "../services/locker.service";
|
||||||
|
import { Copy } from "lucide-react";
|
||||||
|
import { AnimatePresence } from "motion/react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { useLoading } from "../hooks/useLoading";
|
||||||
|
|
||||||
|
function LockersRegistration() {
|
||||||
|
const location = useLocation();
|
||||||
|
const showToast = useToast();
|
||||||
|
const { setIsLoading } = useLoading();
|
||||||
|
|
||||||
|
const { noOfLockers, cabinetId } = location.state;
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [notification, setNotification] = useState({
|
||||||
|
visible: false,
|
||||||
|
message: "",
|
||||||
|
type: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const initLockers = Array(parseInt(noOfLockers))
|
||||||
|
.fill()
|
||||||
|
.map(() => ({
|
||||||
|
id: "",
|
||||||
|
size: "",
|
||||||
|
keyId: "",
|
||||||
|
idValid: true,
|
||||||
|
sizeValid: true,
|
||||||
|
keyIdValid: true,
|
||||||
|
}));
|
||||||
|
const [lockerValues, setLockerValues] = useState(initLockers);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
console.log("submitting");
|
||||||
|
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) {
|
||||||
|
const errorMessage =
|
||||||
|
duplicateLockerIds.length || duplicateKeyIds.length
|
||||||
|
? "Please ensure all IDs are unique."
|
||||||
|
: "Inavlid Ids";
|
||||||
|
showToast(errorMessage, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await lockerService.registerLockers(
|
||||||
|
cabinetId,
|
||||||
|
lockerValues
|
||||||
|
);
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: `Cabinet creation successful. Cabinet ID: ${response.data.cabinetId}`,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setNotification({
|
||||||
|
visible: true,
|
||||||
|
message: `Error registering lockers. ${error.message}`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lockerDetails = lockerValues.map((locker, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex gap-12 items-center">
|
||||||
|
<label className="text-lg text-bold dark:text-primary-dark">{`Locker ${
|
||||||
|
index + 1
|
||||||
|
}`}</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
value={locker.id}
|
||||||
|
disabled={submitting}
|
||||||
|
className={clsx(
|
||||||
|
"w-64 h-9 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey",
|
||||||
|
!locker.idValid && "border-error text-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValues = [...lockerValues];
|
||||||
|
newValues[index] = {
|
||||||
|
...newValues[index],
|
||||||
|
id: e.target.value.toUpperCase(),
|
||||||
|
idValid: true,
|
||||||
|
};
|
||||||
|
setLockerValues(newValues);
|
||||||
|
}}
|
||||||
|
placeholder="Locker ID"
|
||||||
|
type="text"
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={locker.size}
|
||||||
|
disabled={submitting}
|
||||||
|
className={clsx(
|
||||||
|
"w-64 h-9 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey",
|
||||||
|
!locker.sizeValid && "border-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValues = [...lockerValues];
|
||||||
|
newValues[index] = {
|
||||||
|
...newValues[index],
|
||||||
|
size: e.target.value,
|
||||||
|
sizeValid: true,
|
||||||
|
};
|
||||||
|
setLockerValues(newValues);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Size
|
||||||
|
</option>
|
||||||
|
<option value="small">Small</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="large">Large</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
value={locker.keyId}
|
||||||
|
disabled={submitting}
|
||||||
|
className={clsx(
|
||||||
|
"w-64 h-9 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey",
|
||||||
|
!locker.keyIdValid && "border-error"
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValues = [...lockerValues];
|
||||||
|
newValues[index] = {
|
||||||
|
...newValues[index],
|
||||||
|
keyId: e.target.value.toUpperCase(),
|
||||||
|
keyIdValid: true,
|
||||||
|
};
|
||||||
|
setLockerValues(newValues);
|
||||||
|
}}
|
||||||
|
placeholder="Key ID"
|
||||||
|
type="text"
|
||||||
|
maxLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{notification.visible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className={clsx(
|
||||||
|
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
|
||||||
|
notification.type === "error"
|
||||||
|
? "bg-error-surface text-error"
|
||||||
|
: "bg-success-surface text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{notification.message.split(":").map((msg, index) => {
|
||||||
|
return index === 1 ? (
|
||||||
|
<span key={index} className="border-b border-dashed">
|
||||||
|
{msg}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key={index}>{msg}</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Copy
|
||||||
|
cursor={"pointer"}
|
||||||
|
size={15}
|
||||||
|
onClick={navigator.clipboard.writeText(
|
||||||
|
notification.message.split(":")[1].trim()
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div className="relative">
|
||||||
|
{notification.type === "success" && (
|
||||||
|
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
|
||||||
|
)}
|
||||||
|
<FormBox title="Locker Registration" disabled={submitting}>
|
||||||
|
<div className="px-4 pt-7 text-2xl font-bold text-primary dark:text-primary-dark">
|
||||||
|
{cabinetId}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 p-4">{lockerDetails}</div>
|
||||||
|
<Button
|
||||||
|
disabled={submitting}
|
||||||
|
text="Register"
|
||||||
|
onClick={(e) => {
|
||||||
|
handleSubmit(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LockersRegistration;
|
@ -1,9 +1,9 @@
|
|||||||
function Placeholder() {
|
function Placeholder() {
|
||||||
return (
|
return (
|
||||||
<div className="text-2xl text-center h-max font-body text-primary dark:text-primary-dark">
|
<div className="flex justify-center items-center h-full">
|
||||||
<h1>Placeholder</h1>
|
<p className="text-3xl text-primary">Placeholder</p>
|
||||||
</div >
|
</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;
|
30
src/services/locker.service.js
Normal file
30
src/services/locker.service.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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}`);
|
||||||
|
}
|
||||||
|
};
|
12
src/util/productList.js
Normal file
12
src/util/productList.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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' },
|
||||||
|
{ productCode: '2001', productCodeDescription: 'MIS', interestCategory: '1003', interestCategoryDescription: 'NON MEMBER - SENIOR CITIZEN', payableGl: '16137', paidGl: '62125' },
|
||||||
|
{ productCode: '2002', productCodeDescription: 'FIXED DEPOSIT', interestCategory: '1006', interestCategoryDescription: 'NONMEMBER - SENIOR CITIZEN', payableGl: '16009', paidGl: '62109' },
|
||||||
|
{ productCode: '2002', productCodeDescription: 'CASH CERTIFICATE', interestCategory: '1001', interestCategoryDescription: 'MEMBER', payableGl: '16011', paidGl: '62111' },
|
||||||
|
{ productCode: '1101', productCodeDescription: 'SAVINGS DEPOSIT- MEMBER', interestCategory: '1347', interestCategoryDescription: 'COMPOUNDING', payableGl: '16301', paidGl: '62117' }
|
||||||
|
]
|
||||||
|
export default productInfo;
|
@ -7,4 +7,8 @@ const getUserInfoFromSession = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 };
|
@ -10,6 +10,25 @@ export default {
|
|||||||
white: '#FFFFFF',
|
white: '#FFFFFF',
|
||||||
black: '#000000',
|
black: '#000000',
|
||||||
grey: '#979797',
|
grey: '#979797',
|
||||||
|
error: {
|
||||||
|
DEFAULT: '#E5254B',
|
||||||
|
dark: '#E5254B',
|
||||||
|
surface: {DEFAULT: '#FCE9ED', dark: '#FCE9ED'}
|
||||||
|
},
|
||||||
|
onToast: {
|
||||||
|
DEFAULT: '#646564',
|
||||||
|
dark: '#646564',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
DEFAULT: '#EA7000',
|
||||||
|
dark: '#EA7000',
|
||||||
|
surface: {DEFAULT: '#FDF1E5', dark: '#FDF1E5'}
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
DEFAULT: '#038100',
|
||||||
|
dark: '#038100',
|
||||||
|
surface: {DEFAULT: '#E6F2E5', dark: '#E6F2E5'}
|
||||||
|
},
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: '#008C46',
|
DEFAULT: '#008C46',
|
||||||
@ -42,6 +61,22 @@ export default {
|
|||||||
fontSize: {
|
fontSize: {
|
||||||
title: '40px',
|
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: [
|
plugins: [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user