wip: Add logic for e mandate

This commit is contained in:
2025-09-25 11:36:58 +05:30
parent 960624f934
commit 6beed098c2
3 changed files with 246 additions and 68 deletions

View File

@@ -88,6 +88,7 @@ export default function Login() {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Login-Type': 'emandate',
}, },
body: JSON.stringify({ body: JSON.stringify({
customerNo: CIF, customerNo: CIF,
@@ -103,33 +104,34 @@ export default function Login() {
message: data?.error || "Internal Server Error", message: data?.error || "Internal Server Error",
autoClose: 5000, autoClose: 5000,
}); });
localStorage.removeItem("access_token"); regenerateCaptcha()
localStorage.removeItem("mandate_token");
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
return; return;
} }
setIsLogging(true); setIsLogging(true);
if (response.ok) { if (response.ok) {
console.log(data); console.log(data);
const token = data.token; const token = data.token;
localStorage.setItem("emandate_token", token); localStorage.setItem("mandate_token", token);
// localStorage.setItem("pswExpiryDate", data.loginPswExpiry); // localStorage.setItem("pswExpiryDate", data.loginPswExpiry);
if (data.FirstTimeLogin === true) { if (data.FirstTimeLogin === true) {
notifications.show({ notifications.show({
withBorder: true, withBorder: true,
color: "red", color: "red",
title: "Error", title: "Error",
message: "Please set your credential into Internet Banking before login.", message: "Please go to Internet Banking, set your credentials, and then try logging in here again.",
autoClose: 5000, autoClose: 5000,
}); });
} }
else { else {
router.push("/mandate_page"); router.push("/eMandate/mandate_page");
} }
} }
else { else {
regenerateCaptcha();
setIsLogging(false); setIsLogging(false);
notifications.show({ notifications.show({
withBorder: true, withBorder: true,

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
Text, Text,
Title, Title,
@@ -13,12 +13,16 @@ import {
Container, Container,
ActionIcon, ActionIcon,
Divider, Divider,
Modal,
TextInput,
} from "@mantine/core"; } from "@mantine/core";
import { Providers } from "@/app/providers"; import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import NextImage from "next/image"; import NextImage from "next/image";
import logo from "@/app/image/logo1.jpg"; import logo from "@/app/image/logo1.jpg";
import { IconLogout } from "@tabler/icons-react"; import { IconLogout, IconX } from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
import { useMediaQuery } from "@mantine/hooks";
type Mandate = { type Mandate = {
id: string; id: string;
@@ -33,15 +37,29 @@ type Mandate = {
const MandateCard = ({ const MandateCard = ({
mandate, mandate,
onAccept, onAction,
onReject,
}: { }: {
mandate: Mandate; mandate: Mandate;
onAccept: (id: string) => void; onAction: (id: string, action: "accept" | "reject") => void;
onReject: (id: string) => void;
}) => { }) => {
const [agreed, setAgreed] = useState(false); const [agreed, setAgreed] = useState(false);
const handleClick = (action: "accept" | "reject") => {
if (!agreed) {
notifications.show({
withBorder: true,
icon: <IconX size={18} />,
color: "red",
title: "Error",
message:
"Please agree to the debit of mandate processing charges first.",
autoClose: 4000,
});
return;
}
onAction(mandate.id, action);
};
return ( return (
<Card shadow="sm" radius="md" p="lg" withBorder> <Card shadow="sm" radius="md" p="lg" withBorder>
<Text fw={600}>{mandate.category}</Text> <Text fw={600}>{mandate.category}</Text>
@@ -62,19 +80,10 @@ const MandateCard = ({
/> />
<Group mt="md" grow> <Group mt="md" grow>
<Button <Button variant="outline" color="red" onClick={() => handleClick("reject")}>
variant="outline"
color="red"
disabled={!agreed}
onClick={() => onReject(mandate.id)}
>
Reject Reject
</Button> </Button>
<Button <Button color="green" onClick={() => handleClick("accept")}>
color="green"
disabled={!agreed}
onClick={() => onAccept(mandate.id)}
>
Accept Accept
</Button> </Button>
</Group> </Group>
@@ -84,6 +93,64 @@ const MandateCard = ({
export default function MandatePage() { export default function MandatePage() {
const router = useRouter(); const router = useRouter();
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [custname, setCustname] = useState<string | null>(null);
const isMobile = useMediaQuery("(max-width: 768px)");
// OTP Modal states
const [otpModalOpen, setOtpModalOpen] = useState(false);
const [otp, setOtp] = useState("");
const [pendingAction, setPendingAction] = useState<{ id: string; action: "accept" | "reject" } | null>(null);
useEffect(() => {
const token = localStorage.getItem("mandate_token");
if (!token) {
setAuthorized(false);
router.push("/eMandate/login");
} else {
setAuthorized(true);
handleFetchUserName();
}
}, []);
const handleLogout = () => {
localStorage.removeItem("mandate_token");
localStorage.removeItem("user_name");
localStorage.removeItem("userMobNo");
router.push("/eMandate/login");
};
const handleFetchUserName = async () => {
try {
const token = localStorage.getItem("mandate_token");
const response = await fetch("/api/customer", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) throw new Error();
const data = await response.json();
if (Array.isArray(data) && data.length > 0) {
const name = data[0].custname;
const mobileNumber = data[0].mobileno;
localStorage.setItem("user_name", name);
localStorage.setItem("userMobNo", mobileNumber);
setCustname(name);
}
} catch {
notifications.show({
withBorder: true,
color: "red",
title: "Please try again later",
message: "Unable to Fetch, Please try again later",
autoClose: 5000,
});
}
};
// Dummy mandates
const [mandates] = useState<Mandate[]>([ const [mandates] = useState<Mandate[]>([
{ {
id: "1", id: "1",
@@ -105,21 +172,95 @@ export default function MandatePage() {
firstCollection: "2025-10-01", firstCollection: "2025-10-01",
finalCollection: "2030-10-01", finalCollection: "2030-10-01",
}, },
]); ]);
const handleAccept = (id: string) => { // STEP 1: When Accept/Reject pressed → call send OTP API
alert(`Accepted mandate ${id}`); const handleMandateAction = async (id: string, action: "accept" | "reject") => {
try {
const response = await fetch("/api/otp/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
mobileNumber: '7890544527',
type: 'EMandate'
}),
});
const data = await response.json();
console.log(data)
if (!response.ok) throw new Error("Failed to send OTP");
notifications.show({
withBorder: true,
color: "green",
title: "OTP Sent",
message: "An OTP has been sent to your registered mobile number.",
autoClose: 4000,
});
setPendingAction({ id, action });
setOtp("");
setOtpModalOpen(true);
} catch (err) {
notifications.show({
withBorder: true,
color: "red",
title: "Error",
message: "Failed to send OTP. Please try again.",
autoClose: 5000,
});
}
}; };
const handleReject = (id: string) => { // STEP 2: Verify OTP and complete action
alert(`Rejected mandate ${id}`); const handleOtpSubmit = async () => {
}; // if (!otp) {
const handleLogout = () => { // notifications.show({
localStorage.removeItem("access_token"); // withBorder: true,
router.push("/eMandate/login"); // redirect to login page // color: "red",
// title: "Invalid Input",
// message: "Please enter OTP before proceed",
// autoClose: 5000,
// });
// }
if (!pendingAction) return;
try {
const response = await fetch(`/api/otp/verify?mobileNumber=${7890544527}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
otp: otp,
}),
});
if (!response.ok) throw new Error("Invalid OTP");
notifications.show({
withBorder: true,
color: "green",
title: "Success",
message: `Mandate ${pendingAction.action}ed successfully!`,
autoClose: 4000,
});
setOtpModalOpen(false);
setPendingAction(null);
} catch {
notifications.show({
withBorder: true,
color: "red",
title: "Error",
message: "Invalid OTP. Please try again.",
autoClose: 4000,
});
}
}; };
if (!authorized) return null;
return ( return (
<Providers> <Providers>
<div <div
@@ -137,54 +278,67 @@ export default function MandatePage() {
top: 0, top: 0,
left: 0, left: 0,
width: "100%", width: "100%",
height: "12%", height: "70px",
zIndex: 100, zIndex: 100,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
padding: "0 0.5rem", padding: isMobile ? "0 0.5rem" : "0 1.5rem",
background: background:
"linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)", "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
borderBottom: "1px solid black",
}} }}
> >
<Box style={{ display: "flex", alignItems: "center" }}> <Box style={{ display: "flex", alignItems: "center", flex: 1, minWidth: 0 }}>
<Image <Image
src={logo} src={logo}
component={NextImage} component={NextImage}
fit="contain" fit="contain"
alt="ebanking" alt="ebanking"
style={{ width: "70px", height: "70px", flexShrink: 0 }} style={{
width: isMobile ? "45px" : "70px",
height: isMobile ? "45px" : "70px",
flexShrink: 0,
}}
/> />
{!isMobile && (
<Title <Title
order={2} order={2}
style={{ style={{
fontFamily: "Roboto", fontFamily: "Roboto",
marginLeft: "1rem", marginLeft: "1rem",
color: "white", color: "white",
fontSize: "1.2rem",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}} }}
> >
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD. THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title> </Title>
)}
</Box> </Box>
{/* Logout Icon */} {isMobile ? (
<ActionIcon <ActionIcon variant="subtle" color="white" size="lg" onClick={handleLogout} title="Logout">
<IconLogout size={26} stroke={1.5} />
</ActionIcon>
) : (
<Button
variant="subtle" variant="subtle"
color="white" color="white"
size="lg" size="md"
onClick={handleLogout} onClick={handleLogout}
title="Logout" leftSection={<span style={{ fontSize: "14px", fontWeight: 500 }}>LOGOUT</span>}
> rightSection={<IconLogout size={25} stroke={1.5} />}
<IconLogout size={28} stroke={1.5} /> />
</ActionIcon> )}
</Box> </Box>
{/* WELCOME BAR */} {/* WELCOME BAR */}
<Box <Box
style={{ style={{
position: "fixed", position: "fixed",
top: "70px", // directly below header top: "70px",
left: 0, left: 0,
width: "100%", width: "100%",
padding: "0.3rem 1rem", padding: "0.3rem 1rem",
@@ -193,24 +347,23 @@ export default function MandatePage() {
}} }}
> >
<Text fw={500} style={{ fontFamily: "inter", fontSize: "20px" }}> <Text fw={500} style={{ fontFamily: "inter", fontSize: "20px" }}>
Welcome, John Doe Welcome, {custname ?? ""}
</Text> </Text>
<Text size="xs" c="dimmed" style={{ marginBottom: "4px" }}> <Text size="xs" c="dimmed" style={{ marginBottom: "4px" }}>
Login Time: 9/24/2025, 12:52:05 PM Last logged in at {new Date().toLocaleString()}
</Text> </Text>
<Title order={4} ta="center" style={{ margin: 0 }}> <Title order={4} ta="center" style={{ margin: 0 }}>
KCCB E-Mandate KCCB E-Mandate
</Title> </Title>
<Divider size="xs" color='#99c2ff' /> <Divider size="xs" color="#99c2ff" />
</Box> </Box>
{/* CONTENT */} {/* CONTENT */}
<Box <Box
style={{ style={{
flex: 1, flex: 1,
marginTop: "160px", // header + welcome bar height marginTop: "160px",
marginBottom: "40px", // leave space for footer marginBottom: "40px",
// padding: "1rem",
backgroundColor: "#e6ffff", backgroundColor: "#e6ffff",
overflowY: "auto", overflowY: "auto",
}} }}
@@ -219,11 +372,7 @@ export default function MandatePage() {
<Grid> <Grid>
{mandates.map((mandate) => ( {mandates.map((mandate) => (
<Grid.Col span={{ base: 12, sm: 6 }} key={mandate.id}> <Grid.Col span={{ base: 12, sm: 6 }} key={mandate.id}>
<MandateCard <MandateCard mandate={mandate} onAction={handleMandateAction} />
mandate={mandate}
onAccept={handleAccept}
onReject={handleReject}
/>
</Grid.Col> </Grid.Col>
))} ))}
</Grid> </Grid>
@@ -249,6 +398,31 @@ export default function MandatePage() {
© 2025 The Kangra Central Co-Operative Bank © 2025 The Kangra Central Co-Operative Bank
</Text> </Text>
</Box> </Box>
{/* OTP MODAL */}
<Modal opened={otpModalOpen} onClose={() => setOtpModalOpen(false)} title="Enter OTP" centered>
<Text mb="sm">An OTP has been sent to your registered mobile number.</Text>
<TextInput
label="OTP"
placeholder="Enter OTP"
value={otp}
withAsterisk
onInput={(e) => {
const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 6) setOtp(input);
}}
/>
<Text size="xs">if you did not received the OTP in SMS,you can click click here to resend the SMS</Text>
<Group mt="md" grow>
<Button color="gray" onClick={() => setOtpModalOpen(false)}>
Cancel
</Button>
<Button color="green" onClick={handleOtpSubmit}>
Submit
</Button>
</Group>
</Modal>
</div> </div>
</Providers> </Providers>
); );

View File

@@ -129,6 +129,7 @@ export default function Login() {
message: data?.error || "Internal Server Error", message: data?.error || "Internal Server Error",
autoClose: 5000, autoClose: 5000,
}); });
regenerateCaptcha();
localStorage.removeItem("access_token"); localStorage.removeItem("access_token");
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
@@ -150,6 +151,7 @@ export default function Login() {
} }
else { else {
regenerateCaptcha();
setIsLogging(false); setIsLogging(false);
notifications.show({ notifications.show({
withBorder: true, withBorder: true,
@@ -218,7 +220,7 @@ export default function Login() {
}); });
const data = await res.json(); const data = await res.json();
console.log(data) // console.log(data)
if (!res.ok) { if (!res.ok) {
notifications.show({ notifications.show({
color: "red", color: "red",