fix: design in view profile and account overview

feat : page add for e mandate otp
This commit is contained in:
2025-12-06 13:54:20 +05:30
parent cf9faf2e82
commit ad758eb14d
25 changed files with 1027 additions and 175 deletions

View File

@@ -2,16 +2,36 @@
import React, { useEffect, useState } from "react";
import {
Group,
Container,
Paper,
Select,
Stack,
Text,
Title,
Group,
Badge,
Divider,
Loader,
Center,
Card,
SimpleGrid,
ThemeIcon,
Box,
rem,
Grid,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation";
import {
IconCreditCard,
IconWallet,
IconTrendingUp,
IconBuilding,
IconCircleCheck,
IconAlertCircle,
IconUser,
IconFileText,
IconCircleDot,
} from "@tabler/icons-react";
interface accountData {
stAccountNo: string;
@@ -19,23 +39,21 @@ interface accountData {
stAvailableBalance: string;
custname: string;
stBookingNumber: string;
stApprovedAmount?: string; // optional for loan accounts
stApprovedAmount?: string;
}
export default function AccountDetails() {
const router = useRouter();
export default function App() {
const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]);
const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null);
const [authorized, setAuthorized] = useState<boolean | null>(null);
const [accountDetails, setAccountDetails] = useState<accountData | null>(null);
const searchParams = useSearchParams();
const passedAccNo = searchParams.get("accNo");
const [loading, setLoading] = useState(false);
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
setAuthorized(false);
router.push("/login");
// router.push("/login");
} else {
setAuthorized(true);
}
@@ -51,9 +69,6 @@ export default function AccountDetails() {
value: acc.stAccountNo,
}));
setAccountOptions(options);
if (passedAccNo) {
handleAccountSelection(passedAccNo);
}
}
}
}, [authorized]);
@@ -62,6 +77,8 @@ export default function AccountDetails() {
setSelectedAccNo(accNo);
setAccountDetails(null);
if (!accNo) return;
setLoading(true);
try {
const token = localStorage.getItem("access_token");
const response = await fetch("/api/customer", {
@@ -77,11 +94,10 @@ export default function AccountDetails() {
if (response.ok && Array.isArray(data)) {
const matched = data.find((acc) => acc.stAccountNo === accNo);
if (matched) {
// Simulate approvedBalance for loan accounts
if (matched.stAccountType.toUpperCase().includes("LN")) {
matched.stApprovedAmount = (
parseFloat(matched.stAvailableBalance) + 20000
).toFixed(2); // dummy logic
).toFixed(2);
}
setAccountDetails(matched);
} else {
@@ -102,79 +118,270 @@ export default function AccountDetails() {
title: "Fetch failed",
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;
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md">
Account Details
</Title>
<Stack gap="md">
<Grid gutter="md">
{/* Left side Account Selector */}
<Grid.Col span={{ base: 12, md: 4 }}>
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={4} mb="md">
Account Selector
</Title>
<Select
label="Select Account Number"
placeholder="Choose 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 && (
<Paper withBorder p="md" radius="md" bg="gray.0">
<Stack gap="sm">
<Group p="apart">
<Text size="sm" fw={500} c="dimmed">Account Number</Text>
<Text size="md">{accountDetails.stAccountNo}</Text>
{/* Right side Account Details */}
<Grid.Col span={{ base: 12, md: 8 }}>
<Paper
shadow="sm"
radius="md"
p="md"
withBorder
h={500}
style={{ display: "flex", flexDirection: "column", overflow: "auto" }}
>
<Title order={4} mb="md">
Account Details
</Title>
{/* Loading State */}
{loading && (
<Center style={{ flex: 1 }}>
<Stack align="center" gap="md">
<Loader size="lg" type="dots" />
<Text c="dimmed">Loading account details...</Text>
</Stack>
</Center>
)}
{/* Account Details */}
{!loading && accountDetails && (
<Stack gap="sm" style={{ flex: 1 }}>
{/* Account Header Card */}
<Paper
shadow="md"
p="xs"
radius="md"
style={{
background: "linear-gradient(56deg, rgba(24,140,186,1) 0%, rgba(62,230,132,1) 86%)",
}}
>
<Group justify="space-between" align="flex-start" wrap="wrap" mb="xs">
<Group gap="sm">
<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>
<Badge size="md" variant="white" color={accountColor} radius="md">
{accountDetails.stAccountType}
</Badge>
</Group>
<Group p="apart">
<Text size="sm" fw={500} c="dimmed">Account Type</Text>
<Text size="md">{accountDetails.stAccountType}</Text>
</Group>
<Divider color="white" opacity={0.2} my="sm" />
<Group p="apart">
<Text size="sm" fw={500} c="dimmed">Description</Text>
<Text size="md">{accountDetails.stBookingNumber}</Text>
</Group>
<Box>
<Text size="xs" c="white" opacity={0.8} mb={4}>
Description
</Text>
<Text c="white">{accountDetails.stBookingNumber}</Text>
</Box>
</Paper>
{/* Show Loan-specific fields */}
{accountDetails.stAccountType.toUpperCase().includes("LN") ? (
<>
<Group p="apart">
<Text size="sm" fw={500} c="dimmed">Approved Balance</Text>
<Text size="md" c="gray.8">
{parseFloat(accountDetails.stApprovedAmount || "0").toLocaleString("en-IN", {
minimumFractionDigits: 2,
})}
{/* 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>
<Group p="apart">
<Text size="sm" fw={500} c="dimmed">Available Balance</Text>
<Text size="md" c="red">
{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", {
<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">
<Text size="sm" fw={500} c="dimmed">Available Balance</Text>
<Text size="md" c="green">
{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", {
minimumFractionDigits: 2,
})}
<Card shadow="sm" padding="md" radius="md" withBorder>
<Group gap="xs" mb="xs">
<ThemeIcon size={36} radius="md" variant="light" color="teal">
<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,
})}
</Title>
</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>
)}
{/* Empty State */}
{!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,11 +1,11 @@
"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 { useEffect, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { notifications } from "@mantine/notifications";
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 { generateExcel } from "@/app/_components/statement_download/CsvGenerator";
import { useMediaQuery } from "@mantine/hooks";
@@ -169,7 +169,7 @@ export default function AccountStatementPage() {
{/* Left side form */}
<Grid.Col span={{ base: 12, md: 4 }}>
<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
label="Select Account Number"
placeholder="Choose account number"
@@ -203,7 +203,7 @@ export default function AccountStatementPage() {
{/* Right side transaction list */}
<Grid.Col span={{ base: 12, md: 8 }}>
<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">
<div>
<Text fw={500} ><strong>Account No :</strong> {selectedAccNo}</Text>
@@ -278,7 +278,20 @@ export default function AccountStatementPage() {
</Stack>
</Center>
) : 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 ? (
// ✅ Mobile View Card Layout
<Stack gap="sm">

View File

@@ -126,7 +126,7 @@ export default function AccountSummary() {
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title
order={isMobile ? 4 : 3}
order={isMobile ? 4 : 4}
mb="md"
style={{ textAlign: isMobile ? "center" : "left" }}
>

View File

@@ -190,7 +190,7 @@ const AddBeneficiary: React.FC = () => {
return (
<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">
<Group justify="center">

View File

@@ -217,7 +217,7 @@ export default function ViewBeneficiary() {
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Group justify="space-between" align="center" mb="md" wrap="wrap">
<Title order={3}>My Beneficiaries</Title>
<Title order={4}>My Beneficiaries</Title>
<Button
color="green"

View File

@@ -396,7 +396,7 @@ export default function QuickPay() {
</Modal>
{/* main content */}
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md">
<Title order={4} mb="md">
Quick Pay - Own Bank
</Title>

View File

@@ -397,7 +397,7 @@ export default function SendToBeneficiaryOwn() {
{/* main content */}
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="md">
<Title order={4} mb="md">
Send To Beneficiary
</Title>
<Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md">

View File

@@ -472,16 +472,24 @@ export default function Home() {
Quick Links
</Title>
<Stack gap="xs">
<Button variant="light" color="blue" fullWidth>
Loan EMI Calculator
<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="blue" fullWidth>
<Button variant="light" color="green" fullWidth component="a" href="/BranchLocator" target="_blank">
Branch Locator
</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
</Button>
<Button variant="light" color="blue" fullWidth>
<Button variant="light" color="green" fullWidth component="a" href="/FAQs" target="_blank">
FAQs
</Button>
</Stack>

View File

@@ -303,16 +303,16 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{/* NAVBAR — desktop unchanged, mobile scrollable */}
<Group
style={{
background: "#d3f3bcff",
background: "#c8eeacff",
boxShadow: "0 6px 6px rgba(0,0,0,0.06)",
position: "sticky",
top: isMobile ? 60 : 85,
zIndex: 150,
/* MOBILE FIX 👉 make it scrollable */
/* MOBILE FIX make it scrollable */
overflowX: isMobile ? "auto" : "visible",
whiteSpace: isMobile ? "nowrap" : "normal",
padding: isMobile ? "6px 4px" : "0.8rem",
padding: isMobile ? "6px 4px" : "0.05rem",
}}
>
{navItems.map((item) => {
@@ -324,7 +324,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Group
gap={8}
style={{
padding: isMobile ? "10px 14px" : "12px 24px",
padding: isMobile ? "10px 14px" : "14px 16px",
// borderRadius: isMobile ? 6 : 8,
width: "100%",
transition: "0.2s ease",
@@ -434,7 +434,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<div>
<Text size="sm" fw={500}>The Kangra Central</Text>
<Text size="xs">Co-operative Bank Ltd</Text>
<Text size="sm" fw={500}>Co-operative Bank Ltd</Text>
</div>
</Group>
<Text size="sm" c="dimmed">
@@ -445,9 +445,33 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Grid.Col span={{ base: 12, md: 4 }}>
<Text size="sm" fw={500} mb="md">Quick Links</Text>
<Stack gap="xs">
<Anchor href="#" size="sm" c="dimmed">About Us</Anchor>
<Anchor href="#" size="sm" c="dimmed">Products & Services</Anchor>
<Anchor href="#" size="sm" c="dimmed">Help & Support</Anchor>
<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>
@@ -455,8 +479,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<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">MonFri 10 AM 4 PM</Text>
<Text size="sm" c="dimmed">Sat 10 AM 2 PM</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>

View File

@@ -229,7 +229,7 @@ export default function ChangePassword() {
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm">
<Title order={4} mb="sm">
Change Login Password
</Title>
{/* Scrollable form area */}

View File

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

View File

@@ -10,9 +10,10 @@ import {
Divider,
Loader,
Center,
ActionIcon,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useRouter } from "next/navigation";
import { IconEye, IconEyeOff } from "@tabler/icons-react";
// Response structure from backend
interface ProfileData {
@@ -24,13 +25,13 @@ interface ProfileData {
id: string;
custaddress: string;
pincode: string;
}
export default function ViewProfile() {
const router = useRouter();
const [profileData, setProfileData] = useState<ProfileData | null>(null);
const [loading, setLoading] = useState(true);
const [showCIF, setShowCIF] = useState(false);
const [showPrimaryID, setShowPrimaryID] = useState(false);
// Fetch API with same style as RootLayout
async function handleFetchProfile() {
@@ -79,15 +80,53 @@ export default function ViewProfile() {
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 = ({
label,
value,
link,
masked,
showValue,
onToggle,
}: {
label: string;
value: string;
link?: string;
}) => (
masked?: boolean;
showValue?: boolean;
onToggle?: () => void;
}) => {
const displayValue = masked && !showValue ? maskValue(value) : value;
return (
<Grid align="flex-start" gutter="xs" mb={6}>
<Grid.Col span={3}>
<Text c="dimmed" size="sm" fw={500}>
@@ -95,23 +134,36 @@ export default function ViewProfile() {
</Text>
</Grid.Col>
<Grid.Col span={9}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
{link ? (
<Anchor size="sm" href={link} target="_blank" rel="noopener noreferrer">
{value}
{displayValue}
</Anchor>
) : (
<Text size="sm">{value}</Text>
<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 (
<Paper shadow="xs" radius="md" p="md" withBorder h={500}>
<Title order={4} mb="sm">
{/* <Title order={4} mb="sm">
View Profile
</Title>
<Divider mb="sm" />
<Divider mb="sm" /> */}
{loading ? (
<Center>
@@ -119,22 +171,39 @@ export default function ViewProfile() {
</Center>
) : profileData ? (
<>
<Row label="Customer ID (CIF)" value={profileData.cifNumber} />
<Row label="Customer Name" value={profileData.custname} />
<Row label="ID" value={profileData.id} />
<Row label="Branch No" value={profileData.stBranchNo} />
<Row label="Date of Birth" value={profileData.custdob} />
<Row label="Mobile Number" value={profileData.mobileno} />
{/* Personal Details Section */}
<Title order={4} mb="sm" mt="md">
Personal Details
</Title>
<Divider mb="sm" />
<Row
label="Address"
value={profileData.custaddress}
// link={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
// profileData.custaddress
// )}`}
label="Customer ID (CIF)"
value={profileData.cifNumber}
masked={true}
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} />
{/* 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">

View File

@@ -225,7 +225,7 @@ export default function SetTransactionLimit() {
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm">
<Title order={4} mb="sm">
Set Transaction Limit
</Title>

View File

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

View File

@@ -262,7 +262,7 @@ export default function SetPreferredNameSimple() {
return (
<Paper shadow="sm" radius="md" p="md" withBorder h={500}>
<Title order={3} mb="sm">
<Title order={4} mb="sm">
Set Preferred Name
</Title>

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

@@ -167,12 +167,13 @@ function LoginEmandate() {
}),
});
const result = await response.json();
localStorage.setItem("Validate_data", result);
console.log("Validate Result : ", result);
if (response.ok) {
router.push("/eMandate/mandate_page");
router.push("/eMandate/otp_page");
}
else {
console.log(result);
console.log("validation failed: response",result);
notifications.show({
withBorder: true,
color: "red",

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,394 @@
"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) {
setTimeout(() => router.push("/eMandate/mandate_page"), 1500);
} 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: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -54,27 +54,29 @@ export default function CustomCarousel() {
{images.map((img, i) => (
<Box
key={i}
onClick={() => {
if (i === 0) window.open("https://dicgc.org.in", "_blank");
}}
style={{
flex: '0 0 100%',
scrollSnapAlign: 'start',
height: '250px',
minWidth: '100%',
maxWidth: '100%',
borderRadius: '8px',
backgroundColor: 'white',
flex: "0 0 100%",
scrollSnapAlign: "start",
height: "250px",
minWidth: "100%",
cursor: i === 0 ? "pointer" : "default",
}}
>
<Image
src={img.src}
alt={`Slide ${i + 1}`}
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</Box>
))}
</Box>
{/* Left Scroll Button */}
@@ -111,4 +113,3 @@ export default function CustomCarousel() {
</Box>
);
}

View File

@@ -8,7 +8,7 @@ import { sendOtp, verifyLoginOtp } from '@/app/_util/otp';
import NextImage from "next/image";
import styles from './page.module.css';
import logo from '@/app/image/logo1.jpg';
import frontPage from '@/app/image/ib_front_2.jpg';
import frontPage from '@/app/image/ib_front_3.jpg';
import dynamic from 'next/dynamic';
import { generateCaptcha } from '@/app/captcha';
import { IconRefresh, IconShieldLockFilled } from "@tabler/icons-react";
@@ -534,8 +534,30 @@ export default function Login() {
style={{ width: "100%", height: "100%" }}
/>
</div>
<Box w={{ base: "100%", md: "45%" }} p="lg">
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 550, height: '70vh', justifyContent: 'space-between' }} >
<Box
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}>
<TextInput
label="User ID / User Name"
@@ -627,7 +649,7 @@ export default function Login() {
{isLogging ? "Processing..." : buttonLabel}
</Button>
<Box mt="xs">
<Text size="sm">
<Text size="md">
<Text component="span" c="red" fw={600}>Note: </Text>
<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.
@@ -638,6 +660,7 @@ export default function Login() {
</Card>
</Box>
</div>
{/* Carousel and Notes */}
<Flex direction={{ base: "column", md: "row" }} mt="md" px="md" py="sm" gap="sm">
<Box w={{ base: "100%", md: "85%" }}>
@@ -647,7 +670,14 @@ export default function Login() {
<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" 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>
</Flex>
{/* Footer */}