diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index f9cb827..8c9d43e 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -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" } \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 8d954cd..c2cf242 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -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" }] }, diff --git a/src/main.jsx b/src/main.jsx index 3590604..b5138f8 100644 --- a/src/main.jsx +++ b/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: , + }, + { + path: "operation/locker", + element: , + }, + { + path: "operation/locker/status", + element: , + }, + { + path: "operation/locker/key-swap", + element: , } ], }, diff --git a/src/pages/KeySwap.jsx b/src/pages/KeySwap.jsx new file mode 100644 index 0000000..076c641 --- /dev/null +++ b/src/pages/KeySwap.jsx @@ -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 ( + + {field.type === "input" ? ( + + ) : ( + + )} + + ); + }; + + return ( +
+ + {notification.visible && ( + + {notification.message.split(":").map((msg, index) => { + return index === 1 ? ( + + {msg} + + ) : ( + {msg} + ); + })} + + + )} + +
+ {notification.type === "success" && ( +
+ )} + +
+ {formFields.map(renderField)} +
+
+
+ ) +} + +export default KeySwap; diff --git a/src/pages/LockerMaintenance.jsx b/src/pages/LockerMaintenance.jsx new file mode 100644 index 0000000..8cf0eb1 --- /dev/null +++ b/src/pages/LockerMaintenance.jsx @@ -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 ( +
+ +
+
+ +
+ + + {!operation.valid && ( + + Invalid Operation + + )} + +
+
+
+
+ ); +} + +export default LockerMaintenance; diff --git a/src/pages/LockerStatus.jsx b/src/pages/LockerStatus.jsx new file mode 100644 index 0000000..3aaddea --- /dev/null +++ b/src/pages/LockerStatus.jsx @@ -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 ( + + {field.type === "input" ? ( + + ) : ( + + )} + + ); + }; + + return ( +
+ + {notification.visible && ( + + {notification.message} + + )} + +
+ {notification.type === "success" && ( +
+ )} + +
+ {formFields.map(renderField)} +
+
+
+ ); +} + +export default LockerStatus; diff --git a/src/pages/LockersRegistration.jsx b/src/pages/LockersRegistration.jsx index 2ced813..70a64c3 100644 --- a/src/pages/LockersRegistration.jsx +++ b/src/pages/LockersRegistration.jsx @@ -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 ); diff --git a/src/services/locker.service.js b/src/services/locker.service.js index 8e8c0ac..47a0c39 100644 --- a/src/services/locker.service.js +++ b/src/services/locker.service.js @@ -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 }); + } }; \ No newline at end of file