wip: Frontend for quick pay

This commit is contained in:
2025-07-18 11:49:42 +05:30
parent a6af487b67
commit f67319762f
8 changed files with 466 additions and 122 deletions

View File

@@ -96,12 +96,12 @@ export default function AccountStatementPage() {
}); });
return; return;
} }
if (end.diff(start, "day") > 60) { if (end.diff(start, "day") > 90) {
notifications.show({ notifications.show({
withBorder: true, withBorder: true,
color: "red", color: "red",
title: "Invalid Date range", 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, autoClose: 4000,
}); });
return; return;

View File

@@ -1,110 +1,392 @@
"use client"; "use client";
import React, { useEffect, useState } from "react";
import { import {
Box,
Button, Button,
Flex,
Group, Group,
Paper,
Radio, Radio,
ScrollArea,
Select, Select,
Stack,
Text, Text,
TextInput, TextInput,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useState } from "react"; import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation";
export default function FundTransferForm() { interface accountData {
const [paymentMethod, setPaymentMethod] = useState("imps"); 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 ( return (
<Box <Paper shadow="sm" radius="md" p="md" withBorder h={400}>
maw={1000} <Title order={3} mb="md">
p="lg" Quick Pay
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
</Title> </Title>
<Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md">
{/* Transfer From */} <Group justify="center">
<div> <Radio value="own" label="Own Bank" />
<Text size="sm" fw={500}> <Radio value="outside" label="Outside Bank" />
Transfer from
</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> </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> </Radio.Group>
</div>
{/* IFSC + Bank Name */} {bankType === "own" ? (
<Group grow gap="sm"> <ScrollArea style={{ flex: 1 }}>
<TextInput label="Payee Bank IFSC" placeholder="Enter IFSC code" size="sm" /> <div style={{ maxHeight: "320px", overflowY: "auto" }}>
<TextInput label="Payee Bank Name" placeholder="Enter bank name" size="sm" /> <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>
{/* Buttons */} <Group align="center" gap="sm">
<Group justify="flex-end" mt="sm"> <Button variant="filled" color="blue" onClick={handleValidate}>
<Button variant="default" size="sm"> Validate
Cancel
</Button> </Button>
<Button size="sm">PROCEED TO PAY</Button> {validationStatus === "success" && <Text c="green">{beneficiaryName}</Text>}
{validationStatus === "error" && <Text c="red">{beneficiaryName}</Text>}
</Group> </Group>
</Flex>
</Box> <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>
)}
</Paper>
); );
} }

View File

@@ -44,7 +44,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
return data; 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({ notifications.show({
withBorder: true, withBorder: true,
color: "red", color: "red",

View 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>
);
}
}

View 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>
)
}

View File

@@ -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>
}

View File

@@ -1,3 +0,0 @@
export default function Page() {
return <p>Test Page 1</p>
}

View File

@@ -1,3 +0,0 @@
export default function Page() {
return <p>Test Page 2</p>
}