This commit is contained in:
2025-12-23 11:17:20 +05:30
52 changed files with 5657 additions and 1211 deletions

934
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"exceljs": "^4.4.0",
"html2pdf.js": "^0.12.0", "html2pdf.js": "^0.12.0",
"ib": "file:", "ib": "file:",
"IB": "file:", "IB": "file:",

BIN
public/kccb_watermark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -2,16 +2,36 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
Group, Container,
Paper, Paper,
Select, Select,
Stack, Stack,
Text, Text,
Title, Title,
Group,
Badge,
Divider,
Loader,
Center,
Card,
SimpleGrid,
ThemeIcon,
Box,
rem,
Grid,
} from "@mantine/core"; } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation"; import {
import { useSearchParams } from "next/navigation"; IconCreditCard,
IconWallet,
IconTrendingUp,
IconBuilding,
IconCircleCheck,
IconAlertCircle,
IconUser,
IconFileText,
IconCircleDot,
} from "@tabler/icons-react";
interface accountData { interface accountData {
stAccountNo: string; stAccountNo: string;
@@ -19,23 +39,21 @@ interface accountData {
stAvailableBalance: string; stAvailableBalance: string;
custname: string; custname: string;
stBookingNumber: string; stBookingNumber: string;
stApprovedAmount?: string; // optional for loan accounts stApprovedAmount?: string;
} }
export default function AccountDetails() { export default function App() {
const router = useRouter();
const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]); const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]);
const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null); const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null);
const [authorized, setAuthorized] = useState<boolean | null>(null); const [authorized, setAuthorized] = useState<boolean | null>(null);
const [accountDetails, setAccountDetails] = useState<accountData | null>(null); const [accountDetails, setAccountDetails] = useState<accountData | null>(null);
const searchParams = useSearchParams(); const [loading, setLoading] = useState(false);
const passedAccNo = searchParams.get("accNo");
useEffect(() => { useEffect(() => {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
if (!token) { if (!token) {
setAuthorized(false); setAuthorized(false);
router.push("/login"); // router.push("/login");
} else { } else {
setAuthorized(true); setAuthorized(true);
} }
@@ -51,9 +69,6 @@ export default function AccountDetails() {
value: acc.stAccountNo, value: acc.stAccountNo,
})); }));
setAccountOptions(options); setAccountOptions(options);
if (passedAccNo) {
handleAccountSelection(passedAccNo);
}
} }
} }
}, [authorized]); }, [authorized]);
@@ -62,6 +77,8 @@ export default function AccountDetails() {
setSelectedAccNo(accNo); setSelectedAccNo(accNo);
setAccountDetails(null); setAccountDetails(null);
if (!accNo) return; if (!accNo) return;
setLoading(true);
try { try {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
const response = await fetch("/api/customer", { const response = await fetch("/api/customer", {
@@ -77,11 +94,10 @@ export default function AccountDetails() {
if (response.ok && Array.isArray(data)) { if (response.ok && Array.isArray(data)) {
const matched = data.find((acc) => acc.stAccountNo === accNo); const matched = data.find((acc) => acc.stAccountNo === accNo);
if (matched) { if (matched) {
// Simulate approvedBalance for loan accounts
if (matched.stAccountType.toUpperCase().includes("LN")) { if (matched.stAccountType.toUpperCase().includes("LN")) {
matched.stApprovedAmount = ( matched.stApprovedAmount = (
parseFloat(matched.stAvailableBalance) + 20000 parseFloat(matched.stAvailableBalance) + 20000
).toFixed(2); // dummy logic ).toFixed(2);
} }
setAccountDetails(matched); setAccountDetails(matched);
} else { } else {
@@ -102,79 +118,270 @@ export default function AccountDetails() {
title: "Fetch failed", title: "Fetch failed",
message: "Could not fetch account details. Try again.", message: "Could not fetch account details. Try again.",
}); });
} finally {
setLoading(false);
} }
}; };
const getAccountIcon = (accountType: string) => {
const type = accountType.toUpperCase();
if (type.includes("LN")) return IconTrendingUp;
if (type.includes("SA") || type.includes("SB") || type.includes("CC") || type.includes("OD") || type.includes("CA")) return IconWallet;
return IconBuilding;
};
const getAccountColor = (accountType: string) => {
const type = accountType.toUpperCase();
if (type.includes("LN")) return "violet";
if (type.includes("SA") || type.includes("SB") || type.includes("CC") || type.includes("OD") || type.includes("CA")) return "blue";
return "cyan";
};
const isLoanAccount = accountDetails?.stAccountType.toUpperCase().includes("LN");
const AccountIcon = accountDetails ? getAccountIcon(accountDetails.stAccountType) : IconCreditCard;
const accountColor = accountDetails ? getAccountColor(accountDetails.stAccountType) : "blue";
if (!authorized) return null; if (!authorized) return null;
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
<Title order={3} mb="md">
Account Details
</Title>
<Stack gap="md"> <Grid gutter="md">
<Select {/* Left side Account Selector */}
label="Select Account Number" <Grid.Col span={{ base: 12, md: 4 }}>
placeholder="Choose account number" <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
data={accountOptions} <Title order={4} mb="md">
value={selectedAccNo} Account Selector
onChange={handleAccountSelection} </Title>
searchable <Select
/> label="Select Account Number"
placeholder="Choose an account number"
data={accountOptions}
value={selectedAccNo}
onChange={handleAccountSelection}
searchable
size="sm"
leftSection={<IconCreditCard size={20} />}
styles={{
input: {
borderRadius: rem(12),
},
}}
/>
</Paper>
</Grid.Col>
{accountDetails && ( {/* Right side Account Details */}
<Paper withBorder p="md" radius="md" bg="gray.0"> <Grid.Col span={{ base: 12, md: 8 }}>
<Stack gap="sm"> <Paper
<Group p="apart"> shadow="sm"
<Text size="sm" fw={500} c="dimmed">Account Number</Text> radius="md"
<Text size="md">{accountDetails.stAccountNo}</Text> p="md"
</Group> withBorder
h={500}
style={{ display: "flex", flexDirection: "column", overflow: "auto" }}
>
<Title order={4} mb="md">
Account Details
</Title>
<Group p="apart"> {/* Loading State */}
<Text size="sm" fw={500} c="dimmed">Account Type</Text> {loading && (
<Text size="md">{accountDetails.stAccountType}</Text> <Center style={{ flex: 1 }}>
</Group> <Stack align="center" gap="md">
<Loader size="lg" type="dots" />
<Text c="dimmed">Loading account details...</Text>
</Stack>
</Center>
)}
<Group p="apart"> {/* Account Details */}
<Text size="sm" fw={500} c="dimmed">Description</Text> {!loading && accountDetails && (
<Text size="md">{accountDetails.stBookingNumber}</Text> <Stack gap="sm" style={{ flex: 1 }}>
</Group> {/* Account Header Card */}
<Paper
{/* Show Loan-specific fields */} shadow="md"
{accountDetails.stAccountType.toUpperCase().includes("LN") ? ( p="xs"
<> radius="md"
<Group p="apart"> style={{
<Text size="sm" fw={500} c="dimmed">Approved Balance</Text> background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
<Text size="md" c="gray.8"> }}
{parseFloat(accountDetails.stApprovedAmount || "0").toLocaleString("en-IN", { >
minimumFractionDigits: 2, <Group justify="space-between" align="flex-start" wrap="wrap" mb="xs">
})} <Group gap="sm">
</Text> <ThemeIcon size={48} radius="md" variant="white" color={accountColor}>
<AccountIcon size={28} />
</ThemeIcon>
<Box>
<Text size="xs" c="white" opacity={0.8}>
Account Number
</Text>
<Title order={3} c="white" style={{ letterSpacing: "0.5px" }}>
{accountDetails.stAccountNo}
</Title>
</Box>
</Group> </Group>
<Group p="apart"> <Badge size="md" variant="white" color={accountColor} radius="md">
<Text size="sm" fw={500} c="dimmed">Available Balance</Text> {accountDetails.stAccountType}
<Text size="md" c="red"> </Badge>
{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", { </Group>
minimumFractionDigits: 2,
})} <Divider color="white" opacity={0.2} my="sm" />
</Text>
</Group> <Box>
</> <Text size="xs" c="white" opacity={0.8} mb={4}>
Description
</Text>
<Text c="white">{accountDetails.stBookingNumber}</Text>
</Box>
</Paper>
{/* Balance Cards */}
{isLoanAccount ? (
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
{/* Approved Balance */}
<Card shadow="sm" padding="md" radius="md" withBorder>
<Group gap="xs" mb="xs">
<ThemeIcon size={32} radius="md" variant="light" color="teal">
<IconCircleCheck size={20} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Approved Balance
</Text>
</Group>
<Title order={3} c="dark">
{parseFloat(accountDetails.stApprovedAmount || "0").toLocaleString(
"en-IN",
{
minimumFractionDigits: 2,
}
)}
</Title>
</Card>
{/* Outstanding Amount */}
<Card shadow="sm" padding="md" radius="md" withBorder>
<Group gap="xs" mb="xs">
<ThemeIcon size={32} radius="md" variant="light" color="red">
<IconAlertCircle size={20} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Outstanding Amount
</Text>
</Group>
<Title order={3} c="red">
{parseFloat(accountDetails.stAvailableBalance).toLocaleString(
"en-IN",
{
minimumFractionDigits: 2,
}
)}
</Title>
</Card>
</SimpleGrid>
) : ( ) : (
<Group p="apart"> <Card shadow="sm" padding="md" radius="md" withBorder>
<Text size="sm" fw={500} c="dimmed">Available Balance</Text> <Group gap="xs" mb="xs">
<Text size="md" c="green"> <ThemeIcon size={36} radius="md" variant="light" color="teal">
{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", { <IconWallet size={24} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Available Balance
</Text>
</Group>
<Title order={2} c="teal">
{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
})} })}
</Text> </Title>
</Group> </Card>
)} )}
{/* Account Information */}
<Paper shadow="sm" p="md" radius="md" withBorder>
<Title order={5} mb="md">
Account Information
</Title>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<Stack gap={4}>
<Group gap="xs">
<ThemeIcon size={18} radius="xl" variant="light" color="gray">
<IconUser size={12} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Account Holder
</Text>
</Group>
<Text size="sm">{accountDetails.custname}</Text>
</Stack>
<Stack gap={4}>
<Group gap="xs">
<ThemeIcon size={18} radius="xl" variant="light" color="gray">
<IconCreditCard size={12} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Account Type
</Text>
</Group>
<Text size="sm">{accountDetails.stAccountType}</Text>
</Stack>
<Stack gap={4}>
<Group gap="xs">
<ThemeIcon size={18} radius="xl" variant="light" color="gray">
<IconFileText size={12} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Booking Number
</Text>
</Group>
<Text size="sm">{accountDetails.stBookingNumber}</Text>
</Stack>
<Stack gap={4}>
<Group gap="xs">
<ThemeIcon size={18} radius="xl" variant="light" color="gray">
<IconCircleDot size={12} />
</ThemeIcon>
<Text size="xs" c="dimmed">
Status
</Text>
</Group>
<Badge
color="teal"
variant="light"
size="md"
leftSection={<IconCircleDot size={10} />}
>
Active
</Badge>
</Stack>
</SimpleGrid>
</Paper>
</Stack> </Stack>
</Paper> )}
)}
</Stack> {/* Empty State */}
</Paper> {!loading && !accountDetails && (
<Center style={{ flex: 1 }}>
<Stack align="center" gap="md">
<ThemeIcon size={56} radius="xl" variant="light" color="gray">
<IconCreditCard size={28} />
</ThemeIcon>
<Text c="dimmed" size="sm">
{selectedAccNo
? "No account details available"
: "Please select an account to view details"}
</Text>
</Stack>
</Center>
)}
</Paper>
</Grid.Col>
</Grid>
); );
} }

View File

@@ -1,13 +1,13 @@
"use client"; "use client";
import { Paper, Select, Title, Button, Text, Grid, ScrollArea, Table, Divider, Center, Loader, Stack, Group, Card } from "@mantine/core"; import { Paper, Select, Title, Button, Text, Grid, ScrollArea, Table, Divider, Center, Loader, Stack, Group, Card, ThemeIcon } from "@mantine/core";
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { IconFileSpreadsheet, IconFileText, IconFileTypePdf } from "@tabler/icons-react"; import { IconCopy, IconFileSpreadsheet, IconFileText, IconFileTypePdf } from "@tabler/icons-react";
import { generatePDF } from "@/app/_components/statement_download/PdfGenerator"; import { generatePDF } from "@/app/_components/statement_download/PdfGenerator";
import { generateCSV } from "@/app/_components/statement_download/CsvGenerator"; import { generateExcel } from "@/app/_components/statement_download/CsvGenerator";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
export default function AccountStatementPage() { export default function AccountStatementPage() {
@@ -168,8 +168,8 @@ export default function AccountStatementPage() {
<Grid gutter="md"> <Grid gutter="md">
{/* Left side form */} {/* Left side form */}
<Grid.Col span={{ base: 12, md: 4 }}> <Grid.Col span={{ base: 12, md: 4 }}>
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={4} mb="sm">Account Transactions</Title> <Title order={4} mb="sm">Transaction Filters</Title>
<Select <Select
label="Select Account Number" label="Select Account Number"
placeholder="Choose account number" placeholder="Choose account number"
@@ -202,8 +202,8 @@ export default function AccountStatementPage() {
{/* Right side transaction list */} {/* Right side transaction list */}
<Grid.Col span={{ base: 12, md: 8 }}> <Grid.Col span={{ base: 12, md: 8 }}>
<Paper shadow="sm" radius="md" p="md" withBorder h={400} style={{ display: 'flex', flexDirection: 'column' }}> <Paper shadow="sm" radius="md" p="md" withBorder h={500} style={{ display: 'flex', flexDirection: 'column' }}>
<Title order={5} mb="xs">Account Transactions</Title> <Title order={4} mb="xs">Account Transactions</Title>
<Group justify="space-between" align="center" mt="sm"> <Group justify="space-between" align="center" mt="sm">
<div> <div>
<Text fw={500} ><strong>Account No :</strong> {selectedAccNo}</Text> <Text fw={500} ><strong>Account No :</strong> {selectedAccNo}</Text>
@@ -223,17 +223,34 @@ export default function AccountStatementPage() {
size={22} size={22}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => onClick={() =>
generatePDF(selectedAccNo || "", availableBalance || "0", transactions, generatePDF(
selectedAccNo || "",
availableBalance || "0",
transactions,
localStorage.getItem("remitter_name") || "", localStorage.getItem("remitter_name") || "",
startDate ? dayjs(startDate).format("DD/MM/YYYY") : "", startDate ? dayjs(startDate).format("DD/MM/YYYY") : "",
endDate ? dayjs(endDate).format("DD/MM/YYYY") : "") endDate ? dayjs(endDate).format("DD/MM/YYYY") : "",
localStorage.getItem("remitter_branch_no") || "",
localStorage.getItem("remitter_cif_no") || "",
localStorage.getItem("remitter_address") || ""
)
} }
/> />
<IconFileSpreadsheet <IconFileSpreadsheet
size={22} size={22}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClick={() => onClick={() =>
generateCSV(selectedAccNo || "NA", availableBalance || "0.00", transactions) generateExcel(
selectedAccNo || "",
availableBalance || "0",
transactions,
localStorage.getItem("remitter_name") || "",
startDate ? dayjs(startDate).format("DD/MM/YYYY") : "",
endDate ? dayjs(endDate).format("DD/MM/YYYY") : "",
localStorage.getItem("remitter_branch_no") || "",
localStorage.getItem("remitter_cif_no") || "",
localStorage.getItem("remitter_address") || ""
)
} }
/> />
@@ -261,7 +278,20 @@ export default function AccountStatementPage() {
</Stack> </Stack>
</Center> </Center>
) : transactions.length === 0 ? ( ) : transactions.length === 0 ? (
<p>No transactions found.</p> // <p>No transactions found.</p>
<Center style={{ flex: 1 }}>
<Stack align="center" gap="md">
<ThemeIcon size={56} radius="xl" variant="light" color="gray">
<IconCopy
size={28} />
</ThemeIcon>
<Text c="dimmed" size="sm">
{selectedAccNo
? "No account details available"
: "Please select the filters to get the details"}
</Text>
</Stack>
</Center>
) : isMobile ? ( ) : isMobile ? (
// ✅ Mobile View Card Layout // ✅ Mobile View Card Layout
<Stack gap="sm"> <Stack gap="sm">
@@ -291,13 +321,68 @@ export default function AccountStatementPage() {
) : ( ) : (
// ✅ Desktop View Table Layout // ✅ Desktop View Table Layout
<Table style={{ borderCollapse: "collapse", width: "100%" }}> <Table style={{ borderCollapse: "collapse", width: "100%" }}>
<thead style={{ backgroundColor: "#3385ff" }}> <thead
{/* <tr> style={{
<th style={{ ...cellStyle, textAlign: "left", color: "white" }}>Narration</th> background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
<th style={{ ...cellStyle, textAlign: "left", color: "white" }}>Date</th> position: "sticky",
<th style={{ ...cellStyle, textAlign: "right", color: "white" }}>Amount (₹)</th> top: 0,
<th style={{ ...cellStyle, textAlign: "right", color: "white" }}>Balance (₹)</th> zIndex: 5,
</tr> */} }}
>
<tr>
<th
style={{
...cellStyle,
textAlign: "left",
color: "white",
position: "sticky",
top: 0,
// background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
zIndex: 10,
}}
>
Narration
</th>
<th
style={{
...cellStyle,
textAlign: "left",
color: "white",
position: "sticky",
top: 0,
// background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
zIndex: 10,
}}
>
Date
</th>
<th
style={{
...cellStyle,
textAlign: "right",
color: "white",
position: "sticky",
top: 0,
// background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
zIndex: 10,
}}
>
Amount ()
</th>
<th
style={{
...cellStyle,
textAlign: "right",
color: "white",
position: "sticky",
top: 0,
// background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
zIndex: 10,
}}
>
Balance ()
</th>
</tr>
</thead> </thead>
<tbody> <tbody>
{transactions.map((txn, i) => ( {transactions.map((txn, i) => (

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { Box, Burger, Button, Divider, Drawer, Stack, Text } from '@mantine/core'; import { Box, Burger, Button, Divider, Drawer, ScrollArea, SegmentedControl, Stack, Text } from '@mantine/core';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -16,7 +16,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const [drawerOpened, setDrawerOpened] = useState(false); const [drawerOpened, setDrawerOpened] = useState(false);
const links = [ const links = [
{ label: "Account Summary", href: "/accounts" }, { label: " Account Summary", href: "/accounts" },
{ label: "Account Statement ", href: "/accounts/account_statement" }, { label: "Account Statement ", href: "/accounts/account_statement" },
{ label: "Account Details", href: "/accounts/account_details" }, { label: "Account Details", href: "/accounts/account_details" },
]; ];
@@ -33,55 +33,62 @@ export default function Layout({ children }: { children: React.ReactNode }) {
if (authorized) { if (authorized) {
return ( return (
<Box style={{ display: "flex", height: "100%", flexDirection: isMobile ? "column" : "row" }}> <Box style={{ display: "flex", height: "100%", flexDirection: "column" }}>
{/* Desktop Sidebar */} {/* ---------------- DESKTOP SIDEBAR ---------------- */}
{!isMobile && ( {!isMobile && (
<Box <>
style={{ {/* Segmented Tabs */}
width: "16%", <Box mb="1rem">
backgroundColor: "#c5e4f9", <ScrollArea type="never" offsetScrollbars>
borderRight: "1px solid #ccc", <SegmentedControl
}} fullWidth
> value={pathname}
<Stack style={{ background: "#228be6", height: "10%", alignItems: "center" }}> onChange={(value) => router.push(value)}
<Text fw={700} c="white" style={{ textAlign: "center", marginTop: "10px" }}> data={links.map((link) => ({
My Accounts label: link.label,
</Text> value: link.href,
</Stack> }))}
styles={{
root: {
backgroundColor: "#e9ecef",
borderRadius: 999,
padding: 4,
},
control: {
borderRadius: 999,
transition: "0.3s",
},
<Stack gap="sm" justify="flex-start" style={{ padding: "1rem" }}> indicator: {
{links.map((link) => { borderRadius: 999,
const isActive = pathname === link.href; background: "linear-gradient(90deg, #02a355 0%, #5483c9ff 100%)",
return ( boxShadow: "0 3px 8px rgba(0,0,0,0.15)",
<Text },
key={link.href}
component={Link} label: {
href={link.href} fontSize: 13,
c={isActive ? "darkblue" : "blue"} padding: "6px 10px",
style={{ textAlign: "center",
textDecoration: isActive ? "underline" : "none", fontWeight: 600,
fontWeight: isActive ? 600 : 400, color: "#000",
}} },
> }}
{link.label} />
</Text> </ScrollArea>
); </Box>
})} </>
</Stack>
</Box>
)} )}
{/* Mobile: Burger & Drawer */} {/* ---------------- MOBILE TOP BAR ---------------- */}
{isMobile && ( {isMobile && (
<> <>
{/* Top header with burger and title */}
<Box <Box
style={{ style={{
backgroundColor: "#228be6", background: "linear-gradient(135deg, #1e88e5 0%, #1565c0 100%)",
// padding: "0.5rem 1rem",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
padding: "0.7rem 1rem",
}} }}
> >
<Burger <Burger
@@ -90,11 +97,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
size="sm" size="sm"
color="white" color="white"
/> />
<Text fw={500} c="white"> <Text fw={600} c="white">
My Accounts Accounts
</Text> </Text>
</Box> </Box>
{/* MOBILE DRAWER */}
<Drawer <Drawer
opened={drawerOpened} opened={drawerOpened}
onClose={() => setDrawerOpened(false)} onClose={() => setDrawerOpened(false)}
@@ -103,27 +111,31 @@ export default function Layout({ children }: { children: React.ReactNode }) {
overlayProps={{ color: "black", opacity: 0.55, blur: 3 }} overlayProps={{ color: "black", opacity: 0.55, blur: 3 }}
styles={{ styles={{
root: { root: {
backgroundColor: "#e6f5ff", // soft background for drawer backgroundColor: "#eaf4ff",
// borderLeft: "4px solid #228be6",
// borderRadius: "8px",
}, },
}} }}
> >
{/* Logo and Drawer Header */} {/* Drawer Header */}
<Box style={{ display: "flex", alignItems: "center", marginBottom: "1rem" }}> <Box
<> style={{
<Image src={logo} alt="KCCB Logo" width={40} height={40} style={{ borderRadius: "50%" }} /> display: "flex",
<Text alignItems: "center",
fw={700} marginBottom: "1rem",
ml="10px" }}
style={{ fontSize: "18px", color: "#228be6" }} >
> <Image
My Accounts src={logo}
</Text> alt="KCCB Logo"
</> width={45}
height={45}
style={{ borderRadius: "50%" }}
/>
<Text fw={700} ml="10px" style={{ fontSize: "19px", color: "#1565c0" }}>
Accounts
</Text>
</Box> </Box>
{/* Menu Items */} {/* Drawer Items */}
<Stack gap="sm"> <Stack gap="sm">
{links.map((link) => { {links.map((link) => {
const isActive = pathname === link.href; const isActive = pathname === link.href;
@@ -138,22 +150,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
style={{ style={{
justifyContent: "flex-start", justifyContent: "flex-start",
fontWeight: isActive ? 600 : 400, fontWeight: isActive ? 600 : 400,
textDecoration: isActive ? "underline" : "none", color: isActive ? "#fff" : "#1565c0",
color: isActive ? "#fff" : "#228be6", backgroundColor: isActive ? "#1565c0" : "#dceeff",
backgroundColor: isActive ? "#228be6" : "#dceeff",
borderRadius: "8px", borderRadius: "8px",
padding: "10px 12px", padding: "10px 12px",
transition: "0.3s", transition: "0.2s",
}}
onMouseEnter={(e) => {
const target = e.currentTarget as unknown as HTMLElement;
target.style.backgroundColor = "#228be6";
target.style.color = "#fff";
}}
onMouseLeave={(e) => {
const target = e.currentTarget as unknown as HTMLElement;
target.style.backgroundColor = isActive ? "#228be6" : "#dceeff";
target.style.color = isActive ? "#fff" : "#228be6";
}} }}
onClick={() => setDrawerOpened(false)} onClick={() => setDrawerOpened(false)}
> >
@@ -166,9 +167,14 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</> </>
)} )}
{/* ---------------- CONTENT AREA ---------------- */}
{/* Content Area */} <Box
<Box style={{ flex: 1, padding: isMobile ? "0.5rem" : "1rem", overflowY: "auto" }}> style={{
flex: 1,
padding: isMobile ? "0.5rem" : "1rem",
overflowY: "auto",
}}
>
{children} {children}
</Box> </Box>
</Box> </Box>

View File

@@ -106,7 +106,7 @@ export default function AccountSummary() {
<ScrollArea> <ScrollArea>
<Table style={{ borderCollapse: "collapse", width: "100%" }}> <Table style={{ borderCollapse: "collapse", width: "100%" }}>
<thead> <thead>
<tr style={{ backgroundColor: "#3385ff" }}> <tr style={{ background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)", }}>
<th style={{ ...cellStyle, textAlign: "left" }}>Account Type</th> <th style={{ ...cellStyle, textAlign: "left" }}>Account Type</th>
<th style={{ ...cellStyle, textAlign: "right" }}>Account No.</th> <th style={{ ...cellStyle, textAlign: "right" }}>Account No.</th>
{title.includes("Deposit Accounts (INR)") ? {title.includes("Deposit Accounts (INR)") ?
@@ -124,16 +124,16 @@ export default function AccountSummary() {
if (!authorized) return null; if (!authorized) return null;
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title <Title
order={isMobile ? 4 : 3} order={isMobile ? 4 : 4}
mb="md" mb="md"
style={{ textAlign: isMobile ? "center" : "left" }} style={{ textAlign: isMobile ? "center" : "left" }}
> >
Account Summary Account Summary
</Title> </Title>
{/* Responsive layout: Group for desktop, Stack for mobile */} {/* Responsive layout: Group for desktop, Stack for mobile */}
{isMobile ? ( {isMobile ? (
<Stack gap="md"> <Stack gap="md">
{depositAccounts.length > 0 && {depositAccounts.length > 0 &&

View File

@@ -135,7 +135,7 @@ export default function AddBeneficiaryOthers() {
}, [ifsccode]); }, [ifsccode]);
const validateAndSendOtp = async () => { const validateAndSendOtp = async () => {
if (!bankName || !ifsccode || !branchName || !accountNo || !confirmAccountNo || !beneficiaryType) { if (!bankName || !ifsccode || !branchName || !accountNo || !confirmAccountNo || !beneficiaryType || !nickName) {
notifications.show({ notifications.show({
withBorder: true, withBorder: true,
color: "red", color: "red",
@@ -145,20 +145,18 @@ export default function AddBeneficiaryOthers() {
}); });
return; return;
} }
const trimmedIfsc = ifsccode.trim().toUpperCase(); const trimmedIfsc = ifsccode.trim().toUpperCase();
const isValidIfscCode = (code: string) => { // const isValidIfscCode = (code: string) => {
return /^[A-Z]{4}0[0-9]{6}$/.test(code); // return /^[A-Z]{4}0[0-9]{6}$/.test(code);
}; // };
// if (!isValidIfscCode(trimmedIfsc)) {
if (!isValidIfscCode(trimmedIfsc)) { // notifications.show({
notifications.show({ // title: "Invalid IFSC Code",
title: "Invalid IFSC Code", // message: "Must be 11 characters: 4 uppercase letters, 0, then 6 digits (e.g., HDFC0123456)",
message: "Must be 11 characters: 4 uppercase letters, 0, then 6 digits (e.g., HDFC0123456)", // color: "red",
color: "red", // });
}); // return;
return; // }
}
if (accountNo.length < 10 || accountNo.length > 17) { if (accountNo.length < 10 || accountNo.length > 17) {
notifications.show({ notifications.show({
@@ -185,23 +183,46 @@ export default function AddBeneficiaryOthers() {
try { try {
setLoading(true); setLoading(true);
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
const response = await fetch(`/api/beneficiary/validate/outside-bank?accountNo=${accountNo}&ifscCode=${ifsccode}&remitterName=""`, let response;
{ // IF IFSC starts with "KACE" (within bank)
method: "GET", if (ifsccode.startsWith("KACE")) {
headers: { response = await fetch(
"Content-Type": "application/json", `/api/beneficiary/validate/within-bank?accountNumber=${accountNo}`,
"X-Login-Type": "IB", {
Authorization: `Bearer ${token}`, method: "GET",
}, headers: {
} "Content-Type": "application/json",
); "X-Login-Type": "IB",
Authorization: `Bearer ${token}`,
},
}
);
}
// ELSE: other banks → keep your old API
else {
response = await fetch(
`/api/beneficiary/validate/outside-bank?accountNo=${accountNo}&ifscCode=${ifsccode}&remitterName=""`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "IB",
Authorization: `Bearer ${token}`,
},
}
);
}
const data = await response.json(); const data = await response.json();
if (response.ok && data?.name) { if (response.ok && data?.name) {
setBeneficiaryName(data.name); setBeneficiaryName(data.name);
setValidationStatus("success"); setValidationStatus("success");
setIsVisibilityLocked(true); setIsVisibilityLocked(true);
setOtpSent(true); setOtpSent(true);
const otp = await handleSendOtp();
await handleSendOtp();
notifications.show({ notifications.show({
withBorder: true, withBorder: true,
color: "green", color: "green",
@@ -309,6 +330,7 @@ export default function AddBeneficiaryOthers() {
setConfirmAccountNo(''); setConfirmAccountNo('');
setBeneficiaryName(''); setBeneficiaryName('');
setNickName(''); setNickName('');
setOtp('');
setBeneficiaryType(null); setBeneficiaryType(null);
setIsVisibilityLocked(false); setIsVisibilityLocked(false);
setOtpSent(false); setOtpSent(false);
@@ -408,6 +430,16 @@ export default function AddBeneficiaryOthers() {
withAsterisk withAsterisk
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={4}>
<TextInput
label="Nick Name (Optional)"
placeholder="Enter nickname (optional)"
value={nickName}
required
onChange={(e) => setNickName(e.currentTarget.value)}
/>
</Grid.Col>
<Grid.Col span={4}> <Grid.Col span={4}>
<TextInput <TextInput
label="Beneficiary Name" label="Beneficiary Name"
@@ -417,15 +449,6 @@ export default function AddBeneficiaryOthers() {
required required
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={4}>
<TextInput
label="Nick Name (Optional)"
placeholder="Enter nickname (optional)"
value={nickName}
onChange={(e) => setNickName(e.currentTarget.value)}
/>
</Grid.Col>
{!otpSent && ( {!otpSent && (
<Grid.Col > <Grid.Col >
<Group gap="sm"> <Group gap="sm">
@@ -473,7 +496,7 @@ export default function AddBeneficiaryOthers() {
{!otpVerified ? ( {!otpVerified ? (
<Button onClick={verify_otp}>Validate OTP</Button> <Button onClick={verify_otp}>Validate OTP</Button>
) : ( ) : (
<Button color="blue" size="sm" onClick={AddBen}> <Button size="sm" onClick={AddBen}>
Add Add
</Button> </Button>
)} )}

View File

@@ -4,11 +4,11 @@ import React, { useEffect, useState } from 'react';
import {TextInput,Button,Select,Title,Paper,Grid,Group,Radio,Text,PasswordInput} from '@mantine/core'; import {TextInput,Button,Select,Title,Paper,Grid,Group,Radio,Text,PasswordInput} from '@mantine/core';
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import AddBeneficiaryOthers from './addBeneficiaryOthers'; import AddBeneficiaryOthers from '@/app/(main)/beneficiary/add_beneficiary/addBeneficiaryOthers';
const bankOptions = [ const bankOptions = [
{ value: 'KCCB', label: 'KCCB - The Kangra Central Co-Operative Bank' }, { value: 'KCCB', label: 'KCCB - The Kangra Central Co-Operative Bank Ltd.' },
]; ];
const AddBeneficiary: React.FC = () => { const AddBeneficiary: React.FC = () => {
@@ -189,8 +189,8 @@ const AddBeneficiary: React.FC = () => {
if (!authorized) return null; if (!authorized) return null;
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md">Add Beneficiary</Title> <Title order={4} mb="md">Add Beneficiary</Title>
{/* <Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md"> {/* <Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md">
<Group justify="center"> <Group justify="center">

View File

@@ -0,0 +1,182 @@
"use client";
import {
Box,
Stack,
Text,
SegmentedControl,
ScrollArea,
Burger,
Drawer,
Button,
} from "@mantine/core";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useMediaQuery } from "@mantine/hooks";
import Image from "next/image";
import logo from "@/app/image/logo1.jpg";
export default function Layout({ children }: { children: React.ReactNode }) {
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const router = useRouter();
const pathname = usePathname();
const isMobile = useMediaQuery("(max-width: 768px)");
const [drawerOpened, setDrawerOpened] = useState(false);
/* Beneficiary Options */
const links = [
{ label: "View Beneficiary", href: "/beneficiary" },
{ label: "Add Beneficiary", href: "/beneficiary/add_beneficiary" },
];
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
SetAuthorized(false);
router.push("/login");
} else {
SetAuthorized(true);
}
}, []);
if (!authorized) return null;
return (
<Box style={{ display: "flex", height: "100%", flexDirection: "column" }}>
{/* ---------------- DESKTOP HEADER ---------------- */}
{!isMobile && (
<>
{/* Segmented Tabs */}
<Box mb="1rem">
<ScrollArea type="never" offsetScrollbars>
<SegmentedControl
fullWidth
value={pathname}
onChange={(value) => router.push(value)}
data={links.map((link) => ({
label: link.label,
value: link.href,
}))}
styles={{
root: {
backgroundColor: "#e9ecef",
borderRadius: 999,
padding: 4,
},
control: {
borderRadius: 999,
transition: "0.3s",
},
indicator: {
borderRadius: 999,
background: "linear-gradient(90deg, #02a355 0%, #5483c9ff 100%)",
boxShadow: "0 3px 8px rgba(0,0,0,0.15)",
},
label: {
fontSize: 13,
padding: "6px 10px",
textAlign: "center",
fontWeight: 600,
color: "#000",
},
}}
/>
</ScrollArea>
</Box>
</>
)}
{/* ---------------- MOBILE HEADER ---------------- */}
{isMobile && (
<>
<Box
style={{
backgroundColor: "#228be6",
padding: "0.8rem 1rem",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Burger
opened={drawerOpened}
onClick={() => setDrawerOpened(true)}
size="sm"
color="white"
/>
<Text fw={600} c="white">
Beneficiary
</Text>
</Box>
{/* Drawer for Mobile */}
<Drawer
opened={drawerOpened}
onClose={() => setDrawerOpened(false)}
padding="md"
size="75%"
overlayProps={{ color: "black", opacity: 0.55, blur: 3 }}
>
{/* Drawer Header */}
<Box style={{ display: "flex", alignItems: "center", marginBottom: "1rem" }}>
<Image
src={logo}
alt="KCCB Logo"
width={40}
height={40}
style={{ borderRadius: "50%" }}
/>
<Text fw={700} ml="10px" style={{ color: "#228be6", fontSize: "18px" }}>
Beneficiary
</Text>
</Box>
{/* Drawer Menu Items */}
<Stack gap="sm">
{links.map((link) => {
const isActive = pathname === link.href;
return (
<Button
key={link.href}
component={Link}
href={link.href}
fullWidth
variant="light"
style={{
justifyContent: "flex-start",
backgroundColor: isActive ? "#228be6" : "#e2efff",
color: isActive ? "#fff" : "#1b69c7",
fontWeight: isActive ? 600 : 400,
borderRadius: 10,
padding: "10px",
}}
onClick={() => setDrawerOpened(false)}
>
{link.label}
</Button>
);
})}
</Stack>
</Drawer>
</>
)}
{/* ---------------- CONTENT BODY ---------------- */}
<Box
style={{
flex: 1,
padding: isMobile ? "0.8rem" : "1rem",
overflowY: "auto",
}}
>
{children}
</Box>
</Box >
);
}

View File

@@ -0,0 +1,12 @@
"use client";
import React from "react";
import ViewBeneficiary from "./view_beneficiary/page";
export default function Beneficiary() {
return (
<ViewBeneficiary />
);
}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Center, Group, Loader, Paper, ScrollArea, Table, Text, Title, TextInput, Button, Modal } from "@mantine/core"; import { Center, Group, Loader, Paper, ScrollArea, Table, Text, Title, TextInput, Button, Modal, PasswordInput } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Image from "next/image"; import Image from "next/image";
@@ -165,7 +165,7 @@ export default function ViewBeneficiary() {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Login-Type": "IB", "X-Login-Type": "IB",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}); });
@@ -215,24 +215,42 @@ export default function ViewBeneficiary() {
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md">My Beneficiaries</Title> <Group justify="space-between" align="center" mb="md" wrap="wrap">
<Title order={4}>My Beneficiaries</Title>
<Button
color="green"
variant="filled"
size="sm"
onClick={() => router.push("/beneficiary/add_beneficiary")}
style={{ minWidth: "150px" }}
>
+ Add Beneficiary
</Button>
</Group>
{beneficiaries.length === 0 ? ( {beneficiaries.length === 0 ? (
<Text>No beneficiaries found.</Text> <Text>No beneficiaries found.</Text>
) : ( ) : (
<> <>
<ScrollArea h={300} type="always"> <ScrollArea h={400} type="always">
<Table highlightOnHover withTableBorder stickyHeader style={{ borderCollapse: "collapse", width: "100%" }}> <Table highlightOnHover withTableBorder stickyHeader style={{ borderCollapse: "collapse", width: "100%" }}>
<Table.Thead> <thead style={{
<Table.Tr style={{ backgroundColor: "#3385ff" }}> background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
<Table.Th>Bank</Table.Th> position: "sticky",
top: 0,
zIndex: 5,
}}>
<tr>
<Table.Th >Bank</Table.Th>
<Table.Th style={{ textAlign: "right" }}>Account No</Table.Th> <Table.Th style={{ textAlign: "right" }}>Account No</Table.Th>
<Table.Th>Name</Table.Th> <Table.Th >Name</Table.Th>
<Table.Th>Type</Table.Th> <Table.Th >Type</Table.Th>
<Table.Th style={{ textAlign: "center" }}>IFSC</Table.Th> <Table.Th style={{ textAlign: "center" }}>IFSC</Table.Th>
<Table.Th style={{ textAlign: "center" }}>Action Icon</Table.Th> <Table.Th style={{ textAlign: "center" }}>Action Icon</Table.Th>
</Table.Tr> </tr>
</Table.Thead> </thead>
<Table.Tbody> <Table.Tbody>
{beneficiaries.map((b, i) => ( {beneficiaries.map((b, i) => (
<Table.Tr key={i}> <Table.Tr key={i}>
@@ -295,7 +313,7 @@ export default function ViewBeneficiary() {
) : ( ) : (
<> <>
<Text mb="sm">Enter OTP sent to your registered number:</Text> <Text mb="sm">Enter OTP sent to your registered number:</Text>
<TextInput <PasswordInput
value={otp} value={otp}
onChange={(e) => setOtp(e.currentTarget.value)} onChange={(e) => setOtp(e.currentTarget.value)}
placeholder="Enter OTP" placeholder="Enter OTP"

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { Box, Burger, Button, Divider, Drawer, Stack, Text } from '@mantine/core'; import { Box, Burger, Button, Drawer, ScrollArea, SegmentedControl, Stack, Text } from "@mantine/core";
import { usePathname } from 'next/navigation'; import { usePathname } from "next/navigation";
import Link from 'next/link'; import Link from "next/link";
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Image from "next/image"; import Image from "next/image";
import logo from "@/app/image/logo1.jpg"; import logo from "@/app/image/logo1.jpg";
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from "@mantine/hooks";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const [authorized, SetAuthorized] = useState<boolean | null>(null); const [authorized, SetAuthorized] = useState<boolean | null>(null);
@@ -16,163 +16,167 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const [drawerOpened, setDrawerOpened] = useState(false); const [drawerOpened, setDrawerOpened] = useState(false);
const links = [ const links = [
{ label: " Quick Pay", href: "/funds_transfer" }, { label: "Quick Pay", href: "/funds_transfer" },
{ label: "Add Beneficiary", href: "/funds_transfer/add_beneficiary" }, { label: "Bank Transfer", href: "/funds_transfer/send_beneficiary" },
{ label: "View Beneficiary ", href: "/funds_transfer/view_beneficiary" },
{ label: "Send to Beneficiary", href: "/funds_transfer/send_beneficiary" },
]; ];
useEffect(() => { useEffect(() => {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
if (!token) { if (!token) {
SetAuthorized(false); SetAuthorized(false);
router.push("/login"); router.push("/login");
} } else {
else {
SetAuthorized(true); SetAuthorized(true);
} }
}, []); }, []);
if (authorized) { if (!authorized) return null;
return (
<Box style={{ display: "flex", height: "100%", flexDirection: isMobile ? "column" : "row" }}> return (
{/* Desktop Sidebar */} <Box style={{ display: "flex", height: "100%", flexDirection: "column" }}>
{!isMobile && ( {/* ---------------- DESKTOP SIDEBAR ---------------- */}
{!isMobile && (
<>
{/* Segmented Tabs */}
<Box mb="1rem">
<ScrollArea type="never" offsetScrollbars>
<SegmentedControl
fullWidth
value={pathname}
onChange={(value) => router.push(value)}
data={links.map((link) => ({
label: link.label,
value: link.href,
}))}
styles={{
root: {
backgroundColor: "#e9ecef",
borderRadius: 999,
padding: 4,
},
control: {
borderRadius: 999,
transition: "0.3s",
},
indicator: {
borderRadius: 999,
background: "linear-gradient(90deg, #02a355 0%, #5483c9ff 100%)",
boxShadow: "0 3px 8px rgba(0,0,0,0.15)",
},
label: {
fontSize: 13,
padding: "6px 10px",
textAlign: "center",
fontWeight: 600,
color: "#000",
},
}}
/>
</ScrollArea>
</Box>
</>
)}
{/* ---------------- MOBILE TOP BAR ---------------- */}
{isMobile && (
<>
<Box <Box
style={{ style={{
width: "16%", background: "linear-gradient(135deg, #1e88e5 0%, #1565c0 100%)",
backgroundColor: "#c5e4f9", display: "flex",
borderRight: "1px solid #ccc", alignItems: "center",
justifyContent: "space-between",
padding: "0.7rem 1rem",
}} }}
> >
<Stack style={{ background: "#228be6", height: "10%", alignItems: "center" }}> <Burger
<Text fw={700} c="white" style={{ textAlign: "center", marginTop: "10px" }}> opened={drawerOpened}
Send Money onClick={() => setDrawerOpened(!drawerOpened)}
</Text> size="sm"
</Stack> color="white"
/>
<Stack gap="sm" justify="flex-start" style={{ padding: "1rem" }}> <Text fw={600} c="white">
{links.map((link) => { Send Money
const isActive = pathname === link.href; </Text>
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>
</Box> </Box>
)}
{/* Mobile: Burger & Drawer */} {/* MOBILE DRAWER */}
{isMobile && ( <Drawer
<> opened={drawerOpened}
{/* Top header with burger and title */} onClose={() => setDrawerOpened(false)}
padding="md"
size="75%"
overlayProps={{ color: "black", opacity: 0.55, blur: 3 }}
styles={{
root: {
backgroundColor: "#eaf4ff",
},
}}
>
{/* Drawer Header */}
<Box <Box
style={{ style={{
backgroundColor: "#228be6",
// padding: "0.5rem 1rem",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", marginBottom: "1rem",
}} }}
> >
<Burger <Image
opened={drawerOpened} src={logo}
onClick={() => setDrawerOpened(!drawerOpened)} alt="KCCB Logo"
size="sm" width={45}
color="white" height={45}
style={{ borderRadius: "50%" }}
/> />
<Text fw={500} c="white"> <Text fw={700} ml="10px" style={{ fontSize: "19px", color: "#1565c0" }}>
Send Money Send Money
</Text> </Text>
</Box> </Box>
<Drawer {/* Drawer Items */}
opened={drawerOpened} <Stack gap="sm">
onClose={() => setDrawerOpened(false)} {links.map((link) => {
padding="md" const isActive = pathname === link.href;
size="75%"
overlayProps={{ color: "black", opacity: 0.55, blur: 3 }} return (
styles={{ <Button
root: { key={link.href}
backgroundColor: "#e6f5ff", // soft background for drawer variant="subtle"
// borderLeft: "4px solid #228be6", component={Link}
// borderRadius: "8px", href={link.href}
}, fullWidth
}} style={{
> justifyContent: "flex-start",
{/* Logo and Drawer Header */} fontWeight: isActive ? 600 : 400,
<Box style={{ display: "flex", alignItems: "center", marginBottom: "1rem" }}> color: isActive ? "#fff" : "#1565c0",
<> backgroundColor: isActive ? "#1565c0" : "#dceeff",
<Image src={logo} alt="KCCB Logo" width={40} height={40} style={{ borderRadius: "50%" }} /> borderRadius: "8px",
<Text padding: "10px 12px",
fw={700} transition: "0.2s",
ml="10px" }}
style={{ fontSize: "18px", color: "#228be6" }} onClick={() => setDrawerOpened(false)}
> >
Send Money {link.label}
</Text> </Button>
</> );
</Box> })}
</Stack>
</Drawer>
</>
)}
{/* Menu Items */} {/* ---------------- CONTENT AREA ---------------- */}
<Stack gap="sm"> <Box
{links.map((link) => { style={{
const isActive = pathname === link.href; flex: 1,
padding: isMobile ? "0.5rem" : "1rem",
return ( overflowY: "auto",
<Button }}
key={link.href} >
variant="subtle" {children}
component={Link}
href={link.href}
fullWidth
style={{
justifyContent: "flex-start",
fontWeight: isActive ? 600 : 400,
textDecoration: isActive ? "underline" : "none",
color: isActive ? "#fff" : "#228be6",
backgroundColor: isActive ? "#228be6" : "#dceeff",
borderRadius: "8px",
padding: "10px 12px",
transition: "0.3s",
}}
onMouseEnter={(e) => {
const target = e.currentTarget as unknown as HTMLElement;
target.style.backgroundColor = "#228be6";
target.style.color = "#fff";
}}
onMouseLeave={(e) => {
const target = e.currentTarget as unknown as HTMLElement;
target.style.backgroundColor = isActive ? "#228be6" : "#dceeff";
target.style.color = isActive ? "#fff" : "#228be6";
}}
onClick={() => setDrawerOpened(false)}
>
{link.label}
</Button>
);
})}
</Stack>
</Drawer>
</>
)}
{/* Content Area */}
<Box style={{ flex: 1, padding: isMobile ? "0.5rem" : "1rem", overflowY: "auto" }}>
{children}
</Box>
</Box> </Box>
); </Box>
} );
} }

View File

@@ -115,7 +115,7 @@ export default function QuickPay() {
}); });
const data = await response.json(); const data = await response.json();
if (response.ok && Array.isArray(data)) { if (response.ok && Array.isArray(data)) {
const filterSAaccount = data.filter((acc) => ['SA', 'SB'].includes(acc.stAccountType)); const filterSAaccount = data.filter((acc) => ['SA', 'SB','CC','OD','CA'].includes(acc.stAccountType));
setAccountData(filterSAaccount); setAccountData(filterSAaccount);
} }
} catch { } catch {
@@ -395,12 +395,12 @@ export default function QuickPay() {
</Group> </Group>
</Modal> </Modal>
{/* main content */} {/* main content */}
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md"> <Title order={4} mb="md">
Quick Pay - Own Bank Quick Pay - Own Bank
</Title> </Title>
<div style={{ maxHeight: "290px", overflowY: "auto" }}> <div style={{ maxHeight: "350px", overflowY: "auto" }}>
<Stack gap="xs"> <Stack gap="xs">
<Group grow> <Group grow>
<Select <Select

View File

@@ -143,7 +143,7 @@ export default function SendToBeneficiaryOwn() {
}); });
const data = await response.json(); const data = await response.json();
if (response.ok && Array.isArray(data)) { if (response.ok && Array.isArray(data)) {
const filterSAaccount = data.filter((acc) => ['SA', 'SB'].includes(acc.stAccountType)); const filterSAaccount = data.filter((acc) => ['SA', 'SB','CC','OD','CA'].includes(acc.stAccountType));
setAccountData(filterSAaccount); setAccountData(filterSAaccount);
} }
} catch { } catch {
@@ -395,11 +395,9 @@ export default function SendToBeneficiaryOwn() {
</Group> </Group>
</Modal> </Modal>
{/* main content */} {/* main content */}
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md"> <Title order={4} mb="md">
Send To Beneficiary Send To Beneficiary
</Title> </Title>
<Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md"> <Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md">

View File

@@ -200,7 +200,7 @@ export default function SendToBeneficiaryOthers() {
}); });
const data = await response.json(); const data = await response.json();
if (response.ok && Array.isArray(data)) { if (response.ok && Array.isArray(data)) {
const filterSAaccount = data.filter((acc) => ['SA', 'SB'].includes(acc.stAccountType)); const filterSAaccount = data.filter((acc) => ['SA', 'SB','CC','OD','CA'].includes(acc.stAccountType));
setAccountData(filterSAaccount); setAccountData(filterSAaccount);
} }
} catch { } catch {
@@ -546,7 +546,7 @@ export default function SendToBeneficiaryOthers() {
{/* main content */} {/* main content */}
{!showIntroModal && ( {!showIntroModal && (
<div style={{ maxHeight: "290px", overflowY: "auto" }}> <div style={{ maxHeight: "350px", overflowY: "auto" }}>
<Stack gap={5} justify="flex-start"> <Stack gap={5} justify="flex-start">
<Group grow gap='xs' > <Group grow gap='xs' >
<Select <Select

View File

@@ -21,6 +21,7 @@ export default function Home() {
const [authorized, SetAuthorized] = useState<boolean | null>(null); const [authorized, SetAuthorized] = useState<boolean | null>(null);
const router = useRouter(); const router = useRouter();
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
const [userName, setUserName] = useState<string>("");
const [accountData, SetAccountData] = useState<accountData[]>([]); const [accountData, SetAccountData] = useState<accountData[]>([]);
const depositAccounts = accountData.filter(acc => acc.stAccountType !== "LN"); const depositAccounts = accountData.filter(acc => acc.stAccountType !== "LN");
const [selectedDA, setSelectedDA] = useState(depositAccounts[0]?.stAccountNo || ""); const [selectedDA, setSelectedDA] = useState(depositAccounts[0]?.stAccountNo || "");
@@ -66,7 +67,7 @@ export default function Home() {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
"X-Login-Type": "IB", "X-Login-Type": "IB",
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
}); });
@@ -117,6 +118,9 @@ export default function Home() {
useEffect(() => { useEffect(() => {
if (authorized) { if (authorized) {
handleFetchUserDetails(); handleFetchUserDetails();
const fullName = localStorage.getItem("remitter_name") || "User";
// const first = fullName.split(" ")[0];
setUserName(fullName);
} }
}, [authorized]); }, [authorized]);
@@ -124,38 +128,188 @@ export default function Home() {
return ( return (
<Providers> <Providers>
<Box p={isMobile ? "8px" : "10px"}> <Box p={isMobile ? "8px" : "10px"}>
<Title order={4} style={{ fontSize: isMobile ? "18px" : "22px" }}>
{/* ---------------------- WELCOME CARD ---------------------- */}
<Title
order={2}
style={{
marginTop: "4px",
fontSize: isMobile ? "20px" : "28px",
fontWeight: 800,
background: "linear-gradient(56deg, rgba(16,114,152,1) 0%, rgba(62,230,132,1) 86%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
>
Welcome, {userName}
</Title>
{/* -------------------- ACCOUNT OVERVIEW HEADER -------------------- */}
<Title
order={4}
style={{
fontSize: isMobile ? "18px" : "22px",
marginTop: isMobile ? "6px" : "10px", // ⭐ ADD THIS GAP
}}
>
Accounts Overview Accounts Overview
</Title> </Title>
{/* Show Balance Switch */}
<Text size="sm" c="dimmed" mb={isMobile ? 10 : 14}>
Your accounts at a glance
</Text>
{/* --------------------- SHOW BALANCE TOGGLE ---------------------- */}
<Group <Group
style={{ style={{
flex: 1, marginBottom: isMobile ? "10px" : "14px",
// padding: isMobile ? "5px" : "10px 10px 4px 10px",
marginLeft: isMobile ? 0 : "10px",
display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "flex-start",
height: "auto", gap: 10,
gap: isMobile ? 5 : 10,
}} }}
> >
<IconEye size={isMobile ? 16 : 20} /> <IconEye size={isMobile ? 16 : 20} />
<Text fw={700} style={{ fontFamily: "inter", fontSize: isMobile ? "14px" : "17px" }}> <Text fw={700} style={{ fontSize: isMobile ? "14px" : "18px" }}>
Show Balance Show Balance
</Text> </Text>
<Switch <Switch
size={isMobile ? "sm" : "md"} size={isMobile ? "sm" : "md"}
onLabel="ON"
offLabel="OFF"
checked={showBalance} checked={showBalance}
onChange={(event) => setShowBalance(event.currentTarget.checked)} onChange={(event) => setShowBalance(event.currentTarget.checked)}
/> />
</Group> </Group>
{/* Cards Section */} {/* ----------------------- DESKTOP VIEW ------------------------ */}
{isMobile ? ( {!isMobile && (
<Group align="flex-start" grow gap="md">
{/* ----------------------- DEPOSIT CARD ----------------------- */}
<Paper p="md" radius="md"
style={{
background: "linear-gradient(56deg,rgba(179,214,227,1) 0%, rgba(142,230,179,1) 86%)",
height: 195,
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}
>
<Group gap="xs">
<IconBuildingBank size={25} />
<Text fw={700}>Deposit Account</Text>
<Select
data={depositAccounts.map((acc) => ({
value: acc.stAccountNo,
label: `${acc.stAccountType}- ${acc.stAccountNo}`,
}))}
value={selectedDA}
// @ts-ignore
onChange={setSelectedDA}
size="xs"
styles={{
input: { backgroundColor: "white", color: "black", marginLeft: 5, width: 140 }
}}
/>
</Group>
<Text c="dimmed">{selectedDAData?.stAccountNo}</Text>
<Title order={2}>
{showBalance
? `${Number(selectedDAData?.stAvailableBalance || 0).toLocaleString("en-IN")}`
: "****"}
</Title>
<Button
fullWidth
disabled={loadingAccountNo === selectedDA}
onClick={() => handleGetAccountStatement(selectedDA)}
>
{loadingAccountNo === selectedDA ? "Loading...Please Wait" : "Get Statement"}
</Button>
</Paper>
{/* ----------------------- LOAN CARD ----------------------- */}
<Paper p="md" radius="md"
style={{
background: "linear-gradient(56deg,rgba(179,214,227,1) 0%, rgba(142,230,179,1) 86%)",
height: 195,
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}
>
<Group gap="xs">
<IconBuildingBank size={25} />
<Text fw={700}>Loan Account</Text>
<Select
data={loanAccounts.map((acc) => ({
value: acc.stAccountNo,
label: `${acc.stAccountType}- ${acc.stAccountNo}`,
}))}
value={selectedLN}
// @ts-ignore
onChange={setSelectedLN}
size="xs"
styles={{
input: { backgroundColor: "white", color: "black", marginLeft: 30, width: 140 }
}}
/>
</Group>
<Text c="dimmed">{selectedLNData?.stAccountNo}</Text>
<Title order={2}>
{showBalance
? `${Number(selectedLNData?.stAvailableBalance || 0).toLocaleString("en-IN")}`
: "****"}
</Title>
<Button
fullWidth
disabled={loadingAccountNo === selectedLN}
onClick={() => handleGetAccountStatement(selectedLN)}
>
{loadingAccountNo === selectedLN ? "Loading...Please Wait" : "Get Statement"}
</Button>
</Paper>
{/* ----------------------- QUICK LINKS ----------------------- */}
<Paper p="md" radius="md" style={{ width: 300, backgroundColor: "#FFFFFF", border: "1px solid grey" }}>
<Title order={5} mb="sm">Quick Links</Title>
<Stack gap="xs">
<Button
variant="light"
color="green"
fullWidth
onClick={() => window.open("https://kccbhp.bank.in/about-us/history-of-kccb/", "_blank")}
>
About Us
</Button>
<Button variant="light" color="green" fullWidth component="a" href="/BranchLocator" target="_blank">
Branch Locator
</Button>
<Button variant="light" color="green" fullWidth component="a" href="/ATMLocator" target="_blank">
ATM Locator
</Button>
<Button variant="light" color="green" fullWidth component="a" href="/CustomerCare" target="_blank">
Customer Care
</Button>
<Button variant="light" color="green" fullWidth component="a" href="/FAQs" target="_blank">
FAQs
</Button>
</Stack>
</Paper>
</Group>
)}
{/* ----------------------- MOBILE VERSION STAYS SAME ----------------------- */}
{isMobile && (
<Stack gap="md" style={{ width: "100%", marginTop: "10px" }}> <Stack gap="md" style={{ width: "100%", marginTop: "10px" }}>
{/* Deposit Account Card */} {/* Deposit Account Card */}
<Paper <Paper
@@ -189,7 +343,7 @@ export default function Home() {
backgroundColor: "white", backgroundColor: "white",
color: "black", color: "black",
marginLeft: 5, marginLeft: 5,
width: "120px", width: "150px",
}, },
}} }}
/> />
@@ -262,7 +416,7 @@ export default function Home() {
backgroundColor: "white", backgroundColor: "white",
color: "black", color: "black",
marginLeft: 5, marginLeft: 5,
width: "120px", width: "150px",
}, },
}} }}
/> />
@@ -318,172 +472,42 @@ export default function Home() {
Quick Links Quick Links
</Title> </Title>
<Stack gap="xs"> <Stack gap="xs">
<Button variant="light" color="blue" fullWidth> <Button
Loan EMI Calculator variant="light"
color="green"
fullWidth
onClick={() => window.open("https://kccbhp.bank.in/about-us/history-of-kccb/", "_blank")}
>
About Us
</Button> </Button>
<Button variant="light" color="blue" fullWidth> <Button variant="light" color="green" fullWidth component="a" href="/BranchLocator" target="_blank">
Branch Locator Branch Locator
</Button> </Button>
<Button variant="light" color="blue" fullWidth> <Button variant="light" color="green" fullWidth component="a" href="/ATMLocator" target="_blank">
ATM Locator
</Button>
<Button variant="light" color="green" fullWidth component="a" href="/CustomerCare" target="_blank">
Customer Care Customer Care
</Button> </Button>
<Button variant="light" color="blue" fullWidth> <Button variant="light" color="green" fullWidth component="a" href="/FAQs" target="_blank">
FAQs FAQs
</Button> </Button>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
) : (
<Group grow gap="md" style={{ width: "100%", marginTop: "10px" }}>
{/* Desktop Cards */}
{/* Deposit Account Card */}
<Paper p="md" radius="md" style={{ backgroundColor: "#c1e0f0", width: 350, height: 195, display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
<Group gap="xs">
<IconBuildingBank size={25} />
<Text fw={700}>Deposit Account</Text>
{depositAccounts.length > 0 ? (
<Select
data={depositAccounts.map((acc) => ({
value: acc.stAccountNo,
label: `${acc.stAccountType}- ${acc.stAccountNo}`,
}))}
value={selectedDA}
// @ts-ignore
onChange={setSelectedDA}
size="xs"
styles={{
input: {
backgroundColor: "white",
color: "black",
marginLeft: 5,
width: 140,
},
}}
/>
) : (
<Text c="dimmed" size="sm" ml="sm">
No deposit account available
</Text>
)}
</Group>
{depositAccounts.length > 0 ? (
<>
<Text c="dimmed">{Number(selectedDAData?.stAccountNo || 0)}</Text>
<Title order={2} mt="md">
{showBalance ? `${Number(selectedDAData?.stAvailableBalance || 0).toLocaleString("en-IN")}` : "****"}
</Title>
<Button fullWidth mt="xs"
// loading={loadingAccountNo === selectedDA}
disabled={loadingAccountNo === selectedDA}
onClick={() => handleGetAccountStatement(selectedDA)}
>
{loadingAccountNo === selectedDA ? "Loading... Please Wait" : "Get Statement"}
</Button>
</>
) : (
<>
<Text c="dimmed" mt="md">
Apply for a deposit account to get started
</Text>
<Button fullWidth mt="xs">
Apply Now
</Button>
</>
)}
</Paper>
{/* Loan Account Card */}
<Paper p="md" radius="md" style={{ backgroundColor: "#c1e0f0", width: 355, height: 195, display: "flex", flexDirection: "column", justifyContent: "space-between" }}>
<Group gap="xs">
<IconBuildingBank size={25} />
<Text fw={700}>Loan Account</Text>
{loanAccounts.length > 0 ? (
<Select
data={loanAccounts.map((acc) => ({
value: acc.stAccountNo,
label: `${acc.stAccountType}- ${acc.stAccountNo}`,
}))}
value={selectedLN}
// @ts-ignore
onChange={setSelectedLN}
size="xs"
styles={{
input: {
backgroundColor: "white",
color: "black",
marginLeft: 30,
width: 140,
},
}}
/>
) : (
<Text c="dimmed" size="sm" ml="sm">
No loan account available
</Text>
)}
</Group>
{loanAccounts.length > 0 ? (
<>
<Text c="dimmed">{Number(selectedLNData?.stAccountNo || 0)}</Text>
<Title order={2} mt="md">
{showBalance ? `${Number(selectedLNData?.stAvailableBalance || 0).toLocaleString("en-IN")}` : "****"}
</Title>
<Button fullWidth mt="xs"
// loading={loadingAccountNo === selectedLN}
disabled={loadingAccountNo === selectedLN}
onClick={() => handleGetAccountStatement(selectedLN)}
>
{/* Get Statement */}
{loadingAccountNo === selectedLN ? "Loading...Please Wait.." : "Get Statement"}
</Button>
</>
) : (
<>
<Text c="dimmed" mt="md">
Apply for a loan account to get started
</Text>
<Button fullWidth mt="xs">
Apply Now
</Button>
</>
)}
</Paper>
{/* Important Links Card */}
<Paper p="md" radius="md" style={{ width: 300, backgroundColor: "#FFFFFF", border: "1px solid grey" }}>
<Title order={5} mb="sm">
Quick Links
</Title>
<Stack gap="xs">
<Button variant="light" color="blue" fullWidth>
Loan EMI Calculator
</Button>
<Button variant="light" color="blue" fullWidth>
Branch Locator
</Button>
<Button variant="light" color="blue" fullWidth>
Customer Care
</Button>
<Button variant="light" color="blue" fullWidth>
FAQs
</Button>
</Stack>
</Paper>
</Group>
)} )}
{/* Notes Section */} {/* -------------------- NOTES SECTION (BOTTOM) --------------------- */}
<Box style={{ padding: "5px", display: "flex", justifyContent: "left" }}> <Box mt="md">
<Stack> <Stack>
<Text fw={700}> ** Book Balance includes uncleared effect.</Text> <Text fw={700}> ** Book Balance includes uncleared effect.</Text>
<Text fw={700}> ** Click on the &quot;Show Balance&quot; to display balance for the Deposit and Loan account.</Text> <Text fw={700}> ** Click &quot;Show Balance&quot; to display account balances.</Text>
<Text fw={400} c="red"> <Text fw={400} c="red">
** Your Password will expire in {PassExpiryRemains} days. ** Your Password will expire in {PassExpiryRemains} days.
</Text> </Text>
</Stack> </Stack>
</Box> </Box>
</Box> </Box>
</Providers> </Providers>
); );

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Box, Button, Divider, Group, Image, Modal, Popover, Stack, Text, Title } from '@mantine/core'; import { Anchor, Box, Button, Container, Divider, Group, Image, Modal, Popover, Stack, Switch, Text, Title, Grid } from '@mantine/core';
import { IconBook, IconCurrencyRupee, IconHome, IconLogout, IconPhoneFilled, IconSettings } from '@tabler/icons-react'; import { IconHome, IconLogout, IconMoon, IconSend, IconSettings, IconSun, IconUserCircle, IconUsers, IconWallet } from '@tabler/icons-react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import { Providers } from '../providers'; import { Providers } from '../providers';
@@ -10,6 +10,7 @@ import NextImage from 'next/image';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { useDisclosure, useMediaQuery } from '@mantine/hooks'; import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { fetchAndStoreUserName } from '../_util/userdetails'; import { fetchAndStoreUserName } from '../_util/userdetails';
import styles from './page.module.css';
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
@@ -20,6 +21,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
const [sessionModal, setSessionModal] = useState(false); const [sessionModal, setSessionModal] = useState(false);
const [countdown, setCountdown] = useState(30); // 30 sec countdown before auto logout const [countdown, setCountdown] = useState(30); // 30 sec countdown before auto logout
const [darkMode, setDarkMode] = useState(false);
const firstName = custname ? custname.split(" ")[0] : "";
const [userOpened, setUserOpened] = useState(false); // Manage dropdown visibility
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
@@ -38,6 +42,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
doLogout() doLogout()
router.push("/login"); router.push("/login");
} }
// Toggle Dark/Light Mode
const toggleDarkMode = () => {
setDarkMode((prevMode) => !prevMode);
};
// When reload and click on back then logout // When reload and click on back then logout
useEffect(() => { useEffect(() => {
@@ -173,8 +181,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
const navItems = [ const navItems = [
{ href: "/home", label: "Home", icon: IconHome }, { href: "/home", label: "Home", icon: IconHome },
{ href: "/accounts", label: "Accounts", icon: IconBook }, { href: "/accounts", label: "Accounts", icon: IconWallet },
{ href: "/funds_transfer", label: "Send Money", icon: IconCurrencyRupee }, { href: "/funds_transfer", label: "Fund Transfer", icon: IconSend },
{ href: "/beneficiary", label: "Beneficiaries", icon: IconUsers },
{ href: "/settings", label: "Settings", icon: IconSettings }, { href: "/settings", label: "Settings", icon: IconSettings },
]; ];
@@ -183,113 +192,182 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="en"> <html lang="en">
<body> <body>
<Providers> <Providers>
<Box style={{ backgroundColor: "#e6ffff", minHeight: "100vh", display: "flex", flexDirection: "column", padding: 0, margin: 0 }}> <Box style={{ minHeight: "100%", display: "flex", flexDirection: "column" }}>
{/* HEADER */} {/* HEADER */}
<Box <Box
component="header"
className={styles.header}
style={{ style={{
width: "100%", width: "100%",
display: "flex", padding: isMobile ? "0.6rem 1rem" : "0.8rem 2rem",
height: "60px", background: darkMode
// flexDirection: "row", ? "linear-gradient(15deg, rgba(229, 101, 22, 1) 55%, rgba(28, 28, 30, 1) 100%)"
: "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start",
// padding: isMobile ? "0.5rem" : "0.5rem 1rem",
background: "linear-gradient(15deg,rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
position: "relative",
// position: "fixed",
}}
>
{/* Logo */}
<Box style={{ width: isMobile ? "48px" : "65px", height: isMobile ? "50px" : "60px" }}>
<Image src={logo} component={NextImage} alt="ebanking" style={{ width: "100%", height: "100%" }} />
</Box>
{/* Title & Phone */}
<Stack gap={isMobile ? 2 : 0} style={{ flex: 1, textAlign: isMobile ? "center" : "left", marginLeft: isMobile ? 0 : "1rem" }}>
<Title order={isMobile ? 5 : 2} style={{ color: "white", fontFamily: "Roboto", lineHeight: 1.2 }}>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text style={{ color: "white", fontSize: isMobile ? "0.8rem" : "0.9rem", textShadow: "1px 1px 2px black" }}>
<IconPhoneFilled size={isMobile ? 16 : 20} /> Toll Free No : 1800-180-8008
</Text>
</Stack>
</Box>
{/* WELCOME + NAV */}
<Box
style={{
flexShrink: 0,
padding: isMobile ? "0.5rem" : "0.5rem 1rem",
display: "flex",
flexDirection: isMobile ? "column" : "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: isMobile ? "flex-start" : "center", color: "white",
gap: isMobile ? "0.5rem" : 0, boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
position: "sticky",
top: 0,
zIndex: 200, // ↑ Increased for stability
}} }}
> >
<Stack gap={isMobile ? 2 : 0} align={isMobile ? "flex-start" : "flex-start"}> <Group gap="md" wrap="nowrap">
<Title order={isMobile ? 5 : 4} style={{ fontFamily: "inter", fontSize: isMobile ? "18px" : "22px" }}> <Image
Welcome, {custname ?? null} src={logo}
</Title> component={NextImage}
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: isMobile ? "11px" : "13px" }}> fit="contain"
Last logged in at {userLastLoginDetails ? new Date(userLastLoginDetails).toLocaleString() : "N/A"} alt="ebanking"
</Text> style={{
</Stack> width: isMobile ? "40px" : "60px",
height: "auto",
}}
/>
<Group mt={isMobile ? "sm" : "md"} gap="sm" style={{ flexWrap: isMobile ? "wrap" : "nowrap" }}> <div>
{navItems.map((item) => { <Title
const isActive = pathname.startsWith(item.href); order={isMobile ? 4 : 3}
const Icon = item.icon; style={{
return ( fontFamily: "Roboto",
<Link key={item.href} href={item.href}> color: "white",
<Button marginBottom: 2,
leftSection={<Icon size={isMobile ? 16 : 20} />} fontSize: isMobile ? "14px" : "22px",
variant={isActive ? "dark" : "subtle"} lineHeight: 1.2,
color={isActive ? "blue" : undefined} }}
size={isMobile ? "xs" : "sm"} >
> THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
{item.label} </Title>
</Button>
</Link>
);
})}
<Popover opened={opened} onChange={close} position="bottom-end" withArrow shadow="md"> {!isMobile && (
<Popover.Target> <Text size="xs" c="white" style={{ opacity: 0.85 }}>
<Button leftSection={<IconLogout size={isMobile ? 16 : 20} />} variant="subtle" size={isMobile ? "xs" : "sm"} onClick={open}> Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
)}
</div>
</Group>
{/* USER BUTTON */}
<Popover
opened={userOpened}
onChange={setUserOpened}
position="bottom-end"
withArrow
shadow="md"
>
<Popover.Target>
<Button
leftSection={<IconUserCircle size={isMobile ? 18 : 22} />}
variant="subtle"
onClick={() => setUserOpened((prev) => !prev)}
color='white'
style={{
fontWeight: 500,
padding: isMobile ? "4px 8px" : "6px 12px",
fontSize: isMobile ? "12px" : "14px",
}}
>
Welcome, {firstName}
</Button>
</Popover.Target>
<Popover.Dropdown style={{ minWidth: 230, padding: 15 }}>
<Stack gap="xs">
<Box>
<Text size="sm" fw={700}>{custname}</Text>
<Text size="xs" c="dimmed">Full Name</Text>
</Box>
<Box>
<Text size="sm">
{userLastLoginDetails
? new Date(userLastLoginDetails).toLocaleString()
: "N/A"}
</Text>
<Text size="xs" c="dimmed">Last Login</Text>
</Box>
<Divider />
<Button
leftSection={<IconLogout size={18} />}
onClick={handleLogout}
>
Logout Logout
</Button> </Button>
</Popover.Target> </Stack>
<Popover.Dropdown> </Popover.Dropdown>
<Text size="sm" mb="sm"> </Popover>
Are you sure you want to logout?
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={close}>
Cancel
</Button>
<Button onClick={handleLogout}>Logout</Button>
</Group>
</Popover.Dropdown>
</Popover>
</Group>
</Box> </Box>
<Divider size="xs" color="#99c2ff" /> {/* NAVBAR — desktop unchanged, mobile scrollable */}
<Group
style={{
background: "#c8eeacff",
boxShadow: "0 6px 6px rgba(0,0,0,0.06)",
position: "sticky",
top: isMobile ? 60 : 85,
zIndex: 150,
{/* CHILDREN */} /* MOBILE FIX make it scrollable */
overflowX: isMobile ? "auto" : "visible",
whiteSpace: isMobile ? "nowrap" : "normal",
padding: isMobile ? "6px 4px" : "0.05rem",
}}
>
{navItems.map((item) => {
const isActive = pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link key={item.href} href={item.href} style={{ textDecoration: "none" }}>
<Group
gap={8}
style={{
padding: isMobile ? "10px 14px" : "14px 16px",
// borderRadius: isMobile ? 6 : 8,
width: "100%",
transition: "0.2s ease",
background: isActive ? "rgba(50, 159, 81, 1)" : "transparent",
color: isActive ? "white" : "#3b3b3b",
fontWeight: isActive ? 600 : 500,
}}
>
<Icon
size={isMobile ? 16 : 18}
color={isActive ? "white" : "#3b3b3b"}
/>
<Text size={isMobile ? "xs" : "sm"} fw={500}>
{item.label}
</Text>
</Group>
</Link>
);
})}
</Group>
{/* CONTENT */}
<Box <Box
style={{ style={{
flex: 1, flex: 1,
overflowY: "auto", backgroundColor: "#f5f7fa",
borderTop: "1px solid #ddd", padding: isMobile ? "0.8rem" : "1.5rem",
borderBottom: "1px solid #ddd",
// padding: isMobile ? "0.5rem" : "1rem",
}} }}
> >
{children} <Box
style={{
margin: "0 auto",
background: "white",
padding: isMobile ? "1rem" : "1.8rem",
borderRadius: "12px",
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
minHeight: "75vh",
}}
>
{children}
</Box>
</Box> </Box>
{/* this model for session logout */} {/* this model for session logout */}
<Modal <Modal
opened={sessionModal} opened={sessionModal}
@@ -317,27 +395,113 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</Stack> </Stack>
</Modal> </Modal>
<Divider size="xs" color="blue" /> {/* FOOTER (desktop same, mobile stacked) */}
{/* FOOTER */}
<Box <Box
component="footer"
style={{ style={{
flexShrink: 0, backgroundColor: "rgba(60, 54, 74, 1)",
display: "flex", paddingTop: "2rem",
justifyContent: "center", paddingBottom: "2rem",
alignItems: "center", color: "white",
backgroundColor: "#f8f9fa",
// padding: isMobile ? "0.25rem" : "0.5rem",
}} }}
> >
<Text c="dimmed" size={isMobile ? "xs" : "sm"}> <Container size="xl">
© 2025 The Kangra Central Co-Operative Bank <Grid gutter="xl">
</Text>
<Grid.Col span={{ base: 12, md: 4 }}>
<Group mb="md">
<Box
style={{
width: 40,
height: 40,
background: 'linear-gradient(135deg, #16a34a 0%, #10b981 100%)',
borderRadius: '50%',
overflow: 'hidden',
}}
>
<Image
src={logo}
component={NextImage}
fit="cover"
alt="ebanking"
style={{
width: "100%",
height: "100%",
borderRadius: "50%",
}}
/>
</Box>
<div>
<Text size="sm" fw={500}>The Kangra Central</Text>
<Text size="sm" fw={500}>Co-operative Bank Ltd</Text>
</div>
</Group>
<Text size="sm" c="dimmed">
Serving the community since inception.
</Text>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Text size="sm" fw={500} mb="md">Quick Links</Text>
<Stack gap="xs">
<Anchor
href="https://kccbhp.bank.in/about-us/history-of-kccb/"
size="sm"
c="dimmed"
target="_blank"
rel="noopener noreferrer"
>
About Us
</Anchor>
<Anchor
href="https://kccbhp.bank.in/products/service-products/service-charges/"
size="sm"
c="dimmed"
target="_blank"
rel="noopener noreferrer"
>
Products & Services
</Anchor>
<Anchor
href="/CustomerCare"
size="sm"
c="dimmed"
target="_blank"
rel="noopener noreferrer"
>
Help & Support
</Anchor>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Text size="sm" fw={500} mb="md">Contact Us</Text>
<Stack gap="xs">
<Text size="sm" c="dimmed">Phone: +91-1800-1808008 </Text>
<Text size="sm" c="dimmed">MonSat 10 AM 5 PM</Text>
<Text size="sm" c="dimmed">(The Second and fourth Saturdays are holidays)</Text>
</Stack>
</Grid.Col>
</Grid>
<Text
size="sm"
c="dimmed"
ta="center"
style={{ borderTop: "1px solid rgba(255,255,255,0.2)", marginTop: 20, paddingTop: 20 }}
>
© 2025 The Kangra Central Co-operative Bank Ltd. All rights reserved.
</Text>
</Container>
</Box> </Box>
</Box> </Box>
</Providers> </Providers>
</body> </body>
</html > </html>
); );
} }
} }

View File

@@ -0,0 +1,74 @@
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.8rem 2rem;
background: linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%);
flex-wrap: wrap; /* Allow wrapping on mobile */
}
.header-text {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
text-align: left;
}
.desktop-text {
color: white;
font-family: Roboto, sans-serif;
font-size: 1.5rem;
line-height: 1.2;
}
.desktop-address {
font-family: Roboto, sans-serif;
color: white;
font-size: 0.9rem;
text-shadow: 1px 1px 2px blue;
margin-top: 0.25rem;
}
/* Mobile styles */
.mobile-text {
color: white;
font-family: Roboto, sans-serif;
font-size: 1rem;
line-height: 1.2;
display: none;
text-align: center;
}
/* Media query for mobile */
@media screen and (max-width: 768px) {
.header {
justify-content: center;
padding: 0.5rem 0.75rem;
}
.header-logo {
width: 50px;
margin-bottom: 0.5rem;
}
.header-text {
text-align: center;
flex-direction: column;
}
.desktop-text,
.desktop-address {
display: none;
}
.mobile-text {
display: block;
font-size: 0.9rem;
}
}

View File

@@ -58,10 +58,6 @@ export default function ChangePassword() {
} }
} }
useEffect(() => { useEffect(() => {
regenerateCaptcha(); regenerateCaptcha();
}, []); }, []);
@@ -232,8 +228,8 @@ export default function ChangePassword() {
}; };
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm"> <Title order={4} mb="sm">
Change Login Password Change Login Password
</Title> </Title>
{/* Scrollable form area */} {/* Scrollable form area */}

View File

@@ -240,8 +240,8 @@ export default function ChangePassword() {
}; };
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm"> <Title order={4} mb="sm">
Change Transaction Password Change Transaction Password
</Title> </Title>

View File

@@ -1,15 +1,24 @@
"use client"; "use client";
import { Box, Burger, Button, Divider, Drawer, Stack, Text } from '@mantine/core';
import { usePathname } from 'next/navigation'; import {
import Link from 'next/link'; Box,
import React, { useEffect, useState } from 'react'; Stack,
import { useRouter } from "next/navigation"; Text,
import { useMediaQuery } from '@mantine/hooks'; SegmentedControl,
ScrollArea,
Burger,
Drawer,
Button,
} from "@mantine/core";
import { usePathname, useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useMediaQuery } from "@mantine/hooks";
import Image from "next/image"; import Image from "next/image";
import logo from "@/app/image/logo1.jpg"; import logo from "@/app/image/logo1.jpg";
import Link from "next/link";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
const [authorized, SetAuthorized] = useState<boolean | null>(null); const [authorized, setAuthorized] = useState<boolean | null>(null);
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
@@ -21,159 +30,164 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{ label: "Change transaction Password", href: "/settings/change_txn_password" }, { label: "Change transaction Password", href: "/settings/change_txn_password" },
{ label: "Set transaction Password", href: "/settings/set_txn_password" }, { label: "Set transaction Password", href: "/settings/set_txn_password" },
{ label: "Preferred Name", href: "/settings/user_name" }, { label: "Preferred Name", href: "/settings/user_name" },
{ label: "Set Transaction Limit ", href: "/settings/set_txn_limit" }, { label: "Set Transaction Limit", href: "/settings/set_txn_limit" },
]; ];
useEffect(() => { useEffect(() => {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
if (!token) { if (!token) {
SetAuthorized(false); setAuthorized(false);
router.push("/login"); router.push("/login");
} } else {
else { setAuthorized(true);
SetAuthorized(true);
} }
}, []); }, []);
if (authorized) { if (!authorized) return null;
return (
<Box style={{ display: "flex", height: "100%", flexDirection: isMobile ? "column" : "row" }}> return (
{/* Desktop Sidebar */} <Box
{!isMobile && ( style={{
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
{/* ---------------- DESKTOP HEADER ---------------- */}
{!isMobile && (
<>
<Box mb="1rem">
<ScrollArea type="never" offsetScrollbars>
<SegmentedControl
fullWidth
value={pathname}
onChange={(value) => router.push(value)}
data={links.map((link) => ({
label: link.label,
value: link.href,
}))}
styles={{
root: {
backgroundColor: "#e9ecef",
borderRadius: 999,
padding: 4,
},
control: {
borderRadius: 999,
transition: "0.3s",
},
indicator: {
borderRadius: 999,
background: "linear-gradient(90deg, #02a355 0%, #5483c9ff 100%)",
boxShadow: "0 3px 8px rgba(0,0,0,0.15)",
},
label: {
fontSize: 13,
padding: "6px 10px",
textAlign: "center",
fontWeight: 600,
color: "#000",
},
}}
/>
</ScrollArea>
</Box>
</>
)}
{/* ---------------- MOBILE HEADER ---------------- */}
{isMobile && (
<>
{/* Mobile Header */}
<Box <Box
style={{ style={{
width: "16%", backgroundColor: "#228be6",
backgroundColor: "#c5e4f9", padding: "0.8rem 1rem",
borderRight: "1px solid #ccc", display: "flex",
alignItems: "center",
justifyContent: "space-between",
}} }}
> >
<Stack style={{ background: "#228be6", height: "10%", alignItems: "center" }}> <Burger
<Text fw={700} c="white" style={{ textAlign: "center", marginTop: "10px" }}> opened={drawerOpened}
Settings onClick={() => setDrawerOpened(true)}
</Text> size="sm"
</Stack> color="white"
/>
<Stack gap="sm" justify="flex-start" style={{ padding: "1rem" }}> <Text fw={600} c="white">
{links.map((link) => { Settings
const isActive = pathname === link.href; </Text>
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>
</Box> </Box>
)}
{/* Mobile: Burger & Drawer */} {/* Mobile Drawer */}
{isMobile && ( <Drawer
<> opened={drawerOpened}
{/* Top header with burger and title */} onClose={() => setDrawerOpened(false)}
padding="md"
size="75%"
overlayProps={{ color: "black", opacity: 0.55, blur: 2 }}
>
<Box <Box
style={{ style={{
backgroundColor: "#228be6",
// padding: "0.5rem 1rem",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", marginBottom: "1.2rem",
}} }}
> >
<Burger <Image
opened={drawerOpened} src={logo}
onClick={() => setDrawerOpened(!drawerOpened)} alt="Logo"
size="sm" width={40}
color="white" height={40}
style={{ borderRadius: "50%" }}
/> />
<Text fw={500} c="white"> <Text fw={700} ml="10px" style={{ color: "#228be6", fontSize: "18px" }}>
Settings Settings
</Text> </Text>
</Box> </Box>
<Drawer <Stack gap="xs">
opened={drawerOpened} {links.map((link) => {
onClose={() => setDrawerOpened(false)} const isActive = pathname === link.href;
padding="md"
size="75%" return (
overlayProps={{ color: "black", opacity: 0.55, blur: 3 }} <Button
styles={{ key={link.href}
root: { component={Link}
backgroundColor: "#e6f5ff", // soft background for drawer href={link.href}
}, fullWidth
}} variant="light"
> style={{
{/* Logo and Drawer Header */} justifyContent: "flex-start",
<Box style={{ display: "flex", alignItems: "center", marginBottom: "1rem" }}> backgroundColor: isActive ? "#228be6" : "#e2efff",
<> color: isActive ? "#fff" : "#1b69c7",
<Image src={logo} alt="KCCB Logo" width={40} height={40} style={{ borderRadius: "50%" }} /> fontWeight: isActive ? 600 : 400,
<Text borderRadius: 10,
fw={700} padding: "10px",
ml="10px" }}
style={{ fontSize: "18px", color: "#228be6" }} onClick={() => setDrawerOpened(false)}
> >
Settings {link.label}
</Text> </Button>
</> );
</Box> })}
</Stack>
</Drawer>
</>
)}
{/* Menu Items */} {/* ---------------- CONTENT AREA ---------------- */}
<Stack gap="sm"> <Box
{links.map((link) => { style={{
const isActive = pathname === link.href; flex: 1,
padding: isMobile ? "0.8rem" : "1rem",
return ( overflowY: "auto",
<Button }}
key={link.href} >
variant="subtle" {children}
component={Link}
href={link.href}
fullWidth
style={{
justifyContent: "flex-start",
fontWeight: isActive ? 600 : 400,
textDecoration: isActive ? "underline" : "none",
color: isActive ? "#fff" : "#228be6",
backgroundColor: isActive ? "#228be6" : "#dceeff",
borderRadius: "8px",
padding: "10px 12px",
transition: "0.3s",
}}
onMouseEnter={(e) => {
const target = e.currentTarget as unknown as HTMLElement;
target.style.backgroundColor = "#228be6";
target.style.color = "#fff";
}}
onMouseLeave={(e) => {
const target = e.currentTarget as unknown as HTMLElement;
target.style.backgroundColor = isActive ? "#228be6" : "#dceeff";
target.style.color = isActive ? "#fff" : "#228be6";
}}
onClick={() => setDrawerOpened(false)}
>
{link.label}
</Button>
);
})}
</Stack>
</Drawer>
</>
)}
{/* Content Area */}
<Box style={{ flex: 1, padding: isMobile ? "0.5rem" : "1rem", overflowY: "auto" }}>
{children}
</Box>
</Box> </Box>
); </Box>
} );
} }

View File

@@ -10,9 +10,10 @@ import {
Divider, Divider,
Loader, Loader,
Center, Center,
ActionIcon,
} from "@mantine/core"; } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation"; import { IconEye, IconEyeOff } from "@tabler/icons-react";
// Response structure from backend // Response structure from backend
interface ProfileData { interface ProfileData {
@@ -24,13 +25,13 @@ interface ProfileData {
id: string; id: string;
custaddress: string; custaddress: string;
pincode: string; pincode: string;
} }
export default function ViewProfile() { export default function ViewProfile() {
const router = useRouter();
const [profileData, setProfileData] = useState<ProfileData | null>(null); const [profileData, setProfileData] = useState<ProfileData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showCIF, setShowCIF] = useState(false);
const [showPrimaryID, setShowPrimaryID] = useState(false);
// Fetch API with same style as RootLayout // Fetch API with same style as RootLayout
async function handleFetchProfile() { async function handleFetchProfile() {
@@ -40,7 +41,7 @@ export default function ViewProfile() {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Login-Type": "IB", "X-Login-Type": "IB",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}); });
@@ -79,39 +80,90 @@ export default function ViewProfile() {
handleFetchProfile(); handleFetchProfile();
}, []); }, []);
const maskValue = (value: string) => {
if (!value || value.length <= 4) return value;
return "*".repeat(value.length - 4) + value.slice(-4);
};
const formatDOB = (dob?: string) => {
if (!dob || dob.length !== 8) return dob;
const dd = dob.slice(0, 2);
const mm = dob.slice(2, 4);
const yyyy = dob.slice(4, 8);
return `${dd}-${mm}-${yyyy}`;
};
const formatMobile = (mobile?: string): string => {
if (!mobile) return "";
// If number starts with country code 91
if (mobile.startsWith("91") && mobile.length === 12) {
return `+91 ${mobile.slice(2, 7)} ${mobile.slice(7)}`;
}
// If already 10-digit number
if (mobile.length === 10) {
return `${mobile.slice(0, 5)} ${mobile.slice(5)}`;
}
return mobile; // fallback
};
const Row = ({ const Row = ({
label, label,
value, value,
link, link,
masked,
showValue,
onToggle,
}: { }: {
label: string; label: string;
value: string; value: string;
link?: string; link?: string;
}) => ( masked?: boolean;
<Grid align="flex-start" gutter="xs" mb={6}> showValue?: boolean;
<Grid.Col span={3}> onToggle?: () => void;
<Text c="dimmed" size="sm" fw={500}> }) => {
{label} const displayValue = masked && !showValue ? maskValue(value) : value;
</Text>
</Grid.Col> return (
<Grid.Col span={9}> <Grid align="flex-start" gutter="xs" mb={6}>
{link ? ( <Grid.Col span={3}>
<Anchor size="sm" href={link} target="_blank" rel="noopener noreferrer"> <Text c="dimmed" size="sm" fw={500}>
{value} {label}
</Anchor> </Text>
) : ( </Grid.Col>
<Text size="sm">{value}</Text> <Grid.Col span={9}>
)} <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
</Grid.Col> {link ? (
</Grid> <Anchor size="sm" href={link} target="_blank" rel="noopener noreferrer">
); {displayValue}
</Anchor>
) : (
<Text size="sm">{displayValue}</Text>
)}
{masked && onToggle && (
<ActionIcon
variant="subtle"
size="sm"
onClick={onToggle}
aria-label={showValue ? "Hide" : "Show"}
>
{showValue ? <IconEyeOff size={16} /> : <IconEye size={16} />}
</ActionIcon>
)}
</div>
</Grid.Col>
</Grid>
);
};
return ( return (
<Paper shadow="xs" radius="md" p="md" withBorder h={400}> <Paper shadow="xs" radius="md" p="md" withBorder h={500}>
<Title order={4} mb="sm"> {/* <Title order={4} mb="sm">
View Profile View Profile
</Title> </Title>
<Divider mb="sm" /> <Divider mb="sm" /> */}
{loading ? ( {loading ? (
<Center> <Center>
@@ -119,22 +171,39 @@ export default function ViewProfile() {
</Center> </Center>
) : profileData ? ( ) : profileData ? (
<> <>
<Row label="Customer ID (CIF)" value={profileData.cifNumber} /> {/* Personal Details Section */}
<Row label="Customer Name" value={profileData.custname} /> <Title order={4} mb="sm" mt="md">
<Row label="ID" value={profileData.id} /> Personal Details
<Row label="Branch No" value={profileData.stBranchNo} /> </Title>
<Row label="Date of Birth" value={profileData.custdob} /> <Divider mb="sm" />
<Row label="Mobile Number" value={profileData.mobileno} />
<Row <Row
label="Address" label="Customer ID (CIF)"
value={profileData.custaddress} value={profileData.cifNumber}
// link={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( masked={true}
// profileData.custaddress showValue={showCIF}
// )}`} onToggle={() => setShowCIF(!showCIF)}
/> />
<Row label="Customer Name" value={profileData.custname} />
<Row label="Branch No" value={profileData.stBranchNo} />
<Row label="Date of Birth" value={formatDOB(profileData.custdob) ?? ""} />
<Row label="Mobile Number" value={formatMobile(profileData.mobileno) ?? ""} />
<Row label="Address" value={profileData.custaddress} />
<Row label="Pincode" value={profileData.pincode} /> <Row label="Pincode" value={profileData.pincode} />
{/* KYC Details Section */}
<Title order={4} mb="sm" mt="md">
KYC Details
</Title>
<Divider mb="sm" />
<Row
label="Primary ID"
value={profileData.id}
masked={true}
showValue={showPrimaryID}
// onToggle={() => setShowPrimaryID(!showPrimaryID)}
/>
</> </>
) : ( ) : (
<Text c="red" size="sm"> <Text c="red" size="sm">

View File

@@ -224,8 +224,8 @@ export default function SetTransactionLimit() {
}; };
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400}> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm"> <Title order={4} mb="sm">
Set Transaction Limit Set Transaction Limit
</Title> </Title>
@@ -237,9 +237,13 @@ export default function SetTransactionLimit() {
value={limit} value={limit}
onChange={(e) => { onChange={(e) => {
const val = e.currentTarget.value; const val = e.currentTarget.value;
// Only allow digits
// Allow only digits AND max value 500000
if (/^\d*$/.test(val)) { if (/^\d*$/.test(val)) {
setLimit(val); const num = Number(val);
if (num <= 500000) {
setLimit(val);
}
} }
}} }}
leftSection={icon} leftSection={icon}
@@ -247,6 +251,7 @@ export default function SetTransactionLimit() {
mb="sm" mb="sm"
readOnly={step !== "form"} readOnly={step !== "form"}
/> />
</Group> </Group>
{dailyLimit !== null ? ( {dailyLimit !== null ? (
<Text size="xs" c="green"> <Text size="xs" c="green">

View File

@@ -240,8 +240,8 @@ export default function ChangePassword() {
}; };
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder h={400} > <Paper shadow="sm" radius="md" p="md" withBorder h={500} >
<Title order={3} mb="sm"> <Title order={4} mb="sm">
Set Transaction Password Set Transaction Password
</Title> </Title>

View File

@@ -261,8 +261,8 @@ export default function SetPreferredNameSimple() {
if (loading) return <Text>Loading...</Text>; if (loading) return <Text>Loading...</Text>;
return ( return (
<Paper shadow="sm" radius="md" p="md" withBorder> <Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm"> <Title order={4} mb="sm">
Set Preferred Name Set Preferred Name
</Title> </Title>

232
src/app/ATMLocator/page.tsx Normal file
View File

@@ -0,0 +1,232 @@
"use client";
import { useEffect, useState } from "react";
import { Box, Button, Group, Stack, Text, Paper, Collapse, Title, Image } from "@mantine/core";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import { useMediaQuery } from "@mantine/hooks";
import NextImage from "next/image";
import logo from "@/app/image/logo1.jpg";
export default function ATMListPage() {
const [atms, setAtms] = useState([]);
const [openedIndex, setOpenedIndex] = useState<number | null>(null);
const [authorized, setAuthorized] = useState<boolean | null>(null);
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
// ✔ Authorization Check
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
setAuthorized(false);
router.push("/login");
} else {
setAuthorized(true);
}
}, []);
// ✔ Fetch ATM Data
useEffect(() => {
async function fetchData() {
try {
const token = localStorage.getItem("access_token");
const response = await fetch("/api/atm", {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "IB",
Authorization: `Bearer ${token}`,
},
});
const result = await response.json();
const list = Array.isArray(result)
? result
: Array.isArray(result.data)
? result.data
: [];
setAtms(list);
} catch (error) {
console.error("ATM list API error:", error);
setAtms([]);
}
}
if (authorized) fetchData();
}, [authorized]);
// Wait until auth is checked
if (authorized === null) return null;
const toggleRow = (index: number) => {
setOpenedIndex((prev) => (prev === index ? null : index));
};
return (
<Box
style={{
height: "100vh",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
{/* HEADER */}
<Box
component="header"
style={{
width: "100%",
padding: isMobile ? "0.6rem 1rem" : "0.8rem 2rem",
background:
"linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
color: "white",
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
flexShrink: 0,
}}
>
<Group gap="md" wrap="nowrap" align="center">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{ width: isMobile ? "45px" : "60px", height: "auto" }}
/>
<div>
<Title
order={isMobile ? 4 : 3}
style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
{!isMobile && (
<Text size="xs" c="white" style={{ opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
)}
</div>
</Group>
</Box>
{/* SECOND FIXED BAR */}
<Box
style={{
width: "100%",
padding: isMobile ? "0.5rem 1rem" : "0.6rem 1.5rem",
background: "#ffffff",
borderBottom: "1px solid #ddd",
position: "sticky",
top: isMobile ? "65px" : "70px",
zIndex: 200,
display: "flex",
alignItems: "center",
}}
>
<Text
fw={700}
size={isMobile ? "md" : "lg"}
ta="center"
style={{ flex: 1 }}
>
List of Available ATMs
</Text>
<Button
variant="light"
color="red"
size={isMobile ? "xs" : "sm"}
onClick={() => window.close()}
style={{ marginLeft: "auto" }}
>
Close
</Button>
</Box>
{/* SCROLLABLE CONTENT */}
<Box
style={{
flex: 1,
overflowY: "auto",
padding: isMobile ? "0.5rem" : "1rem",
paddingTop: isMobile ? "60px" : "40px",
}}
>
<Stack>
{atms.map((atm: any, index: number) => {
const isOpen = openedIndex === index;
return (
<Paper
key={index}
p={isMobile ? "sm" : "md"}
radius="md"
withBorder
shadow="xs"
>
<Group justify="space-between" wrap="nowrap">
<Text fw={600} size={isMobile ? "sm" : "md"}>
{atm.name}
</Text>
<Button
variant="subtle"
size={isMobile ? "xs" : "md"}
onClick={() => toggleRow(index)}
leftSection={
isOpen ? <IconChevronUp size={18} /> : <IconChevronDown size={18} />
}
>
{isOpen ? "Hide" : "View"}
</Button>
</Group>
{/* ATM Extra Info */}
<Collapse in={isOpen}>
<Box mt="md" ml="md">
<Text size={isMobile ? "xs" : "sm"}>
<b>ATM Name:</b> {atm.name}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Availability:</b> 24x7
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Type:</b> Cash Withdrawal Only
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Status:</b>{" "}
<span style={{ color: "green", fontWeight: 600 }}>Active</span>
</Text>
</Box>
</Collapse>
</Paper>
);
})}
</Stack>
</Box>
{/* FOOTER */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: isMobile ? "8px" : "12px 0",
background: "#ffffff",
borderTop: "1px solid #ddd",
flexShrink: 0,
}}
>
<Text size={isMobile ? "xs" : "sm"} c="dimmed">
© 2025 KCC Bank. All rights reserved.
</Text>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,254 @@
"use client";
import { useEffect, useState } from "react";
import { Box, Button, Group, Stack, Text, Paper, Collapse, Title, Image } from "@mantine/core";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import NextImage from 'next/image';
import logo from '@/app/image/logo1.jpg';
import { useRouter } from "next/navigation";
import { useMediaQuery } from "@mantine/hooks";
export default function BranchListPage() {
const [branches, setBranches] = useState([]);
const [openedIndex, setOpenedIndex] = useState<number | null>(null);
const [authorized, SetAuthorized] = useState<boolean | null>(null);
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
// Fetch branch API
useEffect(() => {
async function fetchData() {
try {
const token = localStorage.getItem("access_token");
const response = await fetch('api/branch', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
"X-Login-Type": "IB",
'Authorization': `Bearer ${token}`
},
});
const result = await response.json();
// Handle any response shape
const list =
Array.isArray(result) ? result :
Array.isArray(result.data) ? result.data :
Array.isArray(result.branches) ? result.branches :
[];
setBranches(list);
} catch (error) {
console.error("Error fetching branches:", error);
setBranches([]);
}
}
fetchData();
}, []);
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
SetAuthorized(false);
router.push("/login");
}
else {
SetAuthorized(true);
}
}, []);
const toggleRow = (index: number) => {
setOpenedIndex((prev) => (prev === index ? null : index));
};
if (authorized === null) {
return null; // Prevent rendering until check is done
}
return (
<Box
style={{
height: "100vh",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
{/* HEADER */}
<Box
component="header"
style={{
width: "100%",
padding: isMobile ? "0.6rem 1rem" : "0.8rem 2rem",
background:
"linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
alignItems: isMobile ? "flex-start" : "center",
justifyContent: "space-between",
color: "white",
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
flexShrink: 0,
}}
>
<Group gap="md" wrap="nowrap" align="center">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{ width: isMobile ? "45px" : "60px", height: "auto" }}
/>
<div>
<Title
order={isMobile ? 4 : 3}
style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
{!isMobile && (
<Text size="xs" c="white" style={{ opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
)}
</div>
</Group>
</Box>
{/* SECOND FIXED BAR */}
<Box
style={{
width: "100%",
padding: isMobile ? "0.5rem 1rem" : "0.6rem 1.5rem",
background: "#ffffff",
borderBottom: "1px solid #ddd",
position: "sticky",
top: isMobile ? "65px" : "70px",
zIndex: 200,
display: "flex",
alignItems: "center",
}}
>
<Text
fw={700}
size={isMobile ? "md" : "lg"}
ta="center"
style={{ flex: 1 }}
>
List of Branches
</Text>
<Button
variant="light"
color="red"
size={isMobile ? "xs" : "sm"}
onClick={() => window.close()}
style={{ marginLeft: "auto" }}
>
Close
</Button>
</Box>
{/* SCROLLABLE CONTENT */}
<Box
style={{
flex: 1,
overflowY: "auto",
padding: isMobile ? "0.5rem" : "1rem",
paddingTop: isMobile ? "60px" : "40px",
boxShadow: "0 4px 12px rgba(0,0,0,0.08)"
}}>
<Stack>
{branches.map((branch: any, index: number) => {
const isOpen = openedIndex === index;
return (
<Paper
key={branch.branch_code}
p={isMobile ? "sm" : "md"}
radius="md"
withBorder
shadow="xs"
>
<Group justify="space-between" wrap="nowrap">
<Text fw={600} size={isMobile ? "sm" : "md"}>
{branch.branch_name}
</Text>
<Button
variant="subtle"
size={isMobile ? "xs" : "md"}
onClick={() => toggleRow(index)}
leftSection={
isOpen ? (
<IconChevronUp size={18} />
) : (
<IconChevronDown size={18} />
)
}
>
{isOpen ? "Hide" : "View"}
</Button>
</Group>
<Collapse in={isOpen} transitionDuration={200}>
<Box mt="md" ml="md">
<Text size={isMobile ? "xs" : "sm"}>
<b>Branch Code:</b> {branch.branch_code}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Zone:</b> {branch.zone}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Tehsil:</b> {branch.tehsil}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Block:</b> {branch.block}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>District:</b> {branch.distt_name}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<b>Pincode:</b> {branch.pincode}
</Text>
<Text size={isMobile ? "xs" : "sm"}>
<span style={{ fontWeight: 600, color: "black" }}>
Telephone:
</span>{" "}
<span style={{ color: "blue" }}>
{branch.telephone_no}
</span>
</Text>
</Box>
</Collapse>
</Paper>
);
})}
</Stack>
</Box>
{/* FOOTER */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: isMobile ? "8px" : "12px 0",
background: "#ffffff",
borderTop: "1px solid #ddd",
flexShrink: 0,
}}
>
<Text size={isMobile ? "xs" : "sm"} c="dimmed">
© 2025 KCC Bank. All rights reserved.
</Text>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,243 @@
"use client";
import { useEffect, useState } from "react";
import {
Box,
Button,
Group,
Stack,
Text,
Paper,
Title,
Image,
} from "@mantine/core";
import { IconMail, IconPhone, IconChevronLeft } from "@tabler/icons-react";
import { useMediaQuery } from "@mantine/hooks";
import { useRouter } from "next/navigation";
import NextImage from "next/image";
import logo from "@/app/image/logo1.jpg";
export default function EnquiryPage() {
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
// FAKE CONTACT LIST — replace with your API if needed
const contacts = [
{
title: "Chairman",
email: "chairman@kccb.in",
phone: "01892-222677",
},
{
title: "Managing Director",
email: "md@kccb.in",
phone: "01892-224960",
},
{
title: "General Manager (West)",
email: "gmw@kccb.in",
phone: "01892-223280",
},
{
title: "General Manager (North)",
email: "gmn@kccb.in",
phone: "01892-224607",
},
];
return (
<Box
style={{
height: "100vh",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
{/* HEADER */}
<Box
component="header"
style={{
width: "100%",
padding: isMobile ? "0.6rem 1rem" : "0.8rem 2rem",
background:
"linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
color: "white",
display: "flex",
alignItems: "center",
gap: "1rem",
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
flexShrink: 0,
}}
>
<IconChevronLeft
size={28}
style={{ cursor: "pointer" }}
onClick={() => router.back()}
/>
<Group gap="md" wrap="nowrap" align="center">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{
width: isMobile ? "45px" : "60px",
height: "auto",
}}
/>
<div>
<Title
order={isMobile ? 4 : 3}
style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
{!isMobile && (
<Text size="xs" style={{ color: "white", opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
)}
</div>
</Group>
</Box>
{/* SECOND BAR */}
<Box
style={{
width: "100%",
padding: isMobile ? "0.5rem 1rem" : "0.6rem 1.5rem",
background: "#ffffff",
borderBottom: "1px solid #ddd",
position: "sticky",
top: isMobile ? "65px" : "70px",
zIndex: 200,
display: "flex",
alignItems: "center",
}}
>
<Text
fw={700}
size={isMobile ? "md" : "lg"}
ta="center"
style={{ flex: 1 }}
>
Customer Care
</Text>
<Button
variant="light"
color="red"
size={isMobile ? "xs" : "sm"}
onClick={() => window.close()}
style={{ marginLeft: "auto" }}
>
Close
</Button>
</Box>
{/* BODY CONTENT */}
<Box
style={{
flex: 1,
overflowY: "auto",
padding: isMobile ? "1rem" : "2rem",
background: "#f8f9fa",
}}
>
<Box
style={{
width: "100%",
maxWidth: "1600px",
margin: "0 auto",
}}
>
{/* Complaint Form */}
<Paper
withBorder
p="md"
radius="md"
shadow="sm"
style={{
marginBottom: "1.5rem",
background: "white",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text fw={600}>Complaint Form</Text>
<Button
size="xs"
variant="subtle"
onClick={() => window.open("https://kccbhp.bank.in/complaint-form/", "_blank")}
>
Register Complain
</Button>
</Paper>
{/* Key Contacts Title */}
<Text fw={700} size="lg" mb="sm">
Key Contacts
</Text>
{/* 2-COLUMN GRID */}
<Box
style={{
display: "grid",
gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr",
gap: "1.2rem",
}}
>
{contacts.map((contact, index) => (
<Paper
key={index}
withBorder
p="lg"
radius="md"
shadow="md"
style={{
background: "white",
minHeight: "120px",
}}
>
<Text fw={600} mb="xs" size="md">
{contact.title}
</Text>
<Group gap="xs" mb={6}>
<IconMail size={18} />
<Text size="sm">{contact.email}</Text>
</Group>
<Group gap="xs">
<IconPhone size={18} />
<Text size="sm">{contact.phone}</Text>
</Group>
</Paper>
))}
</Box>
</Box>
</Box>
{/* FOOTER */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: isMobile ? "8px" : "12px 0",
background: "#ffffff",
borderTop: "1px solid #ddd",
flexShrink: 0,
}}
>
<Text size={isMobile ? "xs" : "sm"} c="dimmed">
© 2025 KCC Bank. All rights reserved.
</Text>
</Box>
</Box>
);
}

243
src/app/FAQs/page.tsx Normal file
View File

@@ -0,0 +1,243 @@
"use client";
import { useEffect, useState } from "react";
import {
Box,
Button,
Group,
Stack,
Text,
Paper,
Collapse,
Title,
Image,
} from "@mantine/core";
import { IconChevronLeft, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { useMediaQuery } from "@mantine/hooks";
import { useRouter } from "next/navigation";
import NextImage from "next/image";
import logo from "@/app/image/logo1.jpg";
export default function FAQPage() {
const isMobile = useMediaQuery("(max-width: 768px)");
const router = useRouter();
const [openedIndex, setOpenedIndex] = useState<number | null>(null);
const toggleRow = (index: number) => {
setOpenedIndex((prev) => (prev === index ? null : index));
};
// FAQ DATA
const faqs = [
{
question: "How do I log in to the Internet Banking portal?",
answer:
"Open the login page and enter your User ID and Password. You may also be required to verify using an OTP sent to your registered mobile number.",
},
{
question: "Is my banking information secure on this app?",
answer:
"Yes. All transactions are fully encrypted. We use multi-factor authentication, secure session management, and industry-standard protection mechanisms.",
},
{
question: "How can I check my account balance?",
answer:
"Go to the Accounts section after logging in. All your linked accounts and available balances will be displayed instantly.",
},
{
question: "Can I transfer money to other bank accounts?",
answer:
"Yes. You can transfer funds using NEFT, RTGS, IMPS, or within-bank transfers. Add a beneficiary before initiating a transfer.",
},
{
question: "How do I view my transaction history?",
answer:
"Open the 'Account Statement' or 'Transactions' tab. You can filter by date, download a PDF.",
},
// {
// question: "What should I do if I forget my Internet Banking password?",
// answer:
// "Use the 'Forgot Password' option on the login page and follow the steps to reset your password using OTP verification.",
// },
{
question: "How do I update my mobile number or email ID?",
answer:
"You can update your contact details by visiting your nearest branch.",
},
{
question: "What should I do if my account gets locked?",
answer:
"Your account may lock after multiple incorrect login attempts. To unlock your Internet Banking access, please visit your nearest KCC Bank branch or contact customer care.",
},
];
return (
<Box
style={{
height: "100vh",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
{/* HEADER */}
<Box
component="header"
style={{
width: "100%",
padding: isMobile ? "0.6rem 1rem" : "0.8rem 2rem",
background:
"linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
color: "white",
display: "flex",
alignItems: "center",
gap: "1rem",
boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
flexShrink: 0,
}}
>
<IconChevronLeft
size={28}
style={{ cursor: "pointer" }}
onClick={() => router.back()}
/>
<Group gap="md" wrap="nowrap" align="center">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{
width: isMobile ? "45px" : "60px",
height: "auto",
}}
/>
<div>
<Title
order={isMobile ? 4 : 3}
style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
{!isMobile && (
<Text size="xs" style={{ color: "white", opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
)}
</div>
</Group>
</Box>
{/* SECOND BAR */}
<Box
style={{
width: "100%",
padding: isMobile ? "0.5rem 1rem" : "0.6rem 1.5rem",
background: "#ffffff",
borderBottom: "1px solid #ddd",
position: "sticky",
top: isMobile ? "65px" : "70px",
zIndex: 200,
display: "flex",
alignItems: "center",
}}
>
<Text
fw={700}
size={isMobile ? "md" : "lg"}
ta="center"
style={{ flex: 1 }}
>
Frequently Asked Questions
</Text>
<Button
variant="light"
color="red"
size={isMobile ? "xs" : "sm"}
onClick={() => window.close()}
style={{ marginLeft: "auto" }}
>
Close
</Button>
</Box>
{/* BODY CONTENT */}
<Box
style={{
flex: 1,
overflowY: "auto",
padding: isMobile ? "1rem" : "2rem",
background: "#f8f9fa",
}}
>
<Box
style={{
width: "100%",
maxWidth: "900px",
margin: "0 auto",
}}
>
<Stack>
{faqs.map((item, index) => {
const isOpen = openedIndex === index;
return (
<Paper
key={index}
withBorder
radius="md"
p="md"
shadow="xs"
style={{ cursor: "pointer", background: "white" }}
>
{/* QUESTION */}
<Group
justify="space-between"
onClick={() => toggleRow(index)}
>
<Text fw={600} size={isMobile ? "sm" : "md"}>
{item.question}
</Text>
{isOpen ? (
<IconChevronUp size={20} />
) : (
<IconChevronDown size={20} />
)}
</Group>
{/* ANSWER */}
<Collapse in={isOpen}>
<Text size={isMobile ? "xs" : "sm"} mt="sm" c="dimmed">
{item.answer}
</Text>
</Collapse>
</Paper>
);
})}
</Stack>
</Box>
</Box>
{/* FOOTER */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: isMobile ? "8px" : "12px 0",
background: "#ffffff",
borderTop: "1px solid #ddd",
flexShrink: 0,
}}
>
<Text size={isMobile ? "xs" : "sm"} c="dimmed">
© 2025 KCC Bank. All rights reserved.
</Text>
</Box>
</Box>
);
}

View File

@@ -396,7 +396,7 @@ export default function SetLoginPwd() {
}} }}
> >
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
© 2025 The Kangra Central Co-Operative Bank © 2025 The Kangra Central Co-Operative Bank Ltd.
</Text> </Text>
</Box> </Box>
</div> </div>

View File

@@ -1,60 +1,619 @@
"use client"; "use client";
import dayjs from "dayjs";
interface CsvGeneratorProps { export const generateExcel = (
accountNo: string;
balance: string;
txns: any[];
}
export const generateCSV = (
accountNo: string, accountNo: string,
balance: string, balance: string,
txns: any[] txns: any[],
customerName: string,
periodFrom: string,
periodTo: string,
branchCode: string,
cifNumber: string,
address: string
) => { ) => {
// CSV Header // Import ExcelJS dynamically
let csv = `Bank Statement\n`; const ExcelJS = require("exceljs");
csv += `Account No:,${accountNo}\n`;
csv += `Available Balance:,${balance}\n`;
csv += `Generated:,${new Date().toLocaleString()}\n\n`;
// Column headers // Create a new workbook and worksheet
csv += "Date,Name,Type,Amount\n"; const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Account Statement", {
// Rows pageSetup: {
txns.forEach((txn) => { paperSize: 9, // A4
csv += `${txn.date},${txn.name},${txn.type.toLowerCase()},${txn.amount}\n`; orientation: "portrait",
fitToPage: true,
fitToWidth: 1,
margins: {
left: 0.5,
right: 0.5,
top: 0.75,
bottom: 0.75,
header: 0.3,
footer: 0.3,
},
},
views: [{ showGridLines: false }],
}); });
// Trigger browser download let currentRow = 1;
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a"); /* ---------------------------------------------------------
link.setAttribute("href", url); * 1) HEADER SECTION
link.setAttribute("download", `statement_${accountNo}.csv`); * --------------------------------------------------------- */
document.body.appendChild(link);
link.click(); // Bank Name
document.body.removeChild(link); const headerRow = worksheet.getRow(currentRow);
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const headerCell = worksheet.getCell(`A${currentRow}`);
headerCell.value = "THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.";
headerCell.font = {
name: "Times New Roman",
size: 16,
bold: true,
color: { argb: "FF1a5f3a" },
};
headerCell.alignment = { vertical: "middle", horizontal: "center" };
headerRow.height = 25;
currentRow++;
// Bank Address
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const addressCell = worksheet.getCell(`A${currentRow}`);
addressCell.value = "Head Office: Dharmsala, District Kangra (H.P.), Pin. 176215";
addressCell.font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
addressCell.alignment = { vertical: "middle", horizontal: "center" };
currentRow++;
// e-Statement Service & Date
worksheet.mergeCells(`A${currentRow}:B${currentRow}`);
const eStatementCell = worksheet.getCell(`A${currentRow}`);
eStatementCell.value = "e-Statement Service";
eStatementCell.font = {
name: "Times New Roman",
size: 10,
bold: true,
color: { argb: "FF1a5f3a" },
};
eStatementCell.alignment = { vertical: "middle", horizontal: "left" };
worksheet.mergeCells(`C${currentRow}:D${currentRow}`);
const dateCell = worksheet.getCell(`C${currentRow}`);
dateCell.value = `Generated: ${dayjs().format("DD/MM/YYYY HH:mm")}`;
dateCell.font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
dateCell.alignment = { vertical: "middle", horizontal: "right" };
currentRow++;
// Border line
currentRow++;
/* ---------------------------------------------------------
* 2) ACCOUNT DETAILS SECTION
* --------------------------------------------------------- */
// Account Details Header
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const detailsHeaderCell = worksheet.getCell(`A${currentRow}`);
detailsHeaderCell.value = "ACCOUNT DETAILS";
detailsHeaderCell.font = {
name: "Times New Roman",
size: 11,
bold: true,
color: { argb: "FF1a5f3a" },
};
detailsHeaderCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
detailsHeaderCell.alignment = { vertical: "middle", horizontal: "center" };
detailsHeaderCell.border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
currentRow++;
// Account Name
worksheet.getCell(`A${currentRow}`).value = "Account Name";
worksheet.getCell(`A${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`A${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`B${currentRow}`).value = customerName;
worksheet.getCell(`B${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`B${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`C${currentRow}`).value = "Branch Name";
worksheet.getCell(`C${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`C${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`D${currentRow}`).value = branchCode;
worksheet.getCell(`D${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`D${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
// Apply borders
["A", "B", "C", "D"].forEach((col) => {
worksheet.getCell(`${col}${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
});
currentRow++;
// Account Number
worksheet.getCell(`A${currentRow}`).value = "Account Number";
worksheet.getCell(`A${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`A${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`B${currentRow}`).value = accountNo;
worksheet.getCell(`B${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`B${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`C${currentRow}`).value = "Branch Code";
worksheet.getCell(`C${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`C${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`D${currentRow}`).value = branchCode;
worksheet.getCell(`D${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`D${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
["A", "B", "C", "D"].forEach((col) => {
worksheet.getCell(`${col}${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
});
currentRow++;
// CIF Number
worksheet.getCell(`A${currentRow}`).value = "CIF Number";
worksheet.getCell(`A${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`A${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`B${currentRow}`).value = cifNumber;
worksheet.getCell(`B${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`B${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`C${currentRow}`).value = "Branch Address";
worksheet.getCell(`C${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`C${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.getCell(`D${currentRow}`).value = "DHARAMSHALA, KANGRA";
worksheet.getCell(`D${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`D${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
["A", "B", "C", "D"].forEach((col) => {
worksheet.getCell(`${col}${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
});
currentRow++;
// Address
worksheet.getCell(`A${currentRow}`).value = "Address";
worksheet.getCell(`A${currentRow}`).font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF666666" },
};
worksheet.getCell(`A${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
worksheet.mergeCells(`B${currentRow}:D${currentRow}`);
worksheet.getCell(`B${currentRow}`).value = address;
worksheet.getCell(`B${currentRow}`).font = {
name: "Times New Roman",
size: 10,
bold: true,
};
worksheet.getCell(`B${currentRow}`).fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF9F9F9" },
};
["A", "B", "C", "D"].forEach((col) => {
worksheet.getCell(`${col}${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
});
currentRow++;
currentRow++; // Empty row
/* ---------------------------------------------------------
* 3) WARNING SECTION
* --------------------------------------------------------- */
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const warningCell = worksheet.getCell(`A${currentRow}`);
warningCell.value =
"⚠ NEVER SHARE your Card number, CVV, PIN, OTP, Internet Banking User ID, Password or URB with anyone even if the caller claims to be a bank employee. Sharing these details can lead to unauthorized access to your account.";
warningCell.font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF721c24" },
};
warningCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF8D7DA" },
};
warningCell.border = {
top: { style: "thin", color: { argb: "FFF5C6CB" } },
bottom: { style: "thin", color: { argb: "FFF5C6CB" } },
left: { style: "medium", color: { argb: "FFD32F2F" } },
right: { style: "thin", color: { argb: "FFF5C6CB" } },
};
warningCell.alignment = { vertical: "middle", horizontal: "left", wrapText: true };
worksheet.getRow(currentRow).height = 40;
currentRow++;
currentRow++; // Empty row
/* ---------------------------------------------------------
* 4) STATEMENT PERIOD
* --------------------------------------------------------- */
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const periodCell = worksheet.getCell(`A${currentRow}`);
periodCell.value = `Account statement from ${periodFrom} to ${periodTo}`;
periodCell.font = {
name: "Times New Roman",
size: 11,
bold: true,
color: { argb: "FF1a5f3a" },
};
periodCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF5F5F5" },
};
periodCell.alignment = { vertical: "middle", horizontal: "center" };
periodCell.border = {
top: { style: "medium", color: { argb: "FF1a5f3a" } },
bottom: { style: "medium", color: { argb: "FF1a5f3a" } },
};
worksheet.getRow(currentRow).height = 22;
currentRow++;
currentRow++; // Empty row
/* ---------------------------------------------------------
* 5) TRANSACTION TABLE
* --------------------------------------------------------- */
// Table Headers
const headerRowNum = currentRow;
worksheet.getCell(`A${currentRow}`).value = "Date";
worksheet.getCell(`B${currentRow}`).value = "Mode / Particulars";
worksheet.getCell(`C${currentRow}`).value = "Withdrawals / Deposits";
worksheet.getCell(`D${currentRow}`).value = "Balance";
["A", "B", "C", "D"].forEach((col) => {
const cell = worksheet.getCell(`${col}${currentRow}`);
cell.font = {
name: "Times New Roman",
size: 10,
bold: true,
color: { argb: "FFFFFFFF" },
};
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF2e7d32" },
};
cell.alignment = { vertical: "middle", horizontal: "center" };
cell.border = {
top: { style: "thin", color: { argb: "FF1a5f3a" } },
bottom: { style: "thin", color: { argb: "FF1a5f3a" } },
left: { style: "thin", color: { argb: "FF1a5f3a" } },
right: { style: "thin", color: { argb: "FF1a5f3a" } },
};
});
worksheet.getRow(currentRow).height = 20;
currentRow++;
// Transaction Rows
txns.forEach((t: any) => {
worksheet.getCell(`A${currentRow}`).value = t.date;
worksheet.getCell(`A${currentRow}`).font = {
name: "Times New Roman",
size: 9,
};
worksheet.getCell(`A${currentRow}`).alignment = {
vertical: "middle",
horizontal: "center",
};
worksheet.getCell(`A${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
worksheet.getCell(`B${currentRow}`).value = t.name;
worksheet.getCell(`B${currentRow}`).font = {
name: "Times New Roman",
size: 9,
};
worksheet.getCell(`B${currentRow}`).alignment = {
vertical: "middle",
horizontal: "left",
};
worksheet.getCell(`B${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
const amount = parseFloat(t.amount).toLocaleString("en-IN", {
minimumFractionDigits: 2,
});
worksheet.getCell(`C${currentRow}`).value = `${amount} ${t.type}`;
worksheet.getCell(`C${currentRow}`).font = {
name: "Times New Roman",
size: 9,
bold: true,
color: { argb: t.type === "DR" ? "FFD32F2F" : "FF2E7D32" },
};
worksheet.getCell(`C${currentRow}`).alignment = {
vertical: "middle",
horizontal: "right",
};
worksheet.getCell(`C${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
worksheet.getCell(`D${currentRow}`).value = t.balance;
worksheet.getCell(`D${currentRow}`).font = {
name: "Times New Roman",
size: 9,
bold: true,
};
worksheet.getCell(`D${currentRow}`).alignment = {
vertical: "middle",
horizontal: "right",
};
worksheet.getCell(`D${currentRow}`).border = {
top: { style: "thin", color: { argb: "FFD0D0D0" } },
bottom: { style: "thin", color: { argb: "FFD0D0D0" } },
left: { style: "thin", color: { argb: "FFD0D0D0" } },
right: { style: "thin", color: { argb: "FFD0D0D0" } },
};
currentRow++;
});
currentRow++; // Empty row
/* ---------------------------------------------------------
* 6) END OF STATEMENT
* --------------------------------------------------------- */
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const endCell = worksheet.getCell(`A${currentRow}`);
endCell.value = "*** END OF STATEMENT ***";
endCell.font = {
name: "Times New Roman",
size: 11,
bold: true,
color: { argb: "FF1a5f3a" },
};
endCell.alignment = { vertical: "middle", horizontal: "center" };
endCell.border = {
top: { style: "medium", color: { argb: "FF1a5f3a" } },
};
currentRow++;
currentRow += 2; // Empty rows
/* ---------------------------------------------------------
* 7) IMPORTANT INFORMATION
* --------------------------------------------------------- */
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const infoHeaderCell = worksheet.getCell(`A${currentRow}`);
infoHeaderCell.value = "IMPORTANT INFORMATION:";
infoHeaderCell.font = {
name: "Times New Roman",
size: 11,
bold: true,
color: { argb: "FF1a5f3a" },
};
infoHeaderCell.alignment = { vertical: "middle", horizontal: "left" };
infoHeaderCell.border = {
bottom: { style: "medium", color: { argb: "FF1a5f3a" } },
};
currentRow++;
const importantPoints = [
"The Kangra Central Cooperative Bank Officials or representatives will NEVER ask you for your personal information i.e. your card details, passwords, PIN, CVV, OTP etc. Do not share such details with anyone over phone, SMS or email.",
"Always stay vigilant to suspicious emails. Do not open any suspicious emails.",
"Always stay vigilant when giving out sensitive personal or account information.",
"Beware of messages that instill a sense of urgency (e.g., account will expire unless you \"verify\" your information). Contact the Bank directly if unsure.",
"Always log out of secondary devices and reset your passwords frequently.",
"Use strong passwords: Create strong passwords that are difficult for hackers to guess.",
"Use public Wi-Fi with caution: Be careful when using public Wi-Fi networks.",
"Back up your data regularly to a secure, encrypted, off-site location.",
"Follow corporate security policies: Adhere to your company's security guidelines.",
"Assess third-party app permissions carefully before granting access.",
"Lock your computer and mobile phone when not in use.",
"Don't leave devices unattended. Keep all mobile devices, such as laptops and cell phones, physically secured.",
"Don't leave Bluetooth / Wireless turned on when not in use. Enable them only when needed and in a safe environment.",
];
importantPoints.forEach((point) => {
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const pointCell = worksheet.getCell(`A${currentRow}`);
pointCell.value = `${point}`;
pointCell.font = {
name: "Times New Roman",
size: 9,
color: { argb: "FF333333" },
};
pointCell.alignment = {
vertical: "top",
horizontal: "left",
wrapText: true,
};
worksheet.getRow(currentRow).height = 25;
currentRow++;
});
currentRow += 2; // Empty rows
/* ---------------------------------------------------------
* 8) FOOTER NOTE
* --------------------------------------------------------- */
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const footerCell = worksheet.getCell(`A${currentRow}`);
footerCell.value = "** This is only for information purpose and not for legal use **";
footerCell.font = {
name: "Times New Roman",
size: 9,
italic: true,
color: { argb: "FF666666" },
};
footerCell.alignment = { vertical: "middle", horizontal: "center" };
/* ---------------------------------------------------------
* 9) COLUMN WIDTHS
* --------------------------------------------------------- */
worksheet.getColumn(1).width = 15; // Date
worksheet.getColumn(2).width = 45; // Particulars
worksheet.getColumn(3).width = 25; // Withdrawals/Deposits
worksheet.getColumn(4).width = 20; // Balance
/* ---------------------------------------------------------
* 10) SAVE FILE
* --------------------------------------------------------- */
workbook.xlsx.writeBuffer().then((buffer: any) => {
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `Statement_${accountNo}_${dayjs().format("DDMMYYYY_HHmm")}.xlsx`;
a.click();
window.URL.revokeObjectURL(url);
});
}; };
export default function CsvGenerator({
accountNo,
balance,
txns,
}: CsvGeneratorProps) {
return (
<button
onClick={() => generateCSV(accountNo, balance, txns)}
style={{
padding: "6px 12px",
borderRadius: "6px",
background: "#02a355",
color: "white",
border: "none",
cursor: "pointer",
}}
>
Download CSV
</button>
);
}

View File

@@ -7,107 +7,369 @@ export const generatePDF = (
txns: any[], txns: any[],
customerName: string, customerName: string,
periodFrom: string, periodFrom: string,
periodTo: string periodTo: string,
// branchName: string,
branchCode: string,
cifNumber: string,
address: string
) => { ) => {
const html2pdf = require("html2pdf.js"); const html2pdf = require("html2pdf.js");
// Build rows /* ---------------------------------------------------------
const rows = txns.map( * 1) BUILD TRANSACTION ROWS
(t: any) => ` * --------------------------------------------------------- */
<tr style="page-break-inside: avoid;">
<td style="border:1px solid #ccc;padding:6px;text-align:center;">${t.date}</td>
<td style="border:1px solid #ccc;padding:6px;text-align:left;">${t.name}</td>
<td style="border:1px solid #ccc;padding:6px;text-align:right;color:${t.type === "DR" ? "red" : "green"
}">
${parseFloat(t.amount).toLocaleString("en-IN", {
minimumFractionDigits: 2,
})} ${t.type === "DR" ? "dr." : "cr."}
</td>
<td style="border:1px solid #ccc; padding:6px; text-align:right; color: blue;">${t.balance}</td>
</tr>
`
);
// Content for first page const rows = txns
const content = ` .map((t: any) => {
<div style="font-family:Arial, sans-serif;"> return `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;"> <tr class="txn-row">
<div style="display:flex;align-items:center;gap:10px;"> <td style="border:1px solid #d0d0d0;padding:8px 10px;text-align:center;font-size:11px;">${t.date}</td>
<img src="/logo.jpg" alt="Bank Logo" style="height:50px;" /> <td style="border:1px solid #d0d0d0;padding:8px 10px;text-align:left;font-size:11px;">${t.name}</td>
<h2 style="margin:0;">The Kangra Central Co Operative Bank</h2> <td style="border:1px solid #d0d0d0;padding:8px 10px;text-align:right;font-size:11px;color:${t.type === "DR" ? "#d32f2f" : "#2e7d32"};font-weight:500;">
</div> ${parseFloat(t.amount).toLocaleString("en-IN", {
<div style="font-size:12px;color:#555;"> minimumFractionDigits: 2,
Report generated: ${dayjs().format("DD/MM/YYYY HH:mm")} })} ${t.type}
</td>
<td style="border:1px solid #d0d0d0;padding:8px 10px;text-align:right;font-size:11px;font-weight:500;">${t.balance}</td>
</tr>
`;
})
.join("");
/* ---------------------------------------------------------
* 2) MAIN PAGE CONTENT
* --------------------------------------------------------- */
const firstPage = `
<div style="font-family:'Times New Roman', serif; font-size:12px;line-height:1.5;">
<style>
@page {
margin: 15mm 10mm 20mm 10mm;
}
body {
margin: 0;
padding: 0;
}
.txn-row {
page-break-inside: avoid;
}
table {
page-break-inside: auto;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
</style>
<!-- HEADER -->
<div style="
display:flex;
align-items:center;
gap:12px;
border-bottom:2px solid #1a5f3a;
padding-bottom:12px;
margin-bottom:15px;
">
<img src="/logo.jpg" style="height:55px;width:auto;" />
<div style="flex:1;">
<h2 style="margin:0 0 3px 0;font-size:18px;color:#1a5f3a;letter-spacing:0.3px;">
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</h2>
<p style="margin:0;font-size:10px;color:#666;">Head Office: Dharmsala, District Kangra (H.P.), Pin. 176215</p>
</div>
<div style="text-align:right;">
<div style="font-size:11px;font-weight:bold;color:#1a5f3a;margin-bottom:2px;">e-Statement Service</div>
<div style="font-size:9px;color:#666;">
Generated: ${dayjs().format("DD/MM/YYYY HH:mm")}
</div> </div>
</div> </div>
<hr/>
<h3 style="text-align:center;">Account Statement</h3>
<p><strong>Account No:</strong> ${accountNo}</p>
<p><strong>Account Holder:</strong> ${customerName}</p>
<p><strong>Statement Period:</strong> ${periodFrom} to ${periodTo}</p>
<p><strong>Available Balance:</strong> ₹ ${parseFloat(balance).toLocaleString(
"en-IN",
{ minimumFractionDigits: 2 }
)}</p>
<table style="width:100%;border-collapse:collapse;font-size:12px;margin-top:10px;page-break-inside:auto;">
<thead style="background:#f0f0f0;">
<tr>
<th style="border:1px solid #ccc;padding:6px;text-align:center;">Date</th>
<th style="border:1px solid #ccc;padding:6px;text-align:left;">Description</th>
<th style="border:1px solid #ccc;padding:6px;text-align:right;">Amount (₹)</th>
<th style="border:1px solid #ccc;padding:6px;text-align:right;">Available Amount (₹)</th>
</tr>
</thead>
<tbody>
${rows.join("")}
</tbody>
</table>
</div> </div>
<!-- ACCOUNT DETAILS -->
<table style="
width:100%;
margin-bottom:12px;
border:1px solid #d0d0d0;
background:#f9f9f9;
border-collapse:collapse;
">
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0e0e0;width:50%;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">Account Name</span>
<strong style="font-size:11px;color:#000;">${customerName}</strong>
</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;width:50%;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">Branch Name</span>
<strong style="font-size:11px;color:#000;">${branchCode}</strong>
</td>
</tr>
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0e0e0;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">Account Number</span>
<strong style="font-size:11px;color:#000;">${accountNo}</strong>
</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">Branch Code</span>
<strong style="font-size:11px;color:#000;">${branchCode}</strong>
</td>
</tr>
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #e0e0e0;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">CIF Number</span>
<strong style="font-size:11px;color:#000;">${cifNumber}</strong>
</td>
<td style="padding:8px 12px;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">Branch Address</span>
<strong style="font-size:11px;color:#000;">DHARAMSHALA, KANGRA</strong>
</td>
</tr>
<tr>
<td colspan="2" style="padding:8px 12px;">
<span style="font-size:10px;color:#666;display:block;margin-bottom:2px;">Address</span>
<strong style="font-size:11px;color:#000;">${address}</strong>
</td>
</tr>
</table>
<!-- RED WARNING -->
<div style="
margin-bottom:15px;
border:1px solid #f5c6cb;
background:#f8d7da;
padding:10px 12px;
border-radius:4px;
border-left:4px solid #d32f2f;
">
<div style="display:flex;gap:10px;align-items:flex-start;">
<div style="font-size:16px;color:#d32f2f;line-height:1;margin-top:1px;">&#9888;</div>
<div style="font-size:10px;line-height:1.5;color:#721c24;">
<strong>NEVER SHARE</strong> your Card number, CVV, PIN, OTP, Internet Banking User ID, Password or URB with
anyone even if the caller claims to be a bank employee. Sharing these details can lead to unauthorized
access to your account.
</div>
</div>
</div>
<!-- PERIOD -->
<div style="
text-align:center;
margin:15px 0 12px 0;
padding:10px;
background:#f5f5f5;
border-top:2px solid #1a5f3a;
border-bottom:2px solid #1a5f3a;
">
<h3 style="margin:0;font-size:13px;color:#1a5f3a;font-weight:600;">
Account statement from <strong>${periodFrom}</strong> to <strong>${periodTo}</strong>
</h3>
</div>
<!-- TABLE -->
<table style="
width:100%;
border-collapse:collapse;
font-size:11px;
margin-bottom:15px;
border:1px solid #d0d0d0;
">
<thead style="display:table-header-group;">
<tr style="background:#2e7d32;color:white;">
<th style="border:1px solid #1a5f3a;padding:10px 8px;text-align:center;font-weight:600;font-size:11px;width:15%;">Date</th>
<th style="border:1px solid #1a5f3a;padding:10px 8px;text-align:left;font-weight:600;font-size:11px;width:40%;">Mode / Particulars</th>
<th style="border:1px solid #1a5f3a;padding:10px 8px;text-align:center;font-weight:600;font-size:11px;width:22.5%;">Withdrawals / Deposits</th>
<th style="border:1px solid #1a5f3a;padding:10px 8px;text-align:center;font-weight:600;font-size:11px;width:22.5%;">Balance</th>
</tr>
</thead>
<tbody style="background:white;">${rows}</tbody>
</table>
<!-- END OF STATEMENT -->
<div style="
text-align:center;
margin-top:20px;
padding:15px;
border-top:2px solid #1a5f3a;
page-break-before:auto;
page-break-after:always;
">
<p style="font-weight:bold;font-size:12px;color:#1a5f3a;margin:0;letter-spacing:1px;">
*** END OF STATEMENT ***
</p>
</div>
</div>
`; `;
/* ---------------------------------------------------------
* 3) LAST PAGE
* --------------------------------------------------------- */
const lastPage = `
<div style="font-family:'Times New Roman', serif; font-size:11px;line-height:1.6;margin-top:0;padding-top:20px;">
<div style="
border:2px solid #1a5f3a;
padding:20px 25px;
margin:0 20px;
background:#fafafa;
border-radius:4px;
">
<h3 style="
font-weight:bold;
margin:0 0 15px 0;
font-size:14px;
color:#1a5f3a;
border-bottom:2px solid #1a5f3a;
padding-bottom:8px;
">IMPORTANT INFORMATION:</h3>
<div style="color:#333;">
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
The Kangra Central Cooperative Bank Officials or representatives will NEVER ask you for your personal information i.e. your card details, passwords, PIN, CVV, OTP etc. Do not share such details with anyone over phone, SMS or email.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Always stay vigilant to suspicious emails. Do not open any suspicious emails.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Always stay vigilant when giving out sensitive personal or account information.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Beware of messages that instill a sense of urgency (e.g., account will expire unless you "verify" your information). Contact the Bank directly if unsure.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Always log out of secondary devices and reset your passwords frequently.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Use strong passwords: Create strong passwords that are difficult for hackers to guess.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Use public Wi-Fi with caution: Be careful when using public Wi-Fi networks.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Back up your data regularly to a secure, encrypted, off-site location.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Follow corporate security policies: Adhere to your company's security guidelines.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Assess third-party app permissions carefully before granting access.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Lock your computer and mobile phone when not in use.
</p>
<p style="margin:0 0 10px 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Don't leave devices unattended. Keep all mobile devices, such as laptops and cell phones, physically secured.
</p>
<p style="margin:0 0 0 0;padding-left:15px;position:relative;">
<span style="position:absolute;left:0;color:#1a5f3a;">•</span>
Don't leave Bluetooth / Wireless turned on when not in use. Enable them only when needed and in a safe environment.
</p>
</div>
</div>
</div>
`;
/* ---------------------------------------------------------
* 4) PDF GENERATION
* --------------------------------------------------------- */
const opt = { const opt = {
margin: [20, 10, 20, 10], margin: [15, 10, 20, 10],
filename: `AccountStatement_${accountNo}_${dayjs().format("DD/MM/YYYY HH:mm")}.pdf`, filename: `Statement_${accountNo}_${dayjs().format("DDMMYYYY_HHmm")}.pdf`,
image: { type: "jpeg", quality: 0.98 }, image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2 }, html2canvas: {
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" }, scale: 2,
pagebreak: { mode: ["avoid-all", "css", "legacy"] }, useCORS: true,
letterRendering: true,
},
jsPDF: {
unit: "mm",
format: "a4",
orientation: "portrait",
compress: true,
},
pagebreak: {
mode: ["css", "legacy"],
before: '.page-break-before',
after: '.page-break-after',
avoid: ['tr', '.txn-row']
},
}; };
html2pdf() html2pdf()
.set(opt) .set(opt)
.from(content) .from(firstPage + lastPage)
.toPdf() .toPdf()
.get("pdf") .get("pdf")
.then((pdf: any) => { .then((pdf: any) => {
const totalPages = pdf.internal.getNumberOfPages(); const total = pdf.internal.getNumberOfPages();
const pageWidth = pdf.internal.pageSize.getWidth(); const w = pdf.internal.pageSize.getWidth();
const h = pdf.internal.pageSize.getHeight();
for (let i = 2; i <= totalPages; i++) { const hasGState =
typeof pdf.GState === "function" && typeof pdf.setGState === "function";
for (let i = 1; i <= total; i++) {
pdf.setPage(i); pdf.setPage(i);
pdf.setFontSize(10);
// ✅ Left side Account No /* LIGHT WATERMARK */
pdf.setFont("helvetica", "bold"); try {
// pdf.text(`Account No: ${accountNo}`, 15, 18); if (hasGState) {
const light = pdf.GState({ opacity: 0.03 });
pdf.setGState(light);
pdf.addImage("/kccb_watermark.png", "PNG", 35, 70, 140, 140);
pdf.setGState(pdf.GState({ opacity: 1 }));
} else {
pdf.addImage("/kccb_watermark.png", "PNG", 35, 70, 140, 140);
}
} catch { }
// ✅ Centered Statement Period /* FOOTER */
pdf.setFont("times", "italic");
pdf.setFontSize(9);
pdf.setTextColor(100, 100, 100);
pdf.text( pdf.text(
`Statement Period: ${periodFrom} to ${periodTo}`, "** This is only for information purpose and not for legal use **",
pageWidth / 2, w / 2,
18, h - 12,
{ align: "center" } { align: "center" }
); );
// Footer page numbers /* PAGE NUMBER */
pdf.setFont("times", "normal");
pdf.setFontSize(9); pdf.setFontSize(9);
pdf.text( pdf.setTextColor(0, 0, 0);
`Page ${i} of ${totalPages}`, pdf.text(`Page ${i} of ${total}`, w - 15, h - 12, {
pageWidth - 40, align: "right",
pdf.internal.pageSize.getHeight() - 10 });
);
} }
}) })
.save(); .save();

View File

@@ -2,11 +2,16 @@
import { MantineColorsTuple, createTheme } from "@mantine/core"; import { MantineColorsTuple, createTheme } from "@mantine/core";
// const KccbColors: MantineColorsTuple = [
// "#e3f2fd", "#bbdefb", "#90caf9", "#64b5f6", "#42a5f5",
// "#2196f3", "#1e88e5", "#1976d2", "#1565c0", "#0d47a1"
// ];
const KccbColors: MantineColorsTuple = [ const KccbColors: MantineColorsTuple = [
"#e3f2fd", "#bbdefb", "#90caf9", "#64b5f6", "#42a5f5", "#e8f5e9", "#c8e6c9", "#a5d6a7", "#81c784", "#66bb6a", // Lighter greens
"#2196f3", "#1e88e5", "#1976d2", "#1565c0", "#0d47a1" "#4caf50", "#43a047", "#388e3c", "#2c6f2c", "#1b5e20" // Darker greens
]; ];
export const KccbTheme = createTheme({ export const KccbTheme = createTheme({
/* Put your mantine theme override here */ /* Put your mantine theme override here */
primaryColor: 'kccb-colors', primaryColor: 'kccb-colors',

View File

@@ -8,7 +8,7 @@ export async function fetchAndStoreUserName(token: string) {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Login-Type": "IB", "X-Login-Type": "IB",
"Authorization": `Bearer ${token}`, "Authorization": `Bearer ${token}`,
}, },
}); });
@@ -28,14 +28,20 @@ export async function fetchAndStoreUserName(token: string) {
const data = await response.json(); const data = await response.json();
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
const { custname, mobileno } = data[0]; const { custname, mobileno, cifNumber, stBranchNo,custaddress } = data[0];
localStorage.setItem("remitter_name", custname); localStorage.setItem("remitter_name", custname);
localStorage.setItem("remitter_mobile_no", mobileno); localStorage.setItem("remitter_mobile_no", mobileno);
localStorage.setItem("remitter_cif_no", cifNumber);
localStorage.setItem("remitter_branch_no", stBranchNo);
localStorage.setItem("remitter_address", custaddress);
} }
return true; return true;
} catch (error: any) { } catch (error: any) {
localStorage.removeItem("remitter_name"); localStorage.removeItem("remitter_name");
localStorage.removeItem("remitter_mobile_no"); localStorage.removeItem("remitter_mobile_no");
localStorage.removeItem("remitter_cif_no");
localStorage.removeItem("remitter_branch_no");
localStorage.removeItem("remitter_address");
console.error('Error sending OTP:', error.response?.data || error.message); console.error('Error sending OTP:', error.response?.data || error.message);
notifications.show({ notifications.show({
withBorder: true, withBorder: true,

View File

@@ -411,7 +411,7 @@ export default function Login() {
<Divider size="xs" color="#99c2ff" /> <Divider size="xs" color="#99c2ff" />
<Text size="xs" style={{ textAlign: "center" }}> <Text size="xs" style={{ textAlign: "center" }}>
© 2025 The Kangra Central Co-Operative Bank © 2025 The Kangra Central Co-Operative Bank Ltd.
</Text> </Text>
</div> </div>
</Providers> </Providers>

View File

@@ -0,0 +1,40 @@
import { notifications } from "@mantine/notifications";
export const fetchUserDetails = async () => {
try {
const token = localStorage.getItem("mandate_token");
const response = await fetch("/api/customer", {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "eMandate",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) throw new Error("Failed");
const data = await response.json();
if (Array.isArray(data) && data.length > 0) {
const name = data[0].custname;
const mobile = data[0].mobileno;
localStorage.setItem("user_name", name);
localStorage.setItem("userMobNo", mobile);
return { name, mobile };
}
} catch {
notifications.show({
title: "Please try again later",
message: "Unable to fetch user details",
color: "red",
});
}
return null;
};

View File

@@ -21,26 +21,30 @@ function LoginEmandate() {
const [isLogging, setIsLogging] = useState(false); const [isLogging, setIsLogging] = useState(false);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const data = searchParams.get("data"); const data = searchParams.get("data");
const payload = searchParams.get("payload"); const mandateReqDoc = searchParams.get("mandateReqDoc");
const mndtType = searchParams.get("mndtType");
// useEffect(() => { // useEffect(() => {
// if (payload) { // if (data) {
// try { // console.log("URL parameter 'data':", data);
// const parsed = JSON.parse(decodeURIComponent(payload)); // localStorage.setItem("Emendate_data", data);
// console.log("Received payload from SoftTech:", parsed); // localStorage.setItem("Emendate_req_doc", mandateReqDoc || "");
// localStorage.setItem("Emandate_data", JSON.stringify(parsed)); // localStorage.setItem("Emendate_type", mndtType || "");
// } catch (e) {
// console.error("Failed to parse payload:", e);
// }
// } // }
// }, [payload]); // }, [data]);
useEffect(() => { useEffect(() => {
if (data) { if (data && mandateReqDoc && mndtType) {
console.log("URL parameter 'data':", data); console.log("eMandate parameters found");
localStorage.setItem("Emendate_data", data); localStorage.setItem("Emendate_data", data);
localStorage.setItem("Emendate_req_doc", mandateReqDoc);
localStorage.setItem("Emendate_type", mndtType);
} else {
console.log("eMandate parameters missing — clearing localStorage");
localStorage.removeItem("Emendate_data");
localStorage.removeItem("Emendate_req_doc");
localStorage.removeItem("Emendate_type");
} }
}, [data]); }, [data, mandateReqDoc, mndtType]);
useEffect(() => { useEffect(() => {
const loadCaptcha = async () => { const loadCaptcha = async () => {
@@ -50,7 +54,6 @@ function LoginEmandate() {
loadCaptcha(); loadCaptcha();
}, []); }, []);
const regenerateCaptcha = () => { const regenerateCaptcha = () => {
// setCaptcha(generateCaptcha()); // setCaptcha(generateCaptcha());
const loadCaptcha = async () => { const loadCaptcha = async () => {
@@ -107,6 +110,7 @@ function LoginEmandate() {
return; return;
} }
try { try {
setIsLogging(true);
const response = await fetch('/api/auth/login', { const response = await fetch('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -131,9 +135,9 @@ function LoginEmandate() {
localStorage.removeItem("mandate_token"); localStorage.removeItem("mandate_token");
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
setIsLogging(false);
return; return;
} }
setIsLogging(true);
if (response.ok) { if (response.ok) {
// console.log(data); // console.log(data);
const token = data.token; const token = data.token;
@@ -147,9 +151,40 @@ function LoginEmandate() {
message: "Please go to Internet Banking, set your credentials, and then try logging in here again.", message: "Please go to Internet Banking, set your credentials, and then try logging in here again.",
autoClose: 5000, autoClose: 5000,
}); });
setIsLogging(false);
} }
else { else {
router.push("/eMandate/mandate_page"); console.log("Start to validate soft tech data");
const response = await fetch('/api/e-mandate/validation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Login-Type': 'eMandate',
'Authorization': `Bearer ${localStorage.getItem("mandate_token")}`
},
body: JSON.stringify({
data: localStorage.getItem("Emendate_data"),
mandateRequest: localStorage.getItem("Emendate_req_doc"),
mandateType: localStorage.getItem("Emendate_type"),
}),
});
const result = await response.json();
localStorage.setItem("Validate_data", result.data);
console.log("Validate Result : ", result);
if (response.ok) {
router.push("/eMandate/otp_page");
}
else {
console.log("validation failed: response",result);
notifications.show({
withBorder: true,
color: "red",
title: "Validation failed",
message: "Failed to validate.Please try again later.",
autoClose: 5000,
});
setIsLogging(false);
}
} }
} }
else { else {
@@ -172,51 +207,53 @@ function LoginEmandate() {
message: "Internal Server Error,Please try again Later", message: "Internal Server Error,Please try again Later",
autoClose: 5000, autoClose: 5000,
}); });
setIsLogging(false);
return; return;
} }
} }
return ( if (data && mandateReqDoc && mndtType) {
<Providers> return (
{/* Main Screen */} <Providers>
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto", paddingTop: "5%" }}> {/* Main Screen */}
{/* Header */} <div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto", paddingTop: "5%" }}>
{/* Header */}
<Box className={styles.header}> <Box className={styles.header}>
<Image src={logo} component={NextImage} fit="contain" alt="ebanking" style={{ width: "60px", height: "auto" }} /> <Image src={logo} component={NextImage} fit="contain" alt="ebanking" style={{ width: "60px", height: "auto" }} />
<Box className={styles['header-text']}> <Box className={styles['header-text']}>
<Title className={styles['desktop-text']} order={4}>THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.</Title> <Title className={styles['desktop-text']} order={4}>THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.</Title>
<Text className={styles['desktop-text']} size="xs">Head Office: Dharmshala, District Kangra (H.P), Pin: 176215</Text> <Text className={styles['desktop-text']} size="xs">Head Office: Dharmshala, District Kangra (H.P), Pin: 176215</Text>
<Title className={styles['mobile-text']} order={5}>THE KANGRA CENTRAL</Title> <Title className={styles['mobile-text']} order={5}>THE KANGRA CENTRAL</Title>
<Title className={styles['mobile-text']} order={5}>CO-OPERATIVE BANK LTD.</Title> <Title className={styles['mobile-text']} order={5}>CO-OPERATIVE BANK LTD.</Title>
</Box>
</Box> </Box>
</Box>
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
{/* Movable text */} {/* Movable text */}
<Box <Box
className={styles['desktop-scroll-text']} className={styles['desktop-scroll-text']}
>
<Text
component="span"
style={{
display: "inline-block",
paddingLeft: "100%",
animation: "scroll-left 60s linear infinite",
fontWeight: "bold",
color: "#004d99",
}}
> >
Always login to our Net Banking site directly or through Banks website. <Text
Do not disclose your User Id and Password to any third party and keep Your User Id and Password strictly confidential. component="span"
KCC Bank never asks for User Id,Passwords and Pins through email or phone. style={{
Be ware of Phishing mails with links to fake bank&apos;s websites asking for personal information are in circulation. display: "inline-block",
Please DO NOT Click on the links given in the emails asking for personal details like bank account number, user ID and password. paddingLeft: "100%",
If you had shared your User Id and Password through such mails or links, please change your Password immediately. animation: "scroll-left 60s linear infinite",
Inform the Bank/branch in which your account is maintained for resetting your password. fontWeight: "bold",
</Text> color: "#004d99",
<style> }}
{` >
Always login to our Net Banking site directly or through Banks website.
Do not disclose your User Id and Password to any third party and keep Your User Id and Password strictly confidential.
KCC Bank never asks for User Id,Passwords and Pins through email or phone.
Be ware of Phishing mails with links to fake bank&apos;s websites asking for personal information are in circulation.
Please DO NOT Click on the links given in the emails asking for personal details like bank account number, user ID and password.
If you had shared your User Id and Password through such mails or links, please change your Password immediately.
Inform the Bank/branch in which your account is maintained for resetting your password.
</Text>
<style>
{`
@keyframes scroll-left { @keyframes scroll-left {
0% { transform: translateX(0%); } 0% { transform: translateX(0%); }
100% { transform: translateX(-100%); } 100% { transform: translateX(-100%); }
@@ -225,32 +262,90 @@ function LoginEmandate() {
.desktop-scroll-text { display: none; } .desktop-scroll-text { display: none; }
} }
`} `}
</style> </style>
</Box> </Box>
{/* Main */} {/* Main */}
<Box visibleFrom="md" <Box visibleFrom="md"
style={{ style={{
display: "flex", height: "75vh", overflow: "hidden", position: "relative", display: "flex", height: "75vh", overflow: "hidden", position: "relative",
background: 'linear-gradient(179deg, #3faa56ff 49%, #3aa760ff 80%)' background: 'linear-gradient(179deg, #3faa56ff 49%, #3aa760ff 80%)'
}}> }}>
<div style={{ flex: 1, backgroundColor: "#c1e0f0", position: "relative" }}> <div style={{ flex: 1, backgroundColor: "#c1e0f0", position: "relative" }}>
<Image <Image
fit="cover" fit="cover"
src={frontPage} src={frontPage}
component={NextImage} component={NextImage}
alt="ebanking" alt="ebanking"
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
/> />
</div> </div>
<Box w={{ base: "100%", md: "45%" }} p="lg"> <Box w={{ base: "100%", md: "45%" }} p="lg">
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, margin: "0 auto", height: '68vh' }}> <Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, margin: "0 auto", height: '68vh' }}>
<form onSubmit={handleMandateLogin}>
<Text
ta="center"
fw={700}
style={{ fontSize: "18px" }}
>
E-Mandate Login
</Text>
<TextInput
label="User ID"
placeholder="Enter your CIF No"
value={CIF}
onInput={(e) => {
const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 11) SetCIF(input);
}}
withAsterisk
/>
<PasswordInput
label="Password"
placeholder="Enter your password"
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
withAsterisk
mt="sm"
/>
<Group mt="sm" align="center">
<Box style={{ backgroundColor: "#fff", fontSize: "18px", textDecoration: "line-through", padding: "4px 8px", fontFamily: "Verdana" }}>{captcha}</Box>
<Button size="xs" variant="light" onClick={regenerateCaptcha}>Refresh</Button>
</Group>
<TextInput
label="Enter CAPTCHA"
placeholder="Enter above text"
value={inputCaptcha}
onChange={(e) => setInputCaptcha(e.currentTarget.value)}
withAsterisk
mt="sm"
/>
<Button type="submit" fullWidth mt="md" disabled={isLogging}>
{isLogging ? "Logging..." : "Login"}
</Button>
</form>
</Card>
</Box>
</Box>
{/* Mobile layout */}
<Box
hiddenFrom="md"
style={{
marginTop: "25%",
padding: "1rem",
background: "linear-gradient(179deg, #3faa56ff 49%, #3aa760ff 80%)",
minHeight: "80vh",
display: "flex",
justifyContent: "center",
alignItems: "flex-start",
}}
>
<Card shadow="md" padding="lg" radius="md" style={{ width: "100%" }}>
<form onSubmit={handleMandateLogin}> <form onSubmit={handleMandateLogin}>
<Text <Text ta="center" fw={700} style={{ fontSize: "16px" }}>
ta="center"
fw={700}
style={{ fontSize: "18px" }}
>
E-Mandate Login E-Mandate Login
</Text> </Text>
@@ -263,7 +358,9 @@ function LoginEmandate() {
if (input.length <= 11) SetCIF(input); if (input.length <= 11) SetCIF(input);
}} }}
withAsterisk withAsterisk
mt="sm"
/> />
<PasswordInput <PasswordInput
label="Password" label="Password"
placeholder="Enter your password" placeholder="Enter your password"
@@ -273,10 +370,23 @@ function LoginEmandate() {
mt="sm" mt="sm"
/> />
<Group mt="sm" align="center"> <Group mt="sm" align="center" grow>
<Box style={{ backgroundColor: "#fff", fontSize: "18px", textDecoration: "line-through", padding: "4px 8px", fontFamily: "Verdana" }}>{captcha}</Box> <Box
<Button size="xs" variant="light" onClick={regenerateCaptcha}>Refresh</Button> style={{
backgroundColor: "#fff",
fontSize: "16px",
textDecoration: "line-through",
padding: "4px 8px",
fontFamily: "Verdana",
}}
>
{captcha}
</Box>
<Button size="xs" variant="light" onClick={regenerateCaptcha}>
Refresh
</Button>
</Group> </Group>
<TextInput <TextInput
label="Enter CAPTCHA" label="Enter CAPTCHA"
placeholder="Enter above text" placeholder="Enter above text"
@@ -285,108 +395,57 @@ function LoginEmandate() {
withAsterisk withAsterisk
mt="sm" mt="sm"
/> />
<Button type="submit" fullWidth mt="md" disabled={isLogging}> <Button type="submit" fullWidth mt="md" disabled={isLogging}>
{isLogging ? "Logging..." : "Login"} {isLogging ? "Logging..." : "Login"}
</Button> </Button>
</form> </form>
</Card> </Card>
</Box> </Box>
</Box>
{/* Mobile layout */} {/* Footer */}
<Box <Box
hiddenFrom="md" component="footer"
style={{ style={{
marginTop: "25%", width: "100%",
padding: "1rem", textAlign: "center",
background: "linear-gradient(179deg, #3faa56ff 49%, #3aa760ff 80%)", padding: "10px 0",
minHeight: "80vh", bottom: 0,
display: "flex", left: 0,
justifyContent: "center", zIndex: 1000,
alignItems: "flex-start", fontSize: "14px",
}} }}
> >
<Card shadow="md" padding="lg" radius="md" style={{ width: "100%" }}> <Text>
<form onSubmit={handleMandateLogin}> © 2025 KCC Bank. All rights reserved.
<Text ta="center" fw={700} style={{ fontSize: "16px" }}> </Text>
E-Mandate Login </Box>
</Text> </div>
<TextInput
label="User ID"
placeholder="Enter your CIF No"
value={CIF}
onInput={(e) => {
const input = e.currentTarget.value.replace(/\D/g, "");
if (input.length <= 11) SetCIF(input);
}}
withAsterisk
mt="sm"
/>
<PasswordInput
label="Password"
placeholder="Enter your password"
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
withAsterisk
mt="sm"
/>
<Group mt="sm" align="center" grow>
<Box
style={{
backgroundColor: "#fff",
fontSize: "16px",
textDecoration: "line-through",
padding: "4px 8px",
fontFamily: "Verdana",
}}
>
{captcha}
</Box>
<Button size="xs" variant="light" onClick={regenerateCaptcha}>
Refresh
</Button>
</Group>
<TextInput
label="Enter CAPTCHA"
placeholder="Enter above text"
value={inputCaptcha}
onChange={(e) => setInputCaptcha(e.currentTarget.value)}
withAsterisk
mt="sm"
/>
<Button type="submit" fullWidth mt="md" disabled={isLogging}>
{isLogging ? "Logging..." : "Login"}
</Button>
</form>
</Card>
</Box>
{/* Footer */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: "10px 0",
bottom: 0,
left: 0,
zIndex: 1000,
fontSize: "14px",
}}
>
<Text>
© 2025 KCC Bank. All rights reserved.
</Text>
</Box>
</div> </div>
</Providers>
);
}
else {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100vh",
backgroundColor: "#f8f9fa",
}}
>
<Text c="red" fw={600} size="lg">
Required data not found
</Text>
<Text c="dimmed" size="sm">
Please access this page through the correct eMandate link.
</Text>
</div> </div>
</Providers> );
); }
} }
export default function Login() { export default function Login() {
@@ -396,4 +455,3 @@ export default function Login() {
</Suspense> </Suspense>
); );
} }

View File

@@ -1,27 +1,41 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const formData = await req.formData(); const formData = await req.formData();
const data = formData.get("data"); const data = formData.get("data");
const mandateReqDoc = formData.get("mandateReqDoc"); const mandateReqDoc = formData.get("mandateReqDoc");
const mndtType = formData.get("mndtType"); const mndtType = formData.get("mndtType");
// const { searchParams } = new URL(req.url);
// const data = searchParams.get("data");
// const mandateReqDoc = searchParams.get("mandateReqDoc");
// const mndtType = searchParams.get("mndtType");
console.log("Received from SoftTech:", { data, mandateReqDoc, mndtType }); console.log("Received from SoftTech:", { data, mandateReqDoc, mndtType });
if (!data) { const forwardedHost = req.headers.get("x-forwarded-host") || req.headers.get("host");
return NextResponse.json({ error: "Missing data" }, { status: 400 }); const forwardedProto = req.headers.get("x-forwarded-proto") || "https";
} // const encodedData = String(data);
const encodedData = String(data); // const redirectUrl = `${forwardedProto}://${forwardedHost}/eMandate/login/page?data=${encodedData}`;
// Redirect browser to page UI with data
console.log ("Return to the Urls."); const redirectUrl =`${forwardedProto}://${forwardedHost}/eMandate/login/page?data=${encodeURIComponent(
return NextResponse.redirect( String(data)
new URL(`/eMandate/login/page?data=${encodedData}`, req.url) )}&mandateReqDoc=${encodeURIComponent(String(mandateReqDoc))}&mndtType=${encodeURIComponent(
); String(mndtType)
)}`;
console.log("redirect urls:", redirectUrl);
const response = NextResponse.redirect(redirectUrl, 303); // use 303 instead of 302
response.headers.set("Access-Control-Allow-Origin", "*");
response.headers.set("Access-Control-Allow-Methods", "POST, OPTIONS");
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Origin");
return response;
} catch (error) { } catch (error) {
console.error("Error handling POST:", error); console.error("Error handling POST:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
} }
} }
export async function OPTIONS() {
const res = new NextResponse(null, { status: 204 });
res.headers.set("Access-Control-Allow-Origin", "*");
res.headers.set("Access-Control-Allow-Methods", "POST, OPTIONS");
res.headers.set("Access-Control-Allow-Headers", "Content-Type, Origin");
return res;
}

View File

@@ -0,0 +1,30 @@
/* Header */
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%);
}
.header-text {
flex: 1;
}
/* Desktop header text */
.desktop-text {
color: white;
}
.mobile-text {
color: white;
display: none;
}

View File

@@ -0,0 +1,92 @@
"use client";
import { Text, Title, Box, Image } from "@mantine/core";
import NextImage from "next/image";
import logo from "@/app/image/logo1.jpg";
import { Providers } from "@/app/providers";
import styles from "./page.module.css";
import { useEffect } from "react";
export default function Logout() {
useEffect(() => {
localStorage.removeItem("mandate_token");
localStorage.removeItem("user_name");
localStorage.removeItem("userMobNo");
localStorage.removeItem("Emendate_data");
localStorage.removeItem("Emendate_req_doc");
localStorage.removeItem("Emendate_type");
}, []);
return (
<Providers>
<div
style={{
backgroundColor: "#f8f9fa",
width: "100%",
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
}}
>
{/* Header */}
<Box className={styles.header} mt="md">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="ebanking"
style={{ width: "60px", height: "auto" }}
/>
<Box className={styles["header-text"]}>
<Title className={styles["desktop-text"]} order={4}>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text className={styles["desktop-text"]} size="xs">
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
<Title className={styles["mobile-text"]} order={5}>
THE KANGRA CENTRAL
</Title>
<Title className={styles["mobile-text"]} order={5}>
CO-OPERATIVE BANK LTD.
</Title>
</Box>
</Box>
{/* Centered Message */}
<div
style={{
textAlign: "center",
flexGrow: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<Text c="green" fw={600} size="lg">
You have successfully logged out
</Text>
<Text c="dimmed" size="sm" mt="xs">
Please access this eMandate login page through the correct eMandate link.
</Text>
</div>
{/* Footer */}
<Box
component="footer"
style={{
width: "100%",
textAlign: "center",
padding: "10px 0",
fontSize: "14px",
color: "#6c757d",
}}
>
© 2025 KCC Bank. All rights reserved.
</Box>
</div>
</Providers>
);
}

View File

@@ -117,7 +117,10 @@ export default function MandatePage() {
localStorage.removeItem("mandate_token"); localStorage.removeItem("mandate_token");
localStorage.removeItem("user_name"); localStorage.removeItem("user_name");
localStorage.removeItem("userMobNo"); localStorage.removeItem("userMobNo");
router.push("/eMandate/login"); localStorage.removeItem("Emendate_data");
localStorage.removeItem("Emendate_req_doc");
localStorage.removeItem("Emendate_type");
router.push("/eMandate/logout");
}; };
const handleFetchUserName = async () => { const handleFetchUserName = async () => {
@@ -430,7 +433,7 @@ export default function MandatePage() {
}} }}
> >
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
© 2025 The Kangra Central Co-Operative Bank © 2025 The Kangra Central Co-Operative Bank Ltd.
</Text> </Text>
</Box> </Box>

View File

@@ -0,0 +1,65 @@
import { notifications } from "@mantine/notifications";
export const sendOtp = async (mobile: string) => {
try {
const response = await fetch("/api/otp/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "eMandate",
},
body: JSON.stringify({
mobileNumber: mobile,
type: "EMandate",
}),
});
if (!response.ok) throw new Error("Failed");
notifications.show({
color: "green",
title: "OTP Sent",
message: "An OTP has been sent to your registered mobile number",
});
return true;
} catch {
notifications.show({
color: "red",
title: "Error",
message: "Failed to send OTP",
});
return false;
}
};
export const verifyOtp = async (otp: string, mobile: string) => {
try {
const response = await fetch(`/api/otp/verify?mobileNumber=${mobile}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "eMandate",
},
body: JSON.stringify({ otp }),
});
if (!response.ok) throw new Error("Invalid OTP");
notifications.show({
color: "green",
title: "Success",
message: "OTP verified successfully!",
});
return true;
} catch {
notifications.show({
color: "red",
title: "Verification Failed",
message: "Invalid OTP",
});
return false;
}
};

View File

@@ -0,0 +1,505 @@
"use client";
import React, { useEffect, useState, useRef } from "react";
import {
Text,
Title,
Box,
Image,
Button,
Group,
Container,
ActionIcon,
Divider,
PinInput,
Paper,
Stack,
Center,
Loader,
} from "@mantine/core";
import { Providers } from "@/app/providers";
import { useRouter } from "next/navigation";
import NextImage from "next/image";
import logo from "@/app/image/logo1.jpg";
import { IconLogout, IconShieldCheck, IconRefresh } from "@tabler/icons-react";
import { useMediaQuery } from "@mantine/hooks";
import { sendOtp, verifyOtp } from "../otpUtils";
import { fetchUserDetails } from "../authUtils";
export default function VerifyOtpPage() {
const router = useRouter();
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [custname, setCustname] = useState<string | null>(null);
const [otp, setOtp] = useState("");
const [isVerifying, setIsVerifying] = useState(false);
const [isResending, setIsResending] = useState(false);
const [timer, setTimer] = useState(60);
const [canResend, setCanResend] = useState(false);
const isMobile = useMediaQuery("(max-width: 768px)");
const timerRef = useRef<NodeJS.Timeout | null>(null);
const otpSentRef = useRef(false);
//On First Load: Check Token → Fetch User → Send OTP
useEffect(() => {
const token = localStorage.getItem("mandate_token");
if (!token) {
handleLogout();
setAuthorized(false);
return;
}
setAuthorized(true);
// Get User Name + Mobile
fetchUserDetails().then((res) => {
if (res) {
setCustname(res.name);
if (!otpSentRef.current) {
sendOtp(res.mobile);
// sendOtp("7890544527");
otpSentRef.current = true; // Prevent second OTP
}
}
});
// Prevent back button
const handlePopState = () => {
handleLogout();
};
window.addEventListener("popstate", handlePopState);
window.history.pushState(null, "", window.location.href);
return () => {
window.removeEventListener("popstate", handlePopState);
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
// Timer Countdown
useEffect(() => {
timerRef.current = setInterval(() => {
setTimer((prev) => {
if (prev === 1) {
setCanResend(true);
clearInterval(timerRef.current!);
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timerRef.current!);
}, []);
// Logout
const handleLogout = () => {
localStorage.clear();
router.push("/eMandate/logout");
};
//Resend OTP
const handleResendOtp = async () => {
setIsResending(true);
const mobile = localStorage.getItem("userMobNo");
// const mobile = "7890544527";
if (!mobile) return;
await sendOtp(mobile);
setTimer(60);
setCanResend(false);
setOtp("");
setIsResending(false);
};
// Verify OTP using verifyOtp() Utility
// const handleVerifyOtp = async () => {
// const mobile = localStorage.getItem("userMobNo");
// // const mobile = "7890544527";
// if (!mobile || otp.length !== 6) return;
// setIsVerifying(true);
// const success = await verifyOtp(otp, mobile);
// // if (success) {
// // // const encoded_data = localStorage.getItem("Validate_data");
// // // const res = await fetch(
// // // `https://apiemandate.kccb.in:9035/EMandate/auth-cbs-resp?data=${encoded_data}`,
// // // {
// // // method: "POST",
// // // headers: {
// // // "Content-Type": "application/json",
// // // },
// // // }
// // // );
// // const formData = new FormData();
// // const encoded_data = localStorage.getItem("Validate_data");
// // formData.append("data", encoded_data);
// // const res = await fetch(
// // "https://apiemandate.kccb.in:9035/EMandate/auth-cbs-resp",
// // {
// // method: "POST",
// // body: formData,
// // }
// // );
// // const result = await res.json();
// // console.log(result);
// // if (!res.ok) {
// // throw new Error("CBS response API failed");
// // }
// // if (res.ok) {
// // // navigate only after successful API hit
// // setTimeout(() => {
// // router.push("/eMandate/mandate_page");
// // }, 1500);
// // }
// // }
// if (success) {
// try {
// const encoded_data = localStorage.getItem("Validate_data");
// if (!encoded_data) {
// console.error("Validate_data not found in localStorage");
// return;
// }
// const formData = new FormData();
// formData.append("data", encoded_data);
// const res = await fetch(
// "https://apiemandate.kccb.in:9035/EMandate/auth-cbs-resp",
// {
// method: "POST",
// body: formData,
// }
// );
// if (!res.ok) {
// throw new Error(`CBS response API failed: ${res.status}`);
// }
// const contentType = res.headers.get("content-type");
// const result =
// contentType && contentType.includes("application/json")
// ? await res.json()
// : await res.text();
// console.log("CBS Response:", result);
// setTimeout(() => {
// router.push("/eMandate/mandate_page");
// }, 1000);
// } catch (error) {
// console.error("CBS API Error:", error);
// }
// }
// else {
// setOtp("");
// }
// setIsVerifying(false);
// };
const handleVerifyOtp = async () => {
const mobile = localStorage.getItem("userMobNo");
if (!mobile || otp.length !== 6) return;
setIsVerifying(true);
const success = await verifyOtp(otp, mobile);
if (success) {
const encoded_data = localStorage.getItem("Validate_data");
if (!encoded_data) {
console.error("Validate_data not found in localStorage");
setIsVerifying(false);
return;
}
const url = "https://apiemandate.kccb.in:9035/EMandate/auth-cbs-resp";
console.log("Redirecting (POST) to URL:", url);
console.log("POST Body Data:", { data: encoded_data });
// Create a form for POST redirect
const form = document.createElement("form");
form.method = "POST";
form.action = url;
// Add hidden field "data"
const input = document.createElement("input");
input.type = "hidden";
input.name = "data";
input.value = encoded_data;
form.appendChild(input);
document.body.appendChild(form);
// Submit → browser navigates to URL using POST
form.submit();
return;
}
else {
setOtp("");
}
setIsVerifying(false);
};
if (authorized === null) {
return (
<Center style={{ height: "100vh" }}>
<Loader size="lg" color="green" />
</Center>
);
}
return (
<Providers>
<Box
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
// background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
background: 'linear-gradient(179deg, #3faa56ff 49%, #3aa760ff 80%)'
}}
>
{/* HEADER */}
<Paper
radius={0}
shadow="md"
style={{
position: "sticky",
top: 0,
zIndex: 100,
background: "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
// padding: isMobile ? "0.75rem 1rem" : "1rem 1.5rem",
}}
>
<Container size="xl">
<Group justify="space-between">
<Group gap="md">
<Image
src={logo}
component={NextImage}
fit="contain"
alt="KCC Bank Logo"
style={{
width: isMobile ? "50px" : "70px",
height: isMobile ? "50px" : "70px",
}}
/>
{!isMobile && (
<Title
order={2}
c="white"
style={{
fontSize: "clamp(1rem, 2vw, 1.5rem)",
fontFamily: "Roboto",
}}
>
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
)}
</Group>
{isMobile ? (
<ActionIcon
variant="subtle"
color="white"
size="lg"
onClick={handleLogout}
title="Logout"
>
<IconLogout size={24} />
</ActionIcon>
) : (
<Button
variant="white"
color="gray"
onClick={handleLogout}
leftSection={<IconLogout size={20} />}
>
Logout
</Button>
)}
</Group>
</Container>
</Paper>
{/* WELCOME BAR */}
<Paper
radius={0}
p={isMobile ? "sm" : "md"}
bg="white"
shadow="sm"
style={{ borderBottom: "3px solid #0a7228" }}
>
<Container size="xl">
<Stack gap="xs">
<Text
fw={600}
size={isMobile ? "lg" : "xl"}
style={{ fontFamily: "Inter" }}
>
Welcome, {custname ?? "User"}
</Text>
<Divider />
</Stack>
</Container>
</Paper>
{/* MAIN CONTENT */}
<Box style={{ flex: 1, padding: "2rem 1rem" }}>
<Container size="sm">
<Center>
<Paper
shadow="xl"
radius="xl"
p={isMobile ? "xl" : "2rem"}
style={{
maxWidth: "500px",
width: "100%",
background: "rgba(255, 255, 255, 0.95)",
backdropFilter: "blur(10px)",
}}
>
<Stack gap="xl" align="center">
{/* Icon */}
<Box
style={{
width: "80px",
height: "80px",
borderRadius: "50%",
background:
"linear-gradient(135deg, #0a7228 0%, #2563eb 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<IconShieldCheck size={48} color="white" />
</Box>
{/* Title */}
<Stack gap="xs" align="center">
<Title
order={2}
ta="center"
style={{
fontSize: isMobile ? "1.5rem" : "2rem",
}}
>
OTP Verification
</Title>
<Text size="sm" c="dimmed" ta="center">
Enter the 6-digit OTP sent to your registered mobile
number
</Text>
<Text size="sm" fw={600} c="blue">
{localStorage.getItem("userMobNo")?.replace(
/(\d{2})(\d{4})(\d{4})/,
"+91 $1****$3"
)}
</Text>
</Stack>
{/* OTP Input */}
<Stack gap="md" align="center" w="100%">
<PinInput
size={isMobile ? "lg" : "xl"}
length={6}
value={otp}
onChange={setOtp}
placeholder="○"
type="number"
oneTimeCode
styles={{
input: {
fontSize: isMobile ? "1.5rem" : "2rem",
fontWeight: 600,
borderColor: "#0a7228",
},
}}
/>
{/* Timer */}
<Group gap="xs">
<Text size="sm" c="dimmed">
{canResend ? (
"Didn't receive OTP?"
) : (
<>
Resend OTP in{" "}
<Text component="span" fw={600} c="blue">
{timer}s
</Text>
</>
)}
</Text>
</Group>
{/* Verify Button */}
<Button
fullWidth
size="lg"
onClick={handleVerifyOtp}
loading={isVerifying}
disabled={otp.length !== 6 || isVerifying}
gradient={{ from: "teal", to: "blue", deg: 60 }}
variant="gradient"
radius="md"
>
{isVerifying ? "Verifying..." : "Verify OTP"}
</Button>
{/* Resend Button */}
<Button
fullWidth
variant="light"
size="md"
onClick={handleResendOtp}
disabled={!canResend || isResending}
loading={isResending}
leftSection={<IconRefresh size={18} />}
>
{isResending ? "Sending..." : "Resend OTP"}
</Button>
</Stack>
{/* Info */}
<Paper p="md" radius="md" bg="blue.0" w="100%">
<Text size="xs" c="blue.9" ta="center">
<Text component="span" fw={600}>
Note:{" "}
</Text>
Please do not share your OTP with anyone. KCC Bank
will never ask for your OTP.
</Text>
</Paper>
</Stack>
</Paper>
</Center>
</Container>
</Box>
{/* FOOTER */}
<Paper
radius={0}
p="md"
bg="gray.9"
style={{
borderTop: "1px solid #ddd",
}}
>
<Container size="xl">
<Text size="xs" c="white" ta="center">
© 2025 The Kangra Central Co-Operative Bank Ltd. All rights
reserved.
</Text>
</Container>
</Paper>
</Box>
</Providers>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1,46 +1,46 @@
'use client'; 'use client';
import { Box, Image, ActionIcon } from '@mantine/core'; import { Box, Image, ActionIcon } from '@mantine/core';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
import { useRef, useState, useEffect } from 'react'; import { useRef, useState, useEffect } from 'react';
import DICGC from '@/app/image/DICGC_image.jpg'; import DICGC from '@/app/image/DICGC_image.jpg';
import objective from '@/app/image/objective.jpg'; import objective from '@/app/image/objective.jpg';
import vision from '@/app/image/vision.jpg'; import vision from '@/app/image/vision.jpg';
export default function CustomCarousel() { export default function CustomCarousel() {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const images = [DICGC,vision,objective]; const images = [DICGC, vision, objective];
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const scrollToIndex = (index: number) => { const scrollToIndex = (index: number) => {
const container = scrollRef.current; const container = scrollRef.current;
if (!container) return; if (!container) return;
const slideWidth = container.offsetWidth; // full width per slide const slideWidth = container.offsetWidth; // full width per slide
container.scrollTo({ container.scrollTo({
left: slideWidth * index, left: slideWidth * index,
behavior: 'smooth', behavior: 'smooth',
}); });
}; };
const scrollRight = () => { const scrollRight = () => {
const nextIndex = (currentIndex + 1) % images.length; const nextIndex = (currentIndex + 1) % images.length;
setCurrentIndex(nextIndex); setCurrentIndex(nextIndex);
scrollToIndex(nextIndex); scrollToIndex(nextIndex);
}; };
const scrollLeft = () => { const scrollLeft = () => {
const prevIndex = (currentIndex - 1 + images.length) % images.length; const prevIndex = (currentIndex - 1 + images.length) % images.length;
setCurrentIndex(prevIndex); setCurrentIndex(prevIndex);
scrollToIndex(prevIndex); scrollToIndex(prevIndex);
}; };
useEffect(() => { useEffect(() => {
scrollToIndex(currentIndex); scrollToIndex(currentIndex);
}, [currentIndex]); }, [currentIndex]);
return ( return (
<Box style={{ position: 'relative', width: '90%', overflow: 'hidden',backgroundColor:"white" }}> <Box style={{ position: 'relative', width: '90%', overflow: 'hidden', backgroundColor: "white" }}>
{/* Scrollable container */} {/* Scrollable container */}
<Box <Box
ref={scrollRef} ref={scrollRef}
@@ -54,29 +54,31 @@ export default function CustomCarousel() {
{images.map((img, i) => ( {images.map((img, i) => (
<Box <Box
key={i} key={i}
onClick={() => {
if (i === 0) window.open("https://dicgc.org.in", "_blank");
}}
style={{ style={{
flex: '0 0 100%', flex: "0 0 100%",
scrollSnapAlign: 'start', scrollSnapAlign: "start",
height: '250px', height: "250px",
minWidth: '100%', minWidth: "100%",
maxWidth: '100%', cursor: i === 0 ? "pointer" : "default",
borderRadius: '8px',
backgroundColor: 'white',
}} }}
> >
<Image <Image
src={img.src} src={img.src}
alt={`Slide ${i + 1}`} alt={`Slide ${i + 1}`}
style={{ style={{
width: '100%', width: "100%",
height: '100%', height: "100%",
objectFit: 'contain', objectFit: "contain",
}} }}
/> />
</Box> </Box>
))} ))}
</Box> </Box>
{/* Left Scroll Button */} {/* Left Scroll Button */}
<ActionIcon <ActionIcon
variant="filled" variant="filled"
@@ -92,7 +94,7 @@ export default function CustomCarousel() {
> >
<IconChevronLeft size={24} /> <IconChevronLeft size={24} />
</ActionIcon> </ActionIcon>
{/* Right Scroll Button */} {/* Right Scroll Button */}
<ActionIcon <ActionIcon
variant="filled" variant="filled"
@@ -111,4 +113,3 @@ export default function CustomCarousel() {
</Box> </Box>
); );
} }

View File

@@ -7,9 +7,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0.5rem 1rem; padding: 0.8rem 2rem;
background: linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%); background: linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%);
flex-wrap: wrap; /* allow wrapping on mobile */ flex-wrap: wrap; /* Allow wrapping on mobile */
} }
.header-text { .header-text {
@@ -20,7 +20,6 @@
text-align: left; text-align: left;
} }
/* Desktop text */
.desktop-text { .desktop-text {
color: white; color: white;
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;

View File

@@ -8,7 +8,7 @@ import { sendOtp, verifyLoginOtp } from '@/app/_util/otp';
import NextImage from "next/image"; import NextImage from "next/image";
import styles from './page.module.css'; import styles from './page.module.css';
import logo from '@/app/image/logo1.jpg'; import logo from '@/app/image/logo1.jpg';
import frontPage from '@/app/image/ib_front_1.jpg'; import frontPage from '@/app/image/ib_front_3.jpg';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { generateCaptcha } from '@/app/captcha'; import { generateCaptcha } from '@/app/captcha';
import { IconRefresh, IconShieldLockFilled } from "@tabler/icons-react"; import { IconRefresh, IconShieldLockFilled } from "@tabler/icons-react";
@@ -45,8 +45,9 @@ export default function Login() {
} }
try { try {
// await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: mobile }); const maskedCIF = CIF?.replace(/.(?=.{3})/g, '*');
await sendOtp({ type: 'LOGIN_OTP', username: CIF, mobileNumber: "6297421727" }); await sendOtp({ type: 'LOGIN_OTP', username: maskedCIF, mobileNumber: mobile });
// await sendOtp({ type: 'LOGIN_OTP', username: maskedCIF, mobileNumber: "7890544527" });
notifications.show({ notifications.show({
color: 'orange', color: 'orange',
title: 'OTP Required', title: 'OTP Required',
@@ -78,6 +79,7 @@ export default function Login() {
title: `${err.message}`, title: `${err.message}`,
message: 'OTP verification failed. Please try again later.', message: 'OTP verification failed. Please try again later.',
color: 'red', color: 'red',
autoClose: 700,
}); });
return false; return false;
} }
@@ -441,38 +443,45 @@ export default function Login() {
)} )}
</Modal> </Modal>
{/* Main Screen */} {/* Main Screen */}
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto", paddingTop: "5%" }}> <div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto" }}>
{/* Header */} {/* Header */}
<Box className={styles.header}> <Box
<Image component="header"
src={logo} className={styles.header}
component={NextImage} style={{
fit="contain" width: "100%",
alt="ebanking" padding: "0.8rem 2rem",
style={{ width: "60px", height: "auto" }} background: "linear-gradient(15deg, rgba(10, 114, 40, 1) 55%, rgba(101, 101, 184, 1) 100%)",
/> display: "flex",
<Box className={styles['header-text']}> alignItems: "center",
{/* Desktop */} justifyContent: "space-between",
<Title className={styles['desktop-text']} ref={headerRef} order={2}> color: "white",
THE KANGRA CENTRAL CO-OPERATIVE BANK LTD. boxShadow: "0 2px 6px rgba(0,0,0,0.15)",
</Title> position: "sticky",
<Text className={styles['desktop-address']} size="xs"> top: 0,
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215 zIndex: 100,
</Text> }}
>
{/* Mobile */} <Group gap="md">
<Title className={styles['mobile-text']} order={5}> <Image
THE KANGRA CENTRAL src={logo}
</Title> component={NextImage}
<Title className={styles['mobile-text']} order={5}> fit="contain"
CO-OPERATIVE BANK LTD. alt="ebanking"
</Title> style={{ width: "60px", height: "auto" }}
<Text className={styles['mobile-text']} size="xs"> />
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215 <div>
</Text> <Title order={3} style={{ fontFamily: "Roboto", color: "white", marginBottom: 2 }}>
</Box> THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.
</Title>
<Text size="xs" c="white" style={{ opacity: 0.85 }}>
Head Office: Dharmshala, District Kangra (H.P), Pin: 176215
</Text>
</div>
</Group>
</Box> </Box>
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
{/* Movable text */} {/* Movable text */}
<Box <Box
@@ -526,8 +535,30 @@ export default function Login() {
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
/> />
</div> </div>
<Box w={{ base: "100%", md: "45%" }} p="lg"> <Box
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, height: '70vh', justifyContent: 'space-between' }} > w={{ base: "100%", md: "45%" }}
p="lg"
style={{
height: "100%", // becomes 80vh automatically
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<Card
shadow="md"
padding="xl"
radius="md"
style={{
width: "100%",
height: "100%", // fills Box → fills 80vh
maxWidth: "100%",
overflowY: "auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}
>
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
<TextInput <TextInput
label="User ID / User Name" label="User ID / User Name"
@@ -619,7 +650,7 @@ export default function Login() {
{isLogging ? "Processing..." : buttonLabel} {isLogging ? "Processing..." : buttonLabel}
</Button> </Button>
<Box mt="xs"> <Box mt="xs">
<Text size="sm"> <Text size="md">
<Text component="span" c="red" fw={600}>Note: </Text> <Text component="span" c="red" fw={600}>Note: </Text>
<Text component="span" c="black"> <Text component="span" c="black">
Existing users logging in to the new Internet Banking for the first time should use their CIF number to avoid login issues. Existing users logging in to the new Internet Banking for the first time should use their CIF number to avoid login issues.
@@ -630,6 +661,7 @@ export default function Login() {
</Card> </Card>
</Box> </Box>
</div> </div>
{/* Carousel and Notes */} {/* Carousel and Notes */}
<Flex direction={{ base: "column", md: "row" }} mt="md" px="md" py="sm" gap="sm"> <Flex direction={{ base: "column", md: "row" }} mt="md" px="md" py="sm" gap="sm">
<Box w={{ base: "100%", md: "85%" }}> <Box w={{ base: "100%", md: "85%" }}>
@@ -639,7 +671,14 @@ export default function Login() {
<Title order={2}> <IconShieldLockFilled />Security Notes :</Title> <Title order={2}> <IconShieldLockFilled />Security Notes :</Title>
<Text mt="sm" size="md">When you Login, Your User Id and Password travels in an encrypted and highly secured mode.</Text> <Text mt="sm" size="md">When you Login, Your User Id and Password travels in an encrypted and highly secured mode.</Text>
<Text mt="sm" fs="italic">For more information on Products and Services, Please Visit</Text> <Text mt="sm" fs="italic">For more information on Products and Services, Please Visit</Text>
<Anchor href="http://www.kccb.in/"> http://www.kccb.in/</Anchor> <Anchor
href="https://kccbhp.bank.in/"
target="_blank"
rel="noopener noreferrer"
>
https://kccbhp.bank.in/
</Anchor>
</Box> </Box>
</Flex> </Flex>
{/* Footer */} {/* Footer */}