Add AccountCreation component and product list functionality; update routing and translations

This commit is contained in:
Md Asif 2024-12-22 22:42:48 +05:30
parent a6a67d69d5
commit 8cee8e0383
10 changed files with 517 additions and 3 deletions

View File

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

View File

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

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

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

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

View File

@ -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" },

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

@ -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 />,
}
],
},
]);

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