From 44112f91bde4f4e1e23f651a17638fa14561cedd Mon Sep 17 00:00:00 2001 From: Md Asif Date: Mon, 23 Dec 2024 01:41:29 +0530 Subject: [PATCH] Add ChargeManagement and ChargeEdit components; implement charge management functionality with form validation and routing updates --- src/components/Button.jsx | 13 ++- src/components/FormField.jsx | 8 +- src/main.jsx | 11 ++ src/pages/ChargeEdit.jsx | 198 +++++++++++++++++++++++++++++++++ src/pages/ChargeManagement.jsx | 114 +++++++++++++++++++ src/pages/Placeholder.jsx | 8 +- src/services/locker.service.js | 8 ++ 7 files changed, 348 insertions(+), 12 deletions(-) create mode 100644 src/pages/ChargeEdit.jsx create mode 100644 src/pages/ChargeManagement.jsx diff --git a/src/components/Button.jsx b/src/components/Button.jsx index 08eea22..0111857 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -1,13 +1,15 @@ import PropTypes from 'prop-types'; import { motion } from 'motion/react'; +import clsx from 'clsx'; -function Button({text, onClick}) { +function Button({text, onClick, disabled}) { return ( {text} @@ -16,7 +18,8 @@ function Button({text, onClick}) { Button.propTypes = { text: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool, }; export default Button; diff --git a/src/components/FormField.jsx b/src/components/FormField.jsx index a3385bc..6c1ec04 100644 --- a/src/components/FormField.jsx +++ b/src/components/FormField.jsx @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; import { motion } from "motion/react"; +import clsx from "clsx"; function FormField({ label, children, icon }) { return ( @@ -13,9 +14,10 @@ function FormField({ label, children, icon }) { - {icon} + {icon.icon} )} @@ -27,7 +29,7 @@ function FormField({ label, children, icon }) { FormField.propTypes = { label: PropTypes.string.isRequired, children: PropTypes.node.isRequired, - icon: PropTypes.node, + icon: PropTypes.object.isRequired, }; export default FormField; diff --git a/src/main.jsx b/src/main.jsx index b5138f8..b1c86c9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -12,6 +12,9 @@ import AccountCreation from "./pages/AccountCreation.jsx"; import LockerMaintenance from "./pages/LockerMaintenance.jsx"; import LockerStatus from "./pages/LockerStatus.jsx"; import KeySwap from "./pages/KeySwap.jsx"; +import ChargeManagement from "./pages/ChargeManagement.jsx"; +import ChargeEdit from "./pages/ChargeEdit.jsx"; + const router = createBrowserRouter([ { @@ -49,6 +52,14 @@ const router = createBrowserRouter([ { path: "operation/locker/key-swap", element: , + }, + { + path: "operation/charge-management", + element: , + }, + { + path: "operation/charge-management/change", + element: , } ], }, diff --git a/src/pages/ChargeEdit.jsx b/src/pages/ChargeEdit.jsx new file mode 100644 index 0000000..0e1c611 --- /dev/null +++ b/src/pages/ChargeEdit.jsx @@ -0,0 +1,198 @@ +import { useState, useEffect } from "react"; +import { useLoading } from "../hooks/useLoading"; +import FormField from "../components/FormField"; +import FormInput from "../components/FormInput"; +import FormSelect from "../components/FormSelect"; +import Button from "../components/Button"; +import FormBox from "../components/FormBox"; +import { useTranslation } from "react-i18next"; +import { useToast } from "../hooks/useToast"; +import { useLocation } from "react-router-dom"; +import { lockerService } from "../services/locker.service"; +import clsx from "clsx"; +import { AnimatePresence, motion } from "motion/react"; +import { Pencil } from "lucide-react"; + +function ChargeEdit() { + const [chargeDetails, setChargeDetails] = useState({ + rentAmount: "", + rentAmountEdit: false, + penaltyAmount: "", + penaltyAmountEdit: false, + }); + + const [notification, setNotification] = useState({ + visible: false, + message: "", + type: "", + }); + + const { setIsLoading } = useLoading(); + const { t } = useTranslation(); + const showToast = useToast(); + const location = useLocation(); + const { productCode, interestCategory } = location.state; + + useEffect(() => { + const fetchCharges = async () => { + try { + setIsLoading(true); + const response = await lockerService.getCharges(productCode, interestCategory); + if (response.status === 200) { + const { rent, penalty } = response.data; + setChargeDetails({ + rentAmount: rent, + rentAmountEdit: false, + penaltyAmount: penalty, + penaltyAmountEdit: false, + }); + } else { + setNotification({ + visible: true, + message: response.data.message, + type: "error", + }); + } + } catch (error) { + console.error(error); + setNotification({ + visible: true, + message: error.message, + type: "error", + }); + } finally { + setIsLoading(false); + } + }; + fetchCharges(); + }, [productCode, interestCategory, setIsLoading]); + + const formFields = [ + { + name: "rentAmount", + label: "Rent Amount", + type: "input", + subType: "number", + readOnly: !chargeDetails.rentAmountEdit, + icon: { + icon: , + mode: "plain", + onClick: () => {setChargeDetails({...chargeDetails, rentAmountEdit: true})}, + } + }, + { + name: "penaltyAmount", + label: "Penalty Amount", + type: "input", + subType: "number", + readOnly: !chargeDetails.penaltyAmountEdit, + icon: { + icon: , + mode: "plain", + onClick: () => {setChargeDetails({...chargeDetails, penaltyAmountEdit: true})}, + } + }, + ]; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if(!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit) { + showToast("No changes made", "warning"); + return; + } + try { + setIsLoading(true); + const response = await lockerService.updateCharges( + productCode, + interestCategory, + chargeDetails.rentAmount, + chargeDetails.penaltyAmount + ); + if (response.status === 200) { + setNotification({ + visible: true, + message: response.data.message, + type: "success", + }); + } else { + setNotification({ + visible: true, + message: response.data.message, + type: "error", + }); + } + } catch (error) { + console.error(error); + setNotification({ + visible: true, + message: error.message, + type: "error", + }); + } finally { + setIsLoading(false); + } + }; + + const renderField = (field) => { + const commonProps = { + value: chargeDetails[field.name], + onChange: (e) => { + setChargeDetails({ + ...chargeDetails, + [field.name]: e.target.value, + }); + }, + readOnly: field.readOnly, + className: field.readOnly ? "bg-grey/[0.3]" : "", + }; + return ( + + {field.type === "input" ? ( + + ) : ( + + )} + + ); + }; + + return ( +
+ + {notification.visible && ( + + {notification.message} + + )} + +
+ {notification.type === "success" && ( +
+ )} + +
+ {formFields.map(renderField)} +
+
+
+ ); +} + +export default ChargeEdit; diff --git a/src/pages/ChargeManagement.jsx b/src/pages/ChargeManagement.jsx new file mode 100644 index 0000000..e8cd521 --- /dev/null +++ b/src/pages/ChargeManagement.jsx @@ -0,0 +1,114 @@ +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import FormField from "../components/FormField"; +import FormInput from "../components/FormInput"; +import FormSelect from "../components/FormSelect"; +import Button from "../components/Button"; +import FormBox from "../components/FormBox"; +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { useToast } from "../hooks/useToast"; + +function ChargeManagement() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const showToast = useToast(); + + const [productDetails, setProductDetails] = useState({ + productCode: "", + interestCategory: "", + productCodeValid: true, + interestCategoryValid: true, + }); + + const formFields = [ + { + name: "productCode", + label: t("productCode"), + type: "input", + subType: "number", + maxLength: 4, + validate: (value) => /^[0-9]{4}/.test(value), + }, + { + name: "interestCategory", + label: t("interestCategory"), + type: "input", + subType: "number", + maxLength: 4, + validate: (value) => /^[0-9]{4}/.test(value), + }, + ]; + + const handleSubmit = (e) => { + e.preventDefault(); + const newProductDetails = { ...productDetails }; + let isValid = true; + + formFields.forEach((field) => { + if (!field.validate(newProductDetails[field.name])) { + isValid = false; + newProductDetails[`${field.name}Valid`] = false; + } + }); + + if (!isValid) { + setProductDetails(newProductDetails); + showToast(t("highlightedFieldsInvalid"), "error"); + return; + } + + navigate("change", { + state: { + productCode: productDetails.productCode, + interestCategory: productDetails.interestCategory, + }, + }); + }; + + const renderField = (field) => { + const commonProps = { + value: productDetails[field.name], + onChange: (e) => { + const newLockerDetails = { ...productDetails }; + if (field.subType === "number") { + e.target.value = e.target.value.replace(/\D/g, ""); + if (e.target.value.length > field.maxLength) { + return; + } + } + + newLockerDetails[field.name] = e.target.value; + newLockerDetails[`${field.name}Valid`] = true; + setProductDetails(newLockerDetails); + }, + maxLength: field.maxLength, + className: clsx(!productDetails[`${field.name}Valid`] && "border-error"), + }; + + return ( + + {field.type === "input" ? ( + + ) : ( + + )} + + ); + }; + + return ( + +
+ {formFields.map(renderField)} +
+