feat: api integration for account tab

fix: layout design
feat : add screen for account tab
This commit is contained in:
2025-07-13 19:21:53 +05:30
parent 1023646751
commit 26e6dea82b
7 changed files with 442 additions and 266 deletions

View File

@@ -0,0 +1,222 @@
"use client";
import { Paper, Select, Title, Button, Text, Grid, ScrollArea, Table, Divider } from "@mantine/core";
import { DateInput } from '@mantine/dates';
import { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { notifications } from "@mantine/notifications";
import dayjs from 'dayjs';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.extend(customParseFormat);
export default function AccountStatementPage() {
const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]);
const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null);
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null);
const [transactions, setTransactions] = useState<any[]>([]);
const searchParams = useSearchParams();
const passedAccNo = searchParams.get("accNo");
useEffect(() => {
const saved = sessionStorage.getItem("accountData");
if (saved) {
const parsed = JSON.parse(saved);
const options = parsed.map((acc: any) => ({
label: `${acc.stAccountNo} - ${acc.stAccountType}`,
value: acc.stAccountNo,
}));
setAccountOptions(options);
if (passedAccNo) {
setSelectedAccNo(passedAccNo);
//Automatically fetch last 5 transactions if accNo is passed
const token = localStorage.getItem("access_token");
fetch(`/api/transactions/account/${passedAccNo}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
})
.then(res => res.json())
.then(data => {
if (Array.isArray(data)) {
const last5 = data.slice(-5).reverse();
setTransactions(last5);
}
})
.catch(() => {
notifications.show({
withBorder: true,
color: "red",
title: "Fetch Failed",
message: "Could not load recent transactions.",
autoClose: 5000,
});
});
}
}
}, [passedAccNo]);
const handleAccountTransaction = async () => {
if (!selectedAccNo || !startDate || !endDate) {
notifications.show({
withBorder: true,
color: "red",
title: "Missing field",
message: "Please select Account number,Start date and End date",
autoClose: 5000,
});
return;
}
const start = dayjs(startDate);
const end = dayjs(endDate);
const today = dayjs().startOf('day');
if (end.isAfter(today)) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid End Date",
message: "End date can not be the future date.",
autoClose: 4000,
});
return;
}
if (start.isAfter(end)) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid Start Date",
message: "Start date can not be less than end date",
autoClose: 4000,
});
return;
}
if (end.diff(start, "day") > 60) {
notifications.show({
withBorder: true,
color: "red",
title: "Invalid Date range",
message: "End date must be within 60 days from start date",
autoClose: 4000,
});
return;
}
try {
const token = localStorage.getItem("access_token");
const response = await fetch(`/api/transactions/account/${selectedAccNo}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
if (response.ok && Array.isArray(data)) {
const filterData = data.filter((txn: any) => {
const txnDate = dayjs(txn.date, 'DD/MM/YYYY');
return txnDate.isSameOrAfter(start) && txnDate.isSameOrBefore(end);
});
setTransactions(filterData);
}
} catch {
notifications.show({
withBorder: true,
color: "red",
title: "Please try again later",
message: "Unable to Fetch Account Transaction, Please try again later",
autoClose: 5000,
});
}
};
const cellStyle = {
border: "1px solid #ccc",
padding: "8px",
};
return (
<Grid gutter="md">
{/* Left side form */}
<Grid.Col span={{ base: 12, md: 4 }}>
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
<Title order={4} mb="sm">Account Statement</Title>
<Select
label="Select Account Number"
placeholder="Choose account number"
data={accountOptions}
value={selectedAccNo}
onChange={setSelectedAccNo}
searchable
/>
<DateInput
label="Start Date"
value={startDate}
onChange={setStartDate}
placeholder="Enter start date"
/>
<DateInput
label="End Date"
value={endDate}
onChange={setEndDate}
placeholder="Enter end date"
/>
<Button fullWidth mt="md" onClick={handleAccountTransaction}>
Proceed
</Button>
</Paper>
</Grid.Col>
{/* Right side transaction list */}
<Grid.Col span={{ base: 12, md: 8 }}>
<Paper shadow="sm" radius="md" p="md" withBorder h={400} style={{ display: 'flex', flexDirection: 'column' }}>
<Title order={5} mb="xs">Account Transaction Statement</Title>
<Text fw={500} fs="italic" >Account No : {selectedAccNo}</Text>
<Divider size="xs" />
<ScrollArea style={{ flex: 1 }}>
{transactions.length === 0 ? (
<p>No transactions found.</p>
) : (
<>
<Text fs="italic" c='#228be6' ta='center'>
{!startDate && !endDate ? 'Last 5 Transactions'
: startDate && endDate ? `Transactions from ${dayjs(startDate).format("DD/MM/YYYY")} to ${dayjs(endDate).format("DD/MM/YYYY")}`
: ""}
</Text>
<Table style={{ borderCollapse: "collapse", width: '100%' }}>
<thead style={{ backgroundColor: "#3385ff" }}>
{/* <tr>
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Name</th>
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Date</th>
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Type</th>
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Amount(₹)</th>
</tr> */}
</thead>
<tbody style={{ maxHeight: '250px', overflowY: 'auto', width: '100%' }}>
{transactions.map((txn, i) => (
<tr key={i}>
<td style={{ ...cellStyle, textAlign: "left" }}> {txn.name || "—"}</td>
<td style={{ ...cellStyle, textAlign: "left" }}>{txn.date || "—"}</td>
{/* <td style={{ ...cellStyle, textAlign: "left" }}>{txn.type}</td> */}
<td style={{ ...cellStyle, textAlign: "left", color: txn.type === "DR" ? "#e03131" : "#2f9e44" }}>
{parseFloat(txn.amount).toLocaleString("en-IN", {
minimumFractionDigits: 2,
})}
</td>
</tr>
))}
</tbody>
</Table>
</>
)}
</ScrollArea>
</Paper>
</Grid.Col >
</Grid >
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import { Suspense, useEffect, useState } from "react";
import AccountStatementPage from "./accountStatement";
import { useRouter } from "next/navigation";
export default function AccountStatement() {
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
SetAuthorized(false);
router.push("/login");
}
else {
SetAuthorized(true);
}
}, []);
if (authorized) {
return (
<Suspense fallback={<div>Loading...</div>}>
<AccountStatementPage />
</Suspense>
);
}
}

View File

@@ -2,55 +2,70 @@
import { Divider, Stack, Text } from '@mantine/core';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useRouter } from "next/navigation";
export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); // get current route
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const router = useRouter();
const pathname = usePathname();
const links = [
{ label: "Account Summary", href: "/accounts" },
{ label: "Statement of Account", href: "/accounts/account_statement" },
];
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
SetAuthorized(false);
router.push("/login");
}
else {
SetAuthorized(true);
}
}, []);
return (
<div style={{ display: "flex", height: '100%' }}>
<div
style={{
width: "15%",
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' }}>
Accounts
</Text>
</Stack>
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' }}>
Accounts
</Text>
</Stack>
<Stack gap="sm" justify="flex-start" style={{ padding: '1rem' }}>
{links.map(link => {
const isActive = pathname === link.href || pathname.startsWith(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>
<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>
<div style={{ flex: 1, padding: '1rem' }}>
{children}
</div>
</div>
);
);
}
}

View File

@@ -12,7 +12,7 @@ interface accountData {
custname: string;
}
export default function SavingsAccount() {
export default function AccountSummary() {
const router = useRouter();
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [accountData, setAccountData] = useState<accountData[]>([]);
@@ -30,6 +30,7 @@ export default function SavingsAccount() {
const data = await response.json();
if (response.ok && Array.isArray(data)) {
setAccountData(data);
sessionStorage.setItem('accountData',JSON.stringify(data))
}
} catch {
notifications.show({
@@ -67,7 +68,9 @@ export default function SavingsAccount() {
<tr key={index}>
<td style={{ ...cellStyle, textAlign: "left" }}>{acc.custname}</td>
<td style={{ ...cellStyle, textAlign: "left" }}>{acc.stAccountType}</td>
<td style={{ ...cellStyle, textAlign: "right" }}>{acc.stAccountNo}</td>
<td style={{ ...cellStyle, textAlign: "right", color: '#1c7ed6', cursor: "pointer" }}
onClick={() => router.push(`/accounts/account_statement?accNo=${acc.stAccountNo}`)}>
{acc.stAccountNo}</td>
<td style={{ ...cellStyle, textAlign: "right" }}>
{parseFloat(acc.stAvailableBalance).toLocaleString("en-IN", {
minimumFractionDigits: 2,

View File

@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Providers } from "../../providers";
import { notifications } from '@mantine/notifications';
import StatementModel from './statementModel';
interface accountData {
stAccountNo: string;
@@ -17,13 +16,6 @@ interface accountData {
activeAccounts: string;
}
interface statementData {
name: string;
date: string;
amount: string;
type: string;
}
export default function Home() {
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const router = useRouter();
@@ -35,9 +27,6 @@ export default function Home() {
const [selectedLN, setSelectedLN] = useState(loanAccounts[0]?.stAccountNo || "");
const selectedLNData = loanAccounts.find(acc => acc.stAccountNo === selectedLN);
const [showBalance, setShowBalance] = useState(false);
const [openStatement, setOpenStatement] = useState(false);
const [statementData, setStatementData] = useState(null);
const [loading, setLoading] = useState(false);
async function handleFetchUserDetails() {
@@ -75,40 +64,9 @@ export default function Home() {
}
async function handleGetAccountStatement(accountNo: string) {
// e.preventDefault();
setOpenStatement(true);
setLoading(true);
try {
const token = localStorage.getItem("access_token");
const response = await fetch(`api//transactions/account/${accountNo}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
});
const data = await response.json();
if (response.ok) {
console.log(data);
setStatementData(data);
}
else { throw new Error(); }
}
catch {
notifications.show({
withBorder: true,
color: "red",
title: "Please try again later",
message: "Unable to Fetch the statement, Please try again later",
autoClose: 5000,
});
}
finally {
setLoading(false);
}
router.push(`/accounts/account_statement?accNo=${accountNo}`);
}
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
@@ -140,7 +98,7 @@ export default function Home() {
</Group>
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "flex-start", marginLeft: '15px' }}>
<Group grow gap="xl">
<Paper p="md" radius="md" style={{ backgroundColor: '#c1e0f0', width: 350}}>
<Paper p="md" radius="md" style={{ backgroundColor: '#c1e0f0', width: 350 }}>
<Group gap='xs'>
<IconBuildingBank size={25} />
<Text fw={700}>Deposit Account</Text>
@@ -168,13 +126,7 @@ export default function Home() {
<Title order={2} mt="md">
{showBalance ? `${Number(selectedDAData?.stAvailableBalance || 0).toLocaleString('en-IN')}` : "****"}
</Title>
<Button fullWidth mt="xs" onClick={() => handleGetAccountStatement(selectedDA)}>Get Statement</Button>
<StatementModel
opened={openStatement}
onClose={() => setOpenStatement(false)}
loading={loading}
data={statementData} error={''} />
</Paper>
<Paper p="md" radius="md" style={{ backgroundColor: '#c1e0f0', width: 350 }}>
<Group gap='xs'>
@@ -205,11 +157,6 @@ export default function Home() {
{showBalance ? `${Number(selectedLNData?.stAvailableBalance || 0).toLocaleString('en-IN')}` : "****"}
</Title>
<Button fullWidth mt="xs" onClick={() => handleGetAccountStatement(selectedLN)}>Get Statement</Button>
<StatementModel
opened={openStatement}
onClose={() => setOpenStatement(false)}
loading={loading}
data={statementData} error={''} />
</Paper>
<Paper p="md" radius="md" style={{ width: 300, backgroundColor: '#FFFFFF', marginLeft: '130px', border: '1px solid grey' }}>
<Title order={5} mb="sm">Important Links</Title>

View File

@@ -1,51 +0,0 @@
'use client';
import React from "react";
import { Modal, Text, Loader } from "@mantine/core";
type StatementItem = {
date: string;
type: string;
amount: number;
};
interface StatementModalProps {
opened: boolean;
onClose: () => void;
loading: boolean;
error: string;
data: StatementItem[] | null;
}
const StatementModal: React.FC<StatementModalProps> = ({
opened,
onClose,
loading,
error,
data,
}) => {
return (
<Modal opened={opened} onClose={onClose} title="Account Statement" size="lg">
{loading && <Loader />}
{error && <Text c="red">{error}</Text>}
{!loading && !error && data && data.length > 0 && (
<div>
{data.map((item, index) => (
<div key={index} style={{ marginBottom: 10 }}>
<Text>Date: {item.date}</Text>
<Text>Type: {item.type}</Text>
<Text>Amount: {item.amount}</Text>
</div>
))}
</div>
)}
{!loading && !error && data && data.length === 0 && (
<Text>No transactions found.</Text>
)}
</Modal>
);
};
export default StatementModal;

View File

@@ -12,6 +12,7 @@ import { notifications } from '@mantine/notifications';
export default function RootLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const [userLastLoginDetails, setUserLastLoginDetails] = useState(null);
async function handleLogout(e: React.FormEvent) {
@@ -19,6 +20,16 @@ export default function RootLayout({ children }: { children: React.ReactNode })
localStorage.removeItem("access_token");
router.push("/login");
}
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
SetAuthorized(false);
router.push("/login");
}
else {
SetAuthorized(true);
}
}, []);
async function handleFetchUserDetails(e: React.FormEvent) {
e.preventDefault();
@@ -46,7 +57,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
useEffect(() => {
const fetchLoginTime = async () => {
const result = await handleFetchUserDetails({ preventDefault: () => {} } as React.FormEvent);
const result = await handleFetchUserDetails({ preventDefault: () => { } } as React.FormEvent);
if (result) {
setUserLastLoginDetails(result.last_login);
}
@@ -61,114 +72,115 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{ href: "/settings", label: "Settings", icon: IconSettings },
];
return (
<html lang="en">
<body>
<Providers>
<div style={{ backgroundColor: "#e6ffff", height: "100vh", display: "flex", flexDirection: "column", padding: 0, margin: 0 }}>
<Box
style={{
height: "60px",
position: 'relative',
width: '100%',
display: "flex",
justifyContent: "flex-start",
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
}}
>
<Image
fit="cover"
src={logo}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
<Text
if (authorized) {
return (
<html lang="en">
<body>
<Providers>
<div style={{ backgroundColor: "#e6ffff", height: "100vh", display: "flex", flexDirection: "column", padding: 0, margin: 0 }}>
<Box
style={{
position: 'absolute',
top: '50%',
left: '80%',
color: 'white',
textShadow: '1px 1px 2px black',
height: "60px",
position: 'relative',
width: '100%',
display: "flex",
justifyContent: "flex-start",
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
}}
>
<IconPhoneFilled size={20} /> Toll Free No : 1800-180-8008
</Text>
</Box>
<div
style={{
flexShrink: 0,
padding: '0.5rem 1rem',
display: "flex",
justifyContent: 'space-between',
alignItems: "center",
}}
>
<Stack gap={0} align="flex-start">
<Title order={4} style={{ fontFamily: "inter", fontSize: '22px' }}>
Welcome, Rajat Kumar Maharana
</Title>
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}>
Last logged in at {userLastLoginDetails ? new Date(userLastLoginDetails).toLocaleString() : "N/A"}
<Image
fit="cover"
src={logo}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "100%" }}
/>
<Text
style={{
position: 'absolute',
top: '50%',
left: '80%',
color: 'white',
textShadow: '1px 1px 2px black',
}}
>
<IconPhoneFilled size={20} /> Toll Free No : 1800-180-8008
</Text>
</Stack>
</Box>
<Group mt="md" gap="sm">
{navItems.map((item) => {
const isActive = pathname === item.href;
const Icon = item.icon;
return (
<Link key={item.href} href={item.href}>
<Button
leftSection={<Icon size={20} />}
variant={isActive ? "light" : "subtle"}
color={isActive ? "blue" : undefined}
>
{item.label}
</Button>
</Link>
);
})}
<Button leftSection={<IconLogout size={20} />} variant="subtle" onClick={handleLogout}>
Logout
</Button>
</Group>
<div
style={{
flexShrink: 0,
padding: '0.5rem 1rem',
display: "flex",
justifyContent: 'space-between',
alignItems: "center",
}}
>
<Stack gap={0} align="flex-start">
<Title order={4} style={{ fontFamily: "inter", fontSize: '22px' }}>
Welcome, Rajat Kumar Maharana
</Title>
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}>
Last logged in at {userLastLoginDetails ? new Date(userLastLoginDetails).toLocaleString() : "N/A"}
</Text>
</Stack>
<Group mt="md" gap="sm">
{navItems.map((item) => {
const isActive = pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link key={item.href} href={item.href}>
<Button
leftSection={<Icon size={20} />}
variant={isActive ? "light" : "subtle"}
color={isActive ? "blue" : undefined}
>
{item.label}
</Button>
</Link>
);
})}
<Button leftSection={<IconLogout size={20} />} variant="subtle" onClick={handleLogout}>
Logout
</Button>
</Group>
</div>
<Divider size="xs" color='#99c2ff' />
<div
style={{
flex: 1,
overflowY: "auto",
borderTop: '1px solid #ddd',
borderBottom: '1px solid #ddd',
}}
>
{children}
</div>
<Divider size="xs" color='blue' />
<Box
style={{
flexShrink: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f8f9fa",
marginTop: "0.5rem",
}}
>
<Text c="dimmed" size="xs">
© 2025 Kangra Central Co-Operative Bank
</Text>
</Box>
</div>
<Divider size="xs" color='#99c2ff' />
<div
style={{
flex: 1,
overflowY: "auto",
borderTop: '1px solid #ddd',
borderBottom: '1px solid #ddd',
}}
>
{children}
</div>
<Divider size="xs" color='blue' />
<Box
style={{
flexShrink: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f8f9fa",
marginTop: "0.5rem",
}}
>
<Text c="dimmed" size="xs">
© 2025 Kangra Central Co-Operative Bank
</Text>
</Box>
</div>
</Providers>
</body>
</html>
);
</Providers>
</body>
</html>
);
}
}