Wip: update the login page,

feat: update Otp in set login and transaction page.
Feat: Add validation for external account in add beneficiary.
This commit is contained in:
2025-08-14 17:05:21 +05:30
parent c9181881e0
commit 78426747e5
20 changed files with 687 additions and 474 deletions

View File

@@ -37,8 +37,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
>
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
Accounts
<Text fw={700} c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
My Accounts
</Text>
</Stack>

View File

@@ -1,19 +1,9 @@
"use client";
import React, { useEffect, useState } from 'react';
import {
TextInput,
Button,
Select,
Title,
Paper,
Grid,
Group,
Radio,
Text,
PasswordInput,
} from '@mantine/core';
import { TextInput, Button, Grid, Text, PasswordInput } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { useRouter } from "next/navigation";
const MockOthersAccountValidation =
[
@@ -31,8 +21,9 @@ const MockOthersAccountValidation =
},
]
const AddBeneficiaryOthers: React.FC = () => {
export default function AddBeneficiaryOthers() {
const router = useRouter();
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [bankName, setBankName] = useState('');
const [ifsccode, setIfsccode] = useState('');
const [branchName, setBranchName] = useState('');
@@ -40,24 +31,28 @@ const AddBeneficiaryOthers: React.FC = () => {
const [confirmAccountNo, setConfirmAccountNo] = useState('');
const [nickName, setNickName] = useState('');
const [beneficiaryName, setBeneficiaryName] = useState<string | null>(null);
const [otp, setOtp] = useState('');
const [generatedOtp, setGeneratedOtp] = useState('');
const [otpSent, setOtpSent] = useState(false);
const [otpVerified, setOtpVerified] = useState(false);
const [validationStatus, setValidationStatus] = useState<'success' | 'error' | null>(null);
const [isVisibilityLocked, setIsVisibilityLocked] = useState(false);
const [showPayeeAcc, setShowPayeeAcc] = useState(true);
const getFullMaskedAccount = (acc: string) => { return "X".repeat(acc.length); };
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
setAuthorized(false);
router.push("/login");
} else {
setAuthorized(true);
}
}, []);
useEffect(() => {
const ifscTrimmed = ifsccode.trim();
if (ifscTrimmed.length === 11) { // Only call API when IFSC is valid length
const fetchIfscDetails = async () => {
try {
@@ -115,15 +110,6 @@ const AddBeneficiaryOthers: React.FC = () => {
return;
}
// if ( !/^[A-Za-z ]{1,5}$/.test(bankName)) {
// notifications.show({
// color: "red",
// title: "Invalid Bank Name",
// message: "Use only alphabets/spaces, max 100 characters.",
// });
// return;
// }
const trimmedIfsc = ifsccode.trim().toUpperCase();
const isValidIfscCode = (code: string) => {
return /^[A-Z]{4}0[0-9]{6}$/.test(code);
@@ -161,12 +147,25 @@ const AddBeneficiaryOthers: React.FC = () => {
}
// Validation for now
try {
const matched = MockOthersAccountValidation.find(
(entry) => entry.stBenAccountNo === accountNo
);
// const matched = MockOthersAccountValidation.find(
// (entry) => entry.stBenAccountNo === accountNo
// );
if (matched) {
setBeneficiaryName(matched.stBenName);
const token = localStorage.getItem("access_token");
const response = await fetch(
`http://localhost:8080/api/beneficiary/validate/outside-bank?accountNo=${accountNo}&ifscCode=${ifsccode}&remitterName=""`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
}
);
const data = await response.json();
if (response.ok && data?.name) {
setBeneficiaryName(data.name);
setValidationStatus("success");
setIsVisibilityLocked(true);
setOtpSent(true);
@@ -190,55 +189,6 @@ const AddBeneficiaryOthers: React.FC = () => {
setBeneficiaryName("Something went wrong");
setValidationStatus("error");
}
// Later need to add api
// try {
// // API will be changed
// const token = localStorage.getItem("access_token");
// const response = await fetch(`/api/beneficiary/validate/within-bank?accountNumber=${accountNo}`, {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// Authorization: `Bearer ${token}`,
// },
// });
// const data = await response.json();
// if (response.ok && data?.name) {
// setBeneficiaryName(data.name);
// setValidationStatus("success");
// setIsVisibilityLocked(true);
// setOtpSent(true);
// await handleGenerateOtp();
// notifications.show({
// withBorder: true,
// color: "green",
// title: "OTP Sent",
// message: "OTP has been sent to your registered mobile number.",
// autoClose: 5000,
// });
// } else {
// setBeneficiaryName("Invalid beneficiary account number");
// setValidationStatus("error");
// setAccountNo("");
// setConfirmAccountNo("");
// }
// }
// catch (error) {
// setBeneficiaryName("Invalid beneficiary account number");
// setValidationStatus("error");
// notifications.show({
// withBorder: true,
// color: "red",
// title: "Error",
// message: "Could not validate account number.",
// autoClose: 5000,
// });
// }
};
const verifyOtp = () => {
@@ -271,6 +221,7 @@ const AddBeneficiaryOthers: React.FC = () => {
});
};
if (!authorized) return null;
return (
<Grid gutter="md">
@@ -278,9 +229,7 @@ const AddBeneficiaryOthers: React.FC = () => {
<TextInput
label="IFSC Code"
placeholder="e.g.,ABCD0123456"
//data={bankOptions}
value={ifsccode}
// onChange={(e) => setIfsccode(e.currentTarget.value)}
onChange={(e) => {
let val = e.currentTarget.value.toUpperCase();
val = val.replace(/[^A-Z0-9]/g, ""); // block special chars
@@ -305,8 +254,6 @@ const AddBeneficiaryOthers: React.FC = () => {
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label="Branch Name"
@@ -422,4 +369,4 @@ const AddBeneficiaryOthers: React.FC = () => {
);
};
export default AddBeneficiaryOthers;

View File

@@ -1,27 +1,20 @@
"use client";
import React, { useState } from 'react';
import {
TextInput,
Button,
Select,
Title,
Paper,
Grid,
Group,
Radio,
Text,
PasswordInput,
} from '@mantine/core';
import React, { useEffect, useState } from 'react';
import {TextInput,Button,Select,Title,Paper,Grid,Group,Radio,Text,PasswordInput} from '@mantine/core';
import { useRouter } from "next/navigation";
import { notifications } from '@mantine/notifications';
import AddBeneficiaryOthers from './addBeneficiaryOthers';
import { generateOTP } from '@/app/OTPGenerator';
const bankOptions = [
{ value: 'KCCB', label: 'KCCB - The Kangra Central Co-Operative Bank' },
];
const AddBeneficiary: React.FC = () => {
const router = useRouter();
const [bankName, setBankName] = useState('');
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [bankType, setBankType] = useState('own');
const [accountNo, setAccountNo] = useState('');
const [confirmAccountNo, setConfirmAccountNo] = useState('');
@@ -37,11 +30,20 @@ const AddBeneficiary: React.FC = () => {
const getFullMaskedAccount = (acc: string) => { return "X".repeat(acc.length); };
const handleGenerateOtp = async () => {
// const value = await generateOTP(6);
const value = "123456"; // Or generate a random OTP
setGeneratedOtp(value);
return value;
};
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
setAuthorized(false);
router.push("/login");
} else {
setAuthorized(true);
}
}, []);
// const isValidBankName = (name: string) => {
// const regex = /^[A-Za-z\s]{3,5}$/;
// return regex.test(name.trim());
@@ -51,8 +53,6 @@ const AddBeneficiary: React.FC = () => {
// return /^[A-Z]{4}0[0-9]{6}$/.test(code);
// };
const validateAndSendOtp = async () => {
if (!bankName || !accountNo || !confirmAccountNo) {
notifications.show({
@@ -86,10 +86,6 @@ const AddBeneficiary: React.FC = () => {
// return;
// }
if (accountNo.length < 10 || accountNo.length > 17) {
notifications.show({
withBorder: true,
@@ -101,9 +97,6 @@ const AddBeneficiary: React.FC = () => {
return;
}
if (accountNo !== confirmAccountNo) {
notifications.show({
withBorder: true,
@@ -193,6 +186,7 @@ const AddBeneficiary: React.FC = () => {
});
};
if (!authorized) return null;
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
@@ -254,9 +248,9 @@ const AddBeneficiary: React.FC = () => {
// disabled={isVisibilityLocked}
readOnly={isVisibilityLocked}
required
onCopy={(e) => e.preventDefault()}
onPaste={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
onCopy={(e) => e.preventDefault()}
onPaste={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
/>
{validationStatus === "error" && (

View File

@@ -38,7 +38,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
>
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
<Text fw={700} c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
Send Money
</Text>
</Stack>

View File

@@ -6,6 +6,7 @@ import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation";
import { generateOTP } from '@/app/OTPGenerator';
import OutsideQuickPay from "./outside_quick_pay";
import { IconRefresh } from "@tabler/icons-react";
interface accountData {
stAccountNo: string;
@@ -34,6 +35,8 @@ export default function QuickPay() {
const [validationStatus, setValidationStatus] = useState<"success" | "error" | null>(null);
const [beneficiaryName, setBeneficiaryName] = useState<string | null>(null);
const [showOtpField, setShowOtpField] = useState(false);
const [countdown, setCountdown] = useState(60);
const [timerActive, setTimerActive] = useState(false);
const [otp, setOtp] = useState("");
const [generateOtp, setGenerateOtp] = useState("");
@@ -41,6 +44,8 @@ export default function QuickPay() {
// const value = await generateOTP(6);
const value = "123456";
setGenerateOtp(value);
setCountdown(60);
setTimerActive(true);
return value;
}
@@ -94,6 +99,22 @@ export default function QuickPay() {
}
}, [authorized]);
useEffect(() => {
let interval: number | undefined;
if (timerActive && countdown > 0) {
interval = window.setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
}
if (countdown === 0) {
if (interval) clearInterval(interval);
setTimerActive(false);
}
return () => {
if (interval) clearInterval(interval);
};
}, [timerActive, countdown]);
async function handleValidate() {
if (!selectedAccNo || !beneficiaryAcc ||
!confirmBeneficiaryAcc
@@ -257,6 +278,7 @@ export default function QuickPay() {
color: "red",
});
} finally {
setValidationStatus(null);
setSelectedAccNo(null);
setBeneficiaryAcc('');
setBeneficiaryName('');
@@ -313,142 +335,161 @@ export default function QuickPay() {
{/* main content */}
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
<Title order={3} mb="md">
Quick Pay
Quick Pay - Own Bank
</Title>
<div style={{ maxHeight: "320px", overflowY: "auto" }}>
<Stack gap="xs">
<Group grow>
<Select
label="Select Debit Account Number"
placeholder="Choose account number"
data={accountOptions}
value={selectedAccNo}
onChange={setSelectedAccNo}
withAsterisk
readOnly={isVisibilityLocked}
/>
<TextInput
label="Payee Account No"
value={showPayeeAcc ? beneficiaryAcc : getFullMaskedAccount(beneficiaryAcc)}
onChange={(e) => {
const value = e.currentTarget.value;
if (/^\d*$/.test(value)) {
setBeneficiaryAcc(value);
setShowPayeeAcc(true);
}
}}
onBlur={() => setShowPayeeAcc(false)}
onFocus={() => setShowPayeeAcc(true)}
withAsterisk
readOnly={isVisibilityLocked}
/>
<div style={{ maxHeight: "320px", overflowY: "auto" }}>
<Stack gap="xs">
<Group grow>
<Select
label="Select Debit Account Number"
placeholder="Choose account number"
data={accountOptions}
value={selectedAccNo}
onChange={setSelectedAccNo}
withAsterisk
readOnly={isVisibilityLocked}
/>
<TextInput
label="Payee Account No"
value={showPayeeAcc ? beneficiaryAcc : getFullMaskedAccount(beneficiaryAcc)}
onChange={(e) => {
const value = e.currentTarget.value;
if (/^\d*$/.test(value)) {
setBeneficiaryAcc(value);
setShowPayeeAcc(true);
}
}}
onBlur={() => setShowPayeeAcc(false)}
onFocus={() => setShowPayeeAcc(true)}
withAsterisk
readOnly={isVisibilityLocked}
/>
<TextInput
label="Confirm Payee Account No"
value={confirmBeneficiaryAcc}
onChange={(e) => {
const value = e.currentTarget.value;
if (/^\d*$/.test(value)) {
setConfirmBeneficiaryAcc(value);
}
}}
onCopy={(e) => e.preventDefault()}
onPaste={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
withAsterisk
readOnly={isVisibilityLocked}
/>
<TextInput
label="Confirm Payee Account No"
value={confirmBeneficiaryAcc}
onChange={(e) => {
const value = e.currentTarget.value;
if (/^\d*$/.test(value)) {
setConfirmBeneficiaryAcc(value);
}
}}
onCopy={(e) => e.preventDefault()}
onPaste={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
withAsterisk
readOnly={isVisibilityLocked}
/>
</Group>
<Group justify="space-between" >
<Text size="xs" c="green" style={{ visibility: selectedAccount ? "visible" : "hidden" }}>Available Balance :
{selectedAccount ? selectedAccount.stAvailableBalance : 0}
</Text>
<Group justify="center">
{validationStatus === "error" && <Text size="sm" fw={700} ta="right" c="red">{beneficiaryName}</Text>}
</Group>
<Group justify="space-between" >
<Text size="xs" c="green" style={{ visibility: selectedAccount ? "visible" : "hidden" }}>Available Balance :
{selectedAccount ? selectedAccount.stAvailableBalance : 0}
</Text>
<Group justify="center">
{validationStatus === "error" && <Text size="sm" fw={700} ta="right" c="red">{beneficiaryName}</Text>}
</Group>
</Group>
<Group grow>
<TextInput
label="Payee Name"
value={validationStatus === "success" && beneficiaryName ? beneficiaryName : ""}
disabled
readOnly
withAsterisk
/>
</Group>
<Group grow>
<TextInput
label="Payee Name"
value={validationStatus === "success" && beneficiaryName ? beneficiaryName : ""}
disabled
readOnly
withAsterisk
/>
<Select
label="Payee Account Type"
placeholder="Select type"
data={["Savings", "Current"]}
value={beneficiaryType}
onChange={setBeneficiaryType}
withAsterisk
readOnly={showOtpField}
/>
<Select
label="Payee Account Type"
placeholder="Select type"
data={["Savings", "Current"]}
value={beneficiaryType}
onChange={setBeneficiaryType}
withAsterisk
readOnly={showOtpField}
/>
<TextInput
label="Amount"
type="number"
value={amount}
onChange={(e) => setAmount(e.currentTarget.value)}
error={
selectedAccount && Number(amount) > Number(selectedAccount.stAvailableBalance) ?
"Amount exceeds available balance" : false}
withAsterisk
readOnly={showOtpField}
/>
<TextInput
label="Amount"
type="number"
value={amount}
onChange={(e) => setAmount(e.currentTarget.value)}
error={
selectedAccount && Number(amount) > Number(selectedAccount.stAvailableBalance) ?
"Amount exceeds available balance" : false}
withAsterisk
readOnly={showOtpField}
/>
<TextInput
label="Remarks"
placeholder="Enter remarks"
value={remarks}
onChange={(e) => setRemarks(e.currentTarget.value)}
// withAsterisk
readOnly={showOtpField}
withAsterisk
/>
</Group>
<Group grow>
{showOtpField && (
<TextInput
label="Remarks"
placeholder="Enter remarks"
value={remarks}
onChange={(e) => setRemarks(e.currentTarget.value)}
// withAsterisk
readOnly={showOtpField}
withAsterisk
/>
</Group>
<Group grow>
{showOtpField && (
<>
<PasswordInput
label="OTP"
placeholder="Enter OTP"
type="otp"
value={otp}
maxLength={6}
onChange={(e) => setOtp(e.currentTarget.value)}
withAsterisk
disabled={showTxnPassword}
/>
)}
{showTxnPassword && (
<TextInput
label="Transaction Password"
placeholder="Enter transaction password"
type="password"
value={txnPassword}
onChange={(e) => setTxnPassword(e.currentTarget.value)}
withAsterisk
/>
)}
</Group>
{!showTxnPassword && (
timerActive ? (
<Text size="xs" c="dimmed">
Resend OTP will be enabled in 00:{countdown < 10 ? `0${countdown}` : countdown} min
</Text>
) : (
<Button
variant="subtle"
px={8}
onClick={handleGenerateOtp}
leftSection={<IconRefresh size={16} />}
>
Resend
</Button>
)
)}
</>
)}
{showTxnPassword && (
<TextInput
label="Transaction Password"
placeholder="Enter transaction password"
type="password"
value={txnPassword}
onChange={(e) => setTxnPassword(e.currentTarget.value)}
withAsterisk
/>
)}
</Group>
<Group justify="flex-start">
<Button variant="filled" color="blue" onClick={handleValidate} disabled={validationStatus === "success"}>
Validate
</Button>
<Button
variant="filled"
color="blue"
onClick={handleProceed}
loading={isSubmitting}
disabled={validationStatus !== "success"}
>
{!showTxnPassword && showOtpField ? "Validate the OTP" : showTxnPassword ? "Proceed to Pay" : "Proceed"}
</Button>
</Group>
</Stack>
</div>
<Group justify="flex-start">
<Button variant="filled" color="blue" onClick={handleValidate} disabled={validationStatus === "success"}>
Validate
</Button>
<Button
variant="filled"
color="blue"
onClick={handleProceed}
loading={isSubmitting}
disabled={validationStatus !== "success"}
>
{!showTxnPassword && showOtpField ? "Validate the OTP" : showTxnPassword ? "Proceed to Pay" : "Proceed"}
</Button>
</Group>
</Stack>
</div>
{/* ) : (
<div>
<OutsideQuickPay />

View File

@@ -143,7 +143,7 @@ export default function Home() {
{/* Loan Account Card */}
<Paper p="md" radius="md" style={{
backgroundColor: '#c1e0f0', width: 350, height: 195, display: 'flex',
backgroundColor: '#c1e0f0', width: 355, height: 195, display: 'flex',
flexDirection: 'column', justifyContent: 'space-between'
}}
>

View File

@@ -5,7 +5,7 @@ import { IconBook, IconCurrencyRupee, IconHome, IconLogout, IconPhoneFilled, Ico
import Link from 'next/link';
import { useRouter, usePathname } from "next/navigation";
import { Providers } from '../providers';
import logo from '@/app/image/logo.jpg';
import logo from '@/app/image/logo1.jpg';
import NextImage from 'next/image';
import { notifications } from '@mantine/notifications';
@@ -25,7 +25,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
async function handleFetchUserName() {
try {
const token = localStorage.getItem("access_token");
const response = await fetch('api/customer', {
const response = await fetch('/api/customer', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -67,7 +67,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
async function handleFetchUserDetails(e: React.FormEvent) {
e.preventDefault();
const token = localStorage.getItem("access_token");
const response = await fetch('api/auth/user_details', {
const response = await fetch('/api/auth/user_details', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@@ -124,7 +124,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
width: '100%',
display: "flex",
justifyContent: "flex-start",
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
background: "linear-gradient(15deg,rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
}}
>
<Image
@@ -134,6 +134,19 @@ export default function RootLayout({ children }: { children: React.ReactNode })
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
<Title
order={2}
style={{
fontFamily: 'Roboto',
position: 'absolute',
top: '30%',
left: '6%',
color: 'White',
transition: "opacity 0.5s ease-in-out",
}}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text
style={{
position: 'absolute',
@@ -173,7 +186,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Link key={item.href} href={item.href}>
<Button
leftSection={<Icon size={20} />}
variant={isActive ? "light" : "subtle"}
variant={isActive ? "dark" : "subtle"}
color={isActive ? "blue" : undefined}
>
{item.label}
@@ -213,7 +226,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}}
>
<Text c="dimmed" size="xs">
© 2025 Kangra Central Co-Operative Bank
© 2025 The Kangra Central Co-Operative Bank
</Text>
</Box>
</div>

View File

@@ -14,7 +14,7 @@ import {
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { IconEye, IconEyeOff, IconLock } from "@tabler/icons-react";
import CaptchaImage from "@/app/SetPassword/CaptchaImage";
// import CaptchaImage from "@/app/SetPassword/CaptchaImage";
import { generateCaptcha } from "@/app/captcha";
const ChangePassword: React.FC = () => {
@@ -193,7 +193,7 @@ const ChangePassword: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 5 }}>
{/* CAPTCHA Image */}
<div style={{ display: 'flex', alignItems: 'center', height: 30 }}>
<CaptchaImage text={captcha} />
{/* <CaptchaImage text={captcha} /> */}
</div>
{/* Refresh Button */}

View File

@@ -38,7 +38,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
>
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
<Text fw={700} c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
Settings
</Text>
</Stack>