Add locker service methods for status change and key swap; create LockerMaintenance and LockerStatus components with form validation and translations

This commit is contained in:
Md Asif 2024-12-23 00:10:59 +05:30
parent 8cee8e0383
commit 9d72dc6868
8 changed files with 496 additions and 9 deletions

View File

@ -66,5 +66,17 @@
"customerType": "Customer Type",
"collateralFDAccount": "Collateral FD Account",
"rentPayAccount": "Rent Pay Account",
"submit": "Submit"
"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"
}

View File

@ -26,11 +26,10 @@ function Header() {
submenu: [
{ name: "accountCreation", path: "operation/account" },
{ name: "cabinetMaintenance", path: "operation/cabinet" },
{ 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: "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

@ -9,6 +9,9 @@ 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";
const router = createBrowserRouter([
{
@ -34,6 +37,18 @@ const router = createBrowserRouter([
{
path: "operation/account",
element: <AccountCreation />,
},
{
path: "operation/locker",
element: <LockerMaintenance />,
},
{
path: "operation/locker/status",
element: <LockerStatus />,
},
{
path: "operation/locker/key-swap",
element: <KeySwap />,
}
],
},

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

@ -0,0 +1,219 @@
import { useState } from "react";
import { motion, 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 clsx from "clsx";
import FormBox from "../components/FormBox";
import { Copy } from "lucide-react";
function KeySwap() {
const { t } = useTranslation();
const showToast = useToast();
const { isLoading, setIsLoading } = useLoading();
const [notification, setNotification] = useState({
visible: false,
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({
visible: true,
message: response.data.message,
type: "success",
});
} else {
setNotification({
visible: true,
message: response.data.message,
type: "error",
});
}
} catch (error) {
setNotification({
visible: true,
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,
className: clsx(!keySwapDetails[`${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>
<div className="relative">
{notification.type === "success" && (
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
)}
<FormBox title={t("lockerStatus")}>
<div className="p-2 pt-7 flex flex-col gap-4">
{formFields.map(renderField)}
</div>
<Button text={t("submit")} onClick={handleKeySwap} />
</FormBox>
</div>
</div>
)
}
export default KeySwap;

View File

@ -0,0 +1,70 @@
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 { motion, AnimatePresence } from "motion/react";
import clsx from "clsx";
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);
};
return (
<div>
<FormBox title={t("cabinetMaintenance")}>
<div className="p-2 pt-7">
<div className="flex">
<label className="mr-4 text-lg text-black dark:text-primary-dark w-[10%]">
{t("operation")}
</label>
<div className="w-full">
<select
className={clsx(
"w-1/5 h-10 px-2 rounded-full dark:bg-grey dark:text-primary-dark border-2 focus:outline-grey border-grey",
!operation.valid && "border-error"
)}
onChange={(e) =>
setOperation({ value: e.target.value, valid: true })
}
defaultValue={operation.value}
value={operation.value}
>
<option value="" disabled>
{t("select")}
</option>
<option value="status">{t("changeStatus")}</option>
<option value="key-swap">{t("keySwap")}</option>
</select>
<AnimatePresence initial={false}>
{!operation.valid && (
<motion.div
className="w-1/5 text-sm text-error ml-3 pt-1"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
key="cabinetIdError"
>
Invalid Operation
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
<Button text={t("next")} onClick={(e) => handleNext(e)} />
</FormBox>
</div>
);
}
export default LockerMaintenance;

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

@ -0,0 +1,164 @@
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 { useToast } from "../hooks/useToast";
import clsx from "clsx";
import Button from "../components/Button";
import { lockerService } from "../services/locker.service";
import { useLoading } from "../hooks/useLoading";
import { AnimatePresence, motion } from "motion/react";
function LockerStatus() {
const { t } = useTranslation();
const showToast = useToast();
const [lockerDetails, setLockerDetails] = useState({
cabinetId: "",
lockerId: "",
status: "",
cabinetIdValid: true,
lockerIdValid: true,
statusValid: true,
});
const { isLoading, setIsLoading } = useLoading();
const [notification, setNotification] = useState({
visible: false,
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) {
showToast("Highlighted fields are invalid", "error");
return;
}
try {
setIsLoading(true);
const response = await lockerService.changeLockerStatus(
lockerDetails.cabinetId,
lockerDetails.lockerId,
lockerDetails.status
);
setNotification({
visible: true,
message: response.data.message,
type: "success",
});
} catch (error) {
showToast(error.response.data.message, "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,
className: clsx(!lockerDetails[`${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}
</motion.div>
)}
</AnimatePresence>
<div className="relative">
{notification.type === "success" && (
<div className="absolute inset-0 bg-[#fff]/50 z-10 rounded-3xl" />
)}
<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>
</div>
</div>
);
}
export default LockerStatus;

View File

@ -4,7 +4,7 @@ import clsx from "clsx";
import FormBox from "../components/FormBox";
import Button from "../components/Button";
import { useToast } from "../hooks/useToast";
import { lockersService } from "../services/locker.service";
import { lockerService } from "../services/locker.service";
import { Copy } from "lucide-react";
import { AnimatePresence } from "motion/react";
import { motion } from "motion/react";
@ -103,7 +103,7 @@ function LockersRegistration() {
try {
setSubmitting(true);
setIsLoading(true);
const response = await lockersService.registerLockers(
const response = await lockerService.registerLockers(
cabinetId,
lockerValues
);

View File

@ -1,6 +1,6 @@
import api from './api';
export const lockersService = {
export const lockerService = {
registerLockers: async (cabinetId, lockers) => {
return api.post('/cabinet', {
cabinetId,
@ -11,4 +11,12 @@ export const lockersService = {
}))
});
},
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 });
}
};