Compare commits

...

42 Commits

Author SHA1 Message Date
700a9261a8 fixed form title in LockerMaintenance.jsx 2024-12-28 19:55:30 +05:30
ffae2f728b updated translations 2024-12-27 17:24:56 +05:30
307d2621ce remove form diabling div from LockerStatus.jsx 2024-12-27 16:34:18 +05:30
6b19adf03e Refactored LockerMaintenance.jsx to use Form Components 2024-12-27 16:32:57 +05:30
dcfa7a46d8 removed form disabling mechanism and fixed notification bug in CheckInOutManagement.jsx 2024-12-27 16:20:36 +05:30
2afefc7442 Refactored CabinetMaintenance.jsx to use FormField and FormInput 2024-12-26 20:34:53 +05:30
d4fd7601f1 Refactored CabinetCreation.jsx to use FormInput and FromField 2024-12-26 16:29:12 +05:30
bd32eb02d6 added keys in form rows and made noOfLockers integer 2024-12-26 16:28:12 +05:30
bb108f809f refactor: update FormInput and FormSelect components to accept props directly; add FieldsWrapper for improved layout 2024-12-25 21:13:00 +05:30
abad63787b removed unused imports 2024-12-24 19:10:51 +05:30
9f4059e2c6 reactored code to use formfields and formselects 2024-12-24 19:10:20 +05:30
962102d44c refactor: enhance FormInput and FormSelect components with validation feedback and layout adjustments 2024-12-24 01:31:07 +05:30
03c4988ff1 refactor: streamline layout and error handling in CabinetCreation component 2024-12-24 01:06:10 +05:30
27f4597348 refactor: simplify notification state management across components 2024-12-24 01:03:46 +05:30
265d0b2209 refactor: update FormBox title in ChargeManagement component 2024-12-24 00:51:13 +05:30
a56f643301 feat: add CheckInOutLog page and integrate with locker service for check-in/out functionality 2024-12-24 00:50:06 +05:30
8b34a69dca refactor: update Notification component to use message and type props 2024-12-24 00:40:12 +05:30
f7fea99f30 feat: integrate navigation to log page upon successful account validation in CheckInOutManagement 2024-12-24 00:18:32 +05:30
bd461995c7 refactor: improve ChargeEdit component structure 2024-12-24 00:15:51 +05:30
17681e64ad renamed CheckInOut to CheckInOutManagement 2024-12-24 00:15:08 +05:30
154eba0474 fix: update button disabled state color for better accessibility 2024-12-24 00:05:09 +05:30
dea0047007 refactor: replace inline notification handling with Notification component in LockerStatus 2024-12-24 00:01:29 +05:30
f4b7027708 refactor: replace inline notification rendering with Notification component in LockersRegistration 2024-12-23 23:57:52 +05:30
b4b193a9fe feat: add Check In/Out management with notification system and update locker service 2024-12-23 23:49:35 +05:30
9d33cb5372 fix: handle optional state properties and improve animation transitions in LockersRegistration component 2024-12-23 20:47:19 +05:30
48b9b70c4a refactor animations for improved transitions in App and AccountCreation components 2024-12-23 20:47:13 +05:30
cc3943196e added check in out route 2024-12-23 18:39:13 +05:30
71ece53f65 added translation for banner information 2024-12-23 18:29:25 +05:30
7ae4e8dbef enhance animation 2024-12-23 18:25:14 +05:30
a3d4f2b11c added animation to submenu 2024-12-23 17:36:42 +05:30
c1df68b27a edits for other screen sizes 2024-12-23 15:22:20 +05:30
44112f91bd Add ChargeManagement and ChargeEdit components; implement charge management functionality with form validation and routing updates 2024-12-23 01:41:29 +05:30
9d72dc6868 Add locker service methods for status change and key swap; create LockerMaintenance and LockerStatus components with form validation and translations 2024-12-23 00:10:59 +05:30
8cee8e0383 Add AccountCreation component and product list functionality; update routing and translations 2024-12-22 22:42:48 +05:30
a6a67d69d5 Add loading context and loading bar component; integrate axios for API calls and update routing for locker registration 2024-12-22 15:59:24 +05:30
bfe22a61a5 Add motion animations to Button and CabinetMaintenance components; update form validation and styling 2024-12-22 01:38:09 +05:30
442b8e52dd Add CabinetCreation component and form validation; update translations and routing 2024-12-21 23:52:19 +05:30
03747b5251 Add translations for new operations in multiple languages; update CabinetMaintenance component for localization 2024-12-21 21:04:43 +05:30
650a6dbdd0 Refactor ToastProvider and CabinetMaintenance to streamline showToast usage 2024-12-21 20:57:30 +05:30
2b940c3d43 Add audio feedback for toast notifications; implement playAudio function in ToastProvider 2024-12-21 20:56:02 +05:30
96784c02c7 Add ToastProvider component and useToast hook; update CabinetMaintenance for toast notifications; enhance Button styles and Tailwind CSS configuration 2024-12-21 20:34:16 +05:30
835a3cc7fd Refactor cabinet maintenance routing and add CabinetMaintenance component; update Header and main router configuration 2024-12-21 18:11:47 +05:30
44 changed files with 2468 additions and 73 deletions

172
package-lock.json generated
View File

@ -8,11 +8,13 @@
"name": "osaka",
"version": "0.0.0",
"dependencies": {
"axios": "^1.7.9",
"clsx": "^2.1.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"lucide-react": "^0.446.0",
"motion": "^11.15.0",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -1663,6 +1665,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@ -1717,6 +1725,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1933,6 +1952,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -2116,6 +2147,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -2816,6 +2856,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -2843,6 +2903,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -2857,6 +2931,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.14.3",
"motion-utils": "^11.14.3",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3905,6 +4006,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -3928,6 +4050,44 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/motion": {
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-11.15.0.tgz",
"integrity": "sha512-iZ7dwADQJWGsqsSkBhNHdI2LyYWU+hA1Nhy357wCLZq1yHxGImgt3l7Yv0HT/WOskcYDq9nxdedyl4zUv7UFFw==",
"license": "MIT",
"dependencies": {
"framer-motion": "^11.15.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==",
"license": "MIT"
},
"node_modules/motion-utils": {
"version": "11.14.3",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -4506,6 +4666,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -5327,6 +5493,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -10,11 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"clsx": "^2.1.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"lucide-react": "^0.446.0",
"motion": "^11.15.0",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",

BIN
public/audio/error.mp3 Normal file

Binary file not shown.

BIN
public/audio/warning.mp3 Normal file

Binary file not shown.

View File

@ -46,5 +46,9 @@
"hpn_rupay_kcc_time": "সমস্ত রূপে কিসান ক্রেডিট কার্ড লেনদেন ১৯:00 ঘটিকার আগে সম্পন্ন করুন",
"hpn_rupay_kcc_atm": "কিছু অভ্যন্তরীণ পরিবর্তনের কারণে, মাইক্রো-এটিএম এর মাধ্যমে রূপে কিসান ক্রেডিট কার্ড কার্যক্রম বন্ধ করা হয়েছে। সেবা আগামীকাল থেকে পুনরায় চালু হবে।",
"copyright_statement": "কপিরাইট © ২০২৩, টাটা কনসাল্টেন্সি সার্ভিসেস। সমস্ত অধিকার সংরক্ষিত",
"privacy_policy": "গোপনীয়তা নীতি"
"privacy_policy": "গোপনীয়তা নীতি",
"create": "তৈরি করুন",
"operation": "অপারেশন",
"next": "পরবর্তী",
"select": "নির্বাচন করুন"
}

View File

@ -47,5 +47,37 @@
"hpn_rupay_kcc_time": "Complete all Rupay KCC transactions before 19:00 Hrs",
"hpn_rupay_kcc_atm": "Due to some internal changes, Rupay KCC Operations through Micro-ATM has been stopped. The service will resume starting tomorrow.",
"copyright_statement": "Copyright © 2023, Tata Consultancy Services. All rights reserved",
"privacy_policy": "Privacy Policy"
"privacy_policy": "Privacy Policy",
"create": "Create",
"operation": "Operation",
"next": "Next",
"select": "Select",
"cabinetCreation": "Cabinet Creation",
"cabinetId": "Cabinet ID",
"cabinetKeyId": "Cabinet Key ID",
"noOfLockers": "No of Lockers",
"productCode": "Product Code",
"interestCategory": "Interest Category",
"segmentCode": "Segment Code",
"accountHolderType": "Account Holder Type",
"primaryCifNumber": "Primary CIF Number",
"nomineeCifNumber": "Nominee CIF Number",
"activityCode": "Activity Code",
"customerType": "Customer Type",
"collateralFDAccount": "Collateral FD Account",
"rentPayAccount": "Rent Pay Account",
"submit": "Submit",
"lockerId": "Locker ID",
"status": "Status",
"lockerStatus": "Locker Status",
"open": "Open",
"close": "Close",
"reasonForChange": "Reason for Change",
"oldKeyId": "Old Key ID",
"newKeyId": "New Key ID",
"confirmNewKeyId": "Confirm New Key ID",
"highlightedFieldsInvalid": "Highlighted fields are invalid",
"changeStatus": "Change Status",
"keySwap": "Key Swap",
"chargeEdit": "Edit Charges"
}

View File

@ -47,5 +47,10 @@
"hpn_rupay_kcc_time": "सभी रुपे केसीसी लेनदेन 19:00 बजे से पहले पूरा करें",
"hpn_rupay_kcc_atm": "कुछ आंतरिक बदलावों के कारण, माइक्रो-एटीएम के माध्यम से रुपे केसीसी संचालन बंद कर दिया गया है। सेवा कल से फिर से शुरू होगी।",
"copyright_statement": "कॉपीराइट © 2023, टाटा कंसल्टेंसी सर्विसेज। सभी अधिकार सुरक्षित",
"privacy_policy": "गोपनीयता नीति"
"privacy_policy": "गोपनीयता नीति",
"create": "बनाएं",
"operation": "ऑपरेशन",
"next": "अगला",
"select": "चुनें",
"productCode": "प्रोडक्ट कोड"
}

View File

@ -1,15 +1,52 @@
import { Outlet } from "react-router-dom"
import Header from "./components/Header"
import Footer from "./components/Footer"
import { useLocation, useOutlet } from "react-router";
import { useState } from "react";
import Header from "./components/Header";
import Footer from "./components/Footer";
import { AnimatePresence } from "motion/react";
import { motion } from "motion/react";
import { ToastProvider } from "./contexts/Toast";
import { useLoading } from "./hooks/useLoading";
import LoadingBar from "./components/LoadingBar";
import { LoadingProvider } from "./contexts/Loading";
const AnimatedOutlet = () => {
const o = useOutlet();
const [outlet] = useState(o);
return <div>{outlet}</div>;
};
function LoadingBarWrapper() {
const { isLoading } = useLoading();
return isLoading ? <LoadingBar /> : null;
}
function App() {
return <div className="flex flex-col min-h-screen">
<Header />
<main className="flex flex-grow transition-color-mode md:p-7 2xl:p-12 bg-surface dark:bg-surface-dark">
<Outlet />
</main>
<Footer />
</div>
}
const location = useLocation();
return (
<LoadingProvider>
<div className="flex flex-col min-h-screen scrollbar">
<Header />
<LoadingBarWrapper />
<main className="overflow-hidden flex flex-grow transition-color-mode md:p-10 2xl:px-70 bg-surface dark:bg-surface-dark">
<ToastProvider>
<AnimatePresence mode="popLayout">
<motion.div
className="w-full ovwerflow-hidden"
key={location.pathname}
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
>
<AnimatedOutlet />
</motion.div>
</AnimatePresence>
</ToastProvider>
</main>
<Footer />
</div>
</LoadingProvider>
);
}
export default App
export default App;

25
src/components/Button.jsx Normal file
View File

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import { motion } from 'motion/react';
import clsx from 'clsx';
function Button({text, onClick, disabled}) {
return (
<motion.button
whileHover={!disabled && { scale: 1.05 }}
whileTap={!disabled && { scale: 0.95 }}
className={clsx("px-12 py-2 text-lg text-white dark:text-primary-dark rounded-full bg-primary dark:bg-secondary-dark", disabled && "bg-[#cccccc] dark:bg-[#cccccc]")}
onClick={onClick}
disabled={disabled}
>
{text}
</motion.button>
)
}
Button.propTypes = {
text: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool,
};
export default Button;

View File

@ -0,0 +1,21 @@
import PropTypes from "prop-types";
import { motion } from "motion/react";
function FieldError({ text }) {
return (
<motion.div
className="text-sm text-error ml-3 pt-1"
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
>
{text}
</motion.div>
);
}
FieldError.propTypes = {
text: PropTypes.string.isRequired,
};
export default FieldError;

View File

@ -0,0 +1,12 @@
import PropTypes from "prop-types";
function FieldsWrapper({ children, className = "" }) {
return <div className={`flex flex-col gap-4 m-2 my-7 ${className}`}>{children}</div>;
}
FieldsWrapper.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
export default FieldsWrapper;

View File

@ -1,14 +1,27 @@
import { PropTypes } from 'prop-types';
import clsx from 'clsx';
import { PropTypes } from "prop-types";
import clsx from "clsx";
function FormBox({ title, children, alt = false }) {
return (
<div className={clsx(alt ? 'bg-secondary-variant dark:bg-secondary-variant-dark border-secondary-variant dark:border-secondary-variant-dark' : 'bg-surface-variant dark:bg-surface-variant-dark', 'transition-color-mode font-body border-secondary dark:border-secondary-dark border-2 p-4 rounded-3xl relative h-full')}>
<label className={clsx(alt && 'bg-surface dark:bg-surface-dark border-3 border-secondary-variant dark:border-secondary-variant-dark', 'font-body absolute left-11 -top-4 bg-secondary dark:bg-secondary-dark text-primary dark:text-primary-dark font-medium py-1 px-4 rounded-full')}>
<form
className={clsx(
alt
? "bg-secondary-variant dark:bg-secondary-variant-dark border-secondary-variant dark:border-secondary-variant-dark"
: "bg-surface-variant dark:bg-surface-variant-dark",
"transition-color-mode font-body border-secondary dark:border-secondary-dark border-2 p-4 rounded-3xl relative h-full"
)}
>
<label
className={clsx(
alt &&
"bg-surface dark:bg-surface-dark border-3 border-secondary-variant dark:border-secondary-variant-dark",
"font-body absolute left-11 -top-4 bg-secondary dark:bg-secondary-dark text-primary dark:text-primary-dark font-medium py-1 px-4 rounded-full z-20"
)}
>
{title}
</label>
{children}
</div>
</form>
);
}
@ -18,4 +31,4 @@ FormBox.propTypes = {
alt: PropTypes.bool,
};
export default FormBox;
export default FormBox;

View File

@ -0,0 +1,39 @@
import PropTypes from "prop-types";
import { motion } from "motion/react";
import clsx from "clsx";
function FormField({ label, children, icon, variant }) {
return (
<div className="flex items-center">
<label className={clsx(
"mr-20 text-lg text-black dark:text-primary-dark whitespace-nowrap",
variant === 'long' && "sm:w-[5%]"
)}>
{label}
</label>
<div className={clsx("flex w-full gap-4 items-center", variant === 'long' && 'gap-10')}>
{children}
{icon && (
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className={clsx(icon.mode === "plain" ? "text-[#444]" : "bg-primary rounded-full p-2 text-white cursor-pointer")}
onClick={icon.onClick}
>
{icon.icon}
</motion.div>
)}
</div>
</div>
);
}
FormField.propTypes = {
label: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
icon: PropTypes.object,
variant: PropTypes.string
};
export default FormField;

View File

@ -0,0 +1,11 @@
import PropTypes from "prop-types";
function FormHeader({ text }) {
return <h1 className="text-2xl font-medium text-primary mt-5">{text}</h1>;
}
FormHeader.propTypes = {
text: PropTypes.string.isRequired
}
export default FormHeader;

View File

@ -0,0 +1,38 @@
import PropTypes from "prop-types";
import { motion, AnimatePresence } from "motion/react";
import clsx from "clsx";
function FormInput({ props, valid = true, className = "" }) {
return (
<div>
<input
{...props}
className={clsx(
`w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`,
!valid && "border-error"
)}
/>
<AnimatePresence>
{!valid && (
<motion.div
className="text-sm text-error ml-3 pt-1"
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
key="cabinetIdError"
>
Invalid Value
</motion.div>
)}
</AnimatePresence>
</div>
);
}
FormInput.propTypes = {
props: PropTypes.object,
valid: PropTypes.bool,
className: PropTypes.string,
};
export default FormInput;

View File

@ -0,0 +1,46 @@
import PropTypes from "prop-types";
import { AnimatePresence } from "motion/react";
import clsx from "clsx";
import FieldError from "./FieldError";
import { useTranslation } from "react-i18next";
function FormSelect({ props, valid = true, className = "", options }) {
const { t } = useTranslation();
return (
<div>
<select
{...props}
className={clsx(
`w-72 h-10 px-2 rounded-full dark:bg-white dark:text-grey border-2 text-grey focus:outline-grey ${className}`,
!valid && "border-error"
)}
>
<option disabled value="">
{t("select")}
</option>
{options?.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
<AnimatePresence>
{!valid && <FieldError text={"Invalid value"} />}
</AnimatePresence>
</div>
);
}
FormSelect.propTypes = {
props: PropTypes.object,
className: PropTypes.string,
valid: PropTypes.bool,
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
};
export default FormSelect;

View File

@ -24,13 +24,12 @@ function Header() {
{
name: "lockerOperation",
submenu: [
{ name: "accountCreation", path: "account-creation" },
{ name: "cabinetMaintenance", path: "cabinet-maintenance" },
{ name: "lockerMaintenance", path: "locker-maintenance" },
{ name: "rentPenaltyCollection", path: "rent-collection" },
{ name: "chargeManagement", path: "charge-management" },
{ name: "checkInOutManagement", path: "check-in-out" },
{ name: "accountSurrender", path: "account-surrender" }
{ name: "accountCreation", path: "operation/account" },
{ name: "cabinetMaintenance", path: "operation/cabinet" },
{ name: "lockerMaintenance", path: "operation/locker" },
{ name: "chargeManagement", path: "operation/charge-management" },
{ name: "checkInOutManagement", path: "operation/check-in-out" },
{ name: "accountSurrender", path: "operation/account-surrender" }
],
},
{ name: "worklist", submenu: [{ name: "myIntimation", path: "my-intimation" }] },

View 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;

View File

@ -1,33 +1,72 @@
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { useState } from "react";
import { AnimatePresence, motion } from "motion/react";
function SubMenu({ items }) {
function SubMenu({ items, isVisible, onLinkClick }) {
const { t } = useTranslation();
return (
<div className="absolute left-0 z-50 invisible transition-all duration-200 -translate-y-6 shadow-sm opacity-0 top-full border-t-3 border-primary dark:border-primary-dark bg-secondary dark:bg-secondary-dark rounded-2xl shadow-surface-variant-dark dark:shadow-primary group-hover:visible group-hover:translate-y-0 group-hover:opacity-100">
{items.map((subItem, index) => (
<div key={index} className="px-6 py-2 cursor-pointer text-nowrap hover:bg-white dark:hover:bg-secondary-variant-dark first:rounded-t-2xl second-last:rounded-b-2xl first:pt-4 last:pb-4">
<Link to={subItem.path}>{t(subItem.name)}</Link>
<AnimatePresence mode="popLayout">
{isVisible && (
<div>
<motion.div
className={clsx(
"absolute left-0 z-50 shadow-sm top-full border-t-3 border-primary dark:border-primary-dark bg-secondary dark:bg-secondary-dark rounded-2xl shadow-surface-variant-dark dark:shadow-primary"
)}
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
transition={{ duration: 0.3, type: "spring" }}
>
{items.map((subItem, index) => (
<div
key={index}
className="px-3 py-2 cursor-pointer text-nowrap dark:hover:bg-secondary-variant-dark first:rounded-t-2xl first:pt-4 last:pb-4 second-last:rounded-b-2xl hover:bg-white"
onClick={onLinkClick}
>
<Link to={subItem.path}>{t(subItem.name)}</Link>
</div>
))}
<svg
className="absolute top-0 left-6 mt-[-13px] fill-primary dark:fill-primary-dark"
width="16"
height="13"
viewBox="0 0 16 13"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.26795 1C7.03775 -0.333332 8.96225 -0.333334 9.73205 0.999999L14.9282 10C15.698 11.3333 14.7358 13 13.1962 13H2.80385C1.26425 13 0.301996 11.3333 1.0718 10L6.26795 1Z" />
</svg>
</motion.div>
</div>
))}
<svg className="absolute top-0 left-6 mt-[-13px] fill-primary dark:fill-primary-dark" width="16" height="13" viewBox="0 0 16 13" xmlns="http://www.w3.org/2000/svg">
<path d="M6.26795 1C7.03775 -0.333332 8.96225 -0.333334 9.73205 0.999999L14.9282 10C15.698 11.3333 14.7358 13 13.1962 13H2.80385C1.26425 13 0.301996 11.3333 1.0718 10L6.26795 1Z" />
</svg>
</div>
)}
</AnimatePresence>
);
};
}
function MenuBar({ menuItems }) {
const { t } = useTranslation();
const [activeMenu, setActiveMenu] = useState(null);
return (
<div className="flex justify-between pt-5 pb-2 text-base font-body">
{menuItems.map((item, index) =>
<div key={index} className="relative pb-3 cursor-pointer group">
{t(item.name)}
{item.submenu.length > 0 && <SubMenu items={item.submenu} />}
{menuItems.map((item, index) => (
<div
key={index}
className="relative pb-3 cursor-pointer"
onMouseEnter={() => setActiveMenu(index)}
onMouseLeave={() => setActiveMenu(null)}
>
<Link to={item.path}>{t(item.name)}</Link>
{item.submenu.length > 0 && (
<SubMenu
items={item.submenu}
isVisible={activeMenu === index}
onLinkClick={() => setActiveMenu(null)}
/>
)}
</div>
)}
))}
</div>
);
}
@ -42,18 +81,20 @@ MenuBar.propTypes = {
path: PropTypes.string.isRequired,
})
),
path: PropTypes.string
path: PropTypes.string,
})
).isRequired
).isRequired,
};
SubMenu.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired
path: PropTypes.string.isRequired,
})
).isRequired
).isRequired,
isVisible: PropTypes.bool.isRequired,
onLinkClick: PropTypes.func.isRequired,
};
export default MenuBar;

View File

@ -0,0 +1,48 @@
import clsx from "clsx";
import { Copy } from "lucide-react";
import { motion } from "motion/react";
import PropTypes from "prop-types";
function Notification({ message, type }) {
return (
<motion.div
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
className={clsx(
"p-2 pl-8 mb-8 font-body text-lg border-2 rounded-3xl flex items-center gap-2",
type === "error"
? "bg-error-surface text-white border-error"
: type === "success"
? "bg-success text-white border-green-700"
: type === "warning"
? "bg-warning-surface text-white border-warning"
: ""
)}
>
{message.split(":").map((msg, index) => {
return index === 1 ? (
<span key={index} className="border-b border-dashed">
{msg}
</span>
) : (
<span key={index}>{msg}</span>
);
})}
{message.split(":")[1] && (
<Copy
cursor={"pointer"}
size={15}
onClick={navigator.clipboard.writeText(message.split(":")[1].trim())}
/>
)}
</motion.div>
);
}
Notification.propTypes = {
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default Notification;

View 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;

View File

@ -0,0 +1,35 @@
import { motion } from "motion/react";
import ProductListTable from "./ProductListTable";
import PropTypes from "prop-types";
function ProductModal({ productInfo, handleProductSelect }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
key="productList"
className="fixed z-50 inset-0 flex items-center justify-center bg-black/50"
>
<motion.div
initial={{ y: 15, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 15, opacity: 0 }}
className="flex flex-col items-center bg-white p-4 py-8 rounded-3xl w-[60%] max-h-[80%] overflow-auto font-body"
>
<h2 className="text-xl mb-4">Select Product</h2>
<ProductListTable
productInfo={productInfo}
onSelectProduct={handleProductSelect}
/>
</motion.div>
</motion.div>
);
}
ProductModal.propTypes = {
productInfo: PropTypes.object.isRequired,
handleProductSelect: PropTypes.func.isRequired,
};
export default ProductModal;

19
src/contexts/Loading.jsx Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
import { useContext } from "react";
import { ToastContext } from "../contexts/Toast";
export const useToast = () => useContext(ToastContext);

View File

@ -1,33 +1,81 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import './i18n'
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Placeholder from './pages/Placeholder.jsx';
import Home from './pages/Home.jsx'
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import "./i18n";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./pages/Home.jsx";
import CabinetMaintenace from "./pages/CabinetMaintenance.jsx";
import CabinetCreation from "./pages/CabinetCreation.jsx";
import LockersRegistration from "./pages/LockersRegistration.jsx";
import AccountCreation from "./pages/AccountCreation.jsx";
import LockerMaintenance from "./pages/LockerMaintenance.jsx";
import LockerStatus from "./pages/LockerStatus.jsx";
import KeySwap from "./pages/KeySwap.jsx";
import ChargeManagement from "./pages/ChargeManagement.jsx";
import ChargeEdit from "./pages/ChargeEdit.jsx";
import CheckInOutManagement from "./pages/CheckInOutManagement.jsx";
import CheckInOutLog from "./pages/CheckInOutLog.jsx";
const router = createBrowserRouter([
{
path: "/",
element: <App/>,
element: <App />,
children: [
{
index: true,
element: <Home />
element: <Home />,
},
{
path: 'cabinet-maintenance',
element: <Placeholder />
path: "operation/cabinet",
element: <CabinetMaintenace />,
},
{
path: "operation/cabinet/create",
element: <CabinetCreation />,
},
{
path: "operation/cabinet/create/register-lockers",
element: <LockersRegistration />,
},
{
path: "operation/account",
element: <AccountCreation />,
},
{
path: "operation/locker",
element: <LockerMaintenance />,
},
{
path: "operation/locker/status",
element: <LockerStatus />,
},
{
path: "operation/locker/key-swap",
element: <KeySwap />,
},
{
path: "operation/charge-management",
element: <ChargeManagement />,
},
{
path: "operation/charge-management/change",
element: <ChargeEdit />,
},
{
path: "operation/check-in-out",
element: <CheckInOutManagement />
},
{
path: "operation/check-in-out/log",
element: <CheckInOutLog />
}
]
}
]
);
],
},
]);
createRoot(document.getElementById('root')).render(
createRoot(document.getElementById("root")).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
</StrictMode>
);

View File

@ -0,0 +1,241 @@
import { AnimatePresence } from "motion/react";
import { PackageSearch, UserSearch } from "lucide-react";
import { useState } from "react";
import FormBox from "../components/FormBox";
import { useTranslation } from "react-i18next";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import Button from "../components/Button";
import productInfo from "../util/productList";
import Notification from "../components/Notification";
import ProductModal from "../components/ProductModal";
import FormHeader from "../components/FormHeader";
import FieldsWrapper from "../components/FieldsWrapper";
function AccountCreation() {
const { t } = useTranslation();
const [notification] = useState({ message: "", type: "" });
const [showProductModal, setShowProductModal] = useState(false);
const [accountDetails, setAccountDetails] = useState({
productCode: "",
interestCategory: "",
segmentCode: "",
accountHolderType: "",
primaryCifNumber: "",
nomineeCifNumber: "",
activityCode: "",
customerType: "",
collateralFDAccount: "",
rentPayAccount: "",
productCodeValid: true,
interestCategoryValid: true,
segmentCodeValid: true,
accountHolderTypeValid: true,
primaryCifNumberValid: true,
nomineeCifNumberValid: true,
activityCodeValid: true,
customerTypeValid: true,
collateralFDAccountValid: true,
rentPayAccountValid: true,
});
const handleProductSelect = (product) => {
const newAccountDetails = { ...accountDetails };
newAccountDetails.productCode = product.productCode;
newAccountDetails.interestCategory = product.interestCategory;
newAccountDetails.productCodeValid = true;
newAccountDetails.interestCategoryValid = true;
setAccountDetails(newAccountDetails);
setShowProductModal(false);
};
const accountDetailsFields = [
{
label: t("productCode"),
name: "productCode",
type: "input",
subType: "number",
readOnly: true,
validate: (value) => value !== "",
icon: {
icon: <PackageSearch size={18} />,
onClick: () => setShowProductModal(true),
},
},
{
label: t("interestCategory"),
name: "interestCategory",
type: "input",
subType: "number",
readOnly: true,
validate: (value) => value !== "",
},
{
label: t("segmentCode"),
name: "segmentCode",
type: "select",
options: [
{ value: "0706", label: "0706: Individual" },
{ value: "0306", label: "0306: Staff" },
{ value: "5003", label: "5003: Senior Citizen" },
{ value: "5010", label: "5010: SHG" },
{ value: "5000", label: "5000: Bank" },
{ value: "5009", label: "5009: Institutions" },
{ value: "5050", label: "5050: Others" },
{ value: "5007", label: "5007: Society" },
],
validate: (value) => value !== "",
},
{
label: t("accountHolderType"),
name: "accountHolderType",
type: "select",
options: [
{ value: "1", label: "Single" },
{ value: "2", label: "Joint" },
],
validate: (value) => value === "1" || value === "2",
},
{
label: t("primaryCifNumber"),
name: "primaryCifNumber",
type: "input",
subType: "number",
maxLength: 17,
validate: (value) => /^[0-9]{17}$/.test(value),
icon: { icon: <UserSearch size={18} /> },
},
{
label: t("nomineeCifNumber"),
name: "nomineeCifNumber",
type: "input",
subType: "number",
maxLength: 17,
validate: (value) => /^[0-9]{17}$/.test(value),
},
];
const additionalDetailsFields = [
{
label: t("activityCode"),
name: "activityCode",
type: "select",
options: [
{ value: "0701", label: "Direct Agriculture" },
{ value: "0702", label: "Indirect Agriculture" },
{ value: "0703", label: "Agricultural Services Unit" },
{ value: "0704", label: "Farm Irrigation" },
{ value: "0705", label: "Fruits & Vegetables" },
{ value: "0706", label: "Non-Agriculture" },
],
validate: (value) => value !== "",
},
{
label: t("customerType"),
name: "customerType",
type: "select",
options: [
{ value: "0709", label: "Individual" },
{ value: "0701", label: "Corporate" },
],
validate: (value) => value === "0709" || value === "0701",
},
{
label: t("collateralFDAccount"),
name: "collateralFDAccount",
type: "input",
subType: "number",
maxLength: 17,
validate: (value) => /^[0-9]{17}$/.test(value),
},
{
label: t("rentPayAccount"),
name: "rentPayAccount",
type: "input",
subType: "number",
maxLength: 17,
validate: (value) => /^[0-9]{17}$/.test(value),
},
];
const handleSubmit = (e) => {
e.preventDefault();
let isValid = true;
const newValidationState = { ...accountDetails };
// Validate account details fields
[...accountDetailsFields, ...additionalDetailsFields].forEach((field) => {
if (field.validate) {
const fieldIsValid = field.validate(accountDetails[field.name]);
newValidationState[`${field.name}Valid`] = fieldIsValid;
if (!fieldIsValid) isValid = false;
}
});
setAccountDetails(newValidationState);
if (!isValid) {
return;
}
console.log("Form is valid", accountDetails);
};
const renderField = (field) => {
const commonProps = {
value: accountDetails[field.name],
onChange: (e) => {
const newAccountDetails = { ...accountDetails };
newAccountDetails[field.name] = e.target.value;
newAccountDetails[`${field.name}Valid`] = true;
setAccountDetails(newAccountDetails);
},
maxLength: field.maxLength,
type: field.subType,
readOnly: field.readOnly,
};
const valid = accountDetails[`${field.name}Valid`];
return (
<FormField key={field.name} label={field.label} icon={field.icon}>
{field.type === "input" ? (
<FormInput props={commonProps} valid={valid} />
) : (
<FormSelect
props={commonProps}
valid={valid}
options={field.options}
/>
)}
</FormField>
);
};
return (
<div>
<AnimatePresence>
{notification.message !== "" && <Notification {...notification} />}
</AnimatePresence>
<AnimatePresence>
{showProductModal && (
<ProductModal
productInfo={productInfo}
handleProductSelect={handleProductSelect}
/>
)}
</AnimatePresence>
<FormBox title="Account Creation">
<FieldsWrapper>
<FormHeader text={"Account Details"} />
{accountDetailsFields.map(renderField)}
<FormHeader text={"Additional Details"} />
{additionalDetailsFields.map(renderField)}
</FieldsWrapper>
<Button text={t("submit")} onClick={handleSubmit} />
</FormBox>
</div>
);
}
export default AccountCreation;

View File

@ -0,0 +1,113 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import { useNavigate } from "react-router-dom";
import FieldsWrapper from "../components/FieldsWrapper";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
function CabinetCreation() {
const { t } = useTranslation();
const navigate = useNavigate();
const [cabinetDetails, setCabinetDetails] = useState({
cabinetId: "",
cabinetIdValid: true,
cabinetKeyId: "",
cabinetKeyIdValid: true,
noOfLockers: 0,
noOfLockersValid: true,
});
const handleNext = (e) => {
e.preventDefault();
let isFormValid = true;
const newValues = {...cabinetDetails};
formFields.forEach(field => {
if(field.validate) {
const isFieldValid = field.validate(cabinetDetails[field.name])
newValues[`${field.name}Valid`] = isFieldValid;
if(!isFieldValid) isFormValid = false;
}
});
if(!isFormValid) {
setCabinetDetails(newValues);
return;
}
navigate("register-lockers", {
state: {
cabinetId: cabinetDetails.cabinetId,
cabinetKeyId: cabinetDetails.cabinetKeyId,
noOfLockers: parseInt(cabinetDetails.noOfLockers),
},
});
};
const formFields = [
{
name: "cabinetId",
label: t("cabinetId"),
type: "input",
maxLength: 6,
validate: v => /^\w{2}\d{4}$/.test(v)
},
{
name: "cabinetKeyId",
label: t("cabinetKeyId"),
type: "input",
maxLength: 6,
validate: v => /^\w{2}\d{4}$/.test(v)
},
{
name: "noOfLockers",
label: t("noOfLockers"),
type: "input",
subType: "number",
validate: v => parseInt(v) > 0
},
];
const renderField = (field) => {
const commonProps = {
value: cabinetDetails[field.name],
onChange: (e) => {
const newCabinetDetails = { ...cabinetDetails };
newCabinetDetails[field.name] = e.target.value.toUpperCase();
newCabinetDetails[`${field.name}Valid`] = true;
setCabinetDetails(newCabinetDetails);
},
maxLength: field.maxLength,
type: field.subType,
};
const valid = cabinetDetails[`${field.name}Valid`];
return (
<FormField key={field.name} label={field.label}>
{field.type === "input" ? (
<FormInput props={commonProps} valid={valid} />
) : (
<FormSelect
props={commonProps}
valid={valid}
options={field.options}
/>
)}
</FormField>
);
};
return (
<FormBox title={t("cabinetCreation")}>
<FieldsWrapper>
{formFields.map(renderField)}
</FieldsWrapper>
<Button text={t('next')} onClick={handleNext} />
</FormBox>
);
}
export default CabinetCreation;

View File

@ -0,0 +1,65 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import FieldsWrapper from "../components/FieldsWrapper";
import FormField from "../components/FormField";
function CabinetMaintenace() {
const navigate = useNavigate();
const { t } = useTranslation();
const [operation, setOperation] = useState({ value: "", valid: true });
const handleNext = (e) => {
e.preventDefault();
if (operation.value === "") {
setOperation({ value: operation.value, valid: false });
}
navigate(operation.value);
};
const formFields = [
{
name: "value",
label: t("operation"),
options: [{ label: t("create"), value: "create" }],
type: "select",
},
];
const renderField = (field) => {
const commonProps = {
value: operation[field.name],
onChange: (e) => {
const newValues = { ...operation };
newValues[field.name] = e.target.value;
newValues[`${field.name}Valid`] = true;
setOperation(newValues);
},
};
const options = field.options;
return (
<FormField label={field.label}>
{field.type === "input" ? (
<FormInput props={commonProps} />
) : (
<FormSelect props={commonProps} options={options} />
)}
</FormField>
);
};
return (
<div>
<FormBox title={t("cabinetMaintenance")}>
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
<Button text={t("next")} onClick={(e) => handleNext(e)} />
</FormBox>
</div>
);
}
export default CabinetMaintenace;

184
src/pages/ChargeEdit.jsx Normal file
View File

@ -0,0 +1,184 @@
import { useState, useEffect } from "react";
import { useLoading } from "../hooks/useLoading";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import Button from "../components/Button";
import FormBox from "../components/FormBox";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { lockerService } from "../services/locker.service";
import { AnimatePresence } from "motion/react";
import { Pencil } from "lucide-react";
import Notification from "../components/Notification";
import FieldsWrapper from "../components/FieldsWrapper";
function ChargeEdit() {
const [chargeDetails, setChargeDetails] = useState({
rentAmount: "",
rentAmountEdit: false,
penaltyAmount: "",
penaltyAmountEdit: false,
});
const [notification, setNotification] = useState({ message: "", type: "" });
const { setIsLoading } = useLoading();
const { t } = useTranslation();
const location = useLocation();
const productCode = location.state?.productCode;
const interestCategory = location.state?.interestCategory;
useEffect(() => {
const fetchCharges = async () => {
try {
setIsLoading(true);
const response = await lockerService.getCharges(
productCode,
interestCategory
);
if (response.status === 200) {
const { rent, penalty } = response.data;
setChargeDetails({
rentAmount: rent.toString(),
rentAmountEdit: false,
penaltyAmount: penalty.toString(),
penaltyAmountEdit: false,
});
} else {
setNotification({
message: response.data.message,
type: "error",
});
}
} catch (error) {
console.error(error);
setNotification({
message: error.message,
type: "error",
});
} finally {
setIsLoading(false);
}
};
fetchCharges();
}, [productCode, interestCategory, setIsLoading]);
if (!location.state) {
return <></>;
}
const formFields = [
{
name: "rentAmount",
label: "Rent Amount",
type: "input",
subType: "number",
readOnly: !chargeDetails.rentAmountEdit,
icon: {
icon: <Pencil size={22} />,
mode: "plain",
onClick: () => {
setChargeDetails({ ...chargeDetails, rentAmountEdit: true });
},
},
},
{
name: "penaltyAmount",
label: "Penalty Amount",
type: "input",
subType: "number",
readOnly: !chargeDetails.penaltyAmountEdit,
icon: {
icon: <Pencil size={22} />,
mode: "plain",
onClick: () => {
setChargeDetails({ ...chargeDetails, penaltyAmountEdit: true });
},
},
},
];
const handleSubmit = async (e) => {
e.preventDefault();
try {
setIsLoading(true);
const response = await lockerService.updateCharges(
productCode,
interestCategory,
chargeDetails.rentAmount,
chargeDetails.penaltyAmount
);
if (response.status === 200) {
setNotification({
message: response.data.message,
type: "success",
});
} else {
setNotification({
message: response.data.message,
type: "error",
});
}
} catch (error) {
console.error(error);
setNotification({
message: error.message,
type: "error",
});
} finally {
setIsLoading(false);
}
};
const renderField = (field) => {
const commonProps = {
value: chargeDetails[field.name],
onChange: (e) => {
setChargeDetails({
...chargeDetails,
[field.name]: e.target.value,
});
},
readOnly: field.readOnly,
type: field.subType,
};
const className = field.readOnly ? "bg-grey/[0.3]" : "";
return (
<FormField key={field.name} label={field.label} icon={field.icon}>
{field.type === "input" ? (
<FormInput props={commonProps} className={className} />
) : (
<FormSelect
props={commonProps}
options={field.options}
className={className}
/>
)}
</FormField>
);
};
return (
<div>
<AnimatePresence>
{notification.message !== "" && <Notification {...notification} />}
</AnimatePresence>
<FormBox title={t("chargeEdit")}>
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
<Button
text={t("submit")}
onClick={handleSubmit}
disabled={
!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit
}
/>
</FormBox>
</div>
);
}
export default ChargeEdit;

View File

@ -0,0 +1,114 @@
import { useTranslation } from "react-i18next";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import Button from "../components/Button";
import FormBox from "../components/FormBox";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { useToast } from "../hooks/useToast";
import FieldsWrapper from "../components/FieldsWrapper";
function ChargeManagement() {
const { t } = useTranslation();
const navigate = useNavigate();
const showToast = useToast();
const [productDetails, setProductDetails] = useState({
productCode: "",
interestCategory: "",
productCodeValid: true,
interestCategoryValid: true,
});
const formFields = [
{
name: "productCode",
label: t("productCode"),
type: "input",
subType: "number",
maxLength: 4,
validate: (value) => /^[0-9]{4}/.test(value),
},
{
name: "interestCategory",
label: t("interestCategory"),
type: "input",
subType: "number",
maxLength: 4,
validate: (value) => /^[0-9]{4}/.test(value),
},
];
const handleSubmit = (e) => {
e.preventDefault();
const newProductDetails = { ...productDetails };
let isValid = true;
formFields.forEach((field) => {
if (!field.validate(newProductDetails[field.name])) {
isValid = false;
newProductDetails[`${field.name}Valid`] = false;
}
});
if (!isValid) {
setProductDetails(newProductDetails);
showToast(t("highlightedFieldsInvalid"), "error");
return;
}
navigate("change", {
state: {
productCode: productDetails.productCode,
interestCategory: productDetails.interestCategory,
},
});
};
const renderField = (field) => {
const commonProps = {
value: productDetails[field.name],
onChange: (e) => {
const newLockerDetails = { ...productDetails };
if (field.subType === "number") {
e.target.value = e.target.value.replace(/\D/g, "");
if (e.target.value.length > field.maxLength) {
return;
}
}
newLockerDetails[field.name] = e.target.value;
newLockerDetails[`${field.name}Valid`] = true;
setProductDetails(newLockerDetails);
},
maxLength: field.maxLength,
type: field.subType,
readOnly: field.readOnly,
};
const valid = productDetails[`${field.name}Valid`];
return (
<FormField key={field.name} label={field.label} icon={field.icon}>
{field.type === "input" ? (
<FormInput
props={commonProps}
valid={valid}
/>
) : (
<FormSelect props={commonProps} valid={valid} options={field.options} />
)}
</FormField>
);
};
return (
<FormBox title={t("chargeManagement")}>
<FieldsWrapper>
{formFields.map(renderField)}
</FieldsWrapper>
<Button text={t("submit")} onClick={handleSubmit} />
</FormBox>
);
}
export default ChargeManagement;

View File

@ -0,0 +1,90 @@
import { useState } from "react";
import { useLocation } from "react-router-dom";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import Notification from "../components/Notification";
import { lockerService } from "../services/locker.service";
import { useToast } from "../hooks/useToast";
import { useLoading } from "../hooks/useLoading";
function CheckInOutLog() {
const [time, setTime] = useState(null);
const [checkType, setCheckType] = useState("");
const [notification, setNotification] = useState({ message: "", type: "" });
const location = useLocation();
const showToast = useToast();
const { setIsLoading } = useLoading();
const accountNumber = location.state?.accountNumber;
const handleSubmit = async (e) => {
e.preventDefault();
if (time === null || checkType === "") {
showToast("Please fill in all fields", "error");
return;
}
// Add your logic here
try {
setIsLoading(true);
const response = await lockerService.checkInOut(
accountNumber,
time,
checkType
);
if (response.status === 200) {
setNotification({ message: response.data.message, type: "success" });
} else {
console.log(response);
setNotification({ message: response.data.message, type: "error" });
}
} catch (error) {
console.log(error);
setNotification({ message: error.message, type: "error" });
} finally {
setIsLoading(false);
}
};
return (
<div>
{notification.message !== "" && <Notification {...notification} />}
<FormBox title="Check In/Out Log">
<div className="px-4 pt-7 text-2xl font-display font-bold text-primary dark:text-primary-dark">
{accountNumber}
</div>
<div className="p-2 pt-7 flex flex-col gap-4">
<div className="flex">
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
Time
</label>
<input
type="time"
className="w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey text-black"
onChange={(e) => setTime(e.target.value)}
value={time}
/>
</div>
<div className="flex">
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
Check Type
</label>
<select
className="w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey"
onChange={(e) => setCheckType(e.target.value)}
value={checkType}
>
<option value="" disabled>
Select
</option>
<option value="check-in">Check In</option>
<option value="check-out">Check Out</option>
</select>
</div>
</div>
<Button text="Submit" onClick={handleSubmit} />
</FormBox>
</div>
);
}
export default CheckInOutLog;

View File

@ -0,0 +1,86 @@
import FormBox from "../components/FormBox";
import { useState } from "react";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import { Search } from "lucide-react";
import Button from "../components/Button";
import { AnimatePresence } from "motion/react";
import Notification from "../components/Notification";
import { useToast } from "../hooks/useToast";
import { lockerService } from "../services/locker.service";
import { useLoading } from "../hooks/useLoading";
import { useNavigate } from "react-router-dom";
import FieldsWrapper from "../components/FieldsWrapper";
function CheckInOutManagement() {
const [accountNumber, setAccountNumber] = useState("");
const [notification, setNotification] = useState({ message: "", type: "" });
const showToast = useToast();
const { setIsLoading } = useLoading();
const navigate = useNavigate();
const handleNext = async (e) => {
e.preventDefault();
if (accountNumber === "") {
showToast("Account Number is required", "error");
return;
}
try {
setIsLoading(true);
const response = await lockerService.preCheckIn(accountNumber);
console.log(response.data);
if (response.status === 200) {
const data = response.data;
if (data.code === 1) {
navigate("log", { state: { accountNumber } });
} else if (data.code === 2) {
setNotification({
visible: true,
message:
"Monthly access limit exceeded. A fine will be charged for each additional access.",
type: "warning",
});
} else if (data.code === 3) {
setNotification({
visible: true,
message:
"Rent for this account is due. Please pay the rent amount in full to access the locker.",
type: "error",
});
}
}
} catch (error) {
console.log(error);
setNotification(error.message, "error");
} finally {
setIsLoading(false);
}
};
return (
<div>
<AnimatePresence>
{ notification.message !== "" && <Notification {...notification} /> }
</AnimatePresence>
<FormBox title="Check In/Out">
<FieldsWrapper>
<FormField
label="Account Number"
icon={{ icon: <Search size={17} />, onClick: () => {} }}
>
<FormInput
props={{
type: "text",
value: accountNumber,
onChange: (e) => setAccountNumber(e.target.value),
}}
/>
</FormField>
</FieldsWrapper>
<Button text="Next" onClick={handleNext} />
</FormBox>
</div>
);
}
export default CheckInOutManagement;

177
src/pages/KeySwap.jsx Normal file
View File

@ -0,0 +1,177 @@
import { useState } from "react";
import { AnimatePresence } from "motion/react";
import { useTranslation } from "react-i18next";
import { useToast } from "../hooks/useToast";
import { useLoading } from "../hooks/useLoading";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import Button from "../components/Button";
import { lockerService } from "../services/locker.service";
import FormBox from "../components/FormBox";
import Notification from "../components/Notification";
import FieldsWrapper from "../components/FieldsWrapper";
function KeySwap() {
const { t } = useTranslation();
const showToast = useToast();
const { isLoading, setIsLoading } = useLoading();
const [notification, setNotification] = useState({ message: "", type: "" });
const [keySwapDetails, setKeySwapDetails] = useState({
cabinetId: "",
lockerId: "",
reason: "",
oldKey: "",
newKey: "",
newKeyConfirm: "",
cabinetIdValid: true,
lockerIdValid: true,
reasonValid: true,
oldKeyValid: true,
newKeyValid: true,
newKeyConfirmValid: true,
});
const formFields = [
{
name: "cabinetId",
label: t("cabinetId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
},
{
name: "lockerId",
label: t("lockerId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
},
{
name: "reason",
label: t("reasonForChange"),
type: "input",
maxLength: 50,
readOnly: isLoading,
validate: (value) => value !== "",
},
{
name: "oldKey",
label: t("oldKeyId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
},
{
name: "newKey",
label: t("newKeyId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
},
{
name: "newKeyConfirm",
label: t("confirmNewKeyId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => value !== "" && value === keySwapDetails.newKey,
},
];
const handleKeySwap = async (e) => {
e.preventDefault();
let valid = true;
for (const field of formFields) {
if (!field.validate(keySwapDetails[field.name])) {
valid = false;
setKeySwapDetails((prev) => ({
...prev,
[`${field.name}Valid`]: false,
}));
}
}
if (!valid) {
showToast(t("highlightedFieldsInvalid"), "error");
return;
}
setIsLoading(true);
try {
const response = await lockerService.keySwap(
keySwapDetails.cabinetId,
keySwapDetails.lockerId,
keySwapDetails.reason,
keySwapDetails.oldKey,
keySwapDetails.newKey
);
if (response.status === 200) {
setNotification({
message: response.data.message,
type: "success",
});
} else {
setNotification({
message: response.data.message,
type: "error",
});
}
} catch (error) {
setNotification({
message: error.message,
type: "error",
});
} finally {
setIsLoading(false);
}
};
const renderField = (field) => {
const commonProps = {
value: keySwapDetails[field.name],
onChange: (e) => {
const newLockerDetails = { ...keySwapDetails };
newLockerDetails[field.name] = e.target.value.toUpperCase();
newLockerDetails[`${field.name}Valid`] = true;
setKeySwapDetails(newLockerDetails);
},
maxLength: field.maxLength,
type: field.subType,
readOnly: field.readOnly,
};
const valid = keySwapDetails[`${field.name}Valid`];
return (
<FormField key={field.name} label={field.label} icon={field.icon}>
{field.type === "input" ? (
<FormInput props={commonProps} valid={valid} />
) : (
<FormSelect
props={commonProps}
valid={valid}
options={field.options}
/>
)}
</FormField>
);
};
return (
<div>
<AnimatePresence>
{notification.message !== "" && <Notification {...notification} />}
</AnimatePresence>
<FormBox title={t("lockerStatus")}>
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
<Button text={t("submit")} onClick={handleKeySwap} />
</FormBox>
</div>
);
}
export default KeySwap;

View File

@ -0,0 +1,68 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import FieldsWrapper from "../components/FieldsWrapper";
function LockerMaintenance() {
const navigate = useNavigate();
const { t } = useTranslation();
const [operation, setOperation] = useState({ value: "", valid: true });
const handleNext = (e) => {
e.preventDefault();
if (operation.value === "") {
setOperation({ value: operation.value, valid: false });
}
navigate(operation.value);
};
const formFields = [
{
name: "value",
label: t("operation"),
options: [
{ label: t("status"), value: "status" },
{ label: t("keySwap"), value: "key-swap" },
],
type: "select",
},
];
const renderField = (field) => {
const commonProps = {
value: operation[field.name],
onChange: (e) => {
const newValues = { ...operation };
newValues[field.name] = e.target.value;
newValues[`${field.name}Valid`] = true;
setOperation(newValues);
},
};
const options = field.options;
return (
<FormField label={field.label}>
{field.type === "input" ? (
<FormInput props={commonProps} />
) : (
<FormSelect props={commonProps} options={options} />
)}
</FormField>
);
};
return (
<div>
<FormBox title={t("lockerMaintenance")}>
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
<Button text={t("next")} onClick={(e) => handleNext(e)} />
</FormBox>
</div>
);
}
export default LockerMaintenance;

137
src/pages/LockerStatus.jsx Normal file
View File

@ -0,0 +1,137 @@
import FormBox from "../components/FormBox";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import Button from "../components/Button";
import { lockerService } from "../services/locker.service";
import { useLoading } from "../hooks/useLoading";
import { AnimatePresence } from "motion/react";
import Notification from "../components/Notification";
import FieldsWrapper from "../components/FieldsWrapper";
function LockerStatus() {
const { t } = useTranslation();
const [lockerDetails, setLockerDetails] = useState({
cabinetId: "",
lockerId: "",
status: "",
cabinetIdValid: true,
lockerIdValid: true,
statusValid: true,
});
const { isLoading, setIsLoading } = useLoading();
const [notification, setNotification] = useState({ message: "", type: "" });
const formFields = [
{
name: "cabinetId",
label: t("cabinetId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
},
{
name: "lockerId",
label: t("lockerId"),
type: "input",
maxLength: 6,
readOnly: isLoading,
validate: (value) => /^[A-Z]{2}[0-9]{4}/.test(value),
},
{
name: "status",
label: t("status"),
type: "select",
readOnly: isLoading,
options: [
{ value: "open", label: t("open") },
{ value: "close", label: t("close") },
],
validate: (value) => value !== "",
},
];
const handleSubmit = async (e) => {
e.preventDefault();
let isValid = true;
const newValidationState = { ...lockerDetails };
// Validate account details fields
formFields.forEach((field) => {
if (field.validate) {
const fieldIsValid = field.validate(lockerDetails[field.name]);
newValidationState[`${field.name}Valid`] = fieldIsValid;
if (!fieldIsValid) isValid = false;
}
});
setLockerDetails(newValidationState);
if (!isValid) {
return;
}
try {
setIsLoading(true);
const response = await lockerService.changeLockerStatus(
lockerDetails.cabinetId,
lockerDetails.lockerId,
lockerDetails.status
);
setNotification({
message: response.data.message,
type: "success",
});
} catch (error) {
setNotification({
message: error.message,
type: "error",
});
} finally {
setIsLoading(false);
}
};
const renderField = (field) => {
const commonProps = {
value: lockerDetails[field.name],
onChange: (e) => {
const newLockerDetails = { ...lockerDetails };
newLockerDetails[field.name] = e.target.value.toUpperCase();
newLockerDetails[`${field.name}Valid`] = true;
setLockerDetails(newLockerDetails);
},
maxLength: field.maxLength,
type: field.subType,
readOnly: field.readOnly,
};
const valid = lockerDetails[`${field.name}Valid`];
return (
<FormField key={field.name} label={field.label} icon={field.icon}>
{field.type === "input" ? (
<FormInput props={commonProps} valid={valid} />
) : (
<FormSelect props={commonProps} options={field.options} />
)}
</FormField>
);
};
return (
<div>
<AnimatePresence>
{notification.message !== "" && <Notification {...notification} />}
</AnimatePresence>
<FormBox title={t("lockerStatus")}>
<FieldsWrapper>{formFields.map(renderField)}</FieldsWrapper>
<Button text={t("submit")} onClick={handleSubmit} />
</FormBox>
</div>
);
}
export default LockerStatus;

View File

@ -0,0 +1,202 @@
import { useLocation } from "react-router-dom";
import { useState } from "react";
import { useLoading } from "../hooks/useLoading";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import { lockerService } from "../services/locker.service";
import { AnimatePresence } from "motion/react";
import Notification from "../components/Notification";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import FormHeader from "../components/FormHeader";
import FieldsWrapper from "../components/FieldsWrapper";
function LockersRegistration() {
const location = useLocation();
const { setIsLoading } = useLoading();
const [notification, setNotification] = useState({ message: "", type: "" });
const noOfLockers = location.state?.noOfLockers;
const cabinetId = location.state?.cabinetId;
const initLockers = Array(noOfLockers)
.fill()
.map(() => ({
id: "",
size: "",
keyId: "",
idValid: true,
sizeValid: true,
keyIdValid: true,
}));
const [lockerValues, setLockerValues] = useState(initLockers);
if (!location.state) {
return <></>;
}
const handleSubmit = async (e) => {
e.preventDefault();
const idRegex = /^[A-Z]{2}[0-9]{4}$/;
let valid = true;
// Helper function to find duplicates
const findDuplicates = (arr, key) => {
const values = arr.map((item) => item[key]);
return values.filter((item, index) => values.indexOf(item) !== index);
};
// Find duplicates
const duplicateLockerIds = findDuplicates(lockerValues, "id");
const duplicateKeyIds = findDuplicates(lockerValues, "keyId");
const newValues = lockerValues.map((locker) => {
const newLocker = { ...locker };
// Check ID
if (
locker.id === "" ||
!idRegex.test(locker.id) ||
duplicateLockerIds.includes(locker.id)
) {
newLocker.idValid = false;
valid = false;
} else {
newLocker.idValid = true;
}
// Check size
if (locker.size === "") {
newLocker.sizeValid = false;
valid = false;
} else {
newLocker.sizeValid = true;
}
// Check keyId
if (
locker.keyId === "" ||
!idRegex.test(locker.keyId) ||
duplicateKeyIds.includes(locker.keyId)
) {
newLocker.keyIdValid = false;
valid = false;
} else {
newLocker.keyIdValid = true;
}
return newLocker;
});
setLockerValues(newValues);
if (!valid) {
return;
}
try {
setIsLoading(true);
const response = await lockerService.registerLockers(
cabinetId,
lockerValues
);
setNotification({
message: `Cabinet creation successful. Cabinet ID: ${response.data.cabinetId}`,
type: "success",
});
} catch (error) {
console.error(error);
setNotification({
message: `Error registering lockers. ${error.message}`,
type: "error",
});
return;
} finally {
setIsLoading(false);
}
};
const formRow = [
{
label: "Locker ID",
type: "input",
subType: "text",
name: "id",
maxLength: 6,
validate: (value) => /^[A-Z]{2}[0-9]{4}$/.test(value),
},
{
label: "Size",
type: "select",
subType: "text",
name: "size",
options: [
{ label: "Small", value: "1" },
{ label: "Medium", value: "2" },
{ label: "Large", value: "3" },
],
},
{
label: "Key ID",
type: "input",
subType: "text",
name: "keyId",
maxLength: 6,
validate: (value) => /^[A-Z]{2}[0-9]{4}$/.test(value),
},
];
const renderFormRow = (field) => {
const commonProps = {
value: lockerValues[field.name],
onChange: (e) => {
const newLockerValues = [...lockerValues];
newLockerValues[field.name] = e.target.value;
newLockerValues[`${field.name}Valid`] = true;
setLockerValues(newLockerValues);
},
maxLength: field.maxLength,
type: field.subType,
};
const valid = lockerValues[`${field.name}Valid`];
return field.type === "input" ? (
<FormInput key={field.name} props={commonProps} valid={valid} />
) : (
<FormSelect key={field.name} props={commonProps} valid={valid} options={field.options} />
);
};
const formFields = Array(noOfLockers).fill(formRow);
const renderFormFields = (row, index) => {
return (
<FormField key={index} label={`Locker ${index + 1}`} variant="long">
{row.map(renderFormRow)}
</FormField>
);
};
return (
<div>
<AnimatePresence>
{notification.message !== "" && <Notification {...notification} />}
</AnimatePresence>
<div className="relative">
{notification.type === "success" && (
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
)}
<FormBox title="Locker Registration">
<FormHeader text={cabinetId} />
<FieldsWrapper className="">
{formFields.map(renderFormFields)}
</FieldsWrapper>
<Button text="Register" onClick={handleSubmit} />
</FormBox>
</div>
</div>
);
}
export default LockersRegistration;

View File

@ -1,9 +1,9 @@
function Placeholder() {
return (
<div className="text-2xl text-center h-max font-body text-primary dark:text-primary-dark">
<h1>Placeholder</h1>
</div >
<div className="text-center h-full">
<p className="text-3xl text-primary font-bold">Placeholder</p>
</div>
);
}
export default Placeholder;
export default Placeholder;

10
src/services/api.js Normal file
View 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;

View File

@ -0,0 +1,47 @@
import api from "./api";
export const lockerService = {
registerLockers: async (cabinetId, lockers) => {
return api.post("/cabinet", {
cabinetId,
lockers: lockers.map(({ id, size, keyId }) => ({
id,
size,
keyId,
})),
});
},
changeLockerStatus: async (cabinetId, lockerId, status) => {
return api.patch(`/locker/status`, { cabinetId, lockerId, status });
},
keySwap: async (cabinetId, lockerId, reason, oldKey, newKey) => {
return api.patch(`/locker/key`, {
cabinetId,
lockerId,
reason,
oldKey,
newKey,
});
},
updateCharges: async (productCode, interestCategory, rent, penalty) => {
return api.patch(`/charge/${productCode}${interestCategory}`, {
rent,
penalty,
});
},
getCharges: async (productCode, interestCategory) => {
return api.get(`/charge/${productCode}${interestCategory}`);
},
preCheckIn: async (accountNumber) => {
return api.post(`/pre-checkin/${accountNumber}`);
},
checkInOut: async (accountNumber, time, checkType) => {
return api.post(`/check-in-out/${accountNumber}`, { time, checkType });
},
};

8
src/util/productList.js Normal file
View File

@ -0,0 +1,8 @@
const productInfo = [
{ productCode: '3001', productCodeDescription: 'RECURRING DEPOSIT', interestCategory: '1004', interestCategoryDescription: 'NON MEMBER - SENIOR CITIZEN', payableGl: '16010', paidGl: '62110' },
{ productCode: '1101', productCodeDescription: 'CURRENT ACCOUNT', interestCategory: '1009', interestCategoryDescription: 'GENERAL', payableGl: '16018', paidGl: '62115' },
{ productCode: '1101', productCodeDescription: 'SAVINGS DEPOSIT', interestCategory: '1007', interestCategoryDescription: 'NON MEMBER', payableGl: '16301', paidGl: '62117' },
{ productCode: '2002', productCodeDescription: 'CASH CERTIFICATE -GENERAL', interestCategory: '1047', interestCategoryDescription: 'COMPOUNDING', payableGl: '16011', paidGl: '62111' },
{ productCode: '2002', productCodeDescription: 'CASH CERTIFICATE', interestCategory: '1005', interestCategoryDescription: 'NON MEMBER - SENIOR CITIZEN', payableGl: '16011', paidGl: '62111' },
]
export default productInfo;

View File

@ -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 };

View File

@ -9,6 +9,26 @@ export default {
colors: {
white: '#FFFFFF',
black: '#000000',
grey: '#979797',
error: {
DEFAULT: '#A14444',
dark: '#E5254B',
surface: {DEFAULT: '#D26464', dark: '#FCE9ED'}
},
onToast: {
DEFAULT: '#646564',
dark: '#646564',
},
warning: {
DEFAULT: '#A5A513',
dark: '#EA7000',
surface: {DEFAULT: '#C8C820', dark: '#FDF1E5'}
},
success: {
DEFAULT: '#038100',
dark: '#038100',
surface: {DEFAULT: '#E6F2E5', dark: '#E6F2E5'}
},
transparent: 'transparent',
primary: {
DEFAULT: '#008C46',
@ -41,6 +61,22 @@ export default {
fontSize: {
title: '40px',
},
keyframes: {
slideIn: {
'0%': { transform: 'translateX(100%)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
loading: {
'0%': { left: '-10%' },
'30%': { width: '30%' },
'80%': { width: '10%' },
'100%': { left: '100%' },
}
},
},
},
plugins: [