wip: Frontend for quick pay
This commit is contained in:
@@ -96,12 +96,12 @@ export default function AccountStatementPage() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (end.diff(start, "day") > 60) {
|
||||
if (end.diff(start, "day") > 90) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Invalid Date range",
|
||||
message: "End date must be within 60 days from start date",
|
||||
message: "End date must be within 90 days from start date",
|
||||
autoClose: 4000,
|
||||
});
|
||||
return;
|
||||
|
@@ -1,110 +1,392 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Group,
|
||||
Paper,
|
||||
Radio,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function FundTransferForm() {
|
||||
const [paymentMethod, setPaymentMethod] = useState("imps");
|
||||
interface accountData {
|
||||
stAccountNo: string;
|
||||
stAccountType: string;
|
||||
stAvailableBalance: string;
|
||||
custname: string;
|
||||
}
|
||||
|
||||
export default function QuickPay() {
|
||||
const router = useRouter();
|
||||
const [bankType, setBankType] = useState("own");
|
||||
const [authorized, setAuthorized] = useState<boolean | null>(null);
|
||||
const [accountData, setAccountData] = useState<accountData[]>([]);
|
||||
const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null);
|
||||
const [beneficiaryAcc, setBeneficiaryAcc] = useState("");
|
||||
const [confirmBeneficiaryAcc, setConfirmBeneficiaryAcc] = useState("");
|
||||
const [beneficiaryType, setBeneficiaryType] = useState<string | null>(null);
|
||||
const [isVisibilityLocked, setIsVisibilityLocked] = useState(false);
|
||||
const [amount, setAmount] = useState("");
|
||||
const [remarks, setRemarks] = useState("");
|
||||
const [showTxnPassword, setShowTxnPassword] = useState(false);
|
||||
const [txnPassword, setTxnPassword] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<"success" | "error" | null>(null);
|
||||
const [beneficiaryName, setBeneficiaryName] = useState<string | null>(null);
|
||||
const [showOtpField, setShowOtpField] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
|
||||
const accountOptions = accountData.map((acc) => ({
|
||||
value: acc.stAccountNo,
|
||||
label: `${acc.stAccountNo} (${acc.stAccountType})`,
|
||||
}));
|
||||
|
||||
const FetchAccountDetails = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch("/api/customer", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && Array.isArray(data)) {
|
||||
const filterSAaccount =data.filter((acc)=>acc.stAccountType==='SA');
|
||||
setAccountData(filterSAaccount);
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later",
|
||||
message: "Unable to Fetch, Please try again later",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
setAuthorized(false);
|
||||
router.push("/login");
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authorized) {
|
||||
FetchAccountDetails();
|
||||
}
|
||||
}, [authorized]);
|
||||
|
||||
const isValidTxnPassword = (password: string) =>
|
||||
/[A-Z]/i.test(password) && /[0-9]/.test(password) && /[^a-zA-Z0-9]/.test(password);
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!selectedAccNo || !beneficiaryAcc ||
|
||||
!confirmBeneficiaryAcc
|
||||
) {
|
||||
notifications.show({
|
||||
title: "Validation Error",
|
||||
message: "Please fill debit account, beneficiary account number and confirm beneficiary account number",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (beneficiaryAcc.length < 10 || beneficiaryAcc.length > 17) {
|
||||
notifications.show({
|
||||
title: "Invalid Account Number",
|
||||
message: "Please Enter valid account Number",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (beneficiaryAcc !== confirmBeneficiaryAcc) {
|
||||
notifications.show({
|
||||
title: "Mismatch",
|
||||
message: "Beneficiary account numbers do not match",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch(`/api/beneficiary/validate/within-bank?accountNumber=${beneficiaryAcc}`, {
|
||||
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);
|
||||
} else {
|
||||
setBeneficiaryName("Invalid account number");
|
||||
setValidationStatus("error");
|
||||
}
|
||||
} catch {
|
||||
setBeneficiaryName("Invalid account number");
|
||||
setValidationStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleProceed = async () => {
|
||||
if (!selectedAccNo || !beneficiaryAcc || !confirmBeneficiaryAcc || !beneficiaryType || !amount || !remarks) {
|
||||
notifications.show({
|
||||
title: "Validation Error",
|
||||
message: "Please fill all required fields",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationStatus !== "success") {
|
||||
notifications.show({
|
||||
title: "Validation Required",
|
||||
message: "Please validate beneficiary before proceeding",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showTxnPassword) {
|
||||
setShowTxnPassword(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!txnPassword || !isValidTxnPassword(txnPassword)) {
|
||||
notifications.show({
|
||||
title: "Weak Password",
|
||||
message: "Password must contain letter, number, and special character",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showOtpField) {
|
||||
setShowOtpField(true);
|
||||
notifications.show({
|
||||
title: "OTP Sent",
|
||||
message: "Check your registered device for OTP",
|
||||
color: "green",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!otp) {
|
||||
notifications.show({
|
||||
title: "Enter OTP",
|
||||
message: "Please enter the OTP",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const token = localStorage.getItem("access_token");
|
||||
|
||||
const res = await fetch("/api/payment/transfer", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fromAccount: selectedAccNo,
|
||||
toAccount: beneficiaryAcc,
|
||||
toAccountType: beneficiaryType,
|
||||
amount,
|
||||
narration: remarks,
|
||||
txnPassword,
|
||||
otp,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
notifications.show({
|
||||
title: "Success",
|
||||
message: "Transaction successful",
|
||||
color: "green",
|
||||
});
|
||||
|
||||
// Reset
|
||||
setShowTxnPassword(false);
|
||||
setTxnPassword("");
|
||||
setShowOtpField(false);
|
||||
setOtp("");
|
||||
setValidationStatus(null);
|
||||
setBeneficiaryName(null);
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: result?.message || "Transaction failed",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Something went wrong",
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!authorized) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
maw={1000}
|
||||
p="lg"
|
||||
w="1000px"
|
||||
style={{
|
||||
// borderRadius: 9,
|
||||
// boxShadow: '0 0 12px rgba(0, 0, 0, 0.05)',
|
||||
// backgroundColor: '#c5e4f9',
|
||||
width:'150%',
|
||||
marginRight: 100
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
gap={12}
|
||||
style={{ maxWidth: 1000, margin: "0 auto", padding: "1rem 0" }}
|
||||
>
|
||||
<Title order={4} mb={4}>
|
||||
Enter Transaction Details
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
|
||||
<Title order={3} mb="md">
|
||||
Quick Pay
|
||||
</Title>
|
||||
<Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md">
|
||||
<Group justify="center">
|
||||
<Radio value="own" label="Own Bank" />
|
||||
<Radio value="outside" label="Outside Bank" />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
|
||||
{/* Transfer From */}
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
Transfer from
|
||||
{bankType === "own" ? (
|
||||
<ScrollArea style={{ flex: 1 }}>
|
||||
<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
|
||||
disabled={isVisibilityLocked}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Beneficiary Account No"
|
||||
value={beneficiaryAcc}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (/^\d*$/.test(value)) {
|
||||
setBeneficiaryAcc(value);
|
||||
}
|
||||
}}
|
||||
withAsterisk
|
||||
disabled={isVisibilityLocked}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Confirm Beneficiary 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
|
||||
disabled={isVisibilityLocked}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group align="center" gap="sm">
|
||||
<Button variant="filled" color="blue" onClick={handleValidate}>
|
||||
Validate
|
||||
</Button>
|
||||
{validationStatus === "success" && <Text c="green">{beneficiaryName}</Text>}
|
||||
{validationStatus === "error" && <Text c="red">{beneficiaryName}</Text>}
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<Select
|
||||
label="Beneficiary A/c Type"
|
||||
placeholder="Select type"
|
||||
data={["Savings", "Current"]}
|
||||
value={beneficiaryType}
|
||||
onChange={setBeneficiaryType}
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Amount"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Remarks"
|
||||
placeholder="Enter remarks"
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{showTxnPassword && (
|
||||
<TextInput
|
||||
label="Transaction Password"
|
||||
placeholder="Enter transaction password"
|
||||
type="password"
|
||||
value={txnPassword}
|
||||
onChange={(e) => setTxnPassword(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
/>
|
||||
)}
|
||||
|
||||
{showOtpField && (
|
||||
<TextInput
|
||||
label="OTP"
|
||||
placeholder="Enter OTP"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
/>
|
||||
)}
|
||||
|
||||
<Group justify="flex-start">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="blue"
|
||||
onClick={handleProceed}
|
||||
loading={isSubmitting}
|
||||
disabled={validationStatus !== "success"}
|
||||
>
|
||||
{showOtpField ? "Proceed to Pay" : "Proceed"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<Text size="lg" mt="md">
|
||||
hii
|
||||
</Text>
|
||||
<Select
|
||||
placeholder="Select account"
|
||||
data={["Savings - 1234"]}
|
||||
size="sm"
|
||||
mt={4}
|
||||
/>
|
||||
{/* <Text size="xs" c="red" mt={2}>
|
||||
* Total available amount is ₹ *****
|
||||
</Text> */}
|
||||
</div>
|
||||
|
||||
{/* Amount + Remarks */}
|
||||
<Group grow gap="sm">
|
||||
<TextInput label="Amount" placeholder="Enter amount" size="sm" />
|
||||
<TextInput
|
||||
label="Remarks (optional)"
|
||||
placeholder="Remarks"
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
Payment Method
|
||||
</Text>
|
||||
<Radio.Group
|
||||
value={paymentMethod}
|
||||
onChange={setPaymentMethod}
|
||||
name="payment-method"
|
||||
>
|
||||
<Flex direction="column" gap={4}>
|
||||
<Radio
|
||||
value="imps"
|
||||
label="IMPS (Instant Transfer up to 2 Lakh, Available 24x7 365 Days)"
|
||||
/>
|
||||
<Radio
|
||||
value="neft"
|
||||
label="NEFT (Regular Transfer, Available 24x7 365 Days)"
|
||||
/>
|
||||
<Radio
|
||||
value="rtgs"
|
||||
label="RTGS (Transfer above 2 lakh, 7AM - 6PM on RBI Working Days)"
|
||||
/>
|
||||
</Flex>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
{/* IFSC + Bank Name */}
|
||||
<Group grow gap="sm">
|
||||
<TextInput label="Payee Bank IFSC" placeholder="Enter IFSC code" size="sm" />
|
||||
<TextInput label="Payee Bank Name" placeholder="Enter bank name" size="sm" />
|
||||
</Group>
|
||||
|
||||
{/* Buttons */}
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Button variant="default" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">PROCEED TO PAY</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
@@ -44,7 +44,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return data;
|
||||
} else {
|
||||
}
|
||||
else if(response.status ===401 ||data.message === 'invalid or expired token'){
|
||||
// console.log(data);
|
||||
localStorage.removeItem("access_token");
|
||||
router.push('/login');
|
||||
}
|
||||
else {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
|
73
src/app/(main)/settings/layout.tsx
Normal file
73
src/app/(main)/settings/layout.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import { Divider, Stack, Text } from '@mantine/core';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const links = [
|
||||
{ label: "View Profile", href: "/settings" },
|
||||
{ label: "Change Login Password", href: "/settings/change_login_password" },
|
||||
{ label: "Change transaction Password", href: "/settings/change_transaction_password" },
|
||||
{ label: "Set transaction Password", href: "/settings/set_transaction_password" },
|
||||
];
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<div style={{ display: "flex", height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: "16%",
|
||||
backgroundColor: '#c5e4f9',
|
||||
borderRight: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
|
||||
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
Settings
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm" justify="flex-start" style={{ padding: '1rem' }}>
|
||||
{links.map(link => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Text
|
||||
key={link.href}
|
||||
component={Link}
|
||||
href={link.href}
|
||||
c={isActive ? 'darkblue' : 'blue'}
|
||||
style={{
|
||||
textDecoration: isActive ? 'underline' : 'none',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '1rem' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
11
src/app/(main)/settings/page.tsx
Normal file
11
src/app/(main)/settings/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
import { Divider, Stack, Text } from '@mantine/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function settings() {
|
||||
return(
|
||||
<Text>Hii</Text>
|
||||
)
|
||||
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Grid } from "@mantine/core"
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <Grid bg="gray">
|
||||
<Grid.Col span={3}>
|
||||
<Box bg="blue">
|
||||
<p>Test Layout</p>
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<Box bg="yellow">
|
||||
{children}
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
export default function Page() {
|
||||
return <p>Test Page 1</p>
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
export default function Page() {
|
||||
return <p>Test Page 2</p>
|
||||
}
|
Reference in New Issue
Block a user