Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
10a3da8949 | |||
eae989642b | |||
f67319762f | |||
a6af487b67 | |||
26e6dea82b | |||
1023646751 | |||
3ccd7eb690 | |||
df3fa3532f | |||
e3391315dd | |||
9a02cff754 | |||
893dcbc761 | |||
09d61e556c | |||
293a7dbea0 | |||
4b3e89673b | |||
6970d1af0c | |||
011df884d8 | |||
1efe8af7e6 | |||
d52d338a74 | |||
74e6394797 | |||
a46670be1f | |||
6028ed9f5a | |||
bc75470d33 | |||
054c4b8d0e | |||
6f44347947 | |||
92531b02fd | |||
49dae3624f | |||
a86cb87da0 | |||
2e90465c89 |
@@ -1,6 +1,8 @@
|
||||
- npx create-next-app@latest ib --typescript
|
||||
- cd ib
|
||||
- npm install @mantine/core @mantine/hooks
|
||||
- npm install @mantine/carousel@7.4.0 --legacy-peer-deps
|
||||
- npm install embla-carousel-react --legacy-peer-deps
|
||||
|
||||
|
||||
- npm run build
|
||||
|
23
instruction.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
- download Aws cli and Aws session manager
|
||||
- Key generate for KCCB
|
||||
- port forwarding :
|
||||
|
||||
aws ssm start-session --target i-0c850dcf8b85b1447 --document-name --profile kccb AWS-StartPortForwardingSession --parameters "portNumber"=["8080"],"localPortNumber"=["8080"]
|
||||
|
||||
- run the api in localhost then port forward to Postgres
|
||||
aws ssm start-session --target i-0c850dcf8b85b1447 --document-name --profile kccb AWS-StartPortForwardingSession --parameters "portNumber"=["5432"],"localPortNumber"=["5431"]
|
||||
|
||||
- For CBS port forward
|
||||
aws ssm start-session --target i-0c850dcf8b85b1447 --document-name --profile kccb AWS-StartPortForwardingSession --parameters "portNumber"=["8686"],"localPortNumber"=["8686"]
|
||||
______________________________________________________________________
|
||||
|
||||
For database:
|
||||
- aws ssm start-session --target i-0c850dcf8b85b1447 --profile kccb
|
||||
- psql -U postgres
|
||||
- \l
|
||||
- psql -U kmobile_db_owner -d kmobile_banking
|
||||
- password : kmobile
|
||||
- SELECT * FROM users;
|
||||
- \x
|
||||
- \d users; -- see the data type of column
|
||||
- \c kmobile_banking kmobile_app_rw -- alter the user
|
@@ -3,7 +3,16 @@ const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["typeorm", "knex"],
|
||||
},
|
||||
reactStrictMode: true
|
||||
reactStrictMode: true,
|
||||
// For port transfer
|
||||
async rewrites() {
|
||||
return[
|
||||
{
|
||||
source:'/api/:path*',
|
||||
destination: 'http://localhost:8080/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
909
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/carousel": "^7.4.0",
|
||||
"@mantine/charts": "^7.11.1",
|
||||
"@mantine/core": "^7.8.1",
|
||||
"@mantine/dates": "^7.8.1",
|
||||
@@ -21,12 +22,13 @@
|
||||
"basic-ftp": "^5.0.5",
|
||||
"casbin": "^5.29.0",
|
||||
"casbin-basic-adapter": "^1.1.0",
|
||||
"ib": "file:",
|
||||
"IB": "file:",
|
||||
"csv-parse": "^5.5.5",
|
||||
"damerau-levenshtein": "^1.0.8",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"ib": "file:",
|
||||
"IB": "file:",
|
||||
"iron-session": "^8.0.1",
|
||||
"luxon": "^3.4.4",
|
||||
"natural": "^6.10.5",
|
||||
@@ -37,6 +39,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-simple-captcha": "^9.3.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sharp": "^0.33.5",
|
||||
"typeorm": "^0.3.20",
|
||||
|
BIN
public/document/disclaimer.pdf
Normal file
BIN
public/document/grievance.jpg
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
public/document/phishing.pdf
Normal file
BIN
public/document/privacy_policy.pdf
Normal file
BIN
public/document/security_tips.pdf
Normal file
179
src/app/(main)/accounts/account_details/page.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Group,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
interface accountData {
|
||||
stAccountNo: string;
|
||||
stAccountType: string;
|
||||
stAvailableBalance: string;
|
||||
custname: string;
|
||||
stBookingNumber: string;
|
||||
stApprovedAmount?: string; // optional for loan accounts
|
||||
}
|
||||
|
||||
export default function AccountDetails() {
|
||||
const router = useRouter();
|
||||
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");
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
setAuthorized(false);
|
||||
router.push("/login");
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authorized) {
|
||||
const saved = sessionStorage.getItem("accountData");
|
||||
if (saved) {
|
||||
const parsed: accountData[] = JSON.parse(saved);
|
||||
const options = parsed.map((acc) => ({
|
||||
label: `${acc.stAccountNo} - ${acc.stAccountType}`,
|
||||
value: acc.stAccountNo,
|
||||
}));
|
||||
setAccountOptions(options);
|
||||
if (passedAccNo) {
|
||||
handleAccountSelection(passedAccNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [authorized]);
|
||||
|
||||
const handleAccountSelection = async (accNo: string | null) => {
|
||||
setSelectedAccNo(accNo);
|
||||
setAccountDetails(null);
|
||||
if (!accNo) return;
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch("/api/customer", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data: accountData[] = await response.json();
|
||||
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
|
||||
}
|
||||
setAccountDetails(matched);
|
||||
} else {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "orange",
|
||||
title: "Account not found",
|
||||
message: "Selected account was not found in the response.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Fetch failed",
|
||||
message: "Could not fetch account details. Try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!authorized) return null;
|
||||
|
||||
return (
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
|
||||
<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
|
||||
/>
|
||||
|
||||
{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>
|
||||
|
||||
<Group p="apart">
|
||||
<Text size="sm" fw={500} c="dimmed">Account Type</Text>
|
||||
<Text size="md">{accountDetails.stAccountType}</Text>
|
||||
</Group>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
) : (
|
||||
<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,
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
221
src/app/(main)/accounts/account_statement/accountStatement.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
import { Paper, Select, Title, Button, Text, Grid, ScrollArea, Table, Divider } from "@mantine/core";
|
||||
import { DateInput } from '@mantine/dates';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import dayjs from 'dayjs';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
export default function AccountStatementPage() {
|
||||
const [accountOptions, setAccountOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null);
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [transactions, setTransactions] = useState<any[]>([]);
|
||||
const searchParams = useSearchParams();
|
||||
const passedAccNo = searchParams.get("accNo");
|
||||
|
||||
useEffect(() => {
|
||||
const saved = sessionStorage.getItem("accountData");
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
const options = parsed.map((acc: any) => ({
|
||||
label: `${acc.stAccountNo} - ${acc.stAccountType}`,
|
||||
value: acc.stAccountNo,
|
||||
}));
|
||||
setAccountOptions(options);
|
||||
if (passedAccNo) {
|
||||
setSelectedAccNo(passedAccNo);
|
||||
//Automatically fetch last 5 transactions if accNo is passed
|
||||
const token = localStorage.getItem("access_token");
|
||||
fetch(`/api/transactions/account/${passedAccNo}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) {
|
||||
const last5 = data.slice(-5).reverse();
|
||||
setTransactions(last5);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Fetch Failed",
|
||||
message: "Could not load recent transactions.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [passedAccNo]);
|
||||
|
||||
const handleAccountTransaction = async () => {
|
||||
if (!selectedAccNo || !startDate || !endDate) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Missing field",
|
||||
message: "Please select Account number,Start date and End date",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate);
|
||||
const today = dayjs().startOf('day');
|
||||
if (end.isAfter(today)) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Invalid End Date",
|
||||
message: "End date can not be the future date.",
|
||||
autoClose: 4000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (start.isAfter(end)) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Invalid Start Date",
|
||||
message: "Start date can not be less than end date",
|
||||
autoClose: 4000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (end.diff(start, "day") > 90) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Invalid Date range",
|
||||
message: "End date must be within 90 days from start date",
|
||||
autoClose: 4000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch(`/api/transactions/account/${selectedAccNo}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && Array.isArray(data)) {
|
||||
const filterData = data.filter((txn: any) => {
|
||||
const txnDate = dayjs(txn.date, 'DD/MM/YYYY');
|
||||
return txnDate.isSameOrAfter(start) && txnDate.isSameOrBefore(end);
|
||||
});
|
||||
setTransactions(filterData);
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later",
|
||||
message: "Unable to Fetch Account Transaction, Please try again later",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
const cellStyle = {
|
||||
border: "1px solid #ccc",
|
||||
padding: "8px",
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid gutter="md">
|
||||
{/* Left side – form */}
|
||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
|
||||
<Title order={4} mb="sm">Account Statement</Title>
|
||||
<Select
|
||||
label="Select Account Number"
|
||||
placeholder="Choose account number"
|
||||
data={accountOptions}
|
||||
value={selectedAccNo}
|
||||
onChange={setSelectedAccNo}
|
||||
searchable
|
||||
/>
|
||||
<DateInput
|
||||
label="Start Date"
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
placeholder="Enter start date"
|
||||
/>
|
||||
<DateInput
|
||||
label="End Date"
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
placeholder="Enter end date"
|
||||
/>
|
||||
<Button fullWidth mt="md" onClick={handleAccountTransaction}>
|
||||
Proceed
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Right side – transaction list */}
|
||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder h={400} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Title order={5} mb="xs">Account Transaction Statement</Title>
|
||||
<Text fw={500} fs="italic" >Account No : {selectedAccNo}</Text>
|
||||
<Divider size="xs" />
|
||||
<ScrollArea style={{ flex: 1 }}>
|
||||
{transactions.length === 0 ? (
|
||||
<p>No transactions found.</p>
|
||||
) : (
|
||||
<>
|
||||
<Text fs="italic" c='#228be6' ta='center'>
|
||||
{!startDate && !endDate ? 'Last 5 Transactions'
|
||||
: startDate && endDate ? `Transactions from ${dayjs(startDate).format("DD/MM/YYYY")} to ${dayjs(endDate).format("DD/MM/YYYY")}`
|
||||
: ""}
|
||||
</Text>
|
||||
<Table style={{ borderCollapse: "collapse", width: '100%' }}>
|
||||
<thead style={{ backgroundColor: "#3385ff" }}>
|
||||
{/* <tr>
|
||||
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Name</th>
|
||||
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Date</th>
|
||||
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Type</th>
|
||||
<th style={{ ...cellStyle, position: 'sticky', textAlign: "left" }}>Amount(₹)</th>
|
||||
</tr> */}
|
||||
</thead>
|
||||
<tbody style={{ maxHeight: '250px', overflowY: 'auto', width: '100%' }}>
|
||||
{transactions.map((txn, i) => (
|
||||
<tr key={i}>
|
||||
<td style={{ ...cellStyle, textAlign: "left" }}> {txn.name || "—"}</td>
|
||||
<td style={{ ...cellStyle, textAlign: "left" }}>{txn.date || "—"}</td>
|
||||
{/* <td style={{ ...cellStyle, textAlign: "left" }}>{txn.type}</td> */}
|
||||
<td style={{ ...cellStyle, textAlign: "left", color: txn.type === "DR" ? "#e03131" : "#2f9e44" }}>
|
||||
{parseFloat(txn.amount).toLocaleString("en-IN", {
|
||||
minimumFractionDigits: 2,
|
||||
})} <span style={{fontSize:'10px'}}>{txn.type==="DR"?"Dr.":"Cr."}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
</Grid.Col >
|
||||
</Grid >
|
||||
);
|
||||
}
|
||||
|
||||
|
28
src/app/(main)/accounts/account_statement/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import AccountStatementPage from "./accountStatement";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function AccountStatement() {
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
if (authorized) {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<AccountStatementPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
72
src/app/(main)/accounts/layout.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
import { Divider, Stack, Text } from '@mantine/core';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const links = [
|
||||
{ label: "Account Summary", href: "/accounts" },
|
||||
{ label: "Statement of Account", href: "/accounts/account_statement" },
|
||||
{ label: "Account Details", href: "/accounts/account_details" },
|
||||
];
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<div style={{ display: "flex", height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: "16%",
|
||||
backgroundColor: '#c5e4f9',
|
||||
borderRight: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
|
||||
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
Accounts
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm" justify="flex-start" style={{ padding: '1rem' }}>
|
||||
{links.map(link => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Text
|
||||
key={link.href}
|
||||
component={Link}
|
||||
href={link.href}
|
||||
c={isActive ? 'darkblue' : 'blue'}
|
||||
style={{
|
||||
textDecoration: isActive ? 'underline' : 'none',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '1rem' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
141
src/app/(main)/accounts/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { Group, Paper, ScrollArea, Table, Text, Title } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface accountData {
|
||||
stAccountNo: string;
|
||||
stAccountType: string;
|
||||
stAvailableBalance: string;
|
||||
custname: string;
|
||||
}
|
||||
|
||||
export default function AccountSummary() {
|
||||
const router = useRouter();
|
||||
const [authorized, setAuthorized] = useState<boolean | null>(null);
|
||||
const [accountData, setAccountData] = useState<accountData[]>([]);
|
||||
|
||||
async function FetchAccountDetails() {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch("/api/customer", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && Array.isArray(data)) {
|
||||
setAccountData(data);
|
||||
sessionStorage.setItem("accountData", JSON.stringify(data));
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later",
|
||||
message: "Unable to Fetch, Please try again later",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
setAuthorized(false);
|
||||
router.push("/login");
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authorized) {
|
||||
FetchAccountDetails();
|
||||
}
|
||||
}, [authorized]);
|
||||
|
||||
const cellStyle = {
|
||||
border: "1px solid #ccc",
|
||||
padding: "10px",
|
||||
};
|
||||
|
||||
// Filter accounts
|
||||
const depositAccounts = accountData.filter(
|
||||
(acc) => !acc.stAccountType.toUpperCase().includes("LN")
|
||||
);
|
||||
const loanAccounts = accountData.filter((acc) =>
|
||||
acc.stAccountType.toUpperCase().includes("LN")
|
||||
);
|
||||
|
||||
// Function to render table rows
|
||||
const renderRows = (data: accountData[]) =>
|
||||
data.map((acc, index) => (
|
||||
<tr key={index}>
|
||||
<td style={{ ...cellStyle, textAlign: "left" }}>{acc.stAccountType}</td>
|
||||
<td
|
||||
style={{ ...cellStyle, textAlign: "right", color: "#1c7ed6", cursor: "pointer" }}
|
||||
onClick={() => router.push(`/accounts/account_details?accNo=${acc.stAccountNo}`)}
|
||||
>
|
||||
{acc.stAccountNo}
|
||||
</td>
|
||||
<td style={{ ...cellStyle, textAlign: "right" }}>
|
||||
{parseFloat(acc.stAvailableBalance).toLocaleString("en-IN", {
|
||||
minimumFractionDigits: 2,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
// Table component
|
||||
const renderTable = (title: string, rows: JSX.Element[]) => (
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder w="100%"
|
||||
// bg="#97E6B8"
|
||||
>
|
||||
<Title order={4} mb="sm">
|
||||
{title}
|
||||
</Title>
|
||||
<ScrollArea>
|
||||
<Table style={{ borderCollapse: "collapse", width: "100%" }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: "#3385ff" }}>
|
||||
<th style={{ ...cellStyle, textAlign: "left" }}>Account Type</th>
|
||||
<th style={{ ...cellStyle, textAlign: "right" }}>Account No.</th>
|
||||
{title.includes("Deposit Accounts (INR)") ?
|
||||
(<th style={{ ...cellStyle, textAlign: "right" }}> Credit Book Balance</th>)
|
||||
: (<th style={{ ...cellStyle, textAlign: "right" }}>Debit Book Balance</th>)}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder h={400}
|
||||
// bg="linear-gradient(90deg,rgba(195, 218, 227, 1) 0%, rgba(151, 230, 184, 1) 50%)"
|
||||
>
|
||||
<Title order={3} mb="md">Account Summary</Title>
|
||||
<Group align="flex-start" grow>
|
||||
{/* Left table for Deposit Accounts */}
|
||||
{depositAccounts.length > 0 && renderTable("Deposit Accounts (INR)", renderRows(depositAccounts))}
|
||||
|
||||
{/* Right table for Loan Accounts (only shown if available) */}
|
||||
{loanAccounts.length > 0 && renderTable("Loan Accounts (INR)", renderRows(loanAccounts))}
|
||||
</Group>
|
||||
<Text mt="sm" size="xs" c="dimmed">
|
||||
* Book Balance includes uncleared effects.
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
73
src/app/(main)/funds_transfer/layout.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import { Divider, Stack, Text } from '@mantine/core';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const links = [
|
||||
{ label: " Quick Pay", href: "/funds_transfer" },
|
||||
{ label: "Add Beneficiary", href: "/accounts/add_beneficiary" },
|
||||
{ label: "View Beneficiary ", href: "/accounts/view_beneficiary" },
|
||||
{ label: "Send to Beneficiary", href: "/accounts/send_beneficiary" },
|
||||
];
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<div style={{ display: "flex", height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: "16%",
|
||||
backgroundColor: '#c5e4f9',
|
||||
borderRight: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
|
||||
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
Send Money
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm" justify="flex-start" style={{ padding: '1rem' }}>
|
||||
{links.map(link => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Text
|
||||
key={link.href}
|
||||
component={Link}
|
||||
href={link.href}
|
||||
c={isActive ? 'darkblue' : 'blue'}
|
||||
style={{
|
||||
textDecoration: isActive ? 'underline' : 'none',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '1rem' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
455
src/app/(main)/funds_transfer/page.tsx
Normal file
@@ -0,0 +1,455 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Group, Modal, Paper, Radio, ScrollArea, Select, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { generateOTP } from '@/app/OTPGenerator';
|
||||
|
||||
interface accountData {
|
||||
stAccountNo: string;
|
||||
stAccountType: string;
|
||||
stAvailableBalance: string;
|
||||
custname: string;
|
||||
}
|
||||
|
||||
export default function QuickPay() {
|
||||
const router = useRouter();
|
||||
const [bankType, setBankType] = useState("own");
|
||||
const [authorized, setAuthorized] = useState<boolean | null>(null);
|
||||
const [accountData, setAccountData] = useState<accountData[]>([]);
|
||||
const [selectedAccNo, setSelectedAccNo] = useState<string | null>(null);
|
||||
const [beneficiaryAcc, setBeneficiaryAcc] = useState("");
|
||||
const [showPayeeAcc, setShowPayeeAcc] = useState(true);
|
||||
const [confirmBeneficiaryAcc, setConfirmBeneficiaryAcc] = useState("");
|
||||
const [beneficiaryType, setBeneficiaryType] = useState<string | null>(null);
|
||||
const [isVisibilityLocked, setIsVisibilityLocked] = useState(false);
|
||||
const [amount, setAmount] = useState("");
|
||||
const [remarks, setRemarks] = useState("");
|
||||
const [showConfirmModel, setConfirmModel] = useState(false);
|
||||
const [showTxnPassword, setShowTxnPassword] = useState(false);
|
||||
const [txnPassword, setTxnPassword] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<"success" | "error" | null>(null);
|
||||
const [beneficiaryName, setBeneficiaryName] = useState<string | null>(null);
|
||||
const [showOtpField, setShowOtpField] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
const [generateOtp, setGenerateOtp] = useState("");
|
||||
|
||||
async function handleGenerateOtp() {
|
||||
// const value = await generateOTP(6);
|
||||
const value = "123456";
|
||||
setGenerateOtp(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const selectedAccount = accountData.find((acc) => acc.stAccountNo === selectedAccNo);
|
||||
const getFullMaskedAccount = (acc: string) => { return "X".repeat(acc.length); };
|
||||
|
||||
const accountOptions = accountData.map((acc) => ({
|
||||
value: acc.stAccountNo,
|
||||
label: `${acc.stAccountNo} (${acc.stAccountType})`,
|
||||
}));
|
||||
|
||||
const FetchAccountDetails = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch("/api/customer", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && Array.isArray(data)) {
|
||||
const filterSAaccount = data.filter((acc) => acc.stAccountType === 'SA');
|
||||
setAccountData(filterSAaccount);
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later",
|
||||
message: "Unable to Fetch, Please try again later",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
setAuthorized(false);
|
||||
router.push("/login");
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authorized) {
|
||||
FetchAccountDetails();
|
||||
}
|
||||
}, [authorized]);
|
||||
|
||||
|
||||
async function handleValidate() {
|
||||
if (!selectedAccNo || !beneficiaryAcc ||
|
||||
!confirmBeneficiaryAcc
|
||||
) {
|
||||
notifications.show({
|
||||
title: "Validation Error",
|
||||
message: "Please fill debit account, beneficiary account number and confirm beneficiary account number",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (beneficiaryAcc.length < 10 || beneficiaryAcc.length > 17) {
|
||||
notifications.show({
|
||||
title: "Invalid Account Number",
|
||||
message: "Please Enter valid account Number",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (beneficiaryAcc !== confirmBeneficiaryAcc) {
|
||||
notifications.show({
|
||||
title: "Mismatch",
|
||||
message: "Beneficiary account numbers do not match",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch(`/api/beneficiary/validate/within-bank?accountNumber=${beneficiaryAcc}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data?.name) {
|
||||
setBeneficiaryName(data.name);
|
||||
setValidationStatus("success");
|
||||
setIsVisibilityLocked(true);
|
||||
} else {
|
||||
setBeneficiaryName("Invalid account number");
|
||||
setValidationStatus("error");
|
||||
setBeneficiaryAcc("");
|
||||
setConfirmBeneficiaryAcc("");
|
||||
}
|
||||
} catch {
|
||||
setBeneficiaryName("Invalid account number");
|
||||
setValidationStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
async function handleProceed() {
|
||||
if (!selectedAccNo || !beneficiaryAcc || !confirmBeneficiaryAcc || !beneficiaryType || !amount || !remarks) {
|
||||
notifications.show({
|
||||
title: "Validation Error",
|
||||
message: "Please fill all required fields",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationStatus !== "success") {
|
||||
notifications.show({
|
||||
title: "Validation Required",
|
||||
message: "Please validate beneficiary before proceeding",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (parseInt(amount) <= 0) {
|
||||
notifications.show({
|
||||
title: "Invalid amount",
|
||||
message: "Amount Can not be less than Zero",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!showOtpField && !showTxnPassword && !showConfirmModel) {
|
||||
setConfirmModel(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!otp) {
|
||||
notifications.show({
|
||||
title: "Enter OTP",
|
||||
message: "Please enter the OTP",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (otp !== generateOtp) {
|
||||
notifications.show({
|
||||
title: "Invalid OTP",
|
||||
message: "The OTP entered does not match",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showTxnPassword) {
|
||||
setShowTxnPassword(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!txnPassword) {
|
||||
notifications.show({
|
||||
title: "Missing field",
|
||||
message: "Please Enter Transaction Password Before Proceed",
|
||||
color: "red",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const token = localStorage.getItem("access_token");
|
||||
|
||||
const res = await fetch("/api/payment/transfer", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fromAccount: selectedAccNo,
|
||||
toAccount: beneficiaryAcc,
|
||||
toAccountType: beneficiaryType,
|
||||
amount: amount,
|
||||
narration: remarks,
|
||||
tpassword: txnPassword,
|
||||
}),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok) {
|
||||
notifications.show({
|
||||
title: "Success",
|
||||
message: "Transaction successful",
|
||||
color: "green",
|
||||
});
|
||||
setShowTxnPassword(false);
|
||||
setTxnPassword("");
|
||||
setShowOtpField(false);
|
||||
setOtp("");
|
||||
setValidationStatus(null);
|
||||
setBeneficiaryName(null);
|
||||
} else {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: result?.error || "Transaction failed",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Something went wrong",
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!authorized) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={showConfirmModel}
|
||||
onClose={() => setConfirmModel(false)}
|
||||
// title="Confirm Transaction"
|
||||
centered
|
||||
>
|
||||
<Stack>
|
||||
<Title order={4}>Confirm Transaction</Title>
|
||||
<Text><strong>Debit Account:</strong> {selectedAccNo}</Text>
|
||||
<Text><strong>Payee Account:</strong> {beneficiaryAcc}</Text>
|
||||
<Text><strong>Payee Name:</strong> {beneficiaryName}</Text>
|
||||
<Text><strong>Amount:</strong> ₹ {amount}</Text>
|
||||
<Text><strong>Remarks:</strong> {remarks}</Text>
|
||||
</Stack>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button variant="default" onClick={() => setConfirmModel(false)}>Cancel</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={async () => {
|
||||
setConfirmModel(false);
|
||||
const otp = await handleGenerateOtp();
|
||||
setShowOtpField(true);
|
||||
notifications.show({
|
||||
title: "OTP Sent",
|
||||
message: `Check your registered device for OTP`,
|
||||
color: "green",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
{/* main content */}
|
||||
<Paper shadow="sm" radius="md" p="md" withBorder h={400}>
|
||||
<Title order={3} mb="md">
|
||||
Quick Pay
|
||||
</Title>
|
||||
<Radio.Group value={bankType} onChange={setBankType} name="bankType" withAsterisk mb="md">
|
||||
<Group justify="center">
|
||||
<Radio value="own" label="Own Bank" />
|
||||
<Radio value="outside" label="Outside Bank" />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
|
||||
{bankType === "own" ? (
|
||||
<div style={{ maxHeight: "320px", overflowY: "auto" }}>
|
||||
<Stack gap="xs">
|
||||
<Group grow>
|
||||
<Select
|
||||
label="Select Debit Account Number"
|
||||
placeholder="Choose account number"
|
||||
data={accountOptions}
|
||||
value={selectedAccNo}
|
||||
onChange={setSelectedAccNo}
|
||||
withAsterisk
|
||||
readOnly={isVisibilityLocked}
|
||||
/>
|
||||
<TextInput
|
||||
label="Payee Account No"
|
||||
value={showPayeeAcc ? beneficiaryAcc : getFullMaskedAccount(beneficiaryAcc)}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (/^\d*$/.test(value)) {
|
||||
setBeneficiaryAcc(value);
|
||||
setShowPayeeAcc(true);
|
||||
}
|
||||
}}
|
||||
onBlur={() => setShowPayeeAcc(false)}
|
||||
onFocus={() => setShowPayeeAcc(true)}
|
||||
withAsterisk
|
||||
readOnly={isVisibilityLocked}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Confirm Payee Account No"
|
||||
value={confirmBeneficiaryAcc}
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (/^\d*$/.test(value)) {
|
||||
setConfirmBeneficiaryAcc(value);
|
||||
}
|
||||
}}
|
||||
// onCopy={(e) => e.preventDefault()}
|
||||
// onPaste={(e) => e.preventDefault()}
|
||||
// onCut={(e) => e.preventDefault()}
|
||||
withAsterisk
|
||||
readOnly={isVisibilityLocked}
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="space-between" >
|
||||
<Text size="xs" c="green" style={{ visibility: selectedAccount ? "visible" : "hidden" }}>Available Balance :
|
||||
{selectedAccount ? selectedAccount.stAvailableBalance : 0}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
{validationStatus === "error" && <Text size="sm" fw={700} ta="right" c="red">{beneficiaryName}</Text>}
|
||||
</Group>
|
||||
</Group>
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label="Payee Name"
|
||||
value={validationStatus === "success" && beneficiaryName ? beneficiaryName : ""}
|
||||
// disabled
|
||||
readOnly
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Beneficiary A/c Type"
|
||||
placeholder="Select type"
|
||||
data={["Savings", "Current"]}
|
||||
value={beneficiaryType}
|
||||
onChange={setBeneficiaryType}
|
||||
withAsterisk
|
||||
readOnly={showOtpField}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Amount"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.currentTarget.value)}
|
||||
error={
|
||||
selectedAccount && Number(amount) > Number(selectedAccount.stAvailableBalance) ?
|
||||
"Amount exceeds available balance" : false}
|
||||
withAsterisk
|
||||
readOnly={showOtpField}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Remarks"
|
||||
placeholder="Enter remarks"
|
||||
value={remarks}
|
||||
onChange={(e) => setRemarks(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
readOnly={showOtpField}
|
||||
/>
|
||||
</Group>
|
||||
<Group grow>
|
||||
{showOtpField && (
|
||||
<TextInput
|
||||
label="OTP"
|
||||
placeholder="Enter OTP"
|
||||
type="otp"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
disabled={showTxnPassword}
|
||||
/>
|
||||
)}
|
||||
{showTxnPassword && (
|
||||
<TextInput
|
||||
label="Transaction Password"
|
||||
placeholder="Enter transaction password"
|
||||
type="password"
|
||||
value={txnPassword}
|
||||
onChange={(e) => setTxnPassword(e.currentTarget.value)}
|
||||
withAsterisk
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Group justify="flex-start">
|
||||
<Button variant="filled" color="blue" onClick={handleValidate} disabled={validationStatus === "success"}>
|
||||
Validate
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="blue"
|
||||
onClick={handleProceed}
|
||||
loading={isSubmitting}
|
||||
disabled={validationStatus !== "success"}
|
||||
>
|
||||
{!showTxnPassword && showOtpField ? "Validate the OTP" : showTxnPassword ? "Proceed to Pay" : "Proceed"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="lg" mt="md">
|
||||
hii
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
220
src/app/(main)/home/page.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Input, Group, Stack, Text, Title, Box, Select, Paper, Switch } from '@mantine/core';
|
||||
import { IconBuildingBank, IconEye } from '@tabler/icons-react';
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Providers } from "../../providers";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
interface accountData {
|
||||
stAccountNo: string;
|
||||
stAccountType: string;
|
||||
stAvailableBalance: string;
|
||||
custname: string;
|
||||
activeAccounts: string;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
const [accountData, SetAccountData] = useState<accountData[]>([]);
|
||||
const depositAccounts = accountData.filter(acc => acc.stAccountType !== "LN");
|
||||
const [selectedDA, setSelectedDA] = useState(depositAccounts[0]?.stAccountNo || "");
|
||||
const selectedDAData = depositAccounts.find(acc => acc.stAccountNo === selectedDA);
|
||||
const loanAccounts = accountData.filter(acc => acc.stAccountType === "LN");
|
||||
const [selectedLN, setSelectedLN] = useState(loanAccounts[0]?.stAccountNo || "");
|
||||
const selectedLNData = loanAccounts.find(acc => acc.stAccountNo === selectedLN);
|
||||
const [showBalance, setShowBalance] = useState(false);
|
||||
|
||||
|
||||
async function handleFetchUserDetails() {
|
||||
// e.preventDefault();
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch('api/customer', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok && Array.isArray(data)) {
|
||||
SetAccountData(data);
|
||||
if (data.length > 0) {
|
||||
const firstDeposit = data.find(acc => acc.stAccountType !== "LN");
|
||||
const firstLoan = data.find(acc => acc.stAccountType === "LN");
|
||||
if (firstDeposit) setSelectedDA(firstDeposit.stAccountNo);
|
||||
if (firstLoan) setSelectedLN(firstLoan.stAccountNo);
|
||||
}
|
||||
}
|
||||
else { throw new Error(); }
|
||||
}
|
||||
catch {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later",
|
||||
message: "Unable to Fetch, Please try again later",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetAccountStatement(accountNo: string) {
|
||||
router.push(`/accounts/account_statement?accNo=${accountNo}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authorized) {
|
||||
handleFetchUserDetails();
|
||||
}
|
||||
}, [authorized]);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<Providers>
|
||||
<div>
|
||||
<Title order={4} style={{ padding: "10px" }}>Accounts Overview</Title>
|
||||
<Group
|
||||
style={{ flex: 1, padding: "10px 10px 4px 10px", marginLeft: '10px', display: "flex", alignItems: "center", justifyContent: "left", height: "1vh" }}>
|
||||
<IconEye size={20} />
|
||||
<Text fw={700} style={{ fontFamily: "inter", fontSize: '17px' }}>Show Balance </Text>
|
||||
<Switch size="md" onLabel="ON" offLabel="OFF" checked={showBalance}
|
||||
onChange={(event) => setShowBalance(event.currentTarget.checked)} />
|
||||
</Group>
|
||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "flex-start", marginLeft: '15px' }}>
|
||||
<Group grow gap="xl">
|
||||
<Paper p="md" radius="md" style={{ backgroundColor: '#c1e0f0', width: 350 }}>
|
||||
<Group gap='xs'>
|
||||
<IconBuildingBank size={25} />
|
||||
<Text fw={700}>Deposit Account</Text>
|
||||
<Select
|
||||
// placeholder="Select A/C No"
|
||||
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">{Number(selectedDAData?.stAccountNo || 0)}</Text>
|
||||
<Title order={2} mt="md">
|
||||
{showBalance ? `₹${Number(selectedDAData?.stAvailableBalance || 0).toLocaleString('en-IN')}` : "****"}
|
||||
</Title>
|
||||
<Button fullWidth mt="xs" onClick={() => handleGetAccountStatement(selectedDA)}>Get Statement</Button>
|
||||
</Paper>
|
||||
<Paper p="md" radius="md" style={{ backgroundColor: '#c1e0f0', width: 350 }}>
|
||||
<Group gap='xs'>
|
||||
<IconBuildingBank size={25} />
|
||||
<Text fw={700}>Loan Account</Text>
|
||||
<Select
|
||||
placeholder="Select A/C No"
|
||||
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">{Number(selectedLNData?.stAccountNo || 0)}</Text>
|
||||
<Title order={2} mt="md">
|
||||
{showBalance ? `₹${Number(selectedLNData?.stAvailableBalance || 0).toLocaleString('en-IN')}` : "****"}
|
||||
</Title>
|
||||
<Button fullWidth mt="xs" onClick={() => handleGetAccountStatement(selectedLN)}>Get Statement</Button>
|
||||
</Paper>
|
||||
<Paper p="md" radius="md" style={{ width: 300, backgroundColor: '#FFFFFF', marginLeft: '130px', border: '1px solid grey' }}>
|
||||
<Title order={5} mb="sm">Important Links</Title>
|
||||
{/* <Title order={5} mb="sm" style={{ textAlign: 'center' }}>Loan EMI Calculator</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>
|
||||
{/* <Group>
|
||||
<TextInput
|
||||
label="Loan Amount"
|
||||
placeholder=""
|
||||
/>
|
||||
<TextInput
|
||||
label="Interest Rate(%)"
|
||||
placeholder=""
|
||||
/>
|
||||
<TextInput
|
||||
label="Loan Tenure(Years)"
|
||||
placeholder=""
|
||||
/>
|
||||
<Button fullWidth style={{textAlign:'center'}}>Calculate</Button>
|
||||
</Group> */}
|
||||
</Paper>
|
||||
</Group>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: 0,
|
||||
padding: "20px",
|
||||
display: "flex",
|
||||
// alignItems: "center",
|
||||
justifyContent: "left",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Title order={4}>Send Money</Title>
|
||||
<Group mt="sm">
|
||||
<Select
|
||||
placeholder="Own / Other Accounts"
|
||||
data={[{ value: 'own', label: 'Own' }, { value: 'other', label: 'Other' }]}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Select Account"
|
||||
data={[{ value: 'acc1', label: 'Account 1' }, { value: 'acc2', label: 'Account 2' }]}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Input placeholder="₹0.00" style={{ width: 100 }} />
|
||||
<Button>Proceed</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
}
|
192
src/app/(main)/layout.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Button, Divider, Group, Image, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconBook, IconCurrencyRupee, IconHome, IconLogout, IconPhoneFilled, IconSettings } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { Providers } from '../providers';
|
||||
import logo from '@/app/image/logo.jpg';
|
||||
import NextImage from 'next/image';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const [userLastLoginDetails, setUserLastLoginDetails] = useState(null);
|
||||
|
||||
async function handleLogout(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem("access_token");
|
||||
router.push("/login");
|
||||
}
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function handleFetchUserDetails(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch('api/auth/user_details', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
return data;
|
||||
}
|
||||
else if(response.status ===401 ||data.message === 'invalid or expired token'){
|
||||
// console.log(data);
|
||||
localStorage.removeItem("access_token");
|
||||
router.push('/login');
|
||||
}
|
||||
else {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later",
|
||||
message: "Unable to fetch timestamp, please try again later",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLoginTime = async () => {
|
||||
const result = await handleFetchUserDetails({ preventDefault: () => { } } as React.FormEvent);
|
||||
if (result) {
|
||||
setUserLastLoginDetails(result.last_login);
|
||||
}
|
||||
};
|
||||
fetchLoginTime();
|
||||
}, []);
|
||||
|
||||
const navItems = [
|
||||
{ href: "/home", label: "Home", icon: IconHome },
|
||||
{ href: "/accounts", label: "Accounts", icon: IconBook },
|
||||
{ href: "/funds_transfer", label: "Send Money", icon: IconCurrencyRupee },
|
||||
{ href: "/settings", label: "Settings", icon: IconSettings },
|
||||
];
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Providers>
|
||||
<div style={{ backgroundColor: "#e6ffff", height: "100vh", display: "flex", flexDirection: "column", padding: 0, margin: 0 }}>
|
||||
<Box
|
||||
style={{
|
||||
height: "60px",
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
fit="cover"
|
||||
src={logo}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '80%',
|
||||
color: 'white',
|
||||
textShadow: '1px 1px 2px black',
|
||||
}}
|
||||
>
|
||||
<IconPhoneFilled size={20} /> Toll Free No : 1800-180-8008
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
padding: '0.5rem 1rem',
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Stack gap={0} align="flex-start">
|
||||
<Title order={4} style={{ fontFamily: "inter", fontSize: '22px' }}>
|
||||
Welcome, Rajat Kumar Maharana
|
||||
</Title>
|
||||
<Text size="xs" c="gray" style={{ fontFamily: "inter", fontSize: '13px' }}>
|
||||
Last logged in at {userLastLoginDetails ? new Date(userLastLoginDetails).toLocaleString() : "N/A"}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Group mt="md" gap="sm">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname.startsWith(item.href);
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Button
|
||||
leftSection={<Icon size={20} />}
|
||||
variant={isActive ? "light" : "subtle"}
|
||||
color={isActive ? "blue" : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<Button leftSection={<IconLogout size={20} />} variant="subtle" onClick={handleLogout}>
|
||||
Logout
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<Divider size="xs" color='#99c2ff' />
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
borderTop: '1px solid #ddd',
|
||||
borderBottom: '1px solid #ddd',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<Divider size="xs" color='blue' />
|
||||
|
||||
<Box
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#f8f9fa",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Text c="dimmed" size="xs">
|
||||
© 2025 Kangra Central Co-Operative Bank
|
||||
</Text>
|
||||
</Box>
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
73
src/app/(main)/settings/layout.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import { Divider, Stack, Text } from '@mantine/core';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const links = [
|
||||
{ label: "View Profile", href: "/settings" },
|
||||
{ label: "Change Login Password", href: "/settings/change_login_password" },
|
||||
{ label: "Change transaction Password", href: "/settings/change_transaction_password" },
|
||||
{ label: "Set transaction Password", href: "/settings/set_transaction_password" },
|
||||
];
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<div style={{ display: "flex", height: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
width: "16%",
|
||||
backgroundColor: '#c5e4f9',
|
||||
borderRight: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
<Stack style={{ background: '#228be6', height: '10%', alignItems: 'center' }}>
|
||||
<Text fw={700} fs="italic" c='white' style={{ textAlign: 'center', marginTop: '10px' }}>
|
||||
Settings
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm" justify="flex-start" style={{ padding: '1rem' }}>
|
||||
{links.map(link => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Text
|
||||
key={link.href}
|
||||
component={Link}
|
||||
href={link.href}
|
||||
c={isActive ? 'darkblue' : 'blue'}
|
||||
style={{
|
||||
textDecoration: isActive ? 'underline' : 'none',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '1rem' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
11
src/app/(main)/settings/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
import { Divider, Stack, Text } from '@mantine/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function settings() {
|
||||
return(
|
||||
<Text>Hii</Text>
|
||||
)
|
||||
|
||||
}
|
23
src/app/ChangePassword/CaptchaImage.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
const CaptchaImage = ({ text }: { text: string }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (canvas && ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '26px Arial';
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.setTransform(1, 0.1, -0.1, 1, 0, 0);
|
||||
ctx.fillText(text, 10, 30);
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
return <canvas ref={canvasRef} width={120} height={40} />;
|
||||
};
|
||||
|
||||
export default CaptchaImage;
|
62
src/app/ChangePassword/page.module.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
}
|
||||
|
||||
|
||||
.mobileImage {
|
||||
@media (min-width: 48em) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.desktopImage {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@media (max-width: 47.99em) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-wrapper {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-control {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: white;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* First control is left */
|
||||
.gradient-control:first-of-type {
|
||||
left: 0;
|
||||
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.0001));
|
||||
}
|
||||
|
||||
/* Last control is right */
|
||||
.gradient-control:last-of-type {
|
||||
right: 0;
|
||||
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.0001));
|
||||
}
|
268
src/app/ChangePassword/page.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Button, TextInput, PasswordInput, Title, Card, Box, Image } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { useRouter } from "next/navigation";
|
||||
import NextImage from "next/image";
|
||||
import logo from '@/app/image/logo.jpg';
|
||||
import changePwdImage from '@/app/image/changepw.png';
|
||||
import CaptchaImage from './CaptchaImage';
|
||||
import { IconEye, IconEyeOff, IconLogout } from '@tabler/icons-react';
|
||||
|
||||
export default function ChangeLoginPwd() {
|
||||
const router = useRouter();
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [captchaInput, setCaptchaInput] = useState('');
|
||||
const [captchaError, setCaptchaError] = useState('');
|
||||
const [confirmVisible, setConfirmVisible] = useState(false);
|
||||
const toggleConfirmVisibility = () => setConfirmVisible((v) => !v);
|
||||
|
||||
async function handleLogout(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem("access_token");
|
||||
router.push("/login")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
generateCaptcha();
|
||||
}, []);
|
||||
|
||||
const generateCaptcha = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setCaptcha(result);
|
||||
setCaptchaInput('');
|
||||
setCaptchaError('');
|
||||
};
|
||||
|
||||
async function handleSetLoginPassword(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const pwdRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
|
||||
if (!password || !confirmPassword) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Both password fields are required.",
|
||||
message: "Both password fields are required.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
// alert("Both password fields are required.");
|
||||
} else if (password !== confirmPassword) {
|
||||
// alert("Passwords do not match.");
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Passwords do not match.",
|
||||
message: "Passwords do not match.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (!pwdRegex.test(password)) {
|
||||
// alert("Password must contain at least 1 capital letter, 1 number, 1 special character, and be at least 8 characters long.");
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Password must contain at least 1 capital letter, 1 number, 1 special character, and be at least 8 characters long.",
|
||||
message: "Password must contain at least 1 capital letter, 1 number, 1 special character, and be at least 8 characters long.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (captchaInput !== captcha) {
|
||||
setCaptchaError("Incorrect CAPTCHA.");
|
||||
return;
|
||||
}
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch('api/auth/login_password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
login_password: password,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
console.log(data);
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "green",
|
||||
title: "Login Password has been set",
|
||||
message: "Login Password has been set",
|
||||
autoClose: 5000,
|
||||
});
|
||||
router.push("/ChangeTxn");
|
||||
}
|
||||
else {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later ",
|
||||
message: "Please try again later ",
|
||||
autoClose: 5000,
|
||||
});
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<Providers>
|
||||
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto", paddingTop: "5%" }}>
|
||||
<Box style={{
|
||||
position: 'fixed', width: '100%', height: '12%', top: 0, left: 0, zIndex: 100,
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)"
|
||||
}}>
|
||||
<Image
|
||||
// radius="md"
|
||||
fit="cover"
|
||||
src={logo}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<Button style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '90%',
|
||||
color: 'white',
|
||||
textShadow: '1px 1px 2px black',
|
||||
fontSize: "20px"
|
||||
}}
|
||||
leftSection={<IconLogout color='white' />} variant="subtle" onClick={handleLogout}>Logout</Button>
|
||||
</Box>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center", columnGap: "5rem" }} bg="#c1e0f0">
|
||||
<Image h="85vh" fit="contain" component={NextImage} src={changePwdImage} alt="Change Password Image" />
|
||||
<Box h="100%" style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<Card p="xl" w="35vw">
|
||||
<Title order={3}
|
||||
// @ts-ignore
|
||||
align="center" mb="md">Set Login Password</Title>
|
||||
|
||||
<form onSubmit={handleSetLoginPassword}>
|
||||
<PasswordInput
|
||||
label="Login Password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
id="loginPassword"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onPaste={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Confirm Login Password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
|
||||
type={confirmVisible ? 'text' : 'password'}
|
||||
rightSection={
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleConfirmVisibility}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'grey' }}
|
||||
>
|
||||
{confirmVisible ? <IconEyeOff size={18} /> : <IconEye size={18} />}
|
||||
</button>
|
||||
}
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onPaste={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
|
||||
/>
|
||||
|
||||
{/* CAPTCHA */}
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<label style={{ fontWeight: 600 }}>Enter CAPTCHA *</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 5 }}>
|
||||
<CaptchaImage text={captcha} />
|
||||
<Button size="xs" variant="outline" onClick={generateCaptcha}>Refresh</Button>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Enter above text"
|
||||
value={captchaInput}
|
||||
onChange={(e) => setCaptchaInput(e.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
{captchaError && <p style={{ color: 'red' }}>{captchaError}</p>}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
color="blue"
|
||||
>
|
||||
Set
|
||||
</Button>
|
||||
</form>
|
||||
<br></br>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
borderLeft: '1px solid #ccc',
|
||||
paddingLeft: 16,
|
||||
minHeight: 90,
|
||||
|
||||
}}
|
||||
>
|
||||
<Text size="sm">
|
||||
<strong>Note:</strong> Password will contains minimum one alphabet, one digit, one special symbol and total 8 charecters.
|
||||
</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
<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 >
|
||||
);
|
||||
}
|
||||
}
|
24
src/app/ChangeTxn/CaptchaImage.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
|
||||
const CaptchaImage = ({ text }: { text: string }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (canvas && ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.font = '26px Arial';
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.setTransform(1, 0.1, -0.1, 1, 0, 0);
|
||||
ctx.fillText(text, 10, 30);
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
return <canvas ref={canvasRef} width={120} height={40} />;
|
||||
};
|
||||
|
||||
export default CaptchaImage;
|
62
src/app/ChangeTxn/page.modukle.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
}
|
||||
|
||||
|
||||
.mobileImage {
|
||||
@media (min-width: 48em) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.desktopImage {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@media (max-width: 47.99em) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-wrapper {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-control {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: white;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* First control is left */
|
||||
.gradient-control:first-of-type {
|
||||
left: 0;
|
||||
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.0001));
|
||||
}
|
||||
|
||||
/* Last control is right */
|
||||
.gradient-control:last-of-type {
|
||||
right: 0;
|
||||
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.0001));
|
||||
}
|
267
src/app/ChangeTxn/page.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Button, TextInput, PasswordInput, Title, Card, Box, Image } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { useRouter } from "next/navigation";
|
||||
import NextImage from "next/image";
|
||||
import logo from '@/app/image/logo.jpg';
|
||||
import changePwdImage from '@/app/image/changepw.png';
|
||||
import CaptchaImage from './CaptchaImage';
|
||||
import { IconEye, IconEyeOff, IconLogout } from '@tabler/icons-react';
|
||||
|
||||
export default function ChangeTransactionPwd() {
|
||||
const router = useRouter();
|
||||
const [authorized, SetAuthorized] = useState<boolean | null>(null);
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [captchaInput, setCaptchaInput] = useState('');
|
||||
const [captchaError, setCaptchaError] = useState('');
|
||||
const [confirmVisible, setConfirmVisible] = useState(false);
|
||||
const toggleConfirmVisibility = () => setConfirmVisible((v) => !v);
|
||||
|
||||
async function handleLogout(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem("access_token");
|
||||
router.push("/login")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
generateCaptcha();
|
||||
}, []);
|
||||
|
||||
const generateCaptcha = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setCaptcha(result);
|
||||
setCaptchaInput('');
|
||||
setCaptchaError('');
|
||||
};
|
||||
|
||||
async function handleSetTransactionPassword(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const pwdRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
|
||||
if (!password || !confirmPassword) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Both password fields are required.",
|
||||
message: "Both password fields are required.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
// alert("Both password fields are required.");
|
||||
} else if (password !== confirmPassword) {
|
||||
// alert("Passwords do not match.");
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Passwords do not match.",
|
||||
message: "Passwords do not match.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (!pwdRegex.test(password)) {
|
||||
// alert("Password must contain at least 1 capital letter, 1 number, 1 special character, and be at least 8 characters long.");
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Password must contain at least 1 capital letter, 1 number, 1 special character, and be at least 8 characters long.",
|
||||
message: "Password must contain at least 1 capital letter, 1 number, 1 special character, and be at least 8 characters long.",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (captchaInput !== captcha) {
|
||||
setCaptchaError("Incorrect CAPTCHA.");
|
||||
return;
|
||||
}
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await fetch('api/auth/transaction_password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transaction_password: password,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
console.log(data);
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "green",
|
||||
title: "Transaction Password has been set",
|
||||
message: "Transaction Password has been set",
|
||||
autoClose: 5000,
|
||||
});
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Please try again later ",
|
||||
message: "Please try again later ",
|
||||
autoClose: 5000,
|
||||
});
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
SetAuthorized(false);
|
||||
router.push("/login");
|
||||
}
|
||||
else {
|
||||
SetAuthorized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (authorized) {
|
||||
return (
|
||||
<Providers>
|
||||
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto", paddingTop: "5%" }}>
|
||||
<Box style={{
|
||||
position: 'fixed', width: '100%', height: '12%', top: 0, left: 0, zIndex: 100,
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)"
|
||||
}}>
|
||||
<Image
|
||||
// radius="md"
|
||||
fit="cover"
|
||||
src={logo}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<Button style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '90%',
|
||||
color: 'white',
|
||||
textShadow: '1px 1px 2px black',
|
||||
fontSize: "20px"
|
||||
}}
|
||||
leftSection={<IconLogout color='white' />} variant="subtle" onClick={handleLogout}>Logout
|
||||
</Button>
|
||||
</Box>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center", columnGap: "5rem" }} bg="#c1e0f0">
|
||||
<Image h="85vh" fit="contain" component={NextImage} src={changePwdImage} alt="Change Password Image" />
|
||||
<Box h="100%" style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||
<Card p="xl" w="35vw">
|
||||
<Title order={3}
|
||||
// @ts-ignore
|
||||
align="center" mb="md">Set Transaction Password</Title>
|
||||
<form onSubmit={handleSetTransactionPassword}>
|
||||
<PasswordInput
|
||||
label="Transaction Password"
|
||||
placeholder="Enter your Transaction password"
|
||||
required
|
||||
id="loginPassword"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onPaste={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Confirm Transaction Password"
|
||||
placeholder="Re-enter your Transaction password"
|
||||
required
|
||||
id="confirmPassword"
|
||||
value={confirmPassword}
|
||||
type={confirmVisible ? 'text' : 'password'}
|
||||
rightSection={
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleConfirmVisibility}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'grey' }}
|
||||
>
|
||||
{confirmVisible ? <IconEyeOff size={18} /> : <IconEye size={18} />}
|
||||
</button>
|
||||
}
|
||||
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onPaste={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
/>
|
||||
{/* CAPTCHA */}
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<label style={{ fontWeight: 600 }}>Enter CAPTCHA *</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 5 }}>
|
||||
<CaptchaImage text={captcha} />
|
||||
<Button size="xs" variant="outline" onClick={generateCaptcha}>Refresh</Button>
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="Enter above text"
|
||||
value={captchaInput}
|
||||
onChange={(e) => setCaptchaInput(e.currentTarget.value)}
|
||||
required
|
||||
/>
|
||||
{captchaError && <p style={{ color: 'red' }}>{captchaError}</p>}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
color="blue"
|
||||
>
|
||||
Set
|
||||
</Button>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
<br></br>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
borderLeft: '1px solid #ccc',
|
||||
paddingLeft: 16,
|
||||
minHeight: 90,
|
||||
|
||||
}}
|
||||
>
|
||||
<Text size="sm">
|
||||
<strong>Note:</strong> Password will contains minimum one alphabet, one digit, one special symbol and total 8 charecters.
|
||||
</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
<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 >
|
||||
);
|
||||
}
|
||||
}
|
10
src/app/OTPGenerator.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function generateOTP(length: number) {
|
||||
const digits = '0123456789';
|
||||
let otp = '';
|
||||
otp += digits[Math.floor(Math.random() * 9)+1]; //first digit cannot be zero
|
||||
for (let i = 1; i < length; i++) {
|
||||
otp += digits[Math.floor(Math.random() * digits.length)];
|
||||
}
|
||||
// console.log("OTP generate :",otp);
|
||||
return otp;
|
||||
}
|
32
src/app/_components/timeline/RegistrationTimeline.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { Stepper, Button, Group } from '@mantine/core';
|
||||
|
||||
export default function RegistrationTimeline() {
|
||||
const [active, setActive] = useState(1);
|
||||
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stepper active={active} onStepClick={setActive}>
|
||||
<Stepper.Step label="First step" description="Change Login password">
|
||||
|
||||
</Stepper.Step>
|
||||
<Stepper.Step label="Second step" description="Change Tranaction password">
|
||||
|
||||
</Stepper.Step>
|
||||
{/* <Stepper.Step label="Final step" description="Verify OTP">
|
||||
|
||||
</Stepper.Step> */}
|
||||
<Stepper.Completed>
|
||||
Completed, click back button to get to previous step
|
||||
</Stepper.Completed>
|
||||
</Stepper>
|
||||
{/*
|
||||
<Group justify="center" mt="xl">
|
||||
<Button variant="default" onClick={prevStep}>Back</Button>
|
||||
<Button onClick={nextStep}>Next step</Button>
|
||||
</Group> */}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
|
||||
"use client"
|
||||
import React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import User from "../_types/accountNo";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
|
||||
async function queryUser() {
|
||||
let user: User | null = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get<User>('/api/user');
|
||||
user = response.data;
|
||||
} catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.code,
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
const UserContext = React.createContext<User | null | undefined>(undefined);
|
||||
|
||||
function UserContextProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const userQuery = useQuery({
|
||||
queryKey: ['user'],
|
||||
queryFn: queryUser,
|
||||
networkMode: 'always',
|
||||
});
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={userQuery.data}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const UserContextConsumer = UserContext.Consumer;
|
||||
|
||||
export { UserContext, UserContextProvider, UserContextConsumer }
|
@@ -1,39 +0,0 @@
|
||||
import { Entity, Column, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"
|
||||
|
||||
@Entity({ name: 'ticket' })
|
||||
export class Ticket {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
// @PrimaryColumn("int")
|
||||
id!: number
|
||||
|
||||
@PrimaryColumn()
|
||||
ticket_id! : number
|
||||
|
||||
@Column("varchar")
|
||||
category_of_request! : string
|
||||
|
||||
@Column("varchar")
|
||||
nature_of_request! : string
|
||||
|
||||
// @Column({type: 'jsonb',nullable:true})
|
||||
// issue: any
|
||||
@Column({type: 'text',nullable:true})
|
||||
additional_info: any
|
||||
|
||||
@Column("varchar")
|
||||
message!: string
|
||||
|
||||
@Column('varchar')
|
||||
created_by! : string
|
||||
|
||||
@Column('bigint')
|
||||
customer_account_no! : number
|
||||
|
||||
@Column('varchar')
|
||||
created_date! : string
|
||||
|
||||
@Column({type: 'varchar',nullable:true})
|
||||
assign_by! :string
|
||||
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
import { Entity, Column, PrimaryColumn } from "typeorm"
|
||||
|
||||
@Entity({ name: 'user' })
|
||||
export class User {
|
||||
|
||||
@PrimaryColumn("int")
|
||||
id!: number
|
||||
|
||||
@Column("bigint")
|
||||
bank_account_no!: number
|
||||
|
||||
@Column("varchar")
|
||||
title!: string
|
||||
|
||||
@Column("varchar")
|
||||
first_name!: string
|
||||
|
||||
@Column("varchar")
|
||||
middle_name!: string
|
||||
|
||||
@Column("varchar")
|
||||
last_name!: string
|
||||
|
||||
@Column("bigint",{nullable:true})
|
||||
mobile_number!: number
|
||||
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
id,bank_account_no,title,first_name,middle_name,last_name,mobile_number
|
||||
6000,30022497139,Mr.,Rajat,Kumar,Maharana,7890544527
|
||||
6001,30022497138,Ms.,Tomosa,,Sarkar,7890544527
|
|
@@ -1,44 +0,0 @@
|
||||
import { DataSource } from "typeorm"
|
||||
import { User } from "../entities/User"
|
||||
import { Ticket } from "../entities/Ticket";
|
||||
|
||||
class AppDataSource {
|
||||
|
||||
private static dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT ?? '1521'),
|
||||
username: process.env.DB_USER_NAME,
|
||||
password: process.env.DB_USER_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
// dropSchema: process.env.NODE_ENV === 'development',
|
||||
synchronize: process.env.NODE_ENV === 'development',
|
||||
logging: false,
|
||||
entities: [User,Ticket],
|
||||
subscribers: [],
|
||||
migrations: []
|
||||
})
|
||||
private static dataSource_MantisBT = new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT ?? '1521'),
|
||||
username: process.env.DB_USER_NAME,
|
||||
password: process.env.DB_USER_PASS,
|
||||
database: "kccb_ticket_tracker",
|
||||
synchronize: process.env.NODE_ENV === 'development',
|
||||
logging: false,
|
||||
subscribers: [],
|
||||
migrations: []
|
||||
})
|
||||
static async getConnection(): Promise<DataSource> {
|
||||
if (!this.dataSource.isInitialized)
|
||||
await this.dataSource.initialize()
|
||||
return this.dataSource;
|
||||
}
|
||||
static async getMantisConnection(): Promise<DataSource> {
|
||||
if (!this.dataSource_MantisBT.isInitialized)
|
||||
await this.dataSource_MantisBT.initialize()
|
||||
return this.dataSource_MantisBT;
|
||||
}
|
||||
}
|
||||
export default AppDataSource;
|
4
src/app/captcha.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export async function generateCaptcha(length = 6) {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
|
||||
}
|
@@ -2,9 +2,10 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
/* border: 1px solid black; */
|
||||
/* border: 1px solid black; */
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
flex-grow: 1;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navbarMain {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.user {
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: var(--mantine-spacing-md);
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
}
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: var(--mantine-primary-color-light);
|
||||
color: var(--mantine-primary-color-light-color);
|
||||
|
||||
.linkIcon {
|
||||
color: var(--mantine-primary-color-light-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
margin-right: var(--mantine-spacing-sm);
|
||||
width: rem(25px);
|
||||
height: rem(25px);
|
||||
}
|
||||
|
@@ -1,159 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React, { useContext, useState } from "react";
|
||||
import { UserContext } from "../_components/user-context";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { AppShell, Burger, Group, Image, Text, LoadingOverlay, Avatar, UnstyledButton, Title, Skeleton, Center, Switch, NavLink, ScrollArea, Code } from '@mantine/core';
|
||||
import { IconLogout, IconExclamationCircle, IconNotes, IconCategory, IconTicket, IconEye } from '@tabler/icons-react';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import classes from './layout.module.css';
|
||||
import { KccbTheme } from "../_themes/KccbTheme";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await axios.post('/api/auth/logout');
|
||||
} catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.code,
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default function HomeLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
const user = useContext(UserContext);
|
||||
if (user === null) redirect('/login');
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [burgerOpened, { toggle: toggleBurger }] = useDisclosure();
|
||||
const [activeNavItem, setActiveNavItem] = useState<string | null>(null);
|
||||
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ['logout'],
|
||||
mutationFn: logout
|
||||
})
|
||||
|
||||
async function handleLogout(event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
await logoutMutation.mutateAsync();
|
||||
await queryClient.refetchQueries({ queryKey: ['user'] })
|
||||
}
|
||||
|
||||
let navItems: React.ReactNode[] = [];
|
||||
|
||||
navItems.push(<>
|
||||
<NavLink key={'raised-ticket'} label={
|
||||
<>
|
||||
<IconTicket size={20} style={{ marginRight: '5px', transform: 'translateY(5px)' }} />
|
||||
<span style={{ fontSize: '18px' }}>Create Ticket </span>
|
||||
</>
|
||||
}
|
||||
active={activeNavItem === 'raised-ticket' || undefined}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
setActiveNavItem('raised-ticket')
|
||||
router.push('/home/user/raised-ticket')
|
||||
}}
|
||||
/>
|
||||
<NavLink key={'view-ticket'} label={<><IconEye size={20} style={{ marginRight: '8px', transform: 'translateY(5px)' }} />
|
||||
<span style={{ fontSize: '18px' }}>View Ticket </span>
|
||||
</>
|
||||
}
|
||||
active={activeNavItem === 'view-ticket' || undefined}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
setActiveNavItem('view-ticket')
|
||||
router.push('/home/user/view-ticket');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay
|
||||
visible={!user || logoutMutation.isPending || logoutMutation.isSuccess}
|
||||
overlayProps={{ backgroundOpacity: 1 }} />
|
||||
|
||||
{user && <AppShell
|
||||
header={{ height: { base: 60, md: 70, lg: 80 } }}
|
||||
navbar={{
|
||||
width: { base: 200, md: 250, lg: 300 },
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !burgerOpened },
|
||||
}}
|
||||
padding="lg"
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Burger opened={burgerOpened} onClick={toggleBurger} hiddenFrom="sm" size="sm" />
|
||||
<Image
|
||||
radius='md'
|
||||
h={50}
|
||||
w='auto'
|
||||
fit='contain'
|
||||
src={'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-ZljkQXsXUP1-EwnIPZtVq_JWhwGUW7b0_eSMno-bag&s'}
|
||||
/>
|
||||
<Title order={2} className={classes.title}>Customer Request Management</Title>
|
||||
</Group>
|
||||
{/* <Group>
|
||||
|
||||
<UnstyledButton className={classes.user} onClick={() => {
|
||||
setActiveNavItem(null);
|
||||
router.push('/home/user');
|
||||
}}>
|
||||
</UnstyledButton>
|
||||
</Group> */}
|
||||
<Group>
|
||||
<Avatar radius="xl" src={null} alt={user.bank_account_no} color={KccbTheme.primaryColor}>
|
||||
</Avatar>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
Account No: [{user.bank_account_no}]
|
||||
</Text>
|
||||
{/* <Text c='dimmed' size="sm" component="span">User: </Text>
|
||||
<Code c='dimmed'>{user.id}</Code> */}
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar p="md">
|
||||
<Text>Menu</Text>
|
||||
<nav className={classes.navbar}>
|
||||
<div className={classes.navbarMain}>
|
||||
<ScrollArea>
|
||||
{navItems}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</nav>
|
||||
<div className={classes.footer}>
|
||||
<a className={classes.link} onClick={handleLogout}>
|
||||
<IconLogout className={classes.linkIcon} stroke={1.5} />
|
||||
<Text>Logout</Text>
|
||||
</a>
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main>
|
||||
{children}
|
||||
</AppShell.Main>
|
||||
</AppShell>}
|
||||
</>
|
||||
)
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
flex-grow: 1;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navbarMain {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
|
||||
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.user {
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: var(--mantine-spacing-md);
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
}
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: var(--mantine-primary-color-light);
|
||||
color: var(--mantine-primary-color-light-color);
|
||||
|
||||
.linkIcon {
|
||||
color: var(--mantine-primary-color-light-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
margin-right: var(--mantine-spacing-sm);
|
||||
width: rem(25px);
|
||||
height: rem(25px);
|
||||
}
|
||||
|
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { UserContextConsumer } from "../_components/user-context";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<p>Welcome to IB Portal</p>
|
||||
<UserContextConsumer>
|
||||
{
|
||||
user => user && <p><b>Your Present Login Account No: {user.bank_account_no}</b></p>
|
||||
}
|
||||
</UserContextConsumer>
|
||||
<p><li>For raise a complain or assistance ,please click on <b>Create Ticket</b></li></p>
|
||||
<p><li>For track a ticket ,please click on <b>View Ticket</b></li></p>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,263 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ComplaintForm: React.FC = () => {
|
||||
const [category, setCategory] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [additionalFields, setAdditionalFields] = useState<string[]>([]);
|
||||
const [additionalFieldValues, setAdditionalFieldValues] = useState<Record<string, string>>({});
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedCategory = e.target.value;
|
||||
setCategory(selectedCategory);
|
||||
setDescription(""); // Reset description when category changes
|
||||
setAdditionalFields([]);
|
||||
setAdditionalFieldValues({});
|
||||
};
|
||||
|
||||
const handleDescriptionClick = () => {
|
||||
if (!category) {
|
||||
alert("Please select the Category of Complaint first.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!category) {
|
||||
setDescription(""); // Prevent changing description without category
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDescription = e.target.value;
|
||||
setDescription(selectedDescription);
|
||||
|
||||
// Add additional fields for specific descriptions
|
||||
if (selectedDescription === "Transaction Issue") {
|
||||
setAdditionalFields([
|
||||
"Debit Card",
|
||||
"Transaction Amount",
|
||||
"Transaction Reference Number ",
|
||||
"Transaction Date",
|
||||
"ATM ID/Terminal ID",
|
||||
]);
|
||||
}
|
||||
else if(selectedDescription === "Fund Transfer failure"){
|
||||
setAdditionalFields([
|
||||
"Transaction Amount",
|
||||
"Transaction ID/Transaction Reference Number",
|
||||
"Transaction Date"
|
||||
]);
|
||||
}
|
||||
else {
|
||||
setAdditionalFields([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdditionalFieldChange = (field: string, value: string) => {
|
||||
if(field === "Transaction Amount" && isNaN(Number(value)))
|
||||
{
|
||||
alert ('Please enter a valid number');
|
||||
return;
|
||||
}
|
||||
if(field === "Debit Card" && isNaN(Number(value)))
|
||||
{
|
||||
alert ('Please Enter Valid Debit Card Number');
|
||||
return;
|
||||
}
|
||||
setAdditionalFieldValues((prevValues) => ({
|
||||
...prevValues,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
for (const field of additionalFields) {
|
||||
if (!additionalFieldValues[field]) {
|
||||
alert(`Please fill in the ${field}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!message.trim()) {
|
||||
alert("Message field is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const additionalInformation: string[] = [];
|
||||
if (additionalFieldValues['ATM ID/Terminal ID']) {
|
||||
additionalInformation.push(`ATM ID: ${additionalFieldValues['ATM ID/Terminal ID']}`);
|
||||
}
|
||||
if (additionalFieldValues['Debit Card']) {
|
||||
additionalInformation.push(`Debit Card: ${additionalFieldValues['Debit Card']}`);
|
||||
}
|
||||
if (additionalFieldValues['Transaction Amount']) {
|
||||
additionalInformation.push(`Transaction Amount: ${additionalFieldValues['Transaction Amount']}`);
|
||||
}
|
||||
if (additionalFieldValues['Transaction Reference Number ']) {
|
||||
additionalInformation.push(`Transaction Reference Number : ${additionalFieldValues['Transaction Reference Number ']}`);
|
||||
}
|
||||
if (additionalFieldValues['Transaction Date']) {
|
||||
additionalInformation.push(`Transaction Date: ${additionalFieldValues['Transaction Date']}`);
|
||||
}
|
||||
if(additionalFieldValues['Transaction ID/Transaction Reference Number']){
|
||||
additionalInformation.push(`Transaction ID/Transaction Reference Number: ${additionalFieldValues['Transaction ID/Transaction Reference Number']}`);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
summary: category,
|
||||
description: description,
|
||||
additional_information: additionalInformation.join(", "),
|
||||
steps_to_reproduce: message,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post("/api/ticket", requestBody);
|
||||
const data = await response.data;
|
||||
alert(data.message);
|
||||
window.location.reload();
|
||||
}
|
||||
catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px", maxWidth: "500px", textAlign: "left" }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h2 style={{ marginBottom: "8px" }}> Create New Ticket </h2>
|
||||
|
||||
<label style={{ marginTop: "8px" }}>
|
||||
Category of Complaint <span style={{ color: "red" }}>*</span>
|
||||
<select
|
||||
value={category}
|
||||
onChange={handleCategoryChange}
|
||||
required
|
||||
style={{ display: "block", marginBottom: "10px", width: "100%" }}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Category
|
||||
</option>
|
||||
<option value="ATM Related">ATM Related</option>
|
||||
<option value="Internet Banking">Internet Banking</option>
|
||||
<option value="Mobile Banking">Mobile Banking</option>
|
||||
<option value="Others">Others</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Description <span style={{ color: "red" }}>*</span>
|
||||
<select
|
||||
value={description}
|
||||
onClick={handleDescriptionClick}
|
||||
onChange={handleDescriptionChange}
|
||||
required
|
||||
style={{ display: "block", marginBottom: "10px", width: "100%" }}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select Description
|
||||
</option>
|
||||
{category === "ATM Related" && <option value="Transaction Issue">Transaction Issue</option>}
|
||||
{/* {category === "UPI" && <option value="UPI Issue">UPI Issue</option>} */}
|
||||
{category === "Internet Banking" && (
|
||||
<>
|
||||
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
|
||||
<option value="Fund Transfer failure">Fund Transfer Failure</option>
|
||||
<option value="Network Issue">Network Issue</option>
|
||||
</>
|
||||
)}
|
||||
{category === "Mobile Banking" && (
|
||||
<>
|
||||
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
|
||||
<option value="Fund Transfer failure">Fund Transfer Failure</option>
|
||||
<option value="Network Issue">Network Issue</option>
|
||||
</>
|
||||
)}
|
||||
{category === "Others" && (
|
||||
<>
|
||||
{/* <option value="IMPS">IMPS Funds Transfer</option> */}
|
||||
<option value="Others">Others</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{additionalFields.map((field, index) => (
|
||||
<div key={index}>
|
||||
{field === "Transaction Date" ? (
|
||||
<label>
|
||||
{field}: <span style={{ color: "red" }}>*</span>
|
||||
<input
|
||||
type="date"
|
||||
value={additionalFieldValues[field] || ""}
|
||||
onChange={(e) => handleAdditionalFieldChange(field, e.target.value)}
|
||||
required
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<label>
|
||||
{field}: <span style={{ color: "red" }}>*</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Enter ${field}`}
|
||||
value={additionalFieldValues[field] || ""}
|
||||
onChange={(e) => handleAdditionalFieldChange(field, e.target.value)}
|
||||
required
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<label>
|
||||
Message (Max 500 characters) <span style={{ color: "red" }}>*</span>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
maxLength={500}
|
||||
rows={5}
|
||||
required
|
||||
style={{
|
||||
display: "block",
|
||||
marginBottom: "10px",
|
||||
width: "100%",
|
||||
resize: "none",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
backgroundColor: "#4CAF50",
|
||||
color: "white",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplaintForm;
|
@@ -1,332 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Group,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconDisabled, IconEye, IconMessage, IconSearch, IconTrash ,IconArrowsUpDown} from "@tabler/icons-react";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
interface Message {
|
||||
note: string;
|
||||
time:string;
|
||||
}
|
||||
|
||||
interface Ticket {
|
||||
ticket_id: string;
|
||||
category_of_request: string;
|
||||
nature_of_request: string;
|
||||
created_date: string;
|
||||
status: string;
|
||||
message: Message[];
|
||||
}
|
||||
interface TicketDetails {
|
||||
ticket_id: string;
|
||||
category_of_request: string;
|
||||
nature_of_request: string;
|
||||
additional_info: string;
|
||||
message : string;
|
||||
created_date: string;
|
||||
|
||||
}
|
||||
export default function Page() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
|
||||
const [activeMessages, setActiveMessages] = useState<Message[] | null>(null);
|
||||
const [ticketDetails, setTicketDetails] = useState<TicketDetails| null>(null);
|
||||
const [ticketStatus, setTicketStatus] = useState<string| null>(null);
|
||||
const [sortOrder,setSortOrder]= useState<"asc" |"desc">("desc");
|
||||
|
||||
const [detailsOpened, { open: openDetails, close: closeDetails }] = useDisclosure(false);
|
||||
|
||||
// For view ticket details
|
||||
const handleOpenDetails = async (ticketId: string) => {
|
||||
try {
|
||||
const response = await axios.get<TicketDetails>(`/api/ticket/${ticketId}`); // Assuming API provides details
|
||||
setTicketDetails(response.data);
|
||||
openDetails();
|
||||
}
|
||||
catch(error: AxiosError | any) {
|
||||
console.error("Failed to fetch ticket details:", error);
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.message
|
||||
})
|
||||
}
|
||||
};
|
||||
// For parsing additional_info
|
||||
const parseAdditionalInfo = (info: string) => {
|
||||
return info.split(", ").map((item) => {
|
||||
const [key, value] = item.split(": ");
|
||||
return { key: key.trim(), value: value?.trim() || "" };
|
||||
});
|
||||
};
|
||||
// For delete a ticket
|
||||
const handleDelete = async (ticketId: string) => {
|
||||
const isConfirm =window.confirm("Do you want to delete the ticket?");
|
||||
if(!isConfirm)
|
||||
return;
|
||||
try {
|
||||
const response = await axios.delete<TicketDetails>(`/api/ticket/${ticketId}`); // Assuming API provides details
|
||||
const data = await response.data;
|
||||
alert(data.message);
|
||||
}
|
||||
catch(error: AxiosError | any) {
|
||||
console.error("Failed to delete ticket:", error);
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.message
|
||||
})
|
||||
}
|
||||
finally{
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
// Sort and filter tickets based on the time
|
||||
useEffect(() => {
|
||||
const sortedTickets = [...tickets].sort(
|
||||
// (a, b) => new Date(b.created_date).getTime() - new Date(a.created_date).getTime()
|
||||
(a, b) => parseInt(b.ticket_id) - parseInt(a.ticket_id)
|
||||
);
|
||||
const results = sortedTickets.filter((ticket) =>
|
||||
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredTickets(results);
|
||||
}, [searchQuery, tickets]);
|
||||
|
||||
// Fetch tickets from API
|
||||
useEffect(() => {
|
||||
async function fetchTickets() {
|
||||
try {
|
||||
const response = await axios.get<Ticket[]>("/api/ticket");
|
||||
setTickets(response.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch tickets: ", error);
|
||||
}
|
||||
}
|
||||
fetchTickets();
|
||||
}, []);
|
||||
|
||||
// Sorting the ticket when any ticket resolved the go to the end of the list
|
||||
useEffect(() => {
|
||||
const sortedTickets = [...tickets].sort((a, b) => {
|
||||
if (a.status.toLowerCase() === "resolved" && b.status.toLowerCase() !== "resolved") return 1;
|
||||
if (b.status.toLowerCase() === "resolved" && a.status.toLowerCase() !== "resolved") return -1;
|
||||
return parseInt(b.ticket_id) - parseInt(a.ticket_id); // Sort by ticket ID otherwise
|
||||
});
|
||||
const results = sortedTickets.filter(ticket =>
|
||||
ticket.category_of_request.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredTickets(results);
|
||||
}, [searchQuery, tickets]);
|
||||
|
||||
// For sorting ticket history according to time
|
||||
const sortedMessages = activeMessages
|
||||
? [...activeMessages].sort((a, b) => {
|
||||
const timeA = new Date(a.time).getTime();
|
||||
const timeB = new Date(b.time).getTime();
|
||||
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
|
||||
})
|
||||
: [];
|
||||
|
||||
const handleOpenMessage = (messages: Message[],status:string) => {
|
||||
console.log(messages);
|
||||
setActiveMessages(messages);
|
||||
setTicketStatus(status);
|
||||
open();
|
||||
};
|
||||
|
||||
//Add conditional row styles
|
||||
const getRowStyle = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "resolved":
|
||||
// return { backgroundColor: "#d9d9d9", color: "#a6a6a6"}; // Grey for closed
|
||||
return {color: "#a6a6a6"}; // Grey for closed
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const rows = filteredTickets.map((ticket) => (
|
||||
<Table.Tr key={ticket.ticket_id} style={getRowStyle(ticket.status)}> {/* added for coloured grey */}
|
||||
{/* <Table.Tr key={ticket.ticket_id}> */}
|
||||
<Table.Td>{ticket.ticket_id}</Table.Td>
|
||||
<Table.Td>{ticket.category_of_request}</Table.Td>
|
||||
<Table.Td>{ticket.nature_of_request}</Table.Td>
|
||||
<Table.Td>{ticket.created_date}</Table.Td>
|
||||
<Table.Td>{ticket.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<ActionIcon style={getRowStyle(ticket.status)}
|
||||
variant="subtle" color="blue"
|
||||
onClick={() => handleOpenMessage(ticket.message,ticket.status)}
|
||||
>
|
||||
<Tooltip label="Ticket History">
|
||||
<IconMessage />
|
||||
</Tooltip>
|
||||
{/* design for badge */}
|
||||
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Group gap="md" wrap="nowrap" justify="center">
|
||||
<ActionIcon style={getRowStyle(ticket.status)}
|
||||
variant="subtle"
|
||||
onClick={() => handleOpenDetails(ticket.ticket_id)}>
|
||||
<Tooltip label="Details of the Ticket">
|
||||
<IconEye />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
|
||||
{/* Remove delete icon */}
|
||||
{/* <ActionIcon style={getRowStyle(ticket.status)}
|
||||
disabled={ticket.status.toLowerCase()==='resolved'}
|
||||
variant="subtle" color="red"
|
||||
onClick={() => handleDelete(ticket.ticket_id)}>
|
||||
<Tooltip label="Delete the Ticket">
|
||||
<IconTrash/>
|
||||
</Tooltip>
|
||||
</ActionIcon> */}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
));
|
||||
return (
|
||||
<Container fluid>
|
||||
<Title order={3}>View Ticket</Title>
|
||||
<Space h="1rem" />
|
||||
<Flex align="center" justify="space-between" w={720}>
|
||||
<Flex align="center">
|
||||
<Title order={5}>Tickets</Title>
|
||||
<Space w="1rem" />
|
||||
<TextInput
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.currentTarget.value)}
|
||||
radius="md"
|
||||
w={250}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Space h="1.5rem" />
|
||||
<Table
|
||||
w={720}
|
||||
stickyHeader
|
||||
stickyHeaderOffset={60}
|
||||
withTableBorder
|
||||
highlightOnHover
|
||||
horizontalSpacing="lg"
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Ticket ID</Table.Th>
|
||||
<Table.Th>Category</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
<Table.Th>Timestamp</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>History</Table.Th>
|
||||
<Table.Th>Action</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="">
|
||||
<Flex justify="space-between" align="center" >
|
||||
<Title order={4}>Ticket History</Title>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
|
||||
>
|
||||
<Tooltip label={`Sort by ${sortOrder === "asc" ? "Newest " : "Oldest"}`} position="right">
|
||||
<IconArrowsUpDown size={18} />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="sm" style={{ maxHeight: "400px", overflowY: "auto" }}>
|
||||
{sortedMessages?.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: "10px",
|
||||
background: index % 2 === 0 ? "#f1f3f5" : "#dce6f2",
|
||||
borderRadius: "10px",
|
||||
maxWidth: "80%",
|
||||
alignSelf: "flex-start",
|
||||
boxShadow: "0 2px 5px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: 0, fontSize: "14px", color: "black",textAlign: "left" }}>{msg.note}</p>
|
||||
{/* //Fetch the note IST time */}
|
||||
<p style={{ margin: "6px 0 0", fontSize: "10px", color: "grey", textAlign: "right" }}>Internal team send by {new Date(Number(msg.time)*1000).toLocaleString('en-IN',{timeZone: 'Asia/Kolkata'})}</p>
|
||||
</div>
|
||||
))}
|
||||
{ticketStatus?.toLowerCase()=== "resolved" && (<div style={{ textAlign: "center", fontSize: "14px", color: "red", marginTop: "10px" }}>
|
||||
<p>----- Ended Chat ------</p>
|
||||
</div>)}
|
||||
{/* Fixed Close Button */}
|
||||
<div
|
||||
style={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
background: "white",
|
||||
padding: "10px",
|
||||
textAlign: "center",
|
||||
boxShadow: "0px -5px 4px rgb(255, 255, 255)",
|
||||
}}
|
||||
>
|
||||
<Button onClick={close}>Close</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
</Modal>
|
||||
|
||||
<Modal opened={detailsOpened} onClose={closeDetails}>
|
||||
{ticketDetails ? (
|
||||
<Flex direction="column" gap="md">
|
||||
<div><strong>Ticket ID:</strong> {ticketDetails.ticket_id}</div>
|
||||
<div><strong>Category of Complaint:</strong> {ticketDetails.category_of_request}</div>
|
||||
<div><strong>Description:</strong> {ticketDetails.nature_of_request}</div>
|
||||
{/* <div><strong>Additional Information:</strong> {ticketDetails.additional_info}</div> */}
|
||||
{/* Additional Info Table */}
|
||||
<div><strong>Additional Info:</strong></div>
|
||||
<Table withTableBorder withColumnBorders highlightOnHover>
|
||||
<Table.Tbody>
|
||||
{parseAdditionalInfo(ticketDetails.additional_info).map((item, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.key}</Table.Td>
|
||||
<Table.Td>{item.value}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<div><strong>Message:</strong> {ticketDetails.message}</div>
|
||||
<div><strong>Created Date:</strong> {ticketDetails.created_date}</div>
|
||||
<Button onClick={closeDetails} mt="sm">Close</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<p>Loading details...</p>
|
||||
)}
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
BIN
src/app/image/DICGC_image.jpg
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
src/app/image/changepw.png
Normal file
After Width: | Height: | Size: 198 KiB |
BIN
src/app/image/changepw_b.jpg
Normal file
After Width: | Height: | Size: 219 KiB |
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 79 KiB |
BIN
src/app/image/logo.jpg
Normal file
After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 58 KiB |
BIN
src/app/image/objective.jpg
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
src/app/image/vision.jpg
Normal file
After Width: | Height: | Size: 132 KiB |
114
src/app/login/clientCarousel.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'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 [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: '83%', overflow: 'hidden',backgroundColor:"white" }}>
|
||||
{/* Scrollable container */}
|
||||
<Box
|
||||
ref={scrollRef}
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflowX: 'hidden',
|
||||
scrollSnapType: 'x mandatory',
|
||||
scrollBehavior: 'smooth',
|
||||
}}
|
||||
>
|
||||
{images.map((img, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
style={{
|
||||
flex: '0 0 100%',
|
||||
scrollSnapAlign: 'start',
|
||||
height: '250px',
|
||||
minWidth: '100%',
|
||||
maxWidth: '100%',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={img.src}
|
||||
alt={`Slide ${i + 1}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Left Scroll Button */}
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="grey"
|
||||
onClick={scrollLeft}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '10px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<IconChevronLeft size={24} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Right Scroll Button */}
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="grey"
|
||||
onClick={scrollRight}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
right: '10px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<IconChevronRight size={24} />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,195 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import classes from '../page.module.css';
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { TextInput, Button, Container, Title, Image, SimpleGrid, LoadingOverlay, Avatar, Paper, Text, Group, Center } from "@mantine/core";
|
||||
import image from '../helpdesk.png';
|
||||
import { useForm, SubmitHandler } from "react-hook-form";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
|
||||
type OtpInput = {
|
||||
OTP: number
|
||||
}
|
||||
|
||||
type Result = {
|
||||
ok?: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
async function handleResendOTP() {
|
||||
let otp;
|
||||
try {
|
||||
const response = await axios.get("/api/otp");
|
||||
otp = response.data;
|
||||
console.log('OTP resent successfully:', response.data);
|
||||
// Object.assign(response.data);
|
||||
// alert(response.data.message);
|
||||
notifications.show({
|
||||
color: 'green',
|
||||
// title: error.code,
|
||||
message: response.data.message
|
||||
})
|
||||
} catch (error: AxiosError | any) {
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: error.code,
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
return otp;
|
||||
}
|
||||
|
||||
|
||||
async function handleValidateOTP(OtpInput: OtpInput) {
|
||||
let Result: Result = { ok: false }
|
||||
try {
|
||||
const response = await axios.post("/api/otp", OtpInput);
|
||||
{
|
||||
window.location.href = '/home'
|
||||
}
|
||||
Object.assign(Result, response.data);
|
||||
} catch (error: AxiosError | any) {
|
||||
// alert(error.response.data.error);
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: 'red',
|
||||
title: error.response.status,
|
||||
message: error.response.data.error,
|
||||
autoClose: 4000,
|
||||
})
|
||||
}
|
||||
// For countdown reset every time after successful verification
|
||||
sessionStorage.removeItem('countdown');
|
||||
sessionStorage.removeItem('timerStart');
|
||||
return Result;
|
||||
}
|
||||
|
||||
export default function CheckOTP() {
|
||||
const queryClient = useQueryClient();
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<OtpInput>();
|
||||
const [countdown, setCountdown] = useState<number>(90);
|
||||
const [timerActive, setTimerActive] = useState<boolean>(true);
|
||||
|
||||
const validateOTPMutation = useMutation<Result, AxiosError, OtpInput>({
|
||||
mutationKey: ['validateOtp'],
|
||||
mutationFn: handleValidateOTP,
|
||||
})
|
||||
const resendOTPMutation = useQuery({
|
||||
queryKey: ['resendOTP'],
|
||||
queryFn: handleResendOTP,
|
||||
enabled: false,
|
||||
})
|
||||
////
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedCountdown = sessionStorage.getItem('countdown');
|
||||
const savedStartTime = sessionStorage.getItem('timerStart');
|
||||
|
||||
if (savedCountdown && savedStartTime) {
|
||||
const elapsedTime = Math.floor((Date.now() - parseInt(savedStartTime, 10)) / 1000);
|
||||
const remainingTime = parseInt(savedCountdown, 10) - elapsedTime;
|
||||
|
||||
if (remainingTime > 0) {
|
||||
setCountdown(remainingTime);
|
||||
setTimerActive(true);
|
||||
}
|
||||
else {
|
||||
setCountdown(0);
|
||||
setTimerActive(false);
|
||||
sessionStorage.removeItem('countdown');
|
||||
sessionStorage.removeItem('timerStart');
|
||||
}
|
||||
} else {
|
||||
sessionStorage.setItem('countdown', '90');
|
||||
sessionStorage.setItem('timerStart', Date.now().toString());
|
||||
setCountdown(90);
|
||||
setTimerActive(true);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Effect to manage the countdown timer
|
||||
useEffect(() => {
|
||||
if (!timerActive) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
setTimerActive(false);
|
||||
sessionStorage.removeItem('countdown');
|
||||
sessionStorage.removeItem('timerStart');
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval); // Cleanup interval on unmount
|
||||
}, [timerActive]);
|
||||
/////
|
||||
const onSubmit: SubmitHandler<OtpInput> = async (OtpInput) => {
|
||||
const Result = await validateOTPMutation.mutateAsync(OtpInput);
|
||||
if (Result.ok)
|
||||
await queryClient.refetchQueries({ queryKey: ['OTP'] });
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
resendOTPMutation.refetch();
|
||||
setCountdown(90);
|
||||
setTimerActive(true);
|
||||
sessionStorage.setItem('countdown', '90');
|
||||
sessionStorage.setItem('timerStart', Date.now().toString());
|
||||
}
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<SimpleGrid spacing={{ base: 40, sm: 80 }} cols={{ base: 1, sm: 2 }}>
|
||||
<Image src={image.src} className={classes.mobileImage} />
|
||||
<div>
|
||||
<LoadingOverlay visible={validateOTPMutation.isPending || validateOTPMutation.data?.ok || resendOTPMutation.isLoading} />
|
||||
<Paper component='form' className={classes.form} radius={0} p={30} onSubmit={handleSubmit(onSubmit)}>
|
||||
<Avatar
|
||||
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-ZljkQXsXUP1-EwnIPZtVq_JWhwGUW7b0_eSMno-bag&s"
|
||||
alt="Jacob Warnhalter"
|
||||
radius="xxxl"
|
||||
/>
|
||||
<Title order={2} className={classes.title} ta="center" mt="md" mb={50}>
|
||||
Customer Request Management
|
||||
</Title>
|
||||
<TextInput label="Enter One-time password(OTP) *"
|
||||
placeholder="e.g. XXXXX"
|
||||
size="md"
|
||||
maxLength={6}
|
||||
{...register('OTP', { required: true })} />
|
||||
{errors.OTP && <Text c='red'>*Required</Text>}
|
||||
<Center mt="lg">
|
||||
<Text >{timerActive ? `Resend OTP in ${countdown}s` : 'Resend OTP Available'} </Text>
|
||||
</Center>
|
||||
<Group justify="center">
|
||||
<Button mt="xl" size="md" type='submit' >
|
||||
Validate OTP
|
||||
</Button>
|
||||
{validateOTPMutation.data?.message && <Text c='red' ta='center' pt={30}>{validateOTPMutation.data.message}</Text>}
|
||||
<Button mt="xl" size="md" type='submit' color='blue' onClick={handleResend} disabled={timerActive} >
|
||||
Resend OTP
|
||||
</Button>
|
||||
{/* {resendOTPMutation?.data?.message && <Text c='red' ta='center' pt={30}>{resendOTPMutation.data.message}</Text>} */}
|
||||
</Group>
|
||||
</Paper>
|
||||
</div>
|
||||
<Image src={image.src} className={classes.desktopImage} />
|
||||
</SimpleGrid>
|
||||
<footer justify-content='center' color='green'>Copyright © 2025 Tata Consultancy Services, All rights reserved.</footer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -27,3 +27,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-wrapper {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-control {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
color: white;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* First control is left */
|
||||
.gradient-control:first-of-type {
|
||||
left: 0;
|
||||
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.0001));
|
||||
}
|
||||
|
||||
/* Last control is right */
|
||||
.gradient-control:last-of-type {
|
||||
right: 0;
|
||||
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.0001));
|
||||
}
|
||||
|
@@ -1,126 +1,253 @@
|
||||
"use client"
|
||||
|
||||
import React from "react";
|
||||
import { Text, Button, Paper, TextInput, MantineProvider, Image, Box, Anchor, PasswordInput, Title, Checkbox, Card, Group, Flex, Stack } from "@mantine/core";
|
||||
import myImage from '@/app/image/ebanking.jpg';
|
||||
import bgImage from '@/app/image/media.jpg';
|
||||
import frontPage from '@/app/image/ib_front_page.jpg'
|
||||
import NextImage from 'next/image';
|
||||
import { KccbTheme } from "@/app/_themes/KccbTheme";
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Button, TextInput, PasswordInput, Title, Card, Group, Flex, Box, Image, Anchor, Stack, Popover, ActionIcon } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { useRouter } from "next/navigation";
|
||||
import NextImage from "next/image";
|
||||
import logo from '@/app/image/logo.jpg';
|
||||
import frontPage from '@/app/image/ib_front_page.jpg';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { generateCaptcha } from '@/app/captcha';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [CIF, SetCIF] = useState("");
|
||||
const [psw, SetPsw] = useState("");
|
||||
const [captcha, setCaptcha] = useState("");
|
||||
const [inputCaptcha, setInputCaptcha] = useState("");
|
||||
const [isLogging, setIsLogging] = useState(false);
|
||||
const ClientCarousel = dynamic(() => import('./clientCarousel'), { ssr: false });
|
||||
|
||||
useEffect(() => {
|
||||
const loadCaptcha = async () => {
|
||||
const newCaptcha = await generateCaptcha();
|
||||
setCaptcha(newCaptcha);
|
||||
};
|
||||
loadCaptcha();
|
||||
}, []);
|
||||
|
||||
const regenerateCaptcha = () => {
|
||||
// setCaptcha(generateCaptcha());
|
||||
const loadCaptcha = async () => {
|
||||
const newCaptcha = await generateCaptcha();
|
||||
setCaptcha(newCaptcha);
|
||||
};
|
||||
loadCaptcha();
|
||||
setInputCaptcha("");
|
||||
};
|
||||
|
||||
async function handleLogin(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const onlyDigit = /^\d{11}$/;
|
||||
if (!onlyDigit.test(CIF)) {
|
||||
setError('Input value must be 11 digit');
|
||||
}
|
||||
if (inputCaptcha !== captcha) {
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Captcha Error",
|
||||
message: "Please enter the correct captcha",
|
||||
autoClose: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const response = await fetch('api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
customerNo: CIF,
|
||||
password: psw,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
setIsLogging(true);
|
||||
if (response.ok) {
|
||||
console.log(data);
|
||||
const token = data.token;
|
||||
localStorage.setItem("access_token", token);
|
||||
if (data.FirstTimeLogin === true) {
|
||||
router.push("/ChangePassword")
|
||||
}
|
||||
else {
|
||||
router.push("/home");
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
setIsLogging(false);
|
||||
notifications.show({
|
||||
withBorder: true,
|
||||
color: "red",
|
||||
title: "Wrong User Id or Password",
|
||||
message: "Wrong User Id or Password",
|
||||
autoClose: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function Demo() {
|
||||
return (
|
||||
<Providers>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#f8f9fa",
|
||||
padding: "20px",
|
||||
fontFamily: "sans-serif",
|
||||
height: '100vh',
|
||||
// border: "solid black"
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
radius="md"
|
||||
fit="cover"
|
||||
src={myImage}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "100%", height: "12%" }}
|
||||
/>
|
||||
{/* For Text */}
|
||||
{/* <Paper shadow="sm" p="lg" withBorder style={{ maxWidth: 1000, margin: "auto" }}>
|
||||
<div style={{ backgroundColor: "#f8f9fa", width: "100%", height: "auto", paddingTop: "5%" }}>
|
||||
{/* Header */}
|
||||
<Box style={{
|
||||
position: 'fixed', width: '100%', height: '12%', top: 0, left: 0, zIndex: 100,
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
background: "linear-gradient(15deg,rgba(2, 163, 85, 1) 55%, rgba(101, 101, 184, 1) 100%)",
|
||||
// border: "1px solid black"
|
||||
}}>
|
||||
<Image
|
||||
fit="cover"
|
||||
src={logo}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "40%", height: "100%", objectFit: "contain", marginLeft: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: "20px",
|
||||
textAlign: "center",
|
||||
marginBottom: "20px",
|
||||
fontWeight: "bold",
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '64%',
|
||||
color: 'white',
|
||||
textShadow: '1px 1px 2px blue',
|
||||
}}
|
||||
>
|
||||
KCC Bank welcomes you to KCCB - Internet Banking Services
|
||||
{/* <IconBuildingBank/> */}
|
||||
Head Office : Dharmshala, District: Kangra(H.P), Pincode: 176215
|
||||
</Text>
|
||||
</Paper> */}
|
||||
{/* Security Message */}
|
||||
<Box style={{ width: "100%", overflow: "hidden", whiteSpace: "nowrap", padding: "8px 0" }}>
|
||||
<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.
|
||||
⚠️ Do not disclose your User Id and Password to any third party and keep Your UserId and Password strictly confidential.
|
||||
⚠️ KCC Bank never asks for UserId,Passwords and Pins through email or phone.
|
||||
⚠️ Be ware of Phishing mails with links to fake bank'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, userID 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 {
|
||||
0% {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</Box>
|
||||
<div style={{ display: "flex", height: "70vh" }}>
|
||||
<div style={{ flex: 1, backgroundColor: "#f0f0f0", display: "flex" }}>
|
||||
<Image
|
||||
radius="md"
|
||||
fit="cover"
|
||||
src={frontPage}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1, backgroundColor: "#c1e0f0", display: "flex", justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Card shadow="lg" padding="xl" radius="md" style={{ width: 480, height: 400, backgroundColor: '#f0f0f0', boxShadow: '#f0f0f0' }}>
|
||||
<Title order={5} style={{ marginLeft: 10 }}>Welcome To KCC Bank</Title>
|
||||
<Title order={1} style={{ marginLeft: 10, color: "#000066" }}>Internet Banking</Title>
|
||||
{/* <Box style={{ width: "400px", height: "200px", margin: "10px 0 20px 10px", border: "1px solid black", marginLeft: 30 }}>
|
||||
<form style={{ marginLeft: 10, width: "100px", height: "50px" }}>
|
||||
<Flex gap="sm" align="center" justify="flex-start" style={{border: "solid blue",width:"300%"}}>
|
||||
<Text style={{border:"solid black"}}>CIF Number</Text>
|
||||
<TextInput
|
||||
placeholder="Enter your CIF no"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Text style={{border:"solid black"}}>Password : </Text>
|
||||
</Flex>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
{/* Movable text */}
|
||||
<Box
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "0.5%",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
padding: "8px 0",
|
||||
}}
|
||||
>
|
||||
<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.
|
||||
⚠️ Do not disclose your UserId and Password to any third party and keep Your UserId and Password strictly confidential.
|
||||
⚠️ KCC Bank never asks for UserId,Passwords and Pins through email or phone.
|
||||
⚠️ Be ware of Phishing mails with links to fake bank'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, userID and password.
|
||||
⚠️ If you had shard your UserId 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 {
|
||||
0% { transform: translateX(0%); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</Box>
|
||||
{/* Main */}
|
||||
<div style={{ display: "flex", height: "75vh", overflow: "hidden", position: "relative",
|
||||
background:'linear-gradient(to right, #02081eff, #0a3d91)' }}>
|
||||
<div style={{ flex: 1, backgroundColor: "#c1e0f0", position: "relative" }}>
|
||||
<Image
|
||||
fit="cover"
|
||||
src={frontPage}
|
||||
component={NextImage}
|
||||
alt="ebanking"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<Box w={{ base: "100%", md: "50%" }} p="lg">
|
||||
<Card shadow="md" padding="xl" radius="md" style={{ maxWidth: 400,margin: "0 auto",height:'68vh'}}>
|
||||
<form onSubmit={handleLogin}>
|
||||
<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);
|
||||
}}
|
||||
error={error}
|
||||
required
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
value={psw}
|
||||
onChange={(e) => SetPsw(e.currentTarget.value)}
|
||||
required
|
||||
mt="sm"
|
||||
/>
|
||||
<Group mt="sm" align="center">
|
||||
<Box style={{ backgroundColor: "#fff", fontSize: "18px", textDecoration: "line-through", padding: "4px 8px", fontFamily: "cursive" }}>{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)}
|
||||
required
|
||||
mt="sm"
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="md" disabled={isLogging}>
|
||||
{isLogging ? "Logging..." : "Login"}
|
||||
</Button>
|
||||
</form>
|
||||
</Box> */}
|
||||
<Box style={{ width: "370px", padding: "20px", borderRadius: "12px", boxShadow: "0 2px 10px rgba(0,0,0,0.1)", border: "1px solid black", marginLeft: 30 }}>
|
||||
<Flex justify="center" gap="md" >
|
||||
<form>
|
||||
<TextInput
|
||||
label="CIF Number"
|
||||
placeholder="Enter your CIF no"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<br></br>
|
||||
<TextInput
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<br></br>
|
||||
<Button fullWidth variant="filled" mt="md">Login</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Card>
|
||||
</Card>
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
{/* Carousel and Notes */}
|
||||
<Flex direction={{ base: "column", md: "row" }} mt="lg" p="lg">
|
||||
<Box w={{ base: "100%", md: "60%" }}>
|
||||
<ClientCarousel />
|
||||
</Box>
|
||||
<Box w={{ base: "100%", md: "40%" }} p="md" style={{ textAlign: "center" }}>
|
||||
<Title order={2}>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> http://www.kccb.in/</Anchor>
|
||||
</Box>
|
||||
</Flex>
|
||||
{/* 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. |{" "}
|
||||
<Anchor href="document/disclaimer.pdf" target="_blank" rel="noopener noreferrer">Disclaimer</Anchor> |{" "}
|
||||
<Anchor href="document/privacy_policy.pdf" target="_blank" rel="noopener noreferrer">Privacy Policy</Anchor> |{" "}
|
||||
<Anchor href="document/phishing.pdf" target="_blank" rel="noopener noreferrer">Phishing</Anchor> |{" "}
|
||||
<Anchor href="document/security_tips.pdf" target="_blank" rel="noopener noreferrer">Security Tips</Anchor> |{" "}
|
||||
<Anchor href="document/grievance.jpg" target="_blank" rel="noopener noreferrer">Grievances</Anchor>
|
||||
</Text>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Root() {
|
||||
//redirect('/home')
|
||||
redirect('/login')
|
||||
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { UserContextProvider } from './_components/user-context';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { KccbTheme } from './_themes/KccbTheme';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
@@ -11,11 +10,11 @@ const queryClient = new QueryClient();
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MantineProvider theme={KccbTheme} defaultColorScheme='light'>
|
||||
<Notifications position='top-center' />
|
||||
<Notifications position='top-center' />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<UserContextProvider>
|
||||
{children}
|
||||
</UserContextProvider>
|
||||
{/* <UserContextProvider> */}
|
||||
{children}
|
||||
{/* </UserContextProvider> */}
|
||||
</QueryClientProvider>
|
||||
</MantineProvider>
|
||||
)
|
||||
|
125
src/app/registration/page.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
TextInput, PasswordInput, Button, Group, Box, Radio, Text, Paper,
|
||||
Title, Stack, Alert,
|
||||
} from '@mantine/core';
|
||||
import { Providers } from "@/app/providers"
|
||||
|
||||
export default function Register() {
|
||||
const [cif, setCif] = useState('');
|
||||
const [otp, setOtp] = useState('');
|
||||
const [enteredOtp, setEnteredOtp] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [tpin, setTpin] = useState('');
|
||||
const [confirmTpin, setConfirmTpin] = useState('');
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
const newErrors: string[] = [];
|
||||
|
||||
// CIF validation
|
||||
if (!/^\d{10}$/.test(cif)) {
|
||||
newErrors.push('CIF should be exactly 10 numeric digits.');
|
||||
}
|
||||
|
||||
// OTP validation
|
||||
if (otp !== enteredOtp) {
|
||||
newErrors.push('OTP does not match.');
|
||||
}
|
||||
|
||||
// Password validation
|
||||
const strongPassword = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
|
||||
if (!strongPassword.test(password) || password !== confirmPassword) {
|
||||
newErrors.push('Password must be strong and confirmed.');
|
||||
}
|
||||
|
||||
// TPIN validation
|
||||
if (!strongPassword.test(tpin) || tpin !== confirmTpin) {
|
||||
newErrors.push('TPIN must be strong and confirmed.');
|
||||
}
|
||||
|
||||
if (newErrors.length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrors([]);
|
||||
setMessage('Login Successfully ✅');
|
||||
};
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<Box maw={600} mx="auto" p="md">
|
||||
<Title order={2} mb="md"style={{align: "center"}}>Internet Banking Registration</Title>
|
||||
<Paper shadow="xs" p="md">
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="CIF Number"
|
||||
placeholder="Enter 10-digit CIF"
|
||||
value={cif}
|
||||
onChange={(e) => setCif(e.target.value)}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<TextInput
|
||||
label="OTP Sent"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Enter OTP"
|
||||
value={enteredOtp}
|
||||
onChange={(e) => setEnteredOtp(e.target.value)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<PasswordInput
|
||||
label="Login Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Confirm Password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<PasswordInput
|
||||
label="T-PIN"
|
||||
value={tpin}
|
||||
onChange={(e) => setTpin(e.target.value)}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Confirm T-PIN"
|
||||
value={confirmTpin}
|
||||
onChange={(e) => setConfirmTpin(e.target.value)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{errors.length > 0 && (
|
||||
<Alert color="red" title="Errors" mt="sm">
|
||||
{errors.map((err, index) => (
|
||||
<Text key={index} color="red" size="sm">{err}</Text>
|
||||
))}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<Alert color="green" mt="sm">
|
||||
{message}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button fullWidth onClick={handleSubmit}>Process</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Providers>
|
||||
);
|
||||
}
|