Compare commits

28 Commits
main ... dev

Author SHA1 Message Date
10a3da8949 fix: Homepage design
wip :Integrate quick pay for own bank
wip: Design account page as discussed
2025-07-20 11:23:07 +05:30
eae989642b feat : implement the quick pay screen 2025-07-19 16:38:11 +05:30
f67319762f wip: Frontend for quick pay 2025-07-18 11:49:42 +05:30
a6af487b67 fix: fix the layout of fund transfer 2025-07-14 16:11:34 +05:30
26e6dea82b feat: api integration for account tab
fix: layout design
feat : add screen for account tab
2025-07-13 19:21:53 +05:30
1023646751 Fix: layout of the application
Feat: Create page for account Summary
2025-07-12 17:59:30 +05:30
3ccd7eb690 feat : Screen fund transfer 2025-07-10 17:13:06 +05:30
df3fa3532f Fix: Home design 2025-07-10 14:57:42 +05:30
e3391315dd Fix : Error of API integration 2025-07-10 12:45:40 +05:30
9a02cff754 merge both code 2025-07-09 16:43:33 +05:30
893dcbc761 feat : api integration 2025-07-09 16:10:49 +05:30
09d61e556c Feat : Api integrate on login screen, set login password and set transaction password screen 2025-07-09 16:00:24 +05:30
293a7dbea0 change login password and change transaction password pages validation add 2025-07-09 12:09:58 +05:30
4b3e89673b feat: home page update 2025-07-04 16:45:27 +05:30
6970d1af0c fix: change in login page 2025-07-03 15:25:59 +05:30
011df884d8 fix: change in password page 2025-07-03 14:29:03 +05:30
1efe8af7e6 feat: update feature 2025-07-03 11:33:05 +05:30
d52d338a74 feat:change login password and change transaction password page created with all validation 2025-07-03 11:23:15 +05:30
74e6394797 feat : upgrade the home screen 2025-07-03 10:57:21 +05:30
a46670be1f feat : add carousel in login page
feat : add disclaimer,policy in login page
2025-07-01 12:00:01 +05:30
6028ed9f5a ci: Update the login and home screen 2025-06-30 13:07:53 +05:30
bc75470d33 feat: Add captcha in login screen.
feat : add important link in home screen
2025-06-29 17:35:53 +05:30
054c4b8d0e feat :Home page design 2025-06-29 15:19:56 +05:30
6f44347947 fix: login page 2025-06-27 14:09:29 +05:30
92531b02fd feat : connection made between login and home page 2025-06-27 13:59:51 +05:30
49dae3624f feat : update login page 2025-06-26 18:26:46 +05:30
a86cb87da0 feat: Design login page 2025-06-26 17:52:21 +05:30
2e90465c89 fix: 404 error 2025-06-25 16:12:09 +05:30
58 changed files with 3191 additions and 2099 deletions

View File

@@ -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
View 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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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>
);
}

View 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 >
);
}

View File

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

View File

@@ -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>
);
}
}

View 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;
}

View 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>
);
}
}

View 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>
</>
);
}

View 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
View 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>
);
}
}

View 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>
);
}
}

View 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>
)
}

View 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;

View 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));
}

View 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 >
);
}
}

View 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;

View 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
View 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
View 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;
}

View 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> */}
</>
);
}

View File

@@ -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 }

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 id bank_account_no title first_name middle_name last_name mobile_number
2 6000 30022497139 Mr. Rajat Kumar Maharana 7890544527
3 6001 30022497138 Ms. Tomosa Sarkar 7890544527

View File

@@ -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
View 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("");
}

View File

@@ -5,6 +5,7 @@
/* border: 1px solid black; */
}
html, body {
html,
body {
height: 100vh;
}

View File

@@ -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);
}

View File

@@ -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>}
</>
)
}

View File

@@ -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);
}

View File

@@ -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>
</>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
src/app/image/changepw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 79 KiB

BIN
src/app/image/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

BIN
src/app/image/objective.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
src/app/image/vision.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View 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>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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>
);
}

View File

@@ -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));
}

View File

@@ -1,79 +1,169 @@
"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"
}}
>
<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
radius="md"
fit="cover"
src={myImage}
src={logo}
component={NextImage}
alt="ebanking"
style={{ width: "100%", height: "12%" }}
style={{ width: "40%", height: "100%", objectFit: "contain", marginLeft: 0 }}
/>
{/* For Text */}
{/* <Paper shadow="sm" p="lg" withBorder style={{ maxWidth: 1000, margin: "auto" }}>
<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" }}>
</Box>
<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"
}}>
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.
Be ware of Phishing mails with links to fake bank&apos;s websites asking for personal information are in circulation.
Please DO NOT Click on the links given in the emails asking for personal details like bank account number, userID and password.
⚠️ If you had shared your User Id and Password through such mails or links, please change your Password immediately.
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%);
}
0% { transform: translateX(0%); }
100% { transform: translateX(-100%); }
}
`}
</style>
</Box>
<div style={{ display: "flex", height: "70vh" }}>
<div style={{ flex: 1, backgroundColor: "#f0f0f0", display: "flex" }}>
{/* 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
radius="md"
fit="cover"
src={frontPage}
component={NextImage}
@@ -81,44 +171,81 @@ export default function Demo() {
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>
<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
placeholder="Enter your CIF no"
style={{ flex: 1 }}
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
/>
<Text style={{border:"solid black"}}>Password : </Text>
</Flex>
</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
<PasswordInput
label="Password"
placeholder="Enter your password"
style={{ flex: 1 }}
value={psw}
onChange={(e) => SetPsw(e.currentTarget.value)}
required
mt="sm"
/>
<br></br>
<Button fullWidth variant="filled" mt="md">Login</Button>
<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>
</Flex>
</Box>
</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>

View File

@@ -1,7 +1,6 @@
import { redirect } from "next/navigation";
export default function Root() {
//redirect('/home')
redirect('/login')
}

View File

@@ -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';
@@ -13,9 +12,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
<MantineProvider theme={KccbTheme} defaultColorScheme='light'>
<Notifications position='top-center' />
<QueryClientProvider client={queryClient}>
<UserContextProvider>
{/* <UserContextProvider> */}
{children}
</UserContextProvider>
{/* </UserContextProvider> */}
</QueryClientProvider>
</MantineProvider>
)

View 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>
);
}