Add AccountCreation component and product list functionality; update routing and translations
This commit is contained in:
parent
a6a67d69d5
commit
8cee8e0383
@ -55,5 +55,16 @@
|
||||
"cabinetCreation": "Cabinet Creation",
|
||||
"cabinetId": "Cabinet ID",
|
||||
"cabinetKeyId": "Cabinet Key ID",
|
||||
"noOfLockers": "No of Lockers"
|
||||
"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"
|
||||
}
|
@ -6,7 +6,7 @@ function BannerInfo({info}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
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(
|
||||
<BannerInfoElement
|
||||
|
33
src/components/FormField.jsx
Normal file
33
src/components/FormField.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
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="bg-primary rounded-full p-2 text-white cursor-pointer"
|
||||
>
|
||||
{icon}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormField.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
icon: PropTypes.node,
|
||||
};
|
||||
|
||||
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,7 +24,7 @@ function Header() {
|
||||
{
|
||||
name: "lockerOperation",
|
||||
submenu: [
|
||||
{ name: "accountCreation", path: "account-creation" },
|
||||
{ name: "accountCreation", path: "operation/account" },
|
||||
{ name: "cabinetMaintenance", path: "operation/cabinet" },
|
||||
{ name: "lockerMaintenance", path: "locker-maintenance" },
|
||||
{ name: "rentPenaltyCollection", path: "rent-collection" },
|
||||
|
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;
|
@ -8,6 +8,7 @@ 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";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -30,6 +31,10 @@ const router = createBrowserRouter([
|
||||
path: "operation/cabinet/create/register-lockers",
|
||||
element: <LockersRegistration />,
|
||||
},
|
||||
{
|
||||
path: "operation/account",
|
||||
element: <AccountCreation />,
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
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;
|
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;
|
Loading…
x
Reference in New Issue
Block a user