fix: design in view profile and account overview
feat : page add for e mandate otp
This commit is contained in:
@@ -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">
|
||||
<Select
|
||||
label="Select Account Number"
|
||||
placeholder="Choose account number"
|
||||
data={accountOptions}
|
||||
value={selectedAccNo}
|
||||
onChange={handleAccountSelection}
|
||||
searchable
|
||||
/>
|
||||
<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 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>
|
||||
</Group>
|
||||
{/* 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>
|
||||
|
||||
<Group p="apart">
|
||||
<Text size="sm" fw={500} c="dimmed">Account Type</Text>
|
||||
<Text size="md">{accountDetails.stAccountType}</Text>
|
||||
</Group>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
<Group p="apart">
|
||||
<Text size="sm" fw={500} c="dimmed">Description</Text>
|
||||
<Text size="md">{accountDetails.stBookingNumber}</Text>
|
||||
</Group>
|
||||
|
||||
{/* 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,
|
||||
})}
|
||||
</Text>
|
||||
{/* 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>
|
||||
<Group p="apart">
|
||||
<Text size="sm" fw={500} c="dimmed">Available Balance</Text>
|
||||
<Text size="md" c="red">
|
||||
– ₹{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", {
|
||||
minimumFractionDigits: 2,
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
</>
|
||||
<Badge size="md" variant="white" color={accountColor} radius="md">
|
||||
{accountDetails.stAccountType}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Divider color="white" opacity={0.2} my="sm" />
|
||||
|
||||
<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">
|
||||
<Text size="sm" fw={500} c="dimmed">Available Balance</Text>
|
||||
<Text size="md" c="green">
|
||||
₹{parseFloat(accountDetails.stAvailableBalance).toLocaleString("en-IN", {
|
||||
<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,
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
</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>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -230,9 +230,9 @@ export default function AccountStatementPage() {
|
||||
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") || ""
|
||||
localStorage.getItem("remitter_branch_no") || "",
|
||||
localStorage.getItem("remitter_cif_no") || "",
|
||||
localStorage.getItem("remitter_address") || ""
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -247,9 +247,9 @@ export default function AccountStatementPage() {
|
||||
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") || ""
|
||||
localStorage.getItem("remitter_branch_no") || "",
|
||||
localStorage.getItem("remitter_cif_no") || "",
|
||||
localStorage.getItem("remitter_address") || ""
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -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">
|
||||
|
||||
@@ -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" }}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,18 +445,42 @@ 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>
|
||||
|
||||
<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">Mon–Fri 10 AM – 4 PM</Text>
|
||||
<Text size="sm" c="dimmed">Sat 10 AM – 2 PM</Text>
|
||||
<Text size="sm" c="dimmed">Phone: +91-1800-1808008 </Text>
|
||||
<Text size="sm" c="dimmed">Mon–Sat 10 AM – 5 PM</Text>
|
||||
<Text size="sm" c="dimmed">(The Second and fourth Saturdays are holidays)</Text>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
@@ -40,7 +41,7 @@ export default function ViewProfile() {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Login-Type": "IB",
|
||||
"X-Login-Type": "IB",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
@@ -79,39 +80,90 @@ 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;
|
||||
}) => (
|
||||
<Grid align="flex-start" gutter="xs" mb={6}>
|
||||
<Grid.Col span={3}>
|
||||
<Text c="dimmed" size="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
{link ? (
|
||||
<Anchor size="sm" href={link} target="_blank" rel="noopener noreferrer">
|
||||
{value}
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text size="sm">{value}</Text>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
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}>
|
||||
{label}
|
||||
</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">
|
||||
{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 (
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
40
src/app/eMandate/authUtils.ts
Normal file
40
src/app/eMandate/authUtils.ts
Normal 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;
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
65
src/app/eMandate/otpUtils.ts
Normal file
65
src/app/eMandate/otpUtils.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
394
src/app/eMandate/otp_page/page.tsx
Normal file
394
src/app/eMandate/otp_page/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
src/app/image/DICGC_image - Copy.jpg
Normal file
BIN
src/app/image/DICGC_image - Copy.jpg
Normal file
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 |
BIN
src/app/image/ib_front_3.jpg
Normal file
BIN
src/app/image/ib_front_3.jpg
Normal file
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 |
@@ -1,46 +1,46 @@
|
||||
'use client';
|
||||
|
||||
|
||||
import { Box, Image, ActionIcon } from '@mantine/core';
|
||||
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import DICGC from '@/app/image/DICGC_image.jpg';
|
||||
import objective from '@/app/image/objective.jpg';
|
||||
import vision from '@/app/image/vision.jpg';
|
||||
|
||||
|
||||
export default function CustomCarousel() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const images = [DICGC,vision,objective];
|
||||
const images = [DICGC, vision, objective];
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
|
||||
const slideWidth = container.offsetWidth; // full width per slide
|
||||
container.scrollTo({
|
||||
left: slideWidth * index,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const scrollRight = () => {
|
||||
const nextIndex = (currentIndex + 1) % images.length;
|
||||
setCurrentIndex(nextIndex);
|
||||
scrollToIndex(nextIndex);
|
||||
};
|
||||
|
||||
|
||||
const scrollLeft = () => {
|
||||
const prevIndex = (currentIndex - 1 + images.length) % images.length;
|
||||
setCurrentIndex(prevIndex);
|
||||
scrollToIndex(prevIndex);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
scrollToIndex(currentIndex);
|
||||
}, [currentIndex]);
|
||||
|
||||
|
||||
return (
|
||||
<Box style={{ position: 'relative', width: '90%', overflow: 'hidden',backgroundColor:"white" }}>
|
||||
<Box style={{ position: 'relative', width: '90%', overflow: 'hidden', backgroundColor: "white" }}>
|
||||
{/* Scrollable container */}
|
||||
<Box
|
||||
ref={scrollRef}
|
||||
@@ -54,29 +54,31 @@ 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 */}
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
@@ -92,7 +94,7 @@ export default function CustomCarousel() {
|
||||
>
|
||||
<IconChevronLeft size={24} />
|
||||
</ActionIcon>
|
||||
|
||||
|
||||
{/* Right Scroll Button */}
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
@@ -111,4 +113,3 @@ export default function CustomCarousel() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user