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:
parent
8cee8e0383
commit
9d72dc6868
@ -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"
|
||||
}
|
@ -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" }] },
|
||||
|
15
src/main.jsx
15
src/main.jsx
@ -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
219
src/pages/KeySwap.jsx
Normal 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;
|
70
src/pages/LockerMaintenance.jsx
Normal file
70
src/pages/LockerMaintenance.jsx
Normal 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
164
src/pages/LockerStatus.jsx
Normal 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;
|
@ -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
|
||||
);
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user