Add ChargeManagement and ChargeEdit components; implement charge management functionality with form validation and routing updates

This commit is contained in:
Md Asif 2024-12-23 01:41:29 +05:30
parent 9d72dc6868
commit 44112f91bd
7 changed files with 348 additions and 12 deletions

View File

@ -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 (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-12 py-2 text-lg text-white dark:text-primary-dark rounded-full bg-primary dark:bg-secondary-dark"
whileHover={!disabled && { scale: 1.05 }}
whileTap={!disabled && { scale: 0.95 }}
className={clsx("px-12 py-2 text-lg text-white dark:text-primary-dark rounded-full bg-primary dark:bg-secondary-dark", disabled && "bg-[#ccc] dark:bg-[#ccc]")}
onClick={onClick}
disabled={disabled}
>
{text}
</motion.button>
@ -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;

View File

@ -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 }) {
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="bg-primary rounded-full p-2 text-white cursor-pointer"
className={clsx(icon.mode === "plain" ? "text-[#444]" : "bg-primary rounded-full p-2 text-white cursor-pointer")}
onClick={icon.onClick}
>
{icon}
{icon.icon}
</motion.div>
)}
</div>
@ -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;

View File

@ -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: <KeySwap />,
},
{
path: "operation/charge-management",
element: <ChargeManagement />,
},
{
path: "operation/charge-management/change",
element: <ChargeEdit />,
}
],
},

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

@ -0,0 +1,198 @@
import { useState, useEffect } from "react";
import { useLoading } from "../hooks/useLoading";
import FormField from "../components/FormField";
import FormInput from "../components/FormInput";
import FormSelect from "../components/FormSelect";
import Button from "../components/Button";
import FormBox from "../components/FormBox";
import { useTranslation } from "react-i18next";
import { useToast } from "../hooks/useToast";
import { useLocation } from "react-router-dom";
import { lockerService } from "../services/locker.service";
import clsx from "clsx";
import { AnimatePresence, motion } from "motion/react";
import { Pencil } from "lucide-react";
function ChargeEdit() {
const [chargeDetails, setChargeDetails] = useState({
rentAmount: "",
rentAmountEdit: false,
penaltyAmount: "",
penaltyAmountEdit: false,
});
const [notification, setNotification] = useState({
visible: false,
message: "",
type: "",
});
const { setIsLoading } = useLoading();
const { t } = useTranslation();
const showToast = useToast();
const location = useLocation();
const { productCode, interestCategory } = location.state;
useEffect(() => {
const fetchCharges = async () => {
try {
setIsLoading(true);
const response = await lockerService.getCharges(productCode, interestCategory);
if (response.status === 200) {
const { rent, penalty } = response.data;
setChargeDetails({
rentAmount: rent,
rentAmountEdit: false,
penaltyAmount: penalty,
penaltyAmountEdit: false,
});
} else {
setNotification({
visible: true,
message: response.data.message,
type: "error",
});
}
} catch (error) {
console.error(error);
setNotification({
visible: true,
message: error.message,
type: "error",
});
} finally {
setIsLoading(false);
}
};
fetchCharges();
}, [productCode, interestCategory, setIsLoading]);
const formFields = [
{
name: "rentAmount",
label: "Rent Amount",
type: "input",
subType: "number",
readOnly: !chargeDetails.rentAmountEdit,
icon: {
icon: <Pencil size={22}/>,
mode: "plain",
onClick: () => {setChargeDetails({...chargeDetails, rentAmountEdit: true})},
}
},
{
name: "penaltyAmount",
label: "Penalty Amount",
type: "input",
subType: "number",
readOnly: !chargeDetails.penaltyAmountEdit,
icon: {
icon: <Pencil size={22}/>,
mode: "plain",
onClick: () => {setChargeDetails({...chargeDetails, penaltyAmountEdit: true})},
}
},
];
const handleSubmit = async (e) => {
e.preventDefault();
if(!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit) {
showToast("No changes made", "warning");
return;
}
try {
setIsLoading(true);
const response = await lockerService.updateCharges(
productCode,
interestCategory,
chargeDetails.rentAmount,
chargeDetails.penaltyAmount
);
if (response.status === 200) {
setNotification({
visible: true,
message: response.data.message,
type: "success",
});
} else {
setNotification({
visible: true,
message: response.data.message,
type: "error",
});
}
} catch (error) {
console.error(error);
setNotification({
visible: true,
message: error.message,
type: "error",
});
} finally {
setIsLoading(false);
}
};
const renderField = (field) => {
const commonProps = {
value: chargeDetails[field.name],
onChange: (e) => {
setChargeDetails({
...chargeDetails,
[field.name]: e.target.value,
});
},
readOnly: field.readOnly,
className: field.readOnly ? "bg-grey/[0.3]" : "",
};
return (
<FormField key={field.name} label={field.label} icon={field.icon}>
{field.type === "input" ? (
<FormInput
{...commonProps}
type={field.subType}
readOnly={field.readOnly}
/>
) : (
<FormSelect {...commonProps} options={field.options} />
)}
</FormField>
);
};
return (
<div>
<AnimatePresence>
{notification.visible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx(
"p-4 mb-8 font-body text-center text-xl rounded-2xl flex items-center justify-center gap-2",
notification.type === "error"
? "bg-error-surface text-error"
: "bg-success-surface text-success"
)}
>
{notification.message}
</motion.div>
)}
</AnimatePresence>
<div className="relative">
{notification.type === "success" && (
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
)}
<FormBox title={t("chargeEdit")}>
<div className="p-2 pt-7 flex flex-col gap-4">
{formFields.map(renderField)}
</div>
<Button text={t("submit")} onClick={handleSubmit} disabled={!chargeDetails.rentAmountEdit && !chargeDetails.penaltyAmountEdit}/>
</FormBox>
</div>
</div>
);
}
export default ChargeEdit;

View File

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

View File

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

View File

@ -18,5 +18,13 @@ export const lockerService = {
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}`);
}
};